使用Spring Boot搭建应用开发框架(一) —— 基础架构

 


回到顶部

Spring的简史

第一阶段:XML配置,在Spring1.x时代,使用Spring开发满眼都是xml配置的Bean,随着项目的扩大,我们需要把xml配置文件分放到不同的配置文件里,那时候需要频繁的在开发的类和配置文件之间切换。

第二阶段:注解配置,在Spring2.x时代,Spring提供声明Bean的注解,大大减少了配置量。应用的基本配置用xml,业务配置用注解。

第三阶段:Java配置,从Spring3.x到现在,Spring提供了Java配置,使用Java配置可以让你更理解你所配置的Bean。

Spring Boot:使用“习惯优于配置”的理念让你的项目快速运行起来。使用Spring Boot很容易创建一个独立运行、准生产级别的基于Spring框架的项目,使用Spring Boot你可以不用或者只需要很少的Spring配置。

下面就来使用Spring Boot一步步搭建一个前后端分离的应用开发框架,并且以后不断的去完善这个框架,往里面添加功能。后面以实战为主,不会介绍太多概念,取而代之的是详细的操作。

 

零、开发技术简介

开发平台:windows

开发工具:Intellij IDEA 2017.1

JDK:Java 8

Maven:maven-3.3.9

服务器:tomcat 8.0

数据库:MySQL 5.7

数据源:Druid1.1.6

缓存:Redis 3.2

日志框架:SLF4J+Logback

Spring Boot:1.5.7.RELEASE

ORM框架:MyBatis+通用Mapper

Spring Boot官方文档:Spring Boot Reference Guide

 

一、创建项目

这一节创建项目的基础结构,按照spring boot的思想,将各个不同的功能按照starter的形式拆分开来,做到灵活组合,并简单介绍下Spring Boot相关的东西。

1、创建工程

① 通过File > New > Project,新建工程,选择Spring Initializr,然后Next。

② 尽量为自己的框架想个好点的名字,可以去申请个自己的域名。我这里项目名称为Sunny,项目路径为com.lyyzoo.sunny。

③ 这里先什么都不选,后面再去集成。注意我的Spring Boot版本为1.5.9。Next

④ 定义好工程的目录,用一个专用目录吧,不要在一个目录下和其它东西杂在一起。之后点击Finish。

上面说的这么详细,只有一个目的,从一个开始就做好规范。

⑤ 生成的项目结构如下,可以自己去看下pom.xml里的内容。

2、创建Starter

先创建一个core核心、cache缓存、security授权认证,其它的后面再集成进去。

跟上面一样的方式,在Sunny下创建sunny-starter-core、sunny-starter-cache、sunny-starter-security子模块。

这样分模块后,我们以后需要哪个模块就引入哪个模块即可,如果哪个模块不满足需求,还可以重写该模块。

最终的项目结构如下:

3、启动项目

首先在core模块下来启动并了解SpringBoot项目。

① 在com.lyyzoo.core根目录下,有一个SunnyStarterCoreApplication,这是SpringBoot的入口类,通常是*Application的命名。

入口类里有一个main方法,其实就是一个标准的Java应用的入口方法。在main方法中使用SpringApplication.run启动Spring Boot项目。

然后看看@SpringBootApplication注解,@SpringBootApplication是Spring Boot的核心注解,是一个组合注解。

@EnableAutoConfiguration让Spring Boot根据类路径中的jar包依赖为当前项目进行自动配置。

Spring Boot会自动扫描@SpringBootApplication所在类的同级包以及下级包里的Bean。

② 先启动项目,这里可以看到有一个Spring Boot的启动程序,点击右边的按钮启动项目。看到控制台Spring的标志,就算是启动成功了。

③ 替换默认的banner

可以到http://patorjk.com/software/taag/这个网站生成一个自己项目的banner。创建banner.txt并放到resources根目录下。

4、Spring Boot 配置

① 配置文件

Spring Boot使用一个全局的配置文件application.properties或application.yaml,放置在src/main/resources目录下。我们可以在这个全局配置文件中对一些默认的配置值进行修改。

具体有哪些配置可到官网查找,有非常多的配置,不过大部分使用默认即可。Common application properties

然后,需要为不同的环境配置不同的配置文件,全局使用application-{profile}.properties指定不同环境配置文件。

我这里增加了开发环境(dev)和生产环境(prod)的配置文件,并通过在application.properties中设置spring.profiles.active=dev来指定当前环境。

② starter pom

Spring Boot为我们提供了简化开发绝大多数场景的starter pom,只要使用了应用场景所需的starter pom,无需繁杂的配置,就可以得到Spring Boot为我们提供的自动配置的Bean。

后面我们将会通过加入这些starter来一步步集成我们想要的功能。具体有哪些starter,可以到官网查看:Starters

③ 自动配置

Spring Boot关于自动配置的源码在spring-boot-autoconfigure中如下:

我们可以在application.properties中加入debug=true,查看当前项目中已启用和未启用的自动配置。

我们在application.properties中的配置其实就是覆盖spring-boot-autoconfigure里的默认配置,比如web相关配置在web包下。

常见的如HttpEncodingProperties配置http编码,里面自动配置的编码为UTF-8。

MultipartProperties,上传文件的属性,设置了上传最大文件1M。

ServerProperties,配置内嵌Servlet容器,配置端口、contextPath等等。

之前说@SpringBootApplication是Spring Boot的核心注解,但他的核心功能是由@EnableAutoConfiguration注解提供的。

@EnableAutoConfiguration注解通过@Import导入配置功能,在AutoConfigurationImportSelector中,通过SpringFactoriesLoader.loadFactoryNames扫描META-INF/spring.factories文件。

在spring.factories中,配置了需要自动配置的类,我们也可以通过这种方式添加自己的自动配置。

在spring-boot-autoconfigure下就有一个spring.factories,如下:

说了这么多,只为说明一点,Spring Boot为我们做了很多自动化的配置,搭建快速方便。

但是,正因为它为我们做了很多事情,就有很多坑,有时候,出了问题,我们可能很难找出问题所在,这时候,我们可能就要考虑下是否是自动配置导致的,有可能配置冲突了,或者没有使用上自定义的配置等等。

5、项目结构划分

core是项目的核心模块,结构初步规划如下:

 base是项目的基础核心,定义一些基础类,如BaseController、BaseService等;

    cache是缓存相关;

    config是配置中心,模块所有的配置放到config里统一管理;

    constants里定义系统的常量。

    exception里封装一些基础的异常类;

 system是系统模块;

    util里则是一些通用工具类;

 

二、基础结构功能

1、web支持

只需在pom.xml中加入spring-boot-starter-web的依赖即可。

之后,查看POM的依赖树(插件:Maven Helper),可以看到引入了starter、tomcat、web支持等。可以看出,Sping Boot内嵌了servlet容器,默认tomcat。

自动配置在WebMvcAutoConfiguration和WebMvcProperties里,可自行查看源码,一般我们不需添加其他配置就可以启动这个web项目了。

2、基础功能

在core中添加一些基础的功能支持。

① 首先引入一些常用的依赖库,主要是一些常用工具类,方便以后的开发。

复制代码
 1 <!-- ******************************* 常用依赖库 ********************************** -->
 2 <!-- 针对开发IO流功能的工具类库 -->
 3 <dependency>
 4     <groupId>commons-io</groupId>
 5     <artifactId>commons-io</artifactId>
 6     <version>${commons.io.version}</version>
 7 </dependency>
 8 <!-- 文件上传 -->
 9 <dependency>
10     <groupId>commons-fileupload</groupId>
11     <artifactId>commons-fileupload</artifactId>
12     <version>${commons.fileupload.version}</version>
13     <exclusions>
14         <exclusion>
15             <groupId>commons-io</groupId>
16             <artifactId>commons-io</artifactId>
17         </exclusion>
18     </exclusions>
19 </dependency>
20 <!-- 常用的集合操作,丰富的工具类 -->
21 <dependency>
22     <groupId>commons-collections</groupId>
23     <artifactId>commons-collections</artifactId>
24     <version>${commons.collections.version}</version>
25 </dependency>
26 <!-- 操作javabean的工具包 -->
27 <dependency>
28     <groupId>commons-beanutils</groupId>
29     <artifactId>commons-beanutils</artifactId>
30     <version>${commons.beanutils.version}</version>
31     <exclusions>
32         <exclusion>
33             <groupId>commons-collections</groupId>
34             <artifactId>commons-collections</artifactId>
35         </exclusion>
36     </exclusions>
37 </dependency>
38 <!-- 包含一些通用的编码解码算法. 如:MD5、SHA1、Base64等 -->
39 <dependency>
40     <groupId>commons-codec</groupId>
41     <artifactId>commons-codec</artifactId>
42     <version>${commons.codec.version}</version>
43 </dependency>
44 <!-- 包含丰富的工具类如 StringUtils -->
45 <dependency>
46     <groupId>org.apache.commons</groupId>
47     <artifactId>commons-lang3</artifactId>
48     <version>${commons.lang3.version}</version>
49 </dependency>
50 <!--
51     Guava工程包含了若干被Google的Java项目广泛依赖的核心库. 集合[collections] 、缓存[caching] 、原生类型支持[primitives support] 、
52     并发库[concurrency libraries] 、通用注解[common annotations] 、字符串处理[string processing] 、I/O 等等。
53 -->
54 <dependency>
55     <groupId>com.google.guava</groupId>
56     <artifactId>guava</artifactId>
57     <version>${guava.version}</version>
58 </dependency>
复制代码

版本号如下:

② 在base添加一个Result类,作为前端的返回对象,Controller的直接返回对象都是Result。

复制代码
package com.lyyzoo.core.base;

import com.fasterxml.jackson.annotation.JsonInclude;

import java.io.Serializable;

/**
 * 前端返回对象
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-28
 */
public class Result implements Serializable {
    private static final long serialVersionUID = 1430633339880116031L;

    /**
     * 成功与否标志
     */
    private boolean success = true;
    /**
     * 返回状态码,为空则默认200.前端需要拦截一些常见的状态码如403、404、500等
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Integer status;
    /**
     * 编码,可用于前端处理多语言,不需要则不用返回编码
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String code;
    /**
     * 相关消息
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String msg;
    /**
     * 相关数据
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Object data;


    public Result() {}

    public Result(boolean success) {
        this.success = success;
    }

    public Result(boolean success, Integer status) {
        this.success = success;
        this.status = status;
    }

    public Result(boolean success, String code, String msg){
        this(success);
        this.code = code;
        this.msg = msg;
    }

    public Result(boolean success, Integer status, String code, String msg) {
        this.success = success;
        this.status = status;
        this.code = code;
        this.msg = msg;
    }

    public Result(boolean success, String code, String msg, Object data){
        this(success);
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}
复制代码

之后在util添加生成Result的工具类Results,用于快速方便的创建Result对象。

复制代码
package com.lyyzoo.core.util;

import com.lyyzoo.core.base.Result;

/**
 * Result生成工具类
 *
 * @version 1.0
 * @author bojiangzhou 2017-12-28
 */
public class Results {

    protected Results() {}

    public static Result newResult() {
        return new Result();

    }

    public static Result newResult(boolean success) {
        return new Result(success);
    }

    //
    // 业务调用成功
    // ----------------------------------------------------------------------------------------------------
    public static Result success() {
        return new Result();
    }

    public static Result success(String msg) {
        return new Result(true, null, msg);
    }

    public static Result success(String code, String msg) {
        return new Result(true, code, msg);
    }

    public static Result successWithStatus(Integer status) {
        return new Result(true, status);
    }

    public static Result successWithStatus(Integer status, String msg) {
        return new Result(true, status, null, msg);
    }

    public static Result successWithData(Object data) {
        return new Result(true, null, null, data);
    }

    public static Result successWithData(Object data, String msg) {
        return new Result(true, null, msg, data);
    }

    public static Result successWithData(Object data, String code, String msg) {
        return new Result(true, code, msg, data);
    }

    //
    // 业务调用失败
    // ----------------------------------------------------------------------------------------------------
    public static Result failure() {
        return new Result(false);
    }

    public static Result failure(String msg) {
        return new Result(false, null, msg);
    }

    public static Result failure(String code, String msg) {
        return new Result(false, code, msg);
    }

    public static Result failureWithStatus(Integer status) {
        return new Result(false, status);
    }

    public static Result failureWithStatus(Integer status, String msg) {
        return new Result(false, status, null, msg);
    }

    public static Result failureWithData(Object data) {
        return new Result(false, null, null, data);
    }

    public static Result failureWithData(Object data, String msg) {
        return new Result(false, null, msg, data);
    }

    public static Result failureWithData(Object data, String code, String msg) {
        return new Result(false, code, msg, data);
    }

}
复制代码

③ 在base添加BaseEnum<K, V>枚举接口,定义了获取值和描述的接口。

复制代码
 1 package com.lyyzoo.core.base;
 2 
 3 /**
 4  * 基础枚举接口
 5  *
 6  * @version 1.0
 7  * @author bojiangzhou 2017-12-31
 8  */
 9 public interface BaseEnum<K, V> {
10 
11     /**
12      * 获取编码
13      *
14      * @return 编码
15      */
16     K code();
17 
18     /**
19      * 获取描述
20      * 
21      * @return 描述
22      */
23     V desc();
24 
25 }
复制代码

然后在constants下定义一个基础枚举常量类,我们把一些描述信息维护到枚举里面,尽量不要在代码中直接出现魔法值(如一些编码、中文等),以后的枚举常量类也可以按照这种模式来写。

复制代码
 1 package com.lyyzoo.core.constants;
 2 
 3 import com.lyyzoo.core.base.BaseEnum;
 4 
 5 import java.util.HashMap;
 6 import java.util.Map;
 7 
 8 /**
 9  * 基础枚举值
10  *
11  * @version 1.0
12  * @author bojiangzhou 2018-01-01
13  */
14 public enum BaseEnums implements BaseEnum<String, String> {
15 
16     SUCCESS("request.success", "请求成功"),
17 
18     FAILURE("request.failure", "请求失败"),
19 
20     OPERATION_SUCCESS("operation.success", "操作成功"),
21 
22     OPERATION_FAILURE("operation.failure", "操作失败"),
23 
24     ERROR("system.error", "系统异常"),
25 
26     NOT_FOUND("not_found", "请求资源不存在"),
27 
28     FORBIDDEN("forbidden", "无权限访问"),
29 
30     VERSION_NOT_MATCH("record_not_exists_or_version_not_match", "记录版本不存在或不匹配"),
31 
32     PARAMETER_NOT_NULL("parameter_not_be_null", "参数不能为空");
33 
34     private String code;
35 
36     private String desc;
37 
38     private static Map<String, String> allMap = new HashMap<>();
39 
40     BaseEnums(String code, String desc) {
41         this.code = code;
42         this.desc = desc;
43     }
44 
45     static {
46         for(BaseEnums enums : BaseEnums.values()){
47             allMap.put(enums.code, enums.desc);
48         }
49     }
50 
51     @Override
52     public String code() {
53         return code;
54     }
55 
56     @Override
57     public String desc() {
58         return desc;
59     }
60 
61     public String desc(String code) {
62         return allMap.get(code);
63     }
64 
65 }
复制代码

④ 再添加一个常用的日期工具类对象,主要包含一些常用的日期时间格式化,后续可再继续往里面添加一些公共方法。

复制代码
  1 package com.lyyzoo.core.util;
  2 
  3 
  4 import org.apache.commons.lang3.StringUtils;
  5 import org.apache.commons.lang3.time.DateUtils;
  6 
  7 import java.text.ParseException;
  8 import java.text.SimpleDateFormat;
  9 import java.util.Date;
 10 
 11 /**
 12  * 日期时间工具类
 13  *
 14  * @version 1.0
 15  * @author bojiangzhou 2017-12-28
 16  */
 17 public class Dates {
 18 
 19     /**
 20      * 日期时间匹配格式
 21      */
 22     public interface Pattern {
 23         //
 24         // 常规模式
 25         // ----------------------------------------------------------------------------------------------------
 26         /**
 27          * yyyy-MM-dd
 28          */
 29         String DATE = "yyyy-MM-dd";
 30         /**
 31          * yyyy-MM-dd HH:mm:ss
 32          */
 33         String DATETIME = "yyyy-MM-dd HH:mm:ss";
 34         /**
 35          * yyyy-MM-dd HH:mm
 36          */
 37         String DATETIME_MM = "yyyy-MM-dd HH:mm";
 38         /**
 39          * yyyy-MM-dd HH:mm:ss.SSS
 40          */
 41         String DATETIME_SSS = "yyyy-MM-dd HH:mm:ss.SSS";
 42         /**
 43          * HH:mm
 44          */
 45         String TIME = "HH:mm";
 46         /**
 47          * HH:mm:ss
 48          */
 49         String TIME_SS = "HH:mm:ss";
 50 
 51         //
 52         // 系统时间格式
 53         // ----------------------------------------------------------------------------------------------------
 54         /**
 55          * yyyy/MM/dd
 56          */
 57         String SYS_DATE = "yyyy/MM/dd";
 58         /**
 59          * yyyy/MM/dd HH:mm:ss
 60          */
 61         String SYS_DATETIME = "yyyy/MM/dd HH:mm:ss";
 62         /**
 63          * yyyy/MM/dd HH:mm
 64          */
 65         String SYS_DATETIME_MM = "yyyy/MM/dd HH:mm";
 66         /**
 67          * yyyy/MM/dd HH:mm:ss.SSS
 68          */
 69         String SYS_DATETIME_SSS = "yyyy/MM/dd HH:mm:ss.SSS";
 70 
 71         //
 72         // 无连接符模式
 73         // ----------------------------------------------------------------------------------------------------
 74         /**
 75          * yyyyMMdd
 76          */
 77         String NONE_DATE = "yyyyMMdd";
 78         /**
 79          * yyyyMMddHHmmss
 80          */
 81         String NONE_DATETIME = "yyyyMMddHHmmss";
 82         /**
 83          * yyyyMMddHHmm
 84          */
 85         String NONE_DATETIME_MM = "yyyyMMddHHmm";
 86         /**
 87          * yyyyMMddHHmmssSSS
 88          */
 89         String NONE_DATETIME_SSS = "yyyyMMddHHmmssSSS";
 90     }
 91 
 92     public static final String DEFAULT_PATTERN = Pattern.DATETIME;
 93 
 94     public static final String[] PARSE_PATTERNS = new String[]{
 95             Pattern.DATE,
 96             Pattern.DATETIME,
 97             Pattern.DATETIME_MM,
 98             Pattern.DATETIME_SSS,
 99             Pattern.SYS_DATE,
100             Pattern.SYS_DATETIME,
101             Pattern.SYS_DATETIME_MM,
102             Pattern.SYS_DATETIME_SSS
103     };
104 
105     /**
106      * 格式化日期时间
107      * 
108      * @param date 日期时间
109      *
110      * @return yyyy-MM-dd HH:mm:ss
111      */
112     public static String format(Date date) {
113         return format(date, DEFAULT_PATTERN);
114     }
115 
116     /**
117      * 格式化日期
118      * 
119      * @param date 日期(时间)
120      *
121      * @param pattern 匹配模式 参考:{@link Dates.Pattern}
122      *
123      * @return 格式化后的字符串
124      */
125     public static String format(Date date, String pattern) {
126         if (date == null) {
127             return null;
128         }
129         pattern = StringUtils.isNotBlank(pattern) ? pattern : DEFAULT_PATTERN;
130         SimpleDateFormat sdf = new SimpleDateFormat(pattern);
131         return sdf.format(date);
132     }
133 
134     /**
135      * 解析日期
136      *
137      * @param date 日期字符串
138      *
139      * @return 解析后的日期 默认格式:yyyy-MM-dd HH:mm:ss
140      */
141     public static Date parseDate(String date) {
142         if (StringUtils.isBlank(date)) {
143             return null;
144         }
145         try {
146             return DateUtils.parseDate(date, PARSE_PATTERNS);
147         } catch (ParseException e) {
148             e.printStackTrace();
149         }
150         return null;
151     }
152 
153     /**
154      * 解析日期
155      *
156      * @param date 日期
157      *
158      * @param pattern 格式 参考:{@link Dates.Pattern}
159      *
160      * @return 解析后的日期,默认格式:yyyy-MM-dd HH:mm:ss
161      */
162     public static Date parseDate(String date, String pattern) {
163         if (StringUtils.isBlank(date)) {
164             return null;
165         }
166         String[] parsePatterns;
167         parsePatterns = StringUtils.isNotBlank(pattern) ? new String[]{pattern} : PARSE_PATTERNS;
168         try {
169             return DateUtils.parseDate(date, parsePatterns);
170         } catch (ParseException e) {
171             e.printStackTrace();
172         }
173         return null;
174     }
175 
176 
177 
178 }
复制代码

⑤ Constants定义系统级的通用常量。

复制代码
 1 package com.lyyzoo.core.constants;
 2 
 3 import com.google.common.base.Charsets;
 4 
 5 import java.nio.charset.Charset;
 6 
 7 /**
 8  * 系统级常量类
 9  *
10  * @version 1.0
11  * @author bojiangzhou 2017-12-28
12  */
13 public class Constants {
14 
15     public static final String APP_NAME = "sunny";
16 
17     /**
18      * 系统编码
19      */
20     public static final Charset CHARSET = Charsets.UTF_8;
21 
22     /**
23      * 标识:是/否、启用/禁用等
24      */
25     public interface Flag {
26 
27         Integer YES = 1;
28 
29         Integer NO = 0;
30     }
31 
32     /**
33      * 操作类型
34      */
35     public interface Operation {
36         /**
37          * 添加
38          */
39         String ADD = "add";
40         /**
41          * 更新
42          */
43         String UPDATE = "update";
44         /**
45          * 删除
46          */
47         String DELETE = "delete";
48     }
49 
50     /**
51      * 性别
52      */
53     public interface Sex {
54         /**
55          * 男
56          */
57         Integer MALE = 1;
58         /**
59          * 女
60          */
61         Integer FEMALE = 0;
62     }
63 
64 }
复制代码

⑥ 在base添加空的BaseController、BaseDTO、Service、Mapper,先定义好基础结构,后面再添加功能。

BaseDTO:标准的who字段、版本号、及10个扩展字段。

因为这里用到了@Transient注解,先引入java持久化包:

复制代码
  1 package com.lyyzoo.core.base;
  2 
  3 import com.fasterxml.jackson.annotation.*;
  4 import com.lyyzoo.core.Constants;
  5 import com.lyyzoo.core.util.Dates;
  6 import org.apache.commons.lang3.builder.ToStringBuilder;
  7 import org.apache.commons.lang3.builder.ToStringStyle;
  8 
  9 import javax.persistence.Transient;
 10 import java.io.Serializable;
 11 import java.util.Date;
 12 import java.util.HashMap;
 13 import java.util.Map;
 14 
 15 /**
 16  * 基础实体类
 17  *
 18  * @version 1.0
 19  * @author bojiangzhou 2017-12-29
 20  */
 21 public class BaseDTO implements Serializable {
 22     private static final long serialVersionUID = -4287607489867805101L;
 23 
 24     public static final String FIELD_OPERATE = "operate";
 25     public static final String FIELD_OBJECT_VERSION_NUMBER = "versionNumber";
 26     public static final String FIELD_CREATE_BY = "createBy";
 27     public static final String FIELD_CREATOR = "creator";
 28     public static final String FIELD_CREATE_DATE = "createDate";
 29     public static final String FIELD_UPDATE_BY = "updateBy";
 30     public static final String FIELD_UPDATER = "updater";
 31     public static final String FIELD_UPDATE_DATE = "updateDate";
 32 
 33 
 34     /**
 35      * 操作类型,add/update/delete 参考:{@link Constants.Operation}
 36      */
 37     @Transient
 38     private String _operate;
 39 
 40     /**
 41      * 数据版本号,每发生update则自增,用于实现乐观锁.
 42      */
 43     private Long versionNumber;
 44 
 45     //
 46     // 下面是标准 WHO 字段
 47     // ----------------------------------------------------------------------------------------------------
 48     /**
 49      * 创建人用户名
 50      */
 51     @JsonInclude(JsonInclude.Include.NON_NULL)
 52     private Long createBy;
 53     /**
 54      * 创建人名称
 55      */
 56     @JsonInclude(JsonInclude.Include.NON_NULL)
 57     @Transient
 58     private String creator;
 59     /**
 60      * 创建时间
 61      */
 62     @JsonInclude(JsonInclude.Include.NON_NULL)
 63     @JsonFormat(pattern = Dates.DEFAULT_PATTERN)
 64     private Date createDate;
 65 
 66     /**
 67      * 更新人用户名
 68      */
 69     @JsonInclude(JsonInclude.Include.NON_NULL)
 70     private Long updateBy;
 71     /**
 72      * 更新人名称
 73      */
 74     @JsonInclude(JsonInclude.Include.NON_NULL)
 75     @Transient
 76     private String updater;
 77     /**
 78      * 更新时间
 79      */
 80     @JsonInclude(JsonInclude.Include.NON_NULL)
 81     @JsonFormat(pattern = Dates.DEFAULT_PATTERN)
 82     private Date updateDate;
 83 
 84     /**
 85      * 其它属性
 86      */
 87     @JsonIgnore
 88     @Transient
 89     protected Map<String, Object> innerMap = new HashMap<>();
 90 
 91     //
 92     // 下面是扩展属性字段
 93     // ----------------------------------------------------------------------------------------------------
 94 
 95     @JsonInclude(JsonInclude.Include.NON_NULL)
 96     private String attribute1;
 97 
 98     @JsonInclude(JsonInclude.Include.NON_NULL)
 99     private String attribute2;
100 
101     @JsonInclude(JsonInclude.Include.NON_NULL)
102     private String attribute3;
103 
104     @JsonInclude(JsonInclude.Include.NON_NULL)
105     private String attribute4;
106 
107     @JsonInclude(JsonInclude.Include.NON_NULL)
108     private String attribute5;
109 
110     @JsonInclude(JsonInclude.Include.NON_NULL)
111     private String attribute6;
112 
113     @JsonInclude(JsonInclude.Include.NON_NULL)
114     private String attribute7;
115 
116     @JsonInclude(JsonInclude.Include.NON_NULL)
117     private String attribute8;
118 
119     @JsonInclude(JsonInclude.Include.NON_NULL)
120     private String attribute9;
121 
122     @JsonInclude(JsonInclude.Include.NON_NULL)
123     private String attribute10;
124 
125     public String get_operate() {
126         return _operate;
127     }
128 
129     public void set_operate(String _operate) {
130         this._operate = _operate;
131     }
132 
133     @Override
134     public String toString() {
135         return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
136     }
137 
138     public String toJSONString() {
139         return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
140     }
141 
142     public Long getVersionNumber() {
143         return versionNumber;
144     }
145 
146     public void setVersionNumber(Long versionNumber) {
147         this.versionNumber = versionNumber;
148     }
149 
150     public Long getCreateBy() {
151         return createBy;
152     }
153 
154     public void setCreateBy(Long createBy) {
155         this.createBy = createBy;
156     }
157 
158     public String getCreator() {
159         return creator;
160     }
161 
162     public void setCreator(String creator) {
163         this.creator = creator;
164     }
165 
166     public Date getCreateDate() {
167         return createDate;
168     }
169 
170     public void setCreateDate(Date createDate) {
171         this.createDate = createDate;
172     }
173 
174     public Long getUpdateBy() {
175         return updateBy;
176     }
177 
178     public void setUpdateBy(Long updateBy) {
179         this.updateBy = updateBy;
180     }
181 
182     public String getUpdater() {
183         return updater;
184     }
185 
186     public void setUpdater(String updater) {
187         this.updater = updater;
188     }
189 
190     public Date getUpdateDate() {
191         return updateDate;
192     }
193 
194     public void setUpdateDate(Date updateDate) {
195         this.updateDate = updateDate;
196     }
197 
198     @JsonAnyGetter
199     public Object getAttribute(String key) {
200         return innerMap.get(key);
201     }
202 
203     @JsonAnySetter
204     public void setAttribute(String key, Object obj) {
205         innerMap.put(key, obj);
206     }
207 
208     public String getAttribute1() {
209         return attribute1;
210     }
211 
212     public void setAttribute1(String attribute1) {
213         this.attribute1 = attribute1;
214     }
215 
216     public String getAttribute2() {
217         return attribute2;
218     }
219 
220     public void setAttribute2(String attribute2) {
221         this.attribute2 = attribute2;
222     }
223 
224     public String getAttribute3() {
225         return attribute3;
226     }
227 
228     public void setAttribute3(String attribute3) {
229         this.attribute3 = attribute3;
230     }
231 
232     public String getAttribute4() {
233         return attribute4;
234     }
235 
236     public void setAttribute4(String attribute4) {
237         this.attribute4 = attribute4;
238     }
239 
240     public String getAttribute5() {
241         return attribute5;
242     }
243 
244     public void setAttribute5(String attribute5) {
245         this.attribute5 = attribute5;
246     }
247 
248     public String getAttribute6() {
249         return attribute6;
250     }
251 
252     public void setAttribute6(String attribute6) {
253         this.attribute6 = attribute6;
254     }
255 
256     public String getAttribute7() {
257         return attribute7;
258     }
259 
260     public void setAttribute7(String attribute7) {
261         this.attribute7 = attribute7;
262     }
263 
264     public String getAttribute8() {
265         return attribute8;
266     }
267 
268     public void setAttribute8(String attribute8) {
269         this.attribute8 = attribute8;
270     }
271 
272     public String getAttribute9() {
273         return attribute9;
274     }
275 
276     public void setAttribute9(String attribute9) {
277         this.attribute9 = attribute9;
278     }
279 
280     public String getAttribute10() {
281         return attribute10;
282     }
283 
284     public void setAttribute10(String attribute10) {
285         this.attribute10 = attribute10;
286     }
287 
288 }
复制代码

同时,重写了toString方法,增加了toJsonString方法,使得可以格式化输出DTO的数据:

直接打印DTO,输出的格式大概就是这个样子:

⑦ 在exception添加BaseException,定义一些基础异常类

基础异常类都继承自运行时异常类(RunntimeException),尽可能把受检异常转化为非受检异常,更好的面向接口编程,提高代码的扩展性、稳定性。

BaseException:添加了一个错误编码,其它自定义的异常应当继承该类。

复制代码
 1 package com.lyyzoo.core.exception;
 2 
 3 /**
 4  * 基础异常类
 5  *
 6  * @version 1.0
 7  * @author bojiangzhou 2017-12-31
 8  */
 9 public class BaseException extends RuntimeException {
10     private static final long serialVersionUID = -997101946070796354L;
11 
12     /**
13      * 错误编码
14      */
15     protected String code;
16 
17     public BaseException() {}
18 
19     public BaseException(String message) {
20         super(message);
21     }
22 
23     public BaseException(String code, String message) {
24         super(message);
25         this.code = code;
26     }
27 
28     public String getCode() {
29         return code;
30     }
31 
32     public void setCode(String code) {
33         this.code = code;
34     }
35 }
复制代码

ServiceException:继承BaseException,Service层往Controller抛出的异常。

复制代码
 1 package com.lyyzoo.core.exception;
 2 
 3 /**
 4  * Service层异常
 5  *
 6  * @version 1.0
 7  * @author bojiangzhou 2017-12-31
 8  */
 9 public class ServiceException extends BaseException {
10     private static final long serialVersionUID = 6058294324031642376L;
11 
12     public ServiceException() {}
13 
14     public ServiceException(String message) {
15         super(message);
16     }
17 
18     public ServiceException(String code, String message) {
19         super(code, message);
20     }
21 
22 }
复制代码

3、添加系统用户功能,使用Postman测试接口

① 在system模块下,再分成dto、controller、service、mapper、constants子包,以后一个模块功能开发就是这样一个基础结构。

User:系统用户

复制代码
  1 package com.lyyzoo.core.system.dto;
  2 
  3 import com.fasterxml.jackson.annotation.JsonFormat;
  4 import com.fasterxml.jackson.annotation.JsonInclude;
  5 import com.lyyzoo.core.base.BaseDTO;
  6 import com.lyyzoo.core.util.Dates;
  7 
  8 import java.util.Date;
  9 
 10 /**
 11  * 系统用户
 12  *
 13  * @version 1.0
 14  * @author bojiangzhou 2017-12-31
 15  */
 16 @JsonInclude(JsonInclude.Include.NON_NULL)
 17 public class User extends BaseDTO {
 18     private static final long serialVersionUID = -7395431342743009038L;
 19 
 20     /**
 21      * 用户ID
 22      */
 23     private Long userId;
 24     /**
 25      * 用户名
 26      */
 27     private String username;
 28     /**
 29      * 密码
 30      */
 31     private String password;
 32     /**
 33      * 昵称
 34      */
 35     private String nickname;
 36     /**
 37      * 生日
 38      */
 39     @JsonFormat(pattern = Dates.Pattern.DATE)
 40     private Date birthday;
 41     /**
 42      * 性别:1-男/0-女
 43      */
 44     private Integer sex;
 45     /**
 46      * 是否启用:1/0
 47      */
 48     private Integer enabled;
 49 
 50     public Long getUserId() {
 51         return userId;
 52     }
 53 
 54     public void setUserId(Long userId) {
 55         this.userId = userId;
 56     }
 57 
 58     public String getUsername() {
 59         return username;
 60     }
 61 
 62     public void setUsername(String username) {
 63         this.username = username;
 64     }
 65 
 66     public String getPassword() {
 67         return password;
 68     }
 69 
 70     public void setPassword(String password) {
 71         this.password = password;
 72     }
 73 
 74     public String getNickname() {
 75         return nickname;
 76     }
 77 
 78     public void setNickname(String nickname) {
 79         this.nickname = nickname;
 80     }
 81 
 82     public Date getBirthday() {
 83         return birthday;
 84     }
 85 
 86     public void setBirthday(Date birthday) {
 87         this.birthday = birthday;
 88     }
 89 
 90     public Integer getSex() {
 91         return sex;
 92     }
 93 
 94     public void setSex(Integer sex) {
 95         this.sex = sex;
 96     }
 97 
 98     public Integer getEnabled() {
 99         return enabled;
100     }
101 
102     public void setEnabled(Integer enabled) {
103         this.enabled = enabled;
104     }
105 
106 }
复制代码

UserController:用户控制层;用@RestController注解,前后端分离,因为无需返回视图,采用Restful风格,直接返回数据。

复制代码
 1 package com.lyyzoo.core.system.controller;
 2 
 3 import com.lyyzoo.core.Constants;
 4 import com.lyyzoo.core.base.BaseController;
 5 import com.lyyzoo.core.base.BaseEnums;
 6 import com.lyyzoo.core.base.Result;
 7 import com.lyyzoo.core.system.dto.User;
 8 import com.lyyzoo.core.util.Dates;
 9 import com.lyyzoo.core.util.Results;
10 import org.springframework.web.bind.annotation.PathVariable;
11 import org.springframework.web.bind.annotation.RequestMapping;
12 import org.springframework.web.bind.annotation.RestController;
13 
14 import java.util.ArrayList;
15 import java.util.List;
16 
17 /**
18  * 用户Controller
19  *
20  * @version 1.0
21  * @author bojiangzhou 2017-12-31
22  */
23 @RequestMapping("/sys/user")
24 @RestController
25 public class UserController extends BaseController {
26 
27     private static List<User> userList = new ArrayList<>();
28 
29     // 先静态模拟数据
30     static {
31         User user1 = new User();
32         user1.setUserId(1L);
33         user1.setUsername("lufei");
34         user1.setNickname("蒙奇D路飞");
35         user1.setBirthday(Dates.parseDate("2000-05-05"));
36         user1.setSex(Constants.Sex.MALE);
37         user1.setEnabled(Constants.Flag.YES);
38         userList.add(user1);
39 
40         User user2 = new User();
41         user2.setUserId(2L);
42         user2.setUsername("nami");
43         user2.setNickname("娜美");
44         user2.setBirthday(Dates.parseDate("2000/7/3"));
45         user2.setSex(Constants.Sex.FEMALE);
46         user2.setEnabled(Constants.Flag.YES);
47         userList.add(user2);
48     }
49 
50     @RequestMapping("/queryAll")
51     public Result queryAll(){
52         return Results.successWithData(userList, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description());
53     }
54 
55     @RequestMapping("/queryOne/{userId}")
56     public Result queryOne(@PathVariable Long userId){
57         User user = null;
58         for(User u : userList){
59             if(u.getUserId().longValue() == userId){
60                 user = u;
61             }
62         }
63         return Results.successWithData(user);
64     }
65 }
复制代码

② Postman请求:请求成功,基础的HTTP服务已经实现了。

 

三、集成MyBatis,实现基础Mapper和Service

1、添加JDBC、配置数据源

添加spring-boot-starter-jdbc以支持JDBC访问数据库,然后添加MySql的JDBC驱动mysql-connector-java;

在application.properties里配置mysql的数据库驱动

之后在application-dev.properties里配置开发环境数据库的连接信息,添加之后,Springboot就会自动配置数据源了。

2、集成MyBatis

MyBatis官方为了方便Springboot集成MyBatis,专门提供了一个符合Springboot规范的starter项目,即mybatis-spring-boot-starter。

在application.properties里添加mybatis映射配置:

3、添加MyBatis通用Mapper

通用Mapper可以极大的简化开发,极其方便的进行单表的增删改查。

关于通用Mapper,参考网站地址:

  MyBatis通用Mapper

  MyBatis 相关工具

之后,在core.base下创建自定义的Mapper,按需选择接口。

具体可参考:根据需要自定义接口

复制代码
 1 package com.lyyzoo.core.base;
 2 
 3 import tk.mybatis.mapper.common.BaseMapper;
 4 import tk.mybatis.mapper.common.ConditionMapper;
 5 import tk.mybatis.mapper.common.IdsMapper;
 6 import tk.mybatis.mapper.common.special.InsertListMapper;
 7 
 8 /**
 9  *
10  * BaseMapper
11  *
12  * @name BaseMapper
13  * @version 1.0
14  * @author bojiangzhou 2017-12-31
15  */
16 public interface Mapper<T> extends BaseMapper<T>, ConditionMapper<T>, IdsMapper<T>, InsertListMapper<T> {
17 
18 }
复制代码

定义好基础Mapper后,就具有下图中的基本通用方法了。每个实体类对应的*Mapper继承Mapper<T>来获得基本的增删改查的通用方法。

在application.properties里配置自定义的基础Mapper

4、添加分页插件PageHelper

参考地址:

  MyBatis 分页插件 - PageHelper

  分页插件使用方法

分页插件配置,一般情况下,不需要做任何配置。

之后,我们就可以在代码中使用 PageHelper.startPage(1, 10) 对紧随其后的一个查询进行分页查询,非常方便。

5、配置自动扫描Mapper

在config下创建MyBatisConfig配置文件,通过mapperScannerConfigurer方法配置自动扫描Mapper文件。

复制代码
 1 package com.lyyzoo.core.config;
 2 
 3 import org.springframework.context.annotation.Bean;
 4 import org.springframework.context.annotation.Configuration;
 5 
 6 import tk.mybatis.spring.mapper.MapperScannerConfigurer;
 7 
 8 /**
 9  * MyBatis相关配置.
10  *
11  * @version 1.0
12  * @author bojiangzhou 2018-01-07
13  */
14 @Configuration
15 public class MyBatisConfig {
16 
17     /**
18      * Mapper扫描配置. 自动扫描将Mapper接口生成代理注入到Spring.
19      */
20     @Bean
21     public static MapperScannerConfigurer mapperScannerConfigurer() {
22         MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
23         // 注意这里的扫描路径: 1.不要扫描到自定义的Mapper; 2.定义的路径不要扫描到tk.mybatis.mapper(如定义**.mapper).
24         // 两个做法都会导致扫描到tk.mybatis的Mapper,就会产生重复定义的报错.
25         mapperScannerConfigurer.setBasePackage("**.lyyzoo.**.mapper");
26         return mapperScannerConfigurer;
27     }
28 
29 }
复制代码

注意这里的 MapperScannerConfigurer 是tk.mybatis.spring.mapper.MapperScannerConfigurer,而不是org.mybatis,否则使用通用Mapper的方法时会报类似下面的这种错误

6、定义基础Service

一般来说,我们不能在Controller中直接访问Mapper,因此我们需要加上Service,通过Service访问Mapper。

首先定义基础Service<T>接口,根据Mapper定义基本的增删改查接口方法。

复制代码
  1 package com.lyyzoo.core.base;
  2 
  3 import java.util.List;
  4 
  5 /**
  6  * Service 基础通用接口
  7  *
  8  * @name BaseService
  9  * @version 1.0
 10  * @author bojiangzhou 2017-12-31
 11  */
 12 public interface Service<T> {
 13 
 14     //
 15     // insert
 16     // ----------------------------------------------------------------------------------------------------
 17     /**
 18      * 保存一个实体,null的属性也会保存,不会使用数据库默认值
 19      *
 20      * @param record
 21      * @return
 22      */
 23     T insert(T record);
 24 
 25     /**
 26      * 批量插入,null的属性也会保存,不会使用数据库默认值
 27      *
 28      * @param recordList
 29      * @return
 30      */
 31     List<T> insert(List<T> recordList);
 32 
 33     /**
 34      * 保存一个实体,null的属性不会保存,会使用数据库默认值
 35      *
 36      * @param record
 37      * @return
 38      */
 39     T insertSelective(T record);
 40 
 41     /**
 42      * 批量插入,null的属性不会保存,会使用数据库默认值
 43      *
 44      * @param recordList
 45      * @return
 46      */
 47     List<T> insertSelective(List<T> recordList);
 48 
 49     //
 50     // update
 51     // ----------------------------------------------------------------------------------------------------
 52     /**
 53      * 根据主键更新实体全部字段,null值会被更新
 54      *
 55      * @param record
 56      * @return
 57      */
 58     T update(T record);
 59 
 60     /**
 61      * 批量更新,根据主键更新实体全部字段,null值会被更新
 62      *
 63      * @param recordList
 64      * @return
 65      */
 66     List<T> update(List<T> recordList);
 67 
 68     /**
 69      * 根据主键更新属性不为null的值
 70      *
 71      * @param record
 72      * @return
 73      */
 74     T updateSelective(T record);
 75 
 76     /**
 77      * 批量更新,根据主键更新属性不为null的值
 78      *
 79      * @param recordList
 80      * @return
 81      */
 82     List<T> updateSelective(List<T> recordList);
 83 
 84     //
 85     // delete
 86     // ----------------------------------------------------------------------------------------------------
 87     /**
 88      * 根据主键删除
 89      *
 90      * @param id id不能为空
 91      * @return
 92      */
 93     int delete(Long id);
 94 
 95     /**
 96      * 根据主键字符串进行删除,类中只有存在一个带有@Id注解的字段
 97      *
 98      * @param ids 类似1,2,3
 99      */
100     int delete(String ids);
101 
102     /**
103      * 根据主键删除多个实体,ID数组
104      *
105      * @param ids 类似[1,2,3],不能为空
106      */
107     int delete(Long[] ids);
108 
109     /**
110      * 根据实体属性作为条件进行删除
111      *
112      * @param record
113      * @return
114      */
115     int delete(T record);
116 
117     /**
118      * 根据主键删除多个实体
119      *
120      * @param recordList
121      * @return
122      */
123     int delete(List<T> recordList);
124 
125     //
126     // insert or update or delete
127     // ----------------------------------------------------------------------------------------------------
128     /**
129      * 根据实体的operate决定哪种操作. null的属性也会保存,不会使用数据库默认值
130      *
131      * @param record
132      * @return
133      */
134     T persist(T record);
135 
136     /**
137      * 批量操作.根据实体的operate决定哪种操作. null的属性也会保存,不会使用数据库默认值
138      *
139      * @param recordList
140      * @return
141      */
142     List<T> persist(List<T> recordList);
143 
144     /**
145      * 根据实体的operate决定哪种操作. 根据主键更新属性不为null的值
146      *
147      * @param record
148      * @return
149      */
150     T persistSelective(T record);
151 
152     /**
153      * 批量操作.根据实体的operate决定哪种操作. 根据主键更新属性不为null的值
154      *
155      * @param recordList
156      * @return
157      */
158     List<T> persistSelective(List<T> recordList);
159 
160 
161     //
162     // select
163     // ----------------------------------------------------------------------------------------------------
164     /**
165      * 根据主键查询
166      *
167      * @param id 不能为空
168      * @return
169      */
170     T get(Long id);
171 
172     /**
173      * 根据实体中的属性进行查询,只能有一个返回值,有多个结果是抛出异常
174      *
175      * @param record
176      * @return
177      */
178     T get(T record);
179 
180     /**
181      * 根据字段和值查询 返回一个
182      * @param key 不能为空
183      * @param value 不能为空
184      * @return
185      */
186     T get(String key, Object value);
187 
188 
189     /**
190      * 根据主键字符串进行查询
191      *
192      * @param ids 如 "1,2,3,4"
193      * @return
194      */
195     List<T> select(String ids);
196 
197     /**
198      * 根据实体中的属性值进行查询
199      *
200      * @param record
201      * @return
202      */
203     List<T> select(T record);
204 
205     /**
206      * 根据属性和值查询
207      *
208      * @param key
209      * @param value
210      * @return
211      */
212     List<T> select(String key, Object value);
213 
214     /**
215      * 根据实体中的属性值进行分页查询
216      *
217      * @param record
218      * @param pageNum
219      * @param pageSize
220      * @return
221      */
222     List<T> select(T record, int pageNum, int pageSize);
223 
224     /**
225      * 查询全部结果
226      *
227      * @return
228      */
229     List<T> selectAll();
230 
231     /**
232      * 根据实体中的属性查询总数
233      *
234      * @param record
235      * @return
236      */
237     int count(T record);
238 
239 }
复制代码

然后是实现类BaseService,以后的开发中,Service接口实现Service<T>,Service实现类继承BaseService<T>。

复制代码
  1 package com.lyyzoo.core.base;
  2 
  3 import com.github.pagehelper.PageHelper;
  4 import com.lyyzoo.core.Constants;
  5 import com.lyyzoo.core.util.Arrays;
  6 import com.lyyzoo.core.util.Reflections;
  7 import org.springframework.beans.factory.annotation.Autowired;
  8 import org.springframework.transaction.annotation.Transactional;
  9 import org.springframework.util.Assert;
 10 
 11 import javax.annotation.PostConstruct;
 12 import javax.persistence.Id;
 13 import java.lang.reflect.Field;
 14 import java.util.List;
 15 
 16 /**
 17  * 基础Service实现类
 18  *
 19  * @name BaseServiceImpl
 20  * @version 1.0
 21  * @author bojiangzhou 2018-01-04
 22  */
 23 public abstract class BaseService<T> implements Service<T> {
 24 
 25     //@Qualifier("sunnyMapper")
 26     @Autowired
 27     private Mapper<T> mapper;
 28 
 29     private Class<T> entityClass;
 30 
 31     @SuppressWarnings("unchecked")
 32     @PostConstruct
 33     public void init() {
 34         this.entityClass = Reflections.getClassGenericType(getClass());
 35     }
 36 
 37     //
 38     // insert
 39     // ----------------------------------------------------------------------------------------------------
 40     @Transactional(rollbackFor = Exception.class)
 41     public T insert(T record) {
 42         mapper.insert(record);
 43         return record;
 44     }
 45 
 46     @Transactional(rollbackFor = Exception.class)
 47     public List<T> insert(List<T> recordList) {
 48         mapper.insertList(recordList);
 49         return recordList;
 50     }
 51 
 52     @Transactional(rollbackFor = Exception.class)
 53     public T insertSelective(T record) {
 54         mapper.insertSelective(record);
 55         return record;
 56     }
 57 
 58     @Transactional(rollbackFor = Exception.class)
 59     public List<T> insertSelective(List<T> recordList) {
 60         // 由于Mapper暂未提供Selective的批量插入,此处循环查询. 当然也可参考InsertListMapper自己实现.
 61         for(T record : recordList){
 62             mapper.insertSelective(record);
 63         }
 64         return recordList;
 65     }
 66 
 67     //
 68     // update
 69     // ----------------------------------------------------------------------------------------------------
 70     @Transactional(rollbackFor = Exception.class)
 71     public T update(T record) {
 72         mapper.updateByPrimaryKey(record);
 73         return record;
 74     }
 75 
 76     @Transactional(rollbackFor = Exception.class)
 77     public List<T> update(List<T> recordList) {
 78         // Mapper暂未提供批量更新,此处循实现
 79         for(T record : recordList){
 80             mapper.updateByPrimaryKey(record);
 81         }
 82         return recordList;
 83     }
 84 
 85     @Transactional(rollbackFor = Exception.class)
 86     public T updateSelective(T record) {
 87         mapper.updateByPrimaryKeySelective(record);
 88         return record;
 89     }
 90 
 91     @Transactional(rollbackFor = Exception.class)
 92     public List<T> updateSelective(List<T> recordList) {
 93         // Mapper暂未提供批量更新,此处循实现
 94         for(T record : recordList){
 95             mapper.updateByPrimaryKeySelective(record);
 96         }
 97         return recordList;
 98     }
 99 
100     //
101     // delete
102     // ----------------------------------------------------------------------------------------------------
103     @Transactional(rollbackFor = Exception.class)
104     public int delete(Long id) {
105         return mapper.deleteByIds(id.toString());
106     }
107 
108     @Transactional(rollbackFor = Exception.class)
109     public int delete(String ids) {
110         return mapper.deleteByIds(ids);
111     }
112 
113     @Transactional(rollbackFor = Exception.class)
114     public int delete(Long[] ids) {
115         return mapper.deleteByIds(Arrays.join(ids));
116     }
117 
118     @Transactional(rollbackFor = Exception.class)
119     public int delete(T record) {
120         return mapper.delete(record);
121     }
122 
123     @Transactional(rollbackFor = Exception.class)
124     public int delete(List<T> recordList) {
125         int count = 0;
126         for(T record : recordList){
127             mapper.delete(record);
128             count++;
129         }
130         return count;
131     }
132 
133     //
134     // all operate. insert or update or delete
135     // ----------------------------------------------------------------------------------------------------
136     @Transactional(rollbackFor = Exception.class)
137     public T persist(T record) {
138         BaseDTO dto = (BaseDTO) record;
139         switch (dto.get_operate()) {
140             case Constants.Operation.ADD:
141                 insert(record);
142                 break;
143             case Constants.Operation.UPDATE:
144                 update(record);
145                 break;
146             case Constants.Operation.DELETE:
147                 delete(record);
148                 break;
149             default:
150                 break;
151         }
152         dto.set_operate(null);
153         return record;
154     }
155 
156     @Transactional(rollbackFor = Exception.class)
157     public List<T> persist(List<T> recordList) {
158         for(T record : recordList){
159             BaseDTO dto = (BaseDTO) record;
160             switch (dto.get_operate()) {
161                 case Constants.Operation.ADD:
162                     insert(record);
163                     break;
164                 case Constants.Operation.UPDATE:
165                     update(record);
166                     break;
167                 case Constants.Operation.DELETE:
168                     delete(record);
169                     break;
170                 default:
171                     break;
172             }
173             dto.set_operate(null);
174         }
175         return recordList;
176     }
177 
178     @Transactional(rollbackFor = Exception.class)
179     public T persistSelective(T record) {
180         BaseDTO dto = (BaseDTO) record;
181         switch (dto.get_operate()) {
182             case Constants.Operation.ADD:
183                 insertSelective(record);
184                 break;
185             case Constants.Operation.UPDATE:
186                 updateSelective(record);
187                 break;
188             case Constants.Operation.DELETE:
189                 delete(record);
190                 break;
191             default:
192                 break;
193         }
194         dto.set_operate(null);
195         return record;
196     }
197 
198     @Transactional(rollbackFor = Exception.class)
199     public List<T> persistSelective(List<T> recordList) {
200         for(T record : recordList){
201             BaseDTO dto = (BaseDTO) record;
202             switch (dto.get_operate()) {
203                 case Constants.Operation.ADD:
204                     insertSelective(record);
205                     break;
206                 case Constants.Operation.UPDATE:
207                     updateSelective(record);
208                     break;
209                 case Constants.Operation.DELETE:
210                     delete(record);
211                     break;
212                 default:
213                     break;
214             }
215             dto.set_operate(null);
216         }
217         return recordList;
218     }
219 
220     //
221     // select
222     // ----------------------------------------------------------------------------------------------------
223     public T get(Long id) {
224         T entity = null;
225         try {
226             entity = entityClass.newInstance();
227             Field idField = Reflections.getFieldByAnnotation(entityClass, Id.class);
228             idField.set(entity, id);
229         } catch (Exception e) {
230             e.printStackTrace();
231         }
232 
233         return mapper.selectByPrimaryKey(entity);
234     }
235 
236     public T get(T record) {
237         return mapper.selectOne(record);
238     }
239 
240     public T get(String key, Object value) {
241         T entity = null;
242         try {
243             entity = entityClass.newInstance();
244             Field field = Reflections.getField(entityClass, key);
245             field.set(entity, value);
246         } catch (Exception e) {
247             e.printStackTrace();
248         }
249 
250         return mapper.selectOne(entity);
251     }
252 
253     public List<T> select(String ids) {
254         return mapper.selectByIds(ids);
255     }
256 
257     public List<T> select(T record) {
258 
259         return mapper.select(record);
260     }
261 
262     public List<T> select(String key, Object value) {
263         T entity = null;
264         try {
265             entity = entityClass.newInstance();
266             Field field = Reflections.getField(entityClass, key);
267             field.set(entity, value);
268         } catch (Exception e) {
269             e.printStackTrace();
270         }
271         return mapper.select(entity);
272     }
273 
274     public List<T> select(T record, int pageNum, int pageSize) {
275         PageHelper.startPage(pageNum, pageSize);
276         return mapper.select(record);
277     }
278 
279     public List<T> selectAll() {
280         return mapper.selectAll();
281     }
282 
283     public int count(T record) {
284         return mapper.selectCount(record);
285     }
286 
287 }
复制代码

BaseService的实现用到了反射工具类Reflections:

复制代码
  1 package com.lyyzoo.core.util;
  2 
  3 import org.slf4j.Logger;
  4 import org.slf4j.LoggerFactory;
  5 
  6 import java.lang.reflect.Field;
  7 import java.lang.reflect.Modifier;
  8 import java.lang.reflect.ParameterizedType;
  9 import java.lang.reflect.Type;
 10 
 11 /**
 12  * 反射工具类.
 13  *
 14  * @version 1.0
 15  * @author bojiangzhou 2018-01-06
 16  */
 17 
 18 public abstract class Reflections {
 19 
 20     private static Logger logger = LoggerFactory.getLogger(Reflections.class);
 21 
 22     /**
 23      * 通过反射, 获得Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处. 如无法找到, 返回Object.class.
 24      *
 25      * @param clazz class类
 26      *
 27      * @return the 返回第一个声明的泛型类型. 如果没有,则返回Object.class
 28      */
 29     @SuppressWarnings("unchecked")
 30     public static Class getClassGenericType(final Class clazz) {
 31         return getClassGenericType(clazz, 0);
 32     }
 33 
 34     /**
 35      * 通过反射, 获得Class定义中声明的父类的泛型参数的类型. 如无法找到, 返回Object.class.
 36      *
 37      * @param clazz class类
 38      *
 39      * @param index 获取第几个泛型参数的类型,默认从0开始,即第一个
 40      *
 41      * @return 返回第index个泛型参数类型.
 42      */
 43     public static Class getClassGenericType(final Class clazz, final int index) {
 44         Type genType = clazz.getGenericSuperclass();
 45 
 46         if (!(genType instanceof ParameterizedType)) {
 47             return Object.class;
 48         }
 49 
 50         Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
 51 
 52         if (index >= params.length || index < 0) {
 53             logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + params.length);
 54             return Object.class;
 55         }
 56         if (!(params[index] instanceof Class)) {
 57             logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
 58             return Object.class;
 59         }
 60 
 61         return (Class) params[index];
 62     }
 63 
 64     /**
 65      * 根据注解类型获取实体的Field
 66      *
 67      * @param entityClass 实体类型
 68      * 
 69      * @param annotationClass 注解类型
 70      *
 71      * @return 返回第一个有该注解类型的Field,如果没有则返回null.
 72      */
 73     @SuppressWarnings("unchecked")
 74     public static Field getFieldByAnnotation(Class entityClass, Class annotationClass) {
 75         Field[] fields = entityClass.getDeclaredFields();
 76         for (Field field : fields) {
 77             if (field.getAnnotation(annotationClass) != null) {
 78                 makeAccessible(field);
 79                 return field;
 80             }
 81         }
 82         return null;
 83     }
 84 
 85     /**
 86      * 获取实体的字段
 87      *
 88      * @param entityClass 实体类型
 89      *
 90      * @param fieldName 字段名称
 91      *
 92      * @return 该字段名称对应的字段,如果没有则返回null.
 93      */
 94     public static Field getField(Class entityClass, String fieldName){
 95         try {
 96             Field field = entityClass.getDeclaredField(fieldName);
 97             makeAccessible(field);
 98             return field;
 99         } catch (NoSuchFieldException e) {
100             e.printStackTrace();
101         }
102         return null;
103     }
104 
105 
106     /**
107      * 改变private/protected的成员变量为public.
108      */
109     public static void makeAccessible(Field field) {
110         if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
111             field.setAccessible(true);
112         }
113     }
114 
115 }
复制代码

7、数据持久化测试

① 实体映射

实体类按照如下规则和数据库表进行转换,注解全部是JPA中的注解:

  • 表名默认使用类名,驼峰转下划线(只对大写字母进行处理),如UserInfo默认对应的表名为user_info

  • 表名可以使@Table(name = "tableName")进行指定,对不符合第一条默认规则的可以通过这种方式指定表名。

  • 字段默认和@Column一样,都会作为表字段,表字段默认为Java对象的Field名字驼峰转下划线形式。

  • 可以使用@Column(name = "fieldName")指定不符合第3条规则的字段名。

  • 使用@Transient注解可以忽略字段,添加该注解的字段不会作为表字段使用,注意,如果没有与表关联,一定要用@Transient标注。

  • 建议一定是有一个@Id注解作为主键的字段,可以有多个@Id注解的字段作为联合主键。

  • 默认情况下,实体类中如果不存在包含@Id注解的字段,所有的字段都会作为主键字段进行使用(这种效率极低)。

  • 由于基本类型,如int作为实体类字段时会有默认值0,而且无法消除,所以实体类中建议不要使用基本类型。

User实体主要加了@Table注解,映射表名;然后在userId上标注主键注解;其它字段如果没加@Transient注解的默认都会作为表字段。

复制代码
  1 package com.lyyzoo.core.system.dto;
  2 
  3 import com.fasterxml.jackson.annotation.JsonFormat;
  4 import com.fasterxml.jackson.annotation.JsonInclude;
  5 import com.lyyzoo.core.base.BaseDTO;
  6 import com.lyyzoo.core.util.Dates;
  7 
  8 import javax.persistence.*;
  9 import java.util.Date;
 10 import java.util.List;
 11 
 12 /**
 13  * 系统用户
 14  *
 15  * @name User
 16  * @version 1.0
 17  * @author bojiangzhou 2017-12-31
 18  */
 19 @JsonInclude(JsonInclude.Include.NON_NULL)
 20 @Table(name = "SYS_USER")
 21 public class User extends BaseDTO {
 22     private static final long serialVersionUID = -7395431342743009038L;
 23 
 24     /**
 25      * 用户ID
 26      */
 27     @Id
 28     @GeneratedValue(strategy = GenerationType.IDENTITY)
 29     @OrderBy("DESC")
 30     private Long userId;
 31     /**
 32      * 用户名
 33      */
 34     private String username;
 35     /**
 36      * 密码
 37      */
 38     private String password;
 39     /**
 40      * 昵称
 41      */
 42     private String nickname;
 43     /**
 44      * 生日
 45      */
 46     @JsonFormat(pattern = Dates.Pattern.DATE)
 47     private Date birthday;
 48     /**
 49      * 性别:1-男/0-女
 50      */
 51     private Integer sex;
 52     /**
 53      * 是否启用:1/0
 54      */
 55     private Integer enabled;
 56 
 57 
 58     public Long getUserId() {
 59         return userId;
 60     }
 61 
 62     public void setUserId(Long userId) {
 63         this.userId = userId;
 64     }
 65 
 66     public String getUsername() {
 67         return username;
 68     }
 69 
 70     public void setUsername(String username) {
 71         this.username = username;
 72     }
 73 
 74     public String getPassword() {
 75         return password;
 76     }
 77 
 78     public void setPassword(String password) {
 79         this.password = password;
 80     }
 81 
 82     public String getNickname() {
 83         return nickname;
 84     }
 85 
 86     public void setNickname(String nickname) {
 87         this.nickname = nickname;
 88     }
 89 
 90     public Date getBirthday() {
 91         return birthday;
 92     }
 93 
 94     public void setBirthday(Date birthday) {
 95         this.birthday = birthday;
 96     }
 97 
 98     public Integer getSex() {
 99         return sex;
100     }
101 
102     public void setSex(Integer sex) {
103         this.sex = sex;
104     }
105 
106     public Integer getEnabled() {
107         return enabled;
108     }
109 
110     public void setEnabled(Integer enabled) {
111         this.enabled = enabled;
112     }
113 
114 }
复制代码

② 创建表结构

复制代码
 1 CREATE TABLE `sys_user` (
 2   `USER_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '表ID,主键,供其他表做外键',
 3   `USERNAME` varchar(30) NOT NULL COMMENT '用户名',
 4   `PASSWORD` varchar(100) NOT NULL COMMENT '密码',
 5   `NICKNAME` varchar(30) NOT NULL COMMENT '用户名称',
 6   `BIRTHDAY` date DEFAULT NULL COMMENT '生日',
 7   `SEX` int(1) DEFAULT NULL COMMENT '性别:1-男;0-女',
 8   `ENABLED` int(1) NOT NULL DEFAULT '1' COMMENT '启用标识:1/0',
 9   `VERSION_NUMBER` int(11) NOT NULL DEFAULT '1' COMMENT '行版本号,用来处理锁',
10   `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
11   `CREATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '创建人',
12   `UPDATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '更新人',
13   `UPDATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
14   `ATTRIBUTE1` varchar(150) DEFAULT NULL,
15   `ATTRIBUTE2` varchar(150) DEFAULT NULL,
16   `ATTRIBUTE3` varchar(150) DEFAULT NULL,
17   `ATTRIBUTE4` varchar(150) DEFAULT NULL,
18   `ATTRIBUTE5` varchar(150) DEFAULT NULL,
19   `ATTRIBUTE6` varchar(150) DEFAULT NULL,
20   `ATTRIBUTE7` varchar(150) DEFAULT NULL,
21   `ATTRIBUTE8` varchar(150) DEFAULT NULL,
22   `ATTRIBUTE9` varchar(150) DEFAULT NULL,
23   `ATTRIBUTE10` varchar(150) DEFAULT NULL,
24   PRIMARY KEY (`USER_ID`),
25   UNIQUE KEY `USERNAME` (`USERNAME`)
26 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='系统用户';
复制代码

③ 创建UserMapper

在system.mapper下创建UserMapper接口,继承Mapper<User>:

复制代码
 1 package com.lyyzoo.core.system.mapper;
 2 
 3 import com.lyyzoo.core.base.Mapper;
 4 import com.lyyzoo.core.system.dto.User;
 5 
 6 /**
 7  *
 8  * @name UserMapper
 9  * @version 1.0
10  * @author bojiangzhou 2018-01-06
11  */
12 public interface UserMapper extends Mapper<User> {
13 
14 }
复制代码

④ 创建UserService

在system.service下创建UserService接口,只需继承Service<User>接口即可。

复制代码
 1 package com.lyyzoo.core.system.service;
 2 
 3 import com.lyyzoo.core.base.Service;
 4 import com.lyyzoo.core.system.dto.User;
 5 
 6 /**
 7  * 用户Service接口
 8  *
 9  * @version 1.0
10  * @author bojiangzhou 2018-01-06
11  */
12 public interface UserService extends Service<User> {
13 
14 }
复制代码

在system.service.impl下创建UserServiceImpl实现类,继承BaseService<User>类,实现UserService接口。同时加上@Service注解。

复制代码
 1 package com.lyyzoo.core.system.service.impl;
 2 
 3 import org.springframework.stereotype.Service;
 4 
 5 import com.lyyzoo.core.base.BaseService;
 6 import com.lyyzoo.core.system.dto.User;
 7 import com.lyyzoo.core.system.service.UserService;
 8 
 9 /**
10  * 用户Service实现类
11  *
12  * @version 1.0
13  * @author bojiangzhou 2018-01-06
14  */
15 @Service
16 public class UserServiceImpl extends BaseService<User> implements UserService {
17 
18 }
复制代码

⑤ 修改UserController,注入UserService,增加一些测试API

复制代码
 1 package com.lyyzoo.core.system.controller;
 2 
 3 import com.lyyzoo.core.base.BaseController;
 4 import com.lyyzoo.core.base.BaseEnums;
 5 import com.lyyzoo.core.base.Result;
 6 import com.lyyzoo.core.system.dto.User;
 7 import com.lyyzoo.core.system.service.UserService;
 8 import com.lyyzoo.core.util.Results;
 9 import org.springframework.beans.factory.annotation.Autowired;
10 import org.springframework.web.bind.annotation.*;
11 
12 import javax.validation.Valid;
13 import java.util.List;
14 
15 /**
16  * 用户Controller
17  *
18  * @version 1.0
19  * @author bojiangzhou 2017-12-31
20  */
21 @RequestMapping
22 @RestController
23 public class UserController extends BaseController {
24 
25     @Autowired
26     private UserService userService;
27 
28 
29     @PostMapping("/sys/user/queryAll")
30     public Result queryAll(){
31         List<User> list = userService.selectAll();
32         return Results.successWithData(list, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description());
33     }
34 
35     @RequestMapping("/sys/user/queryOne/{userId}")
36     public Result queryOne(@PathVariable Long userId){
37         User user = userService.get(userId);
38         return Results.successWithData(user);
39     }
40 
41     @PostMapping("/sys/user/save")
42     public Result save(@Valid @RequestBody User user){
43         user = userService.insertSelective(user);
44         return Results.successWithData(user);
45     }
46 
47     @PostMapping("/sys/user/update")
48     public Result update(@Valid @RequestBody List<User> user){
49         user = userService.persistSelective(user);
50         return Results.successWithData(user);
51     }
52 
53     @RequestMapping("/sys/user/delete")
54     public Result delete(User user){
55         userService.delete(user);
56         return Results.success();
57     }
58 
59     @RequestMapping("/sys/user/delete/{userId}")
60     public Result delete(@PathVariable Long userId){
61         userService.delete(userId);
62         return Results.success();
63     }
64 
65 }
复制代码

⑥ 测试结果

查询所有:

批量保存/修改:

8、代码生成器

使用代码生成器来生成基础的代码结构,生成DTO、XML等等。

MyBatis官方提供了代码生成器MyBatis Generator,但一般需要定制化。MyBatis Generator

我这里从网上找了一个使用起来比较方便的界面工具,可生成DTO、Mapper、Mapper.xml,生成之后还需做一些小调整。另需要自己创建对应的Service、Controller。之后有时间再重新定制化一个符合本项目的代码生成器。

mybatis-generator界面工具

 

四、日志及全局异常处理

在前面的测试中,会发现控制台输出的日志不怎么友好,有很多日志也没有输出,不便于查找排查问题。对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。

先贴出一些参考资料:

  logback 配置详解

  日志组件slf4j介绍及配置详解

  Java常用日志框架介绍

1、日志框架简介

Java有很多常用的日志框架,如Log4j、Log4j 2、Commons Logging、Slf4j、Logback等。有时候你可能会感觉有点混乱,下面简单介绍下。

  • Log4j:Apache Log4j是一个基于Java的日志记录工具,是Apache软件基金会的一个项目。

  • Log4j 2:Apache Log4j 2是apache开发的一款Log4j的升级产品。

  • Commons Logging:Apache基金会所属的项目,是一套Java日志接口。

  • Slf4j:类似于Commons Logging,是一套简易Java日志门面,本身并无日志的实现。(Simple Logging Facade for Java,缩写Slf4j)。

  • Logback:一套日志组件的实现(slf4j阵营)。

Commons Logging和Slf4j是日志门面,提供一个统一的高层接口,为各种loging API提供一个简单统一的接口。log4j和Logback则是具体的日志实现方案。可以简单的理解为接口与接口的实现,调用者只需要关注接口而无需关注具体的实现,做到解耦。

比较常用的组合使用方式是Slf4j与Logback组合使用,Commons Logging与Log4j组合使用。

基于下面的一些优点,选用Slf4j+Logback的日志框架:

  • 更快的执行速度,Logback重写了内部的实现,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了

  • 自动清除旧的日志归档文件,通过设置TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 属性,你就可以控制日志归档文件的最大数量

  • Logback拥有远比log4j更丰富的过滤能力,可以不用降低日志级别而记录低级别中的日志。

  • Logback必须配合Slf4j使用。由于Logback和Slf4j是同一个作者,其兼容性不言而喻。

  • 默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台。

2、配置日志

可以看到,只要集成了spring-boot-starter-web,就引入了spring-boot-starter-logging,即slf4j和logback。

其它的几个包:jcl-over-slf4j,代码直接调用common-logging会被桥接到slf4j;jul-to-slf4j,代码直接调用java.util.logging会被桥接到slf4j;log4j-over-slf4j,代码直接调用log4j会被桥接到slf4j。

还需引入janino,如果不加入这个包会报错。

在resources下添加logback.xml配置文件,Logback默认会查找classpath下的logback.xml文件。

具体配置如下,有较详细的注释,很容易看懂。可以通过application.properties配置日志记录级别、日志输出文件目录等。

复制代码
 1 <?xml version="1.0" encoding="UTF-8"?>
 2 
 3 <!-- 级别从高到低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL -->
 4 <!-- 日志输出规则 根据当前ROOT 级别,日志输出时,级别高于root默认的级别时 会输出 -->
 5 <!-- 以下 每个配置的 filter 是过滤掉输出文件里面,会出现高级别文件,依然出现低级别的日志信息,通过filter 过滤只记录本级别的日志 -->
 6 <!-- scan 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。 -->
 7 <!-- scanPeriod 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
 8 <!-- debug 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
 9 <configuration debug="false" scan="false" scanPeriod="5 minutes">
10 
11     <!-- 引入配置文件 -->
12     <property resource="application.properties"/>
13     <property resource="application-${app.env:-dev}.properties"/>
14 
15     <property name="app.name" value="${app.name:-sunny}"/>
16     <property name="app.env" value="${app.env:-dev}"/>
17 
18     <!-- 日志记录级别 -->
19     <property name="logback_level" value="${logback.level:-DEBUG}"/>
20     <!-- 是否输出日志到文件 -->
21     <property name="logback_rolling" value="${logback.rolling:-false}"/>
22     <!-- 设置日志输出目录 -->
23     <property name="logback_rolling_path" value="${logback.rolling.path:-/data/logs}"/>
24     <!-- 日志文件最大大小 -->
25     <property name="logback_max_file_size" value="${logback.max_file_size:-10MB}"/>
26     <!-- 格式化输出:%d:表示日期,%thread:表示线程名,%-5level:级别从左显示5个字符宽度,%logger:日志输出者的名字(通常是所在类的全名),%L:输出代码中的行号,%msg:日志消息,%n:换行符 -->
27     <property name="logback_pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger %L  -| %msg%n"/>
28 
29 
30     <if condition='p("logback_rolling").equals("true")'>
31         <then>
32             <!-- 滚动记录文件 -->
33             <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
34                 <file>${logback_rolling_path}/${app.name}.log</file>
35                 <!-- rollingPolicy:当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名 -->
36                 <!-- TimeBasedRollingPolicy:最常用的滚动策略,它根据时间来制定滚动策略 -->
37                 <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
38                     <!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
39                     <fileNamePattern>${logback_rolling_path}/${app.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
40 
41                     <!-- 日志文件的保存期限为30天 -->
42                     <maxHistory>30</maxHistory>
43 
44                     <timeBasedFileNamingAndTriggeringPolicy  class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
45                         <!-- maxFileSize:这是活动文件的大小,默认值是10MB -->
46                         <maxFileSize>${logback_max_file_size}</maxFileSize>
47                     </timeBasedFileNamingAndTriggeringPolicy>
48                 </rollingPolicy>
49                 <encoder>
50                     <pattern>${logback_pattern}</pattern>
51                     <charset>UTF-8</charset>
52                 </encoder>
53             </appender>
54 
55             <root>
56                 <appender-ref ref="FILE"/>
57             </root>
58         </then>
59     </if>
60 
61 
62     <!-- 将日志打印到控制台 -->
63     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
64         <encoder>
65             <pattern>${logback_pattern}</pattern>
66         </encoder>
67     </appender>
68 
69     <root level="${logback_level}">
70         <appender-ref ref="CONSOLE"/>
71     </root>
72 
73     <contextName>${app.name}</contextName>
74 
75 </configuration>
复制代码

加入配置文件后,就可以看到控制台格式化后的日志输出,还可以看到具体代码行数等,比之前的友好多了。

同时,将日志滚动输出到日志文件,保留历史记录。可通过logback.rolling=false控制是否需要输出日志到文件。

3、使用Logger

配置好之后,就可以使用Logger来输出日志了,使用起来也是非常方便。

* 可以看到引入的包是slf4j.Logger,代码里并没有引用任何一个跟 Logback 相关的类,这便是使用 Slf4j的好处,在需要将日志框架切换为其它日志框架时,无需改动已有的代码。

* LoggerFactory 的 getLogger() 方法接收一个参数,以这个参数决定 logger 的名字,比如第二图中的日志输出。在为 logger 命名时,用类的全限定类名作为 logger name 是最好的策略,这样能够追踪到每一条日志消息的来源

* 可以看到,可以通过提供占位符,以参数化的方式打印日志,避免字符串拼接的不必要损耗,也无需通过logger.isDebugEnabled()这种方式判断是否需要打印。

4、全局异常处理

现在有一个问题,当日志级别设置到INFO级别后,只会输出INFO以上的日志,如INFO、WARN、ERROR,这没毛病,问题是,程序中抛出的异常堆栈(运行时异常)都没有打印了,不利于排查问题。

而且,在某些情况下,我们在Service中想直接把异常往Controller抛出不做处理,但我们不能直接把异常信息输出到客户端,这是非常不友好的。

所以,在config下建一个GlobalExceptionConfig作为全局统一异常处理。主要处理了自定义的ServiceException、AuthorityException、BaseException,以及系统的NoHandlerFoundException和Exception异常。

复制代码
 1 package com.lyyzoo.core.config;
 2 
 3 import org.slf4j.Logger;
 4 import org.slf4j.LoggerFactory;
 5 import org.springframework.http.HttpStatus;
 6 import org.springframework.web.bind.annotation.ExceptionHandler;
 7 import org.springframework.web.bind.annotation.RestControllerAdvice;
 8 import org.springframework.web.servlet.NoHandlerFoundException;
 9 
10 import com.lyyzoo.core.base.Result;
11 import com.lyyzoo.core.constants.BaseEnums;
12 import com.lyyzoo.core.exception.AuthorityException;
13 import com.lyyzoo.core.exception.BaseException;
14 import com.lyyzoo.core.exception.ServiceException;
15 import com.lyyzoo.core.util.Results;
16 
17 /**
18  * 全局异常处理
19  *
20  * @author bojiangzhou 2018-02-06
21  * @version 1.0
22  */
23 @RestControllerAdvice
24 public class GlobalExceptionConfig {
25 
26     private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionConfig.class);
27 
28     /**
29      * 处理 ServiceException 异常
30      */
31     @ExceptionHandler(ServiceException.class)
32     public Result handleServiceException(ServiceException e){
33         Result result = Results.failure(e.getCode(), e.getMessage());
34         result.setStatus(HttpStatus.BAD_REQUEST.value());
35         logger.info("ServiceException[code: {}, message: {}]", e.getCode(), e.getMessage());
36         return result;
37     }
38 
39     /**
40      * 处理 AuthorityException 异常
41      */
42     @ExceptionHandler(AuthorityException.class)
43     public Result handleAuthorityException(AuthorityException e){
44         Result result = Results.failure(BaseEnums.FORBIDDEN.code(), BaseEnums.FORBIDDEN.desc());
45         result.setStatus(HttpStatus.FORBIDDEN.value());
46         logger.info("AuthorityException[code: {}, message: {}]", e.getCode(), e.getMessage());
47         return result;
48     }
49 
50     /**
51      * 处理 NoHandlerFoundException 异常. <br/>
52      * 需配置 [spring.mvc.throw-exception-if-no-handler-found=true]
53      * 需配置 [spring.resources.add-mappings=false]
54      */
55     @ExceptionHandler(NoHandlerFoundException.class)
56     public Result handleNotFoundException(NoHandlerFoundException e){
57         Result result = Results.failure(BaseEnums.NOT_FOUND.code(), BaseEnums.NOT_FOUND.desc());
58         result.setStatus(HttpStatus.NOT_FOUND.value());
59         logger.info(e.getMessage());
60         return result;
61     }
62 
63     /**
64      * 处理 BaseException 异常
65      */
66     @ExceptionHandler(BaseException.class)
67     public Result handleBaseException(BaseException e){
68         Result result = Results.failure(e.getCode(), e.getMessage());
69         result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
70         logger.error("BaseException[code: {}, message: {}]", e.getCode(), e.getMessage(), e);
71         return result;
72     }
73 
74     /**
75      * 处理 Exception 异常
76      */
77     @ExceptionHandler(Exception.class)
78     public Result handleException(Exception e){
79         Result result = Results.failure(BaseEnums.ERROR.code(), BaseEnums.ERROR.desc());
80         result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
81         logger.error(e.getMessage(), e);
82         return result;
83     }
84 
85 }
复制代码

看上面的代码,@ControllAdvice(@RestControllerAdvice可以返回ResponseBody),可看做Controller增强器,可以在@ControllerAdvice作用类下添加@ExceptionHandler,@InitBinder,@ModelAttribute注解的方法来增强Controller,都会作用在被 @RequestMapping 注解的方法上。

使用@ExceptionHandler 拦截异常,我们可以通过该注解实现自定义异常处理。在每个处理方法中,封装Result,返回对应的消息及状态码等。

通过Logger打印对应级别的日志,也可以看到控制台及日志文件中有异常堆栈的输出了。注意除了BaseException、Exception,其它的都只是打印了简单信息,且为INFO级别。Exception是ERROR级别,且打印了堆栈信息。

NoHandlerFoundException 是404异常,这里注意要先关闭DispatcherServlet的NotFound默认异常处理。

测试如下:这种返回结果就比较友好了。

    

 

五、数据库乐观锁

1、乐观锁

在并发修改同一条记录时,为避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存层加锁,要么在数据库层使用乐观锁,使用version作为更新依据【强制】。 —— 《阿里巴巴Java开发手册》

乐观锁,基于数据版本(version)记录机制实现,为数据库表增加一个"version"字段。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。提交数据时,提交的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

因此,这节就来处理BaseDTO中的"version"字段,通过增加一个mybatis插件来实现更新时版本号自动+1。

2、MyBatis插件介绍

MyBatis 允许在己映射语句执行过程中的某一点进行拦截调用。默认情况下, MyBatis 允许使用插件来拦截的接口和方法包括以下几个:

  • Executor (update 、query 、flushStatements 、commit 、rollback getTransaction 、close 、isClosed)

  • ParameterHandler (getParameterObject 、setParameters)

  • ResultSetHandler (handleResul tSets 、handleCursorResultSets、handleOutputParameters)

  • StatementHandler (prepare 、parameterize 、batch update 、query) 

MyBatis 插件实现拦截器接口Interceptor,在实现类中对拦截对象和方法进行处理 。 

  • setProperties:传递插件的参数,可以通过参数来改变插件的行为。

  • plugin:参数 target 就是要拦截的对象,作用就是给被拦截对象生成一个代理对象,并返回。

  • intercept:会覆盖所拦截对象的原方法,Invocation参数可以反射调度原来对象的方法,可以获取到很多有用的东西。

除了需要实现拦截器接口外还需要给实现类配置拦截器签名。 使用 @Intercepts 和 @Signature 这两个注解来配置拦截器要拦截的接口的方法,接口方法对应的签名基本都是固定的。

@Intercepts 注解的属性是一个 @Signature  数组,可以在同 个拦截器中同时拦截不同的接口和方法。

@Signature 注解包含以下三个属性。

  • type:设置拦截接口,可选值是前面提到的4个接口 

  • method:设置拦截接口中的方法名, 可选值是前面4个接口对应的方法,需要和接口匹配 

  • args:设置拦截方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法 。

3、数据版本插件

要实现版本号自动更新,我们需要在SQL被执行前修改SQL,因此我们需要拦截的就是 StatementHandler  接口的 prepare 方法,该方法会在数据库执行前被调用,优先于当前接口的其它方法而被执行。

在 core.plugin 包下新建一个VersionPlugin插件,实现Interceptor拦截器接口。

该接口方法签名如下:

在 interceptor 方法中对 UPDATE 类型的操作,修改原SQL,加入version,修改后的SQL类似下图,更新时就会自动将version+1。同时带上version条件,如果该版本号小于数据库记录版本号,则不会更新。

VersionInterceptor插件:

复制代码
  1 package com.lyyzoo.core.plugins;
  2 
  3 import net.sf.jsqlparser.expression.Expression;
  4 import net.sf.jsqlparser.expression.LongValue;
  5 import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
  6 import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
  7 import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
  8 import net.sf.jsqlparser.parser.CCJSqlParserUtil;
  9 import net.sf.jsqlparser.schema.Column;
 10 import net.sf.jsqlparser.statement.Statement;
 11 import net.sf.jsqlparser.statement.update.Update;
 12 import org.apache.ibatis.executor.statement.StatementHandler;
 13 import org.apache.ibatis.mapping.BoundSql;
 14 import org.apache.ibatis.mapping.MappedStatement;
 15 import org.apache.ibatis.mapping.SqlCommandType;
 16 import org.apache.ibatis.plugin.*;
 17 import org.apache.ibatis.reflection.MetaObject;
 18 import org.apache.ibatis.reflection.SystemMetaObject;
 19 import org.slf4j.Logger;
 20 import org.slf4j.LoggerFactory;
 21 
 22 import java.lang.reflect.Proxy;
 23 import java.sql.Connection;
 24 import java.util.List;
 25 import java.util.Properties;
 26 
 27 /**
 28  * 乐观锁:数据版本插件
 29  *
 30  * @version 1.0
 31  * @author bojiangzhou 2018-02-10
 32  */
 33 @Intercepts(
 34     @Signature(
 35         type = StatementHandler.class,
 36         method = "prepare",
 37         args = {Connection.class, Integer.class}
 38     )
 39 )
 40 public class VersionInterceptor implements Interceptor {
 41 
 42     private static final String VERSION_COLUMN_NAME = "version";
 43 
 44     private static final Logger logger = LoggerFactory.getLogger(VersionInterceptor.class);
 45 
 46     @Override
 47     public Object intercept(Invocation invocation) throws Throwable {
 48         // 获取 StatementHandler,实际是 RoutingStatementHandler
 49         StatementHandler handler = (StatementHandler) processTarget(invocation.getTarget());
 50         // 包装原始对象,便于获取和设置属性
 51         MetaObject metaObject = SystemMetaObject.forObject(handler);
 52         // MappedStatement 是对SQL更高层次的一个封装,这个对象包含了执行SQL所需的各种配置信息
 53         MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
 54         // SQL类型
 55         SqlCommandType sqlType = ms.getSqlCommandType();
 56         if(sqlType != SqlCommandType.UPDATE) {
 57             return invocation.proceed();
 58         }
 59         // 获取版本号
 60         Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_COLUMN_NAME);
 61         if(originalVersion == null || Long.valueOf(originalVersion.toString()) <= 0){
 62             return invocation.proceed();
 63         }
 64         // 获取绑定的SQL
 65         BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
 66         // 原始SQL
 67         String originalSql = boundSql.getSql();
 68         // 加入version的SQL
 69         originalSql = addVersionToSql(originalSql, originalVersion);
 70         // 修改 BoundSql
 71         metaObject.setValue("delegate.boundSql.sql", originalSql);
 72 
 73         // proceed() 可以执行被拦截对象真正的方法,该方法实际上执行了method.invoke(target, args)方法
 74         return invocation.proceed();
 75     }
 76 
 77     /**
 78      * Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象.
 79      *
 80      * @param target 被拦截的对象
 81      * @return 代理对象
 82      */
 83     @Override
 84     public Object plugin(Object target) {
 85         return Plugin.wrap(target, this);
 86     }
 87 
 88     /**
 89      * 设置参数
 90      */
 91     @Override
 92     public void setProperties(Properties properties) {
 93 
 94     }
 95 
 96     /**
 97      * 获取代理的原始对象
 98      *
 99      * @param target
100      * @return
101      */
102     private static Object processTarget(Object target) {
103         if(Proxy.isProxyClass(target.getClass())) {
104             MetaObject mo = SystemMetaObject.forObject(target);
105             return processTarget(mo.getValue("h.target"));
106         }
107         return target;
108     }
109 
110     /**
111      * 为原SQL添加version
112      *
113      * @param originalSql 原SQL
114      * @param originalVersion 原版本号
115      * @return 加入version的SQL
116      */
117     private String addVersionToSql(String originalSql, Object originalVersion){
118         try{
119             Statement stmt = CCJSqlParserUtil.parse(originalSql);
120             if(!(stmt instanceof Update)){
121                 return originalSql;
122             }
123             Update update = (Update)stmt;
124             if(!contains(update)){
125                 buildVersionExpression(update);
126             }
127             Expression where = update.getWhere();
128             if(where != null){
129                 AndExpression and = new AndExpression(where, buildVersionEquals(originalVersion));
130                 update.setWhere(and);
131             }else{
132                 update.setWhere(buildVersionEquals(originalVersion));
133             }
134             return stmt.toString();
135         }catch(Exception e){
136             logger.error(e.getMessage(), e);
137             return originalSql;
138         }
139     }
140 
141     private boolean contains(Update update){
142         List<Column> columns = update.getColumns();
143         for(Column column : columns){
144             if(column.getColumnName().equalsIgnoreCase(VERSION_COLUMN_NAME)){
145                 return true;
146             }
147         }
148         return false;
149     }
150 
151     private void buildVersionExpression(Update update){
152         // 列 version
153         Column versionColumn = new Column();
154         versionColumn.setColumnName(VERSION_COLUMN_NAME);
155         update.getColumns().add(versionColumn);
156 
157         // 值 version+1
158         Addition add = new Addition();
159         add.setLeftExpression(versionColumn);
160         add.setRightExpression(new LongValue(1));
161         update.getExpressions().add(add);
162     }
163 
164     private Expression buildVersionEquals(Object originalVersion){
165         Column column = new Column();
166         column.setColumnName(VERSION_COLUMN_NAME);
167 
168         // 条件 version = originalVersion
169         EqualsTo equal = new EqualsTo();
170         equal.setLeftExpression(column);
171         equal.setRightExpression(new LongValue(originalVersion.toString()));
172         return equal;
173     }
174 
175 }
复制代码

之后还需配置该插件,只需要在MyBatisConfig中加入该配置即可。

最后,如果版本不匹配,更新失败,需要往外抛出异常提醒,所以修改BaseService的update方法,增加检查更新是否失败。

最后,能不用插件尽量不要用插件,因为它将修改MyBatis的底层设计。插件生成的是层层代理对象的责任链模式,通过反射方法运行,会有一定的性能消耗。

我们也可以修改 tk.mapper 生成SQL的方法,加入version,这里通过插件方式实现乐观锁主要是不为了去修改 mapper 的底层源码,比较方便。

 

六、Druid数据库连接池

创建数据库连接是一个很耗时的操作,也很容易对数据库造成安全隐患。对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响程序的性能指标。

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。数据库连接池能明显提高对数据库操作的性能。

参考:

  Druid常见问题集锦

  常用数据库连接池 (DBCP、c3p0、Druid) 配置说明

1、Druid

Druid首先是一个数据库连接池,但它不仅仅是一个数据库连接池,它还包含一个ProxyDriver,一系列内置的JDBC组件库,一个SQLParser。Druid支持所有JDBC兼容的数据库,包括Oracle、MySql、Derby、Postgresql、SQLServer、H2等等。 Druid针对Oracle和MySql做了特别优化,比如Oracle的PSCache内存占用优化,MySql的ping检测优化。Druid在监控、可扩展性、稳定性和性能方面都有明显的优势。Druid提供了Filter-Chain模式的扩展API,可以自己编写Filter拦截JDBC中的任何方法,可以在上面做任何事情,比如说性能监控、SQL审计、用户名密码加密、日志等等。

2、配置

Druid配置到core模块下,只需在application.properties中添加如下配置即可,大部分配置是默认配置,可更改。有详细的注释,比较容易理解。

复制代码
 1 ####################################
 2 # Druid
 3 ####################################
 4 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 5 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
 6 
 7 # 初始化连接大小[0]
 8 spring.datasource.druid.initial-size=1
 9 # 最小空闲连接数[0]
10 spring.datasource.druid.min-idle=1
11 # 最大连接数[8]
12 spring.datasource.druid.max-active=20
13 
14 # 配置获取连接等待超时的时间(毫秒)[-1]
15 spring.datasource.druid.max-wait=60000
16 # 查询超时时间(秒)
17 spring.datasource.druid.query-timeout=90
18 
19 # 用来检测连接是否有效的sql,要求是一个查询语句
20 spring.datasource.druid.validation-query=SELECT 'x'
21 # 申请连接时检测连接可用性[false]
22 spring.datasource.druid.test-on-borrow=false
23 # 归还连接检测[false]
24 spring.datasource.druid.test-on-return=false
25 # 超时是否检测连接可用性[true]
26 spring.datasource.druid.test-while-idle=true
27 
28 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接 (毫秒)
29 spring.datasource.druid.time-between-eviction-runs-millis=60000
30 #  配置一个连接在池中最小生存的时间(毫秒,默认30分钟)
31 spring.datasource.druid.min-evictable-idle-time-millis=300000
32 # 通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat;日志用的filter:log4j;防御sql注入的filter:wall
33 spring.datasource.druid.filters=stat,wall,slf4j
34 # 合并多个DruidDataSource的监控数据
35 spring.datasource.druid.use-global-data-source-stat=true
36 
37 # 是否缓存PreparedStatement. PSCache对支持游标的数据库性能提升巨大,比如说oracle.在mysql下建议关闭.
38 spring.datasource.druid.pool-prepared-statements=false
39 # 每个连接上PSCache的大小
40 spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20
41 
42 # StatViewServlet [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE]
43 spring.datasource.druid.stat-view-servlet.enabled=true
44 spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
45 # 监控页面的用户名和密码
46 spring.datasource.druid.stat-view-servlet.login-username=admin
47 spring.datasource.druid.stat-view-servlet.login-password=admin
48 spring.datasource.druid.stat-view-servlet.reset-enable=false
49 
50 # StatFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter]
51 spring.datasource.druid.filter.stat.db-type=mysql
52 #慢SQL记录
53 spring.datasource.druid.filter.stat.log-slow-sql=true
54 spring.datasource.druid.filter.stat.slow-sql-millis=2000
55 # SQL合并
56 spring.datasource.druid.filter.stat.merge-sql=false
57 
58 # WallFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE-wallfilter]
59 spring.datasource.druid.filter.wall.enabled=true
60 spring.datasource.druid.filter.wall.db-type=mysql
61 spring.datasource.druid.filter.wall.config.delete-allow=false
62 spring.datasource.druid.filter.wall.config.drop-table-allow=false
复制代码

之后启动项目在地址栏输入/druid/index.html并登录就可以看到Druid监控页面:

 

七、Redis缓存

对于如今的一个中小型系统来说,至少也需要一个缓存来缓存热点数据,加快数据的访问数据,这里选用Redis做缓存数据库。在以后可以使用Redis做分布式缓存、做Session共享等。

1、SpringBoot的缓存支持

Spring定义了org.springframework.cache.CacheManager和org.springframework.cache.Cache接口来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口,Cache接口包含缓存的各种操作。

针对不同的缓存技术,需要实现不同的CacheManager,Redis缓存则提供了RedisCacheManager的实现。

我将redis缓存功能放到sunny-starter-cache模块下,cache模块下可以有多种缓存技术,同时,对于其它项目来说,缓存是可插拔的,想用缓存直接引入cache模块即可。

首先引入Redis的依赖:

SpringBoot已经默认为我们自动配置了多个CacheManager的实现,在autoconfigure.cache包下。在Spring Boot 环境下,使用缓存技术只需在项目中导入相关的依赖包即可。

在 RedisCacheConfiguration 里配置了默认的 CacheManager;SpringBoot提供了默认的redis配置,RedisAutoConfiguration 是Redis的自动化配置,比如创建连接池、初始化RedisTemplate等。

2、Redis 配置及声明式缓存支持

Redis 默认配置了 RedisTemplate 和 StringRedisTemplate ,其使用的序列化规则是 JdkSerializationRedisSerializer,缓存到redis后,数据都变成了下面这种样式,非常不易于阅读。

因此,重新配置RedisTemplate,使用 Jackson2JsonRedisSerializer 来序列化 Key 和 Value。同时,增加HashOperations、ValueOperations等Redis数据结构相关的操作,这样比较方便使用。

复制代码
 1 package com.lyyzoo.cache.redis;
 2 
 3 import org.springframework.beans.factory.annotation.Autowired;
 4 import org.springframework.cache.annotation.EnableCaching;
 5 import org.springframework.context.annotation.Bean;
 6 import org.springframework.context.annotation.Configuration;
 7 import org.springframework.data.redis.cache.RedisCacheManager;
 8 import org.springframework.data.redis.connection.RedisConnectionFactory;
 9 import org.springframework.data.redis.core.*;
10 import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
11 import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
12 
13 import com.fasterxml.jackson.annotation.JsonAutoDetect;
14 import com.fasterxml.jackson.annotation.PropertyAccessor;
15 import com.fasterxml.jackson.databind.ObjectMapper;
16 
17 /**
18  * Redis配置.
19  *
20  * 使用@EnableCaching开启声明式缓存支持. 之后就可以使用 @Cacheable/@CachePut/@CacheEvict 注解缓存数据.
21  *
22  * @author bojiangzhou 2018-02-11
23  * @version 1.0
24  */
25 @Configuration
26 @EnableCaching
27 public class RedisConfig {
28     @Autowired
29     private RedisConnectionFactory redisConnectionFactory;
30     @Autowired
31     private Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder;
32 
33     /**
34      * 覆盖默认配置 RedisTemplate,使用 String 类型作为key,设置key/value的序列化规则
35      */
36     @Bean
37     @SuppressWarnings("unchecked")
38     public RedisTemplate<String, Object> redisTemplate() {
39         RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
40         redisTemplate.setConnectionFactory(redisConnectionFactory);
41 
42         // 使用 Jackson2JsonRedisSerialize 替换默认序列化
43         Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
44         ObjectMapper objectMapper = jackson2ObjectMapperBuilder.createXmlMapper(false).build();
45         objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
46         objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
47         jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
48 
49         // 设置value的序列化规则和key的序列化规则
50         redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
51         redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
52         redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
53         redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
54         redisTemplate.afterPropertiesSet();
55 
56         return redisTemplate;
57     }
58 
59     @Bean
60     public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
61         return redisTemplate.opsForHash();
62     }
63 
64     @Bean
65     public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
66         return redisTemplate.opsForValue();
67     }
68 
69     @Bean
70     public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
71         return redisTemplate.opsForList();
72     }
73 
74     @Bean
75     public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
76         return redisTemplate.opsForSet();
77     }
78 
79     @Bean
80     public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
81         return redisTemplate.opsForZSet();
82     }
83 
84     @Bean
85     public RedisCacheManager cacheManager() {
86         RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate());
87         cacheManager.setUsePrefix(true);
88         return cacheManager;
89     }
90 
91 }
复制代码

同时,使用@EnableCaching开启声明式缓存支持,这样就可以使用基于注解的缓存技术。注解缓存是一个对缓存使用的抽象,通过在代码中添加下面的一些注解,达到缓存的效果。

  • @Cacheable:在方法执行前Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;没有则调用方法并将方法返回值放进缓存。

  • @CachePut:将方法的返回值放到缓存中。

  • @CacheEvict:删除缓存中的数据。

 

Redis服务器相关的一些配置可在application.properties中进行配置:

3、Redis工具类

添加一个Redis的统一操作工具,主要是对redis的常用数据类型操作类做了一个归集。

ValueOperations用于操作String类型,HashOperations用于操作hash数据,ListOperations操作List集合,SetOperations操作Set集合,ZSetOperations操作有序集合。

关于redis的key命令和数据类型可参考我的学习笔记:

Redis 学习(一) —— 安装、通用key操作命令

Redis 学习(二) —— 数据类型及操作

复制代码
  1 package com.lyyzoo.cache.redis;
  2 
  3 import org.springframework.beans.factory.annotation.Autowired;
  4 import org.springframework.beans.factory.annotation.Value;
  5 import org.springframework.data.redis.connection.DataType;
  6 import org.springframework.data.redis.core.*;
  7 import org.springframework.stereotype.Component;
  8 
  9 import java.util.Collection;
 10 import java.util.Date;
 11 import java.util.Set;
 12 import java.util.concurrent.TimeUnit;
 13 import java.util.stream.Collectors;
 14 import java.util.stream.Stream;
 15 
 16 /**
 17  * Redis 操作工具
 18  *
 19  * @version 1.0
 20  * @author bojiangzhou 2018-02-12
 21  */
 22 @Component
 23 public class RedisOperator {
 24 
 25     @Autowired
 26     private RedisTemplate<String, Object> redisTemplate;
 27     @Autowired
 28     private ValueOperations<String, String> valueOperator;
 29     @Autowired
 30     private HashOperations<String, String, Object> hashOperator;
 31     @Autowired
 32     private ListOperations<String, Object> listOperator;
 33     @Autowired
 34     private SetOperations<String, Object> setOperator;
 35     @Autowired
 36     private ZSetOperations<String, Object> zSetOperator;
 37 
 38     /**
 39      * 默认过期时长,单位:秒
 40      */
 41     public final static long DEFAULT_EXPIRE = 60 * 60 * 24;
 42 
 43     /** 不设置过期时长 */
 44     public final static long NOT_EXPIRE = -1;
 45 
 46     /**
 47      * Redis的根操作路径
 48      */
 49     @Value("${redis.root:sunny}")
 50     private String category;
 51 
 52     public RedisOperator setCategory(String category) {
 53         this.category = category;
 54         return this;
 55     }
 56 
 57     /**
 58      * 获取Key的全路径
 59      * 
 60      * @param key key
 61      * @return full key
 62      */
 63     public String getFullKey(String key) {
 64         return this.category + ":" + key;
 65     }
 66 
 67 
 68     //
 69     // key
 70     // ------------------------------------------------------------------------------
 71     /**
 72      * 判断key是否存在
 73      *
 74      * <p>
 75      * <i>exists key</i>
 76      *
 77      * @param key key
 78      */
 79     public boolean existsKey(String key) {
 80         return redisTemplate.hasKey(getFullKey(key));
 81     }
 82 
 83     /**
 84      * 判断key存储的值类型
 85      *
 86      * <p>
 87      * <i>type key</i>
 88      *
 89      * @param key key
 90      * @return DataType[string、list、set、zset、hash]
 91      */
 92     public DataType typeKey(String key){
 93         return redisTemplate.type(getFullKey(key));
 94     }
 95 
 96     /**
 97      * 重命名key. 如果newKey已经存在,则newKey的原值被覆盖
 98      *
 99      * <p>
100      * <i>rename oldKey newKey</i>
101      *
102      * @param oldKey oldKeys
103      * @param newKey newKey
104      */
105     public void renameKey(String oldKey, String newKey){
106         redisTemplate.rename(getFullKey(oldKey), getFullKey(newKey));
107     }
108 
109     /**
110      * newKey不存在时才重命名.
111      *
112      * <p>
113      * <i>renamenx oldKey newKey</i>
114      *
115      * @param oldKey oldKey
116      * @param newKey newKey
117      * @return 修改成功返回true
118      */
119     public boolean renameKeyNx(String oldKey, String newKey){
120         return redisTemplate.renameIfAbsent(getFullKey(oldKey), getFullKey(newKey));
121     }
122 
123     /**
124      * 删除key
125      *
126      * <p>
127      * <i>del key</i>
128      *
129      * @param key key
130      */
131     public void deleteKey(String key){
132         redisTemplate.delete(key);
133     }
134 
135     /**
136      * 删除key
137      *
138      * <p>
139      * <i>del key1 key2 ...</i>
140      *
141      * @param keys 可传入多个key
142      */
143     public void deleteKey(String ... keys){
144         Set<String> ks = Stream.of(keys).map(k -> getFullKey(k)).collect(Collectors.toSet());
145         redisTemplate.delete(ks);
146     }
147 
148     /**
149      * 删除key
150      *
151      * <p>
152      * <i>del key1 key2 ...</i>
153      *
154      * @param keys key集合
155      */
156     public void deleteKey(Collection<String> keys){
157         Set<String> ks = keys.stream().map(k -> getFullKey(k)).collect(Collectors.toSet());
158         redisTemplate.delete(ks);
159     }
160 
161     /**
162      * 设置key的生命周期,单位秒
163      *
164      * <p>
165      * <i>expire key seconds</i><br>
166      * <i>pexpire key milliseconds</i>
167      *
168      * @param key key
169      * @param time 时间数
170      * @param timeUnit TimeUnit 时间单位
171      */
172     public void expireKey(String key, long time, TimeUnit timeUnit){
173         redisTemplate.expire(key, time, timeUnit);
174     }
175 
176     /**
177      * 设置key在指定的日期过期
178      *
179      * <p>
180      * <i>expireat key timestamp</i>
181      *
182      * @param key key
183      * @param date 指定日期
184      */
185     public void expireKeyAt(String key, Date date){
186         redisTemplate.expireAt(key, date);
187     }
188 
189     /**
190      * 查询key的生命周期
191      *
192      * <p>
193      * <i>ttl key</i>
194      *
195      * @param key key
196      * @param timeUnit TimeUnit 时间单位
197      * @return 指定时间单位的时间数
198      */
199     public long getKeyExpire(String key, TimeUnit timeUnit){
200         return redisTemplate.getExpire(key, timeUnit);
201     }
202 
203     /**
204      * 将key设置为永久有效
205      *
206      * <p>
207      * <i>persist key</i>
208      *
209      * @param key key
210      */
211     public void persistKey(String key){
212         redisTemplate.persist(key);
213     }
214 
215 
216     /**
217      *
218      * @return RedisTemplate
219      */
220     public RedisTemplate<String, Object> getRedisTemplate() {
221         return redisTemplate;
222     }
223 
224     /**
225      *
226      * @return ValueOperations
227      */
228     public ValueOperations<String, String> getValueOperator() {
229         return valueOperator;
230     }
231 
232     /**
233      *
234      * @return HashOperations
235      */
236     public HashOperations<String, String, Object> getHashOperator() {
237         return hashOperator;
238     }
239 
240     /**
241      *
242      * @return ListOperations
243      */
244     public ListOperations<String, Object> getListOperator() {
245         return listOperator;
246     }
247 
248     /**
249      *
250      * @return SetOperations
251      */
252     public SetOperations<String, Object> getSetOperator() {
253         return setOperator;
254     }
255 
256     /**
257      *
258      * @return ZSetOperations
259      */
260     public ZSetOperations<String, Object> getZSetOperator() {
261         return zSetOperator;
262     }
263 
264 }
复制代码

 

八、Swagger支持API文档

1、Swagger

做前后端分离,前端和后端的唯一联系,变成了API接口;API文档变成了前后端开发人员联系的纽带,变得越来越重要,swagger就是一款让你更好的书写API文档的框架。

Swagger是一个简单又强大的能为你的Restful风格的Api生成文档的工具。在项目中集成这个工具,根据我们自己的配置信息能够自动为我们生成一个api文档展示页,可以在浏览器中直接访问查看项目中的接口信息,同时也可以测试每个api接口。

2、配置

我这里直接使用别人已经整合好的swagger-spring-boot-starter,快速方便。

参考:spring-boot-starter-swagger

新建一个sunny-starter-swagger模块,做到可插拔。

根据文档,一般只需要做些简单的配置即可:

但如果想要显示swagger-ui.html文档展示页,还必须注入swagger资源:

复制代码
 1 package com.lyyzoo.swagger.config;
 2 
 3 import org.springframework.context.annotation.Configuration;
 4 import org.springframework.context.annotation.PropertySource;
 5 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 6 import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 7 
 8 import com.spring4all.swagger.EnableSwagger2Doc;
 9 
10 /**
11  * @version 1.0
12  * @author bojiangzhou 2018-02-19
13  */
14 @Configuration
15 @EnableSwagger2Doc
16 @PropertySource(value = "classpath:application-swagger.properties")
17 public class SunnySwaggerConfig extends WebMvcConfigurerAdapter {
18     /**
19      * 注入swagger资源文件
20      */
21     @Override
22     public void addResourceHandlers(ResourceHandlerRegistry registry) {
23         registry.addResourceHandler("swagger-ui.html")
24                 .addResourceLocations("classpath:/META-INF/resources/");
25         registry.addResourceHandler("/webjars/**")
26                 .addResourceLocations("classpath:/META-INF/resources/webjars/");
27     }
28 
29 }
复制代码

3、使用

一般只需要在Controller加上swagger的注解即可显示对应的文档信息,如@Api、@ApiOperation、@ApiParam等。

常用注解参考:swagger-api-annotations

复制代码
 1 package com.lyyzoo.admin.system.controller;
 2 
 3 import org.springframework.beans.factory.annotation.Autowired;
 4 import org.springframework.web.bind.annotation.*;
 5 
 6 import com.lyyzoo.admin.system.dto.Menu;
 7 import com.lyyzoo.admin.system.service.MenuService;
 8 import com.lyyzoo.core.base.BaseController;
 9 import com.lyyzoo.core.base.Result;
10 import com.lyyzoo.core.util.Results;
11 
12 import io.swagger.annotations.Api;
13 import io.swagger.annotations.ApiImplicitParam;
14 import io.swagger.annotations.ApiOperation;
15 import io.swagger.annotations.ApiParam;
16 
17 @Api(tags = "菜单管理")
18 @RequestMapping
19 @RestController
20 public class MenuController extends BaseController {
21 
22     @Autowired
23     private MenuService service;
24 
25     /**
26      * 查找单个用户
27      *
28      * @param menuId 菜单ID
29      * @return Result
30      */
31     @ApiOperation("查找单个用户")
32     @ApiImplicitParam(name = "menuId", value = "菜单ID", paramType = "path")
33     @GetMapping("/sys/menu/get/{menuId}")
34     public Result get(@PathVariable Long menuId){
35         Menu menu = service.selectById(menuId);
36         return Results.successWithData(menu);
37     }
38 
39     /**
40      * 保存菜单
41      *
42      * @param menu 菜单
43      * @return Result
44      */
45     @ApiOperation("保存菜单")
46     @PostMapping("/sys/menu/save")
47     public Result save(@ApiParam(name = "menu", value = "菜单")@RequestBody Menu menu){
48         menu = service.save(menu);
49         return Results.successWithData(menu);
50     }
51 
52     /**
53      * 删除菜单
54      *
55      * @param menuId 菜单ID
56      * @return Result
57      */
58     @ApiOperation("删除菜单")
59     @ApiImplicitParam(name = "menuId", value = "菜单ID", paramType = "path")
60     @PostMapping("/sys/menu/delete/{menuId}")
61     public Result delete(@PathVariable Long menuId){
62         service.deleteById(menuId);
63         return Results.success();
64     }
65 
66 }
复制代码

之后访问swagger-ui.html页面就可以看到API文档信息了。

如果不需要swagger,在配置文件中配置swagger.enabled=false,或移除sunny-starter-swagger的依赖即可。

 

九、项目优化调整

到这里,项目最基础的一些功能就算完成了,但由于前期的一些设计不合理及未考虑周全等因素,对项目做一些调整。并参考《阿里巴巴Java开发手册》对代码做了一些优化。

1、项目结构

目前项目分为5个模块:

最外层的Sunny作为聚合模块负责管理所有子模块,方便统一构建。并且继承 spring-boot-starter-parent ,其它子模块则继承该模块,方便统一管理 Spring Boot 及本项目的版本。这里已经把Spring Boot的版本升到 1.5.10.RELEASE。

复制代码
 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4     <modelVersion>4.0.0</modelVersion>
 5 
 6     <groupId>com.lyyzoo</groupId>
 7     <artifactId>sunny</artifactId>
 8     <version>0.0.1-SNAPSHOT</version>
 9     <packaging>pom</packaging>
10 
11     <name>Sunny</name>
12     <description>Lyyzoo Base Application development platform</description>
13 
14     <parent>
15         <groupId>org.springframework.boot</groupId>
16         <artifactId>spring-boot-starter-parent</artifactId>
17         <version>1.5.10.RELEASE</version>
18         <relativePath/>
19     </parent>
20 
21     <properties>
22         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
23         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
24         <java.version>1.8</java.version>
25 
26         <sunny.version>0.0.1-SNAPSHOT</sunny.version>
27         <springboot.version>1.5.10.RELEASE</springboot.version>
28     </properties>
29 
30     <modules>
31         <module>sunny-starter</module>
32         <module>sunny-starter-core</module>
33         <module>sunny-starter-cache</module>
34         <module>sunny-starter-security</module>
35         <module>sunny-starter-admin</module>
36         <module>sunny-starter-swagger</module>
37     </modules>
38 
39     <build>
40         <plugins>
41             <plugin>
42                 <groupId>org.springframework.boot</groupId>
43                 <artifactId>spring-boot-maven-plugin</artifactId>
44             </plugin>
45         </plugins>
46     </build>
47 
48 </project>
复制代码

sunny-starter 则引入了其余几个模块,在开发项目时,只需要继承或引入sunny-starter即可,而无需一个个引入各个模块。

复制代码
 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4     <modelVersion>4.0.0</modelVersion>
 5 
 6     <parent>
 7         <groupId>com.lyyzoo</groupId>
 8         <artifactId>sunny</artifactId>
 9         <version>0.0.1-SNAPSHOT</version>
10     </parent>
11 
12     <groupId>com.lyyzoo.parent</groupId>
13     <artifactId>sunny-starter</artifactId>
14     <packaging>jar</packaging>
15 
16     <name>sunny-starter</name>
17     <description>Sunny Parent</description>
18 
19     <dependencies>
20         <!-- core -->
21         <dependency>
22             <groupId>com.lyyzoo.core</groupId>
23             <artifactId>sunny-starter-core</artifactId>
24             <version>${sunny.version}</version>
25         </dependency>
26         <!-- cache -->
27         <dependency>
28             <groupId>com.lyyzoo.cache</groupId>
29             <artifactId>sunny-starter-cache</artifactId>
30             <version>${sunny.version}</version>
31         </dependency>
32         <!-- security -->
33         <dependency>
34             <groupId>com.lyyzoo.security</groupId>
35             <artifactId>sunny-starter-security</artifactId>
36             <version>${sunny.version}</version>
37         </dependency>
38         <!-- admin -->
39         <dependency>
40             <groupId>com.lyyzoo.admin</groupId>
41             <artifactId>sunny-starter-admin</artifactId>
42             <version>${sunny.version}</version>
43         </dependency>
44         <!-- swagger -->
45         <dependency>
46             <groupId>com.lyyzoo.swagger</groupId>
47             <artifactId>sunny-starter-swagger</artifactId>
48             <version>${sunny.version}</version>
49         </dependency>
50 
51     </dependencies>
52 
53     <build>
54         <plugins>
55             <plugin>
56                 <groupId>org.springframework.boot</groupId>
57                 <artifactId>spring-boot-maven-plugin</artifactId>
58             </plugin>
59         </plugins>
60     </build>
61 
62 
63 </project>
复制代码

对于一个Spring Boot项目,应该只有一个入口,即 @SpringBootApplication 注解的类。经测试,其它的模块的配置文件application.properties的配置不会生效,应该是引用了入口模块的配置文件。

所以为了让各个模块的配置文件都能生效,只需使用 @PropertySource 引入该配置文件即可,每个模块都如此。在主模块定义的配置会覆盖其它模块的配置。

2、开发规范

 

十、结语

到此,基础架构篇结束!学习了很多新东西,如Spring Boot、Mapper、Druid;有些知识也深入地学习了,如MyBatis、Redis、日志框架、Maven等等。

在这期间,看完两本书,可参考:《MyBatis从入门到精通》、《JavaEE开发的颠覆者 Spring Boot实战》,另外,开发规范遵从《阿里巴巴Java开发手册》,其它的参考资料都在文中有体现。

 

紧接着,后面会完成 sunny-starter-security 模块的开发,主要使用spring-security技术,开发用户登录及权限控制等。

posted @ 2018-02-24 13:10  门罗的魔术师  阅读(790)  评论(0编辑  收藏  举报