基于SpringBoot开发的个人博客——技术细节

一.背景

  本来是准备考完试发这篇博客的,但因为这两天苏州疫情的原因,考试全部推迟了,所以就先把这篇博客发出来了
其余更细节的内容请移步我与这篇博客的故事,这里面详细讲述了我这两年的故事和这个博客网站的由来

二.前端

  由于我的学习重点都在后端方面,前端相关的技术十分烂,所以主要是通过参考大佬的网站进行修改的,本博客网站主要参考的网站和资源如下:
  首页模板: 燕十三博客模板
  后台控制和前端其它页面: OneStar的博客页面
第一个模板是在LayUI的基础上开发的,很可惜LayUI已经下架了,第二个模板是再SemanticUI基础上开发的。我只有首页参考的第一个模板,但只为了首页,导入了很多只使用了一次的图标和相关资源,总觉得不大划算。其实本来是想把首页用SemanticUI组件重新写一下的,但最近真的没有时间了,明年有时间会把整个前端部分重写一下的。

三.数据相关

  这部分是这篇文章最重要的部分,会对后台控制、数据库、安全、邮箱等相关方面介绍这个博客网站
整体开发参考的资源如下:
  SpringBoot部分:李仁密老师——SpringBoot开发一个小而美的个人博客
  这是一个非常好的SpringBoot开发个人博客的教学视频,但很可惜数据库部分是用的JPA,与我的技术栈不是很相符,不过总的来说,这个教学视频让我收获非常的大
  MyBatis部分:OneStar的博客后端
  在使用MyBatis实现博客的DAO层过程中,遇到了很多的困难,参考了OneStar的博客开发过程,对MyBatis有了更多的了解,尤其是复杂关系

3.1 数据库

  数据库在本地环境使用的是MySQL 8.0.26版本,一定要注意,在MySQL版本8往上配置文件中需要设置时区,否则一定会报错,在配置文件中配置数据源时添加serverTimezone=Asia/Shanghai即可,别的就没什么了
  数据库中各表结构如下:

类型表
id(pk)name
类别ID(主键)类别名
关注者表
id(pk)nameemailstatuscode
关注者ID(主键)姓名邮箱状态(是否验证)验证码
管理员表
id(pk)nameavatarpasswordcreateTimeemail
管理员ID(主键)姓名头像(路径)密码(加密后)创建时间邮箱
图片表
id(pk)picddresspicdescriptionpicnamepictime
图片ID(主键)图片地址(路径)图片描述图片名图片拍摄时间
留言表
id(pk)nicknamecontentavatarcreatTimeparent_message_idadmin-message
留言ID(主键)留言用户昵称留言内容留言头像(路径)创建时间父留言ID是否管理员留言
评论表
id(pk)nicknamecontentavatarblogIdcreatTimeparent_comment_idadmin-comment
评论ID(主键)评论用户昵称评论内容评论头像(路径)博客ID创建时间父评论ID是否管理员评论
博客表
id(pk)appreciationcommentablecontentdescriptioncreatTimeupdateTimefirstPic
博客ID(主键)赞赏开启评论开启内容描述创建时间更新时间首图地址(路径)
flagpublishedrecommendshareStattitleviewtype_id(fk)userId(fk)commentCount
是否原创是否发布是否推荐是否允许转载标题访问量类型ID(外键)用户ID(外键)评论量

3.2 MyBatis

  DAO层使用的是Mybatis框架,本次开发过程中同时使用了注解和XML配置文件两种方式,下面都会介绍.
  MyBatis的注解方式:
  本博客开发过程简单SQL语句均直接使用注解方式实现,在MaBatis中,简单语句使用注解方式十分简单,通过@Select,@Update,@Delete,@Insert四个注解实现,下文将用关注者的DAO层进行介绍
  @Select注解,可以直接在注解中写Select语句,实现select的功能

 根据Id查询关注者信息
 @Select("select * from t_person where id=#{id}")
 Person getPersonById(Long id);


  注意,在Mybatis的拼接SQL语句时可以使用#和$两种方式,这两者的区别:当使用#parameterName方式引用参数的时候,Mybatis会把传入的参数当成是一个字符串,自动添加双引号。$parameterName引用参数时,不做任何处理,直接将值拼接在sql语句中。
  #是一个占位符,$是拼接符
  #的方式引用参数,mybatis会先对sql语句进行预编译,然后再引用值,能够有效防止sql注入,提高安全性。$的方式引用参数,sql语句不进行预编译,因此最好采用#方式
  Insert示例,一定要注意参数和实体类变量名一一对应,如果采用了驼峰命名的方式要设置map-underscore-to-camel-case: true

  将关注者添加进表中
  @Insert("insert into t_person (email,name,status) values(#{email},#{name},#{status})")
  int addUser(Person user);


  其余的简单语句同上就不一一示范了,而复杂关系主要使用@One,@Many,@ResultMap等相关注解,由于我这儿掌握的不太好,而且我觉得复杂关系用注解的话显得非常混乱,不如XML方式清晰,所以就不详细介绍了,可以参考以下链接:
  MyBatis注解详解:Mybatis注解用法
复杂语句示例:将博客与种类绑定,多对多的关系,可以参考如下链接 狂神说的MyBatis笔记

<resultMap id="blog" type="Blog">
        <id property="id" column="id"/>
        <result property="title" column="title"/>
        <result property="createTime" column="create_time"/>
        <result property="recommend" column="recommend"/>
        <result property="published" column="published"/>
        <result property="typeId" column="type_id"/>
        <association property="type" javaType="Type">
            <id property="id" column="id"/>
            <result property="name" column="name"/>
        </association>
</resultMap>
    <!--查询文章管理列表-->
<select id="listAllBlog" resultMap="blog">
        select b.id,b.title,b.create_time,b.recommend,b.published,b.type_id,t.id,t.name from t_blog b left outer join
                t_type t on b.type_id = t.id order by b.create_time desc
</select>
使用association标签绑定

  其实注解和XML两种方式混用并不是很好的方式,这样会导致程序可读性非常的差,所以最好还是采取同一种方式
  MyBatis集成PageHelper直接参考PageHelper使用,本博客系统只在后端文章管理使用了一次分页插件,所以这儿就不过多介绍了
  个人博客的CRUD操作最好集成Redis,实现从Redis中读,从MySQL中写,加快读取速度,这部分内容也是我最近的学习重点,争取在十二月优化完成

四.后端相关

 4.1 Restful风格

  最开始我学RESTful风格的时候,认为只是一种风格而已,没有必要遵守,只是把参数放在了路径中,不用?去拼接了,但是后来的麻烦教我做人了。之前因为自己开发经验不足,在Controller里面接口都是乱写,只会使用@RequestMapping一个注解,直到集成swagger进行接口测试的时候,傻眼了。
  正常的接口:

每个接口的GET \ POST分工明确,各自有不同的功能

  再给大家看看我因为乱用@RequestMapping带来的后果:

看似有这么多接口,但是只对应了一个方法

  产生这问题的原因就是乱使用@RequestMapping,一定要根据请求方式老老实实使用@GetMapping和@PostMapping

 4.2 资源过滤

  由于这是个人博客,所以现在没有提供用户注册功能,因此安全控制直接使用拦截器实现,具体实现如下:
  配置LoginInterceptor,继承抽象类HandlerInterceptorAdapter,再配置WebConfigure实现WebMvcConfigurer接口即可

LoginInterceptor类
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(request.getSession().getAttribute("admin")== null)
        {
            response.sendRedirect("/admin");
            return false;
        }
        return true;
}
WebConfigure类 切记一定要把login排除掉,否则死循环,浏览器提示重定向次数过多
 public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin")
                .excludePathPatterns("/admin/login");
    }

  这种实现方式最大的优点是简单,但功能确实比较弱,只能起到资源过滤的效果,无法实现权限控制等相关功能
  集成SpringSecurity实现相关功能,其实已经实现了,这个月会抽空写一下相关笔记的:
  集成Shiro也可以实现这个功能,但目前还没有实现,等明年重构这个项目时会试试,也不是特别的复杂:

 4.3 集成Swagger

  直接参考: 狂神说一小时学会Swagger

 4.4 邮箱

  采用工具类的方式封装,直接调用即可

public static void createMail(String acc,String neirong,String jieshou,int type) throws Exception {
        Properties prop = new Properties();
        // 开启debug调试,以便在控制台查看
        //prop.setProperty("mail.debug", "true");
        // 设置邮件服务器主机名
        prop.setProperty("mail.host", "smtp.qq.com");
        // 发送服务器需要身份验证
        prop.setProperty("mail.smtp.auth", "true");
        // 发送邮件协议名称
        prop.setProperty("mail.transport.protocol", "smtp");
        // 开启SSL加密,否则会失败
        MailSSLSocketFactory sf = new MailSSLSocketFactory();
        sf.setTrustAllHosts(true);
        prop.put("mail.smtp.ssl.enable", "true");
        prop.put("mail.smtp.ssl.socketFactory", sf);
        // 创建session
        Session session = Session.getInstance(prop);
        // 通过session得到transport对象
        Transport ts = session.getTransport();
        // 连接邮件服务器:邮箱类型,帐号,POP3/SMTP协议授权码 163使用:smtp.163.com
        ts.connect("smtp.qq.com", "账号", "POP码,不是密码");
        // 创建邮件
        Message message = createSimpleMail(acc,session,neirong,jieshou,type);
        // 发送邮件
        ts.sendMessage(message, message.getAllRecipients());
        ts.close();
    }

     //type参数 0代表关注 1代表取消关注 2代表更新通知
    public static MimeMessage createSimpleMail(String acc,Session session,String neirong, String jieshou,int type) throws Exception {

        // 创建邮件对象
        MimeMessage message = new MimeMessage(session);
        // 指明邮件的发件人
        message.setFrom(new InternetAddress("发送者"));
        //内容部分可以直接按照html格式写
        if(type==0)
        {
            // 邮件的标题
            message.setSubject("感谢您关注我");
            // 邮件的文本内容
            message.setContent("内容1");

        }
        else if(type==1)
        {
            message.setSubject("感谢您曾经关注过我");
            message.setContent("内容2","text/html;charset=utf-8");
        }
        else
        {
         message.setSubject("博客更新啦","text/html;charset=utf-8");
         message.setContent("内容3","text/html;charset=utf-8")
        }
        // 指明邮件的收件人,发件人和收件人如果是一样的,那就是自己给自己发
        message.setRecipient(Message.RecipientType.TO, new InternetAddress(jieshou));
        // 返回创建好的邮件对象
        return message;
    }
   //参数neirong是验证码,在邮件类型0中需要使用
   //生成指定长度由int和小写字母构成的验证码
    public static String getRandomString(int length) {
        StringBuffer buffer = new StringBuffer("0123456789abcdefghijklmnopqrstuvwxyz");
        StringBuffer sb = new StringBuffer();
        Random random = new Random();
        int range = buffer.length();
        for (int i = 0; i < length; i ++) {
            sb.append(buffer.charAt(random.nextInt(range)));
        }
        return sb.toString();
    }

  这种实现方式最大的问题在于过于耗时,效率非常的低,在未来的更新中要实现多线程发送邮件

 4.5 集成MarkDown

  采用工具类的方式封装,前端直接调用即可

 //MarkDown中也可以直接写Html语句
 public static String markdownToHtml(String markdown) {
        Parser parser = Parser.builder().build();
        Node document = parser.parse(markdown);
        HtmlRenderer renderer = HtmlRenderer.builder().build();
        return renderer.render(document);
    }

    /**
     * 增加扩展[标题锚点,表格生成]
     * Markdown转换成HTML
     */
    public static String markdownToHtmlExtensions(String markdown) {
        //标题生成id
        Set<Extension> headingAnchorExtensions = Collections.singleton(HeadingAnchorExtension.create());
        //转换table的HTML
        List<Extension> tableExtension = Arrays.asList(TablesExtension.create());
        Parser parser = Parser.builder()
                .extensions(tableExtension)
                .build();
        Node document = parser.parse(markdown);
        HtmlRenderer renderer = HtmlRenderer.builder()
                .extensions(headingAnchorExtensions)
                .extensions(tableExtension)
                .attributeProviderFactory(new AttributeProviderFactory() {
                    public AttributeProvider create(AttributeProviderContext context) {
                        return new CustomAttributeProvider();
                    }
                })
                .build();
        return renderer.render(document);
    }

    //处理标签的属性
    static class CustomAttributeProvider implements AttributeProvider {
        @Override
        public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
            //改变a标签的target属性为_blank
            if (node instanceof Link) {
                attributes.put("target", "_blank");
            }
            if (node instanceof TableBlock) {
                attributes.put("class", "ui celled table");
            }
        }
    }
这是我的MarkDown工具类,您也可以百度搜到很多类似的工具类

  关于MarkDown的具体使用请参考: MarkDown语法详细说明

 4.6 MD5加密

  数据库中的用户密码必须加密,否则在前后端的交互时,很容易被破解,MD5是一种加密算法,不可逆,可以大大提高安全性,此部分直接参考了3.2.7中的教程

public static String code(String str){
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes());
            byte[]byteDigest = md.digest();
            int i;
            StringBuffer buf = new StringBuffer("");
            for (int offset = 0; offset < byteDigest.length; offset++) {
                i = byteDigest[offset];
                if (i < 0)
                    i += 256;
                if (i < 16)
                    buf.append("0");
                buf.append(Integer.toHexString(i));
            }
            //32位加密
            return buf.toString();
            // 16位的加密
            //return buf.toString().substring(8, 24);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

 4.7 其他基本功能

  直接参考: 李仁密老师——SpringBoot开发一个小而美的个人博客
  这个博客的基本功能包括查询,编辑,推荐,最新等相关部分都是学习这个视频开发的,就不在这儿过多赘述了
  如果我有时间的话,会每个模块详细写一下开发过程的

五.部署

  这儿就是最终发布网站的过程了,难度很低,如果您有Linux基础可以跳过第一个视频,本网站最终部署在了阿里云服务器
  阿里云Linux基础:【狂神说Java】Linux最通俗易懂的教程阿里云真实环境学习,时长约十小时
  购买和配置服务器:【狂神说Java】服务器购买及宝塔部署环境说明,时长约半小时
  部署网站:

   最好使用打jar包的方式,不要用war包,jar包方便很多
   在使用Maven打包时,先clean再package,避免历史文件的干扰
   
   移动jar包到系统内,可以使用Xftp,非常方便
   项目目录下,创建start.sh文件
   start.sh文件文件中输入java -jar xxx.jar   //xxx部分为你的jar包名
   
   控制台输入:
   chmod 777 start.sh  //赋予管理员权限
   nohup  ./start.sh&  //不间断运行文件内的指令即可
   
   如果你项目的端口被占用
   netstat -anp | grep 8800   //netstat -anp | grep 端口号
   sudo lsof -i:8800  //sudo lsof -i: 端口号
   sudo kill -9 26191  //sudo kill -9 PID 杀掉对应进程
   
   最好在项目运行前先检查一下端口的状况

六.存在问题

  1.实体类过于臃肿,例如我的博客类,在首页显示等不需要博客全部信息时,联表查询博客所有信息并没有必要,可以考虑新增博客的查询类,博客的首页类等相关查询类,只保留需要的属性即可

 @Data
 @NoArgsConstructor
 @AllArgsConstructor
 public class Blog {
        private Long id;
        private String title;
        private String content;
        private String firstPicture;
        private String flag;
        private Integer views;
        private Integer commentCount;
        private boolean appreciation;
        private boolean shareStatement;
        private boolean commentabled;
        private boolean published;
        private boolean recommend;
        private Date createTime;
        private Date updateTime;
        private Long typeId;
        private Long userId;
        private String description;
        private Type type;
        private User user;
        private List<Comment> comments = new ArrayList<>();
 }
二十多个属性加上使用了Lombok,可读性不是一般的差

  2.邮件发送使用进程实现,在只有两三个关注者时,更新博客,邮件通知都要超过五秒,效率非常的低,此功能一定要通过多线程实现
  3.安全性较差,管理员删除评论直接采用判断Session中是否存在对象,隐藏前端按钮的方式,黑客很容易在Session中插入伪造的对象,从而直接进入后台和删除程序

<a class="delete" href="#" th:href="@{/messages/{id}/delete(id=${message.id})}" onclick="return confirm('确定要删除该评论吗?')" th:if="${session.admin}">删除</a>
直接用Thymeleaf判断Session对象,非常不安全

七.未来发展

  1.数据库优化,修改部分表,使其更符合实际需求,集成Redis,实现从Redis中读,提升性能
  2.整体重构,针对上文提出的问题修改,并且代码符合阿里巴巴编码规范,之后开源
  3.实现用户登录功能,避免用户每次评论都要输入姓名,提供第三方登录的功能,例如QQ登录、百度登录等
  4.背景音乐,前端集成音乐播放功能,锻炼一下前端方面的能力
  5.搜索功能集成ElasticSearch,提高搜索效率

感谢您的观看

end

评论

qq2736305617
你好,能开源一下您现在的代码吗?想学习一下。
V2弹道导弹
回复
测试
测试
1  @ 测试
测试