Vue & SpringBoot 从零实现博客系统 (五)

本文系Vue & SpringBoot从零实现博客系统第五部分 后端代码编写

后端代码编写

前言

  • 这实现这篇博客之前,我学习ssm框架不到两个星期,然后看了一天springboot就开始了,中途遇到很多坑,所以就记下来和大家分享
  • 本来想着继承redis和SpringSecurity,但是由于只有我一个管理员,所以没必要集成SpringSecurity。对于redis来说,因为想赶快出一个能上线的博客,所以一切从简,就没有集成redis

工具 & 插件 & 依赖 & 技术栈

  • MySQL 8.0 + (这个8.0和5.0好像没什么区别)
  • Maven (强大的项目管理工具,依赖包的获取,项目的打包等等一件完成)
  • Intellij IDEA (Jetbrain家族一员,出色的Java IDE)
  • Mybatis(持久层ORM框架,结合官方文档非常容易上手)
  • PageHelper(一个Mybatis插件,用于分页)
  • SpringBoot(简化了Spring和SpringMVC的配置,适合快速开发项目工程)

项目

项目结构

在这里插入图片描述

  • aop 程序切面,用于日志业务
  • controller 控制层,接受前端的request,并返回response
  • dao 持久成,通过SQL和数据库交互
  • domain 实体类,通过mybatis逆向生成
  • generator mybatis逆向的启动类,这个在项目部署上可以删去
  • interceptor 拦截器,用于权限处理
  • service 服务层,处理业务逻辑
    • impl 实现类
    • 接口
  • util 一些工具类,如获取ip对应地址,时间格式转换等等
  • resources / mapoer mybatis需要的xml文件
  • application.properties springboot的配置
  • generatorConfig.xml mybatis的逆向配置类

项目pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.zi10ng</groupId>
    <artifactId>blog</artifactId>
    <version>0.0.4-SNAPSHOT</version>
    <name>blog</name>
    <packaging>jar</packaging>
    <description>blog for zi10ng</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!--MyBatis逆向工程-->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.6</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
            <scope>runtime</scope>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--SpringBoot热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional> <!-- 这个需要为 true 热部署才有效 -->
        </dependency>
        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--aop-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>
        <!--myPages-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.12</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                     <mainClass>cn.zi10ng.blog.BlogApplication</mainClass>
                </configuration>
            </plugin>

            <!--mybatis逆向的插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.2</version>
                <configuration>
                    <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
                    <overwrite>true</overwrite>
                    <verbose>true</verbose>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.16</version>
                    </dependency>
                    <dependency>
                        <groupId>tk.mybatis</groupId>
                        <artifactId>mapper-generator</artifactId>
                        <version>1.0.0</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

</project>

业务流程

当tomcat接收到一个request时,先经过拦截器,拦截器过滤之后到controller层接收请求,之后再到service处理业务逻辑,再之后到dao层与数据库交互,获取数据,再返回。业务逻辑非常简单

项目难点

接口数据结构的封装
  • 在谷歌的过程中,发现好多人都用阿里的fastjson,我嫌麻烦就没有,springboot自带的有jackson我觉着也挺好用的

  • controller接收request时,可能接收一个实体类,也可能接收一个id,还可能接收多个参数(通过map)

  • controller返回response时,可返回一个参数,也可能返回一个实体类,还可能返回多个实体类(通过map)

  • 可以参考下这个博客

  • 如下示例

    @GetMapping("/any/article")
        public List<Object> getArticle(long id) {
            List<Object> list = new ArrayList<>();
            TreeUtils treeUtils = new TreeUtils();
            list.add(articleService.getArticleContentById(id));
            list.add(treeUtils.buildTree(articleService.listCommentOfArticle(id), id));
            list.add(categoryInfoService.listCategoryNameByArticleId(id));
            articleService.updateArticleInfo(id, "traffic", true);
            return list;
        }
    
    @PostMapping("/admin/postArticle")
        public boolean postArticle(@RequestBody Map<String, Object> map) throws IOException {
    
            ObjectMapper objectMapper = new ObjectMapper();
    
            ArticleInfo articleInfo = objectMapper.readValue(
                    objectMapper.writeValueAsString(map.get("articleInfo")), ArticleInfo.class);
            ArticleContent articleContent = objectMapper.readValue(
                    objectMapper.writeValueAsString(map.get("articleContent")), ArticleContent.class);
            ArticleCategory articleCategory = objectMapper.readValue(
                    objectMapper.writeValueAsString(map.get("articleCategory")), ArticleCategory.class);
    
            if (articleService.insertArticle(articleInfo, articleContent, articleCategory)){
                categoryInfoService.updateCategoryNumById(articleCategory.getCategoryId(), 1);
                return true;
            }
            return false;
        }
    
pagehelper的应用

pagehelper作为mybatis的一个插件,对程序的耦合度,就我来说,还是非常高的。比如,在service层中程序程序必须要这样写:

   @Override
    public List<ArticleInfo> listArticleInfoByTime(MyPages myPages) {
        /**代码段**/
        // 下面是必须要求这样写的,这就意味着我不能在Service处理相关的业务逻辑
        PageHelper.startPage(myPages.getPage(), myPages.getSize());
        return articleInfoMapper.listArticleInfoByTime();
    }

在写程序的过程中,有时候就会遇到上面的问题,这就要求

  • 要么有一个好的SQL,

  • 要么在controller处理逻辑。当在controller处理逻辑的时候,我又对其封装了一层(妈呀中间件真的是太强了),即当其返回pagehelper后,又需要逻辑处理的情况下,可以再用一个pagehelper封装刚才的pagehelper,并处理逻辑。如下

    @GetMapping("/any/byTime")
        public PageInfo<ArticleInfoCategory> listArticleInfoByTime(MyPages myPages) {
            // 也可用mybatis 一对多查询
            PageInfo<ArticleInfo> pageInfo = new PageInfo<>(articleService.listArticleInfoByTime(myPages));
            return PageInfo2PageInfo.article2ArticleCategory(pageInfo, categoryInfoService);
        }
    
多级评论/分类的实现

有两种实现方式

  • 一种是直接通过SQL,之前写过sqllite的,但是Mysql的递归我不会写QAQ
  • 第二种就是从数据库拿出数据之后变成一个list,然后再把list按照规律构建成一个树,再作为response返回给前端。我采取第二种

实现方式

在这里我用了一个构建为树的工具类

package cn.zi10ng.blog.util;

import cn.zi10ng.blog.domain.CommentInfo;
import cn.zi10ng.blog.domain.Node;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Zi10ng
 * @date 2019年8月24日21:20:16
 */
public class TreeUtils {
    private List<Long> longs = new ArrayList<>();
    private Node nodeMe = new Node();
    /**
     * 把评论信息的集合转化为一个树
     */
    public Node buildTree(List<CommentInfo> commentInfo, long id){
        Node tree = new Node();
        List<Node> children = new ArrayList<>();
        List<Node> nodeList = new ArrayList<>();
        for (CommentInfo info : commentInfo) {
            children.add(buildNode(info));
        }
        tree.setId(id);
        tree.setChildren(children);
        for (Node child : children) {
            Node node = findNode(children, child.getParentId());
            List<Node> nodes = new ArrayList<>();
            if (node != null) {
                if (node.getChildren() != null) {
                    nodes = node.getChildren();
                    nodes.add(child);
                    node.setChildren(nodes);
                }else {
                    nodes.add(child);
                    node.setChildren(nodes);
                }
                nodeList.add(child);
            }
        }
        for (Node node : nodeList) {
            children.remove(node);
        }
        return tree;
    }
    
    /** 把树转换为list
     * @param node 节点
     * @return list
     */
    public List<Long> travelSubTree(Node node){
        //如果不是父节点的话
        if(node.getChildren() != null) {
            for (Node index : node.getChildren()) {
                longs.add(index.getId());
                if (index.getChildren() != null && index.getChildren().size() > 0 ) {
                    travelSubTree(index);
                }
            }
        }
        return longs;
    }

    /**
     * 拿到某一节点的树以及其子节点
     * @param node 树节点
     * @param id 标识id
     * @return node
     */
    public Node travelTree(Node node, long id){
        if(node != null) {
            for (Node index : node.getChildren()) {
                if (index.getId() == id){
                    return index;
                }
                if (index.getChildren() != null && index.getChildren().size() > 0 ) {
                    nodeMe = travelTree(index, id);
                }
            }
        }
        return nodeMe;
    }
    
    private Node findNode(List<Node> nodes, long id){
        for (Node node : nodes) {
            if (node.getId() == id) {
                return node;
            }
        }
        return null;
    }

    private Node buildNode(CommentInfo info){
        Node node = new Node();
        node.setId(info.getId());
        node.setParentId(info.getParentId());
        node.setObject(info);
        node.setChildren(null);

        return node;
    }

}

Mybatis逆向工具

对于构建一些小项目来说,mybatis的逆向还是很有用的,它会生成对应的domain和简单的mapper

  • 需要引入maven插件和依赖,具体的依赖已经在上面的pom中列出来了,就不详细说了

配置的xml如下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>

    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!--避免生成重复代码的插件-->
        <plugin type="cn.zi10ng.blog.util.OverIsCombinedPlugin"/>

        <!--是否在代码中显示注释-->
        <commentGenerator>
            <property name="suppressDate" value="true"/>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <!--数据库链接地址账号密码-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/world?useSSL=false&amp;serverTimezone=Hongkong&amp;characterEncoding=utf-8&amp;autoReconnect=true"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <!--生成pojo类存放位置-->
        <javaModelGenerator targetPackage="cn.zi10ng.blog.domain" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>
        <!--生成xml映射文件存放位置-->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!--生成mapper类存放位置-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="cn.zi10ng.blog.dao" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!--生成对应表及类名-->
        <table tableName="sys_log" domainObjectName="SysLog" enableCountByExample="false"
               enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="true"
               selectByExampleQueryId="false">
            <!--使用自增长键-->
            <property name="my.isgen.usekeys" value="true"/>
            <!--使用数据库中实际的字段名作为生成的实体类的属性-->
            <property name="useActualColumnNames" value="true"/>
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
        <table>
        ....
        </table>
        ...
    </context>
</generatorConfiguration>

启动类

package cn.zi10ng.blog.generator;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @author Zi10ng
 * @date 2019年7月24日18:01:22
 * mybatis的逆向
 */
public class MybatisGenerator {
    public static void main(String[] args) throws Exception {
        String today = "2019-07-24";

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date now = sdf.parse(today);
        Date d = new Date();

        if (d.getTime() > now.getTime() + 1000 * 60 * 60 * 24) {
            System.err.println("——————未成成功运行——————");
            System.err.println("——————未成成功运行——————");
            System.err.println("本程序具有破坏作用,应该只运行一次,如果必须要再运行,需要修改today变量为今天,如:" + sdf.format(new Date()));
            return;
        }

        List<String> warnings = new ArrayList<>();
        boolean overwrite = true;
        InputStream is = MybatisGenerator.class.getClassLoader().getResource("generatorConfig.xml").openStream();
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(is);
        is.close();
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        myBatisGenerator.generate(null);

        System.out.println("生成代码成功,只能执行一次,以后执行会覆盖掉mapper,pojo,xml 等文件上做的修改");
    }
}
IP解析接口

这里我用了一个免费网站的ip解析接口,将ip解析为地址,具体使用方式为:

package cn.zi10ng.blog.util;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Zi10ng
 * @date 2019年9月9日21:24:13
 * 把ip解析为地区
 */
public class Ip2Region {

    public static String sendGet(String ip){
        try {

            String url = "http://api.online-service.vip/ip3?ip=";

            URL query = new URL(url + ip);

            HttpURLConnection conn = (HttpURLConnection) query.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Accept", "application/json");

            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            StringBuilder sb= new StringBuilder();

            String regEx = "[\\u4e00-\\u9fa5]";
            Pattern p   =   Pattern.compile(regEx);
            Matcher m   =   p.matcher(br.readLine());

            while(m.find()){
                sb.append(m.group());
            }
            return String.valueOf(sb);

        } catch (java.io.IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

一些SQL语句

我们知道,mybatis的SQL映射可使用注解,也可以使用xml。通常情况下,我们一般用注解写比较简单的,xml写比较复杂的,比如if子句等等。但事无绝对,只要文档看的好,注解反而看着更简洁。

一个好的SQL可以帮我们省去很多代码逻辑

  • 返回自增id

        <insert id="insertArticleInfo" parameterType="cn.zi10ng.blog.domain.ArticleInfo" useGeneratedKeys="true" keyProperty="id">
            insert into article_info (title, summary) values (#{title}, #{summary})
        </insert>
    

    之后如果有问题可以参考这篇博客,这也是一个小坑

  • 注解方式实现foreach

      /**
         * 按分类,时间降序查询全部文章
         * @param categoryId 分类id
         * @return list
         */
        @Select({"<script>",
                "select a.*" +
                        "from article_info as a, article_category as ac,category_info as c ",
                        "where c.id = ac.category_id and a.id = ac.article_id ",
                                "and c.id in",
                         "  <foreach item= 'categoryId' index= 'index' collection= 'list'",
                         "      open='(' separator=',' close=')'>",
                         "        #{categoryId}",
                         "  </foreach>",
                        "order by create_by desc",
                 "</script>"})
        @ResultMap("ArticleInfoMap")
        List<ArticleInfo> listArticleInfoByCategory(List<Long> categoryId);
    
  • 如果存在该ip则更新,不存在则创建(这个用于用户的注册和登录)

        <insert id="postUser" parameterType="cn.zi10ng.blog.domain.SysUser">
            INSERT INTO sys_user(
              role ,
              browser ,
              region,
              ip)
            VALUES(
                #{role} ,
                #{browser} ,
                #{region},
                #{ip})
            ON DUPLICATE KEY UPDATE
            <if test="name != null">
                name = #{name},
                connect = #{connect},
                role = #{role}
            </if>
            <if test="name == null">
                num = num + 1
            </if>
        </insert>
    
日期格式的处理
  • 第一点,我们需要用合适的SQL中的data函数去查询出日期(这一步需要读者自行查看相关SQL函数)

  • 第二点,在实体类的get/set方法中,需要对日期格式进行进一步的处理,把日期改为字符串格式

    public String getCreateByStr() {
            if (createBy != null){
                createByStr = DateFormatUtils.data2String(createBy,"yyyy-MM-dd HH:mm:ss");
            }
            return createByStr;
        }
    
        public void setCreateByStr(String createByStr) {
            this.createByStr = createByStr;
        }
    
        public String getModifiedByStr() {
            if (modifiedBy != null){
                modifiedByStr = DateFormatUtils.data2String(modifiedBy,"yyyy-MM-dd HH:mm:ss");
            }
            return modifiedByStr;
        }
    
        public void setModifiedByStr(String modifiedByStr) {
            this.modifiedByStr = modifiedByStr;
        }
    }
    

    日期工具类如下:

    package cn.zi10ng.blog.util;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     * @author Zi10ng
     * @date 2019年7月24日20:54:54
     * 日期转换工具类
     */
    public class DateFormatUtils {
        /**
         * 日期转换为字符串
         * @param date 日期
         * @param str 字符串
         * @return 字符串
         */
        public static String data2String(Date date, String str){
            SimpleDateFormat sdf = new SimpleDateFormat(str);
            return sdf.format(date);
        }
    }
    
    
Servlet的拦截器处理权限

前文已经说到,因为只有我一个管理员,所以没必要用功能强大的SpringSecurity或者小而精的shiro,直接通过过滤器拦截一下就完事了

  • 拦截器的功能就是检测request的URL,看是否含有/admin(这在本系列文章第三部分接口设计中已经提到,如果含有/admin,则说明是管理员权限的接口),如果含有/admin,则查看其是否有请求头中是否有token,且token是否和服务器中保存的一样,如果一样,则放行,不一样则把null作为response返回

    package cn.zi10ng.blog.interceptor;
    
    import cn.zi10ng.blog.service.UserService;
    import cn.zi10ng.blog.util.Md5Utils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * @author stalern
     * @date 2019年9月17日19:01:09
     * 拦截器
     */
    public class AuthenticationInterceptor implements HandlerInterceptor {
    
        @Autowired
        UserService userService;
    
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
            String admin = "admin";
            // 从 http 请求头中取出 token
            String tokenRaw = httpServletRequest.getHeader("token");
            String uri = httpServletRequest.getRequestURI();
            //如果路径中包含admin
            if (uri.contains(admin)) {
                if (tokenRaw == null) {
                    return false;
                } else {
                   // 检查token是否和服务器中的token相同
                }
            }
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest httpServletRequest,
                               HttpServletResponse httpServletResponse,
                               Object o, ModelAndView modelAndView) throws Exception {
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    Object o, Exception e) throws Exception {
        }
    }
    
    
  • 之后还需要配置拦截器,如下

    package cn.zi10ng.blog.interceptor;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * @author stalern
     * @date 2019年9月17日19:16:59
     * 拦截器配置类
     */
    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(authenticationInterceptor())
                    .addPathPatterns("/**");
        }
    
        @Bean
        public AuthenticationInterceptor authenticationInterceptor() {
            return new AuthenticationInterceptor();
        }
    }
    
    

一些教训

  • 前后端分离一定要分装状态码,状态,数据
  • 合理使用SQL语句可以减少业务层的代码
  • 整个项目的完工,让我发现了curd真的很累,一个强大的程序员,必须要掌握基础和底层,下一步,jvm和并发,设计模式,网络编程,冲冲冲
发布了100 篇原创文章 · 获赞 142 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/coder_what/article/details/101262471