加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

基于SpringBoot搭建应用开发框架(一) —— 基础架构

发布时间:2020-12-15 01:13:55 所属栏目:大数据 来源:网络整理
导读:Spring的简史 第一阶段:XML配置,在Spring1.x时代,使用Spring开发满眼都是xml配置的Bean,随着项目的扩大,我们需要把xml配置文件分放到不同的配置文件里,那时候需要频繁的在开发的类和配置文件之间切换。 第二阶段:注解配置,在Spring2.x时代,Spring提

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.9.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</ 5     artifactId 6     version>${commons.io.version} 7  8  文件上传  9 10     >commons-fileupload11     12     >${commons.fileupload.version}13     exclusions14         exclusion15             16             17         18     19 20  常用的集合操作,丰富的工具类 21 22     >commons-collections23     24     >${commons.collections.version}25 26  操作javabean的工具包 27 28     >commons-beanutils29     30     >${commons.beanutils.version}31     32         33             34             35         36     37 38  包含一些通用的编码解码算法. 如:MD5、SHA1、Base64等 39 40     >commons-codec41     42     >${commons.codec.version}43 44  包含丰富的工具类如 StringUtils 45 46     >org.apache.commons47     >commons-lang348     >${commons.lang3.version}49 50 <!--
51     Guava工程包含了若干被Google的Java项目广泛依赖的核心库. 集合[collections] 、缓存[caching] 、原生类型支持[primitives support] 、
52     并发库[concurrency libraries] 、通用注解[common annotations] 、字符串处理[string processing] 、I/O 等等。
53 54 55     >com.google.guava56     >guava57     >${guava.version}58 >
View Code

版本号如下:

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

  1 package com.lyyzoo.core.base;
  2 
  3 import com.fasterxml.jackson.annotation.JsonInclude;
  4 
  5  java.io.Serializable;
  6 
  7 /**
  8  * 前端返回对象
  9  *
 10  * @version 1.0
 11 @author bojiangzhou 2017-12-28
 12  */
 13 public class Result implements Serializable {
 14     private static final long serialVersionUID = 1430633339880116031L;
 15 
 16      17      * 成功与否标志
 18       19     boolean success = true 20      21      * 返回状态码,为空则默认200.前端需要拦截一些常见的状态码如403、404、500等
 22       23     @JsonInclude(JsonInclude.Include.NON_NULL)
 24     private Integer status;
 25      26      * 编码,可用于前端处理多语言,不需要则不用返回编码
 27       28  29      String code;
 30      31      * 相关消息
 32       33  34      String msg;
 35      36      * 相关数据
 37       38  39      Object data;
 40 
 41 
 42     public Result() {}
 43 
 44     public Result(boolean success) {
 45         this.success = success;
 46     }
 47 
 48      success,Integer status) {
 49          50         this.status = status;
 51  52 
 53      54         this(success);
 55         this.code = code;
 56         this.msg = msg;
 57  58 
 59      60          61          62          63          64  65 
 66      67          68          69          70         this.data = data;
 71  72 
 73      isSuccess() {
 74         return 75  76 
 77     void setSuccess( 78          79  80 
 81      Integer getStatus() {
 82          83  84 
 85     void setStatus(Integer status) {
 86          87  88 
 89      String getCode() {
 90          91  92 
 93      setCode(String code) {
 94          95  96 
 97      String getMsg() {
 98          99 100 
101      setMsg(String msg) {
102         103 104 
105      Object getData() {
106         107 108 
109      setData(Object data) {
110         111 112 }
View Code

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

 com.lyyzoo.core.util;
 2 
 com.lyyzoo.core.base.Result;
 4 
 5  6  * Result生成工具类
10  11 class Results {
12 
protected Results() {}
14 
15     static Result newResult() {
16         return new Result();
17 
18 19 
20     static Result newResult(21          Result(success);
22 23 
//
25     // 业务调用成功
26      ----------------------------------------------------------------------------------------------------
27      Result success() {
28         29 30 
 Result success(String msg) {
new Result(true,null,msg);
33 34 
35      Result success(String code,1)">36         38 
39      Result successWithStatus(Integer status) {
40         41 42 
43      Result successWithStatus(Integer status,1)">44         46 
 Result successWithData(Object data) {
48         null,data);
50 
51      Result successWithData(Object data,1)">52         54 
56         57 58 
59     60      业务调用失败
61     62      Result failure() {
63         false);
64 65 
66      Result failure(String msg) {
67         false,1)">68 69 
70      Result failure(String code,1)">71         72 73 
74      Result failureWithStatus(Integer status) {
75         76 77 
78      Result failureWithStatus(Integer status,1)">79         80 81 
82      Result failureWithData(Object data) {
83         84 85 
86      Result failureWithData(Object data,1)">87         88 89 
90     91         92 93 
94 }
View Code

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

 4  * 基础枚举接口
 bojiangzhou 2017-12-31
 8  interface BaseEnum<K,V> {
10 
12      * 获取编码
13      *
14      * @return 编码
15      16     K code();
     * 获取描述
     * 
 描述
22      23     V desc();
24 
25 }
View Code

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

 com.lyyzoo.core.constants;
 com.lyyzoo.core.base.BaseEnum;
 java.util.HashMap;
 java.util.Map;
 7 
 * 基础枚举值
10  bojiangzhou 2018-01-01
13  enum BaseEnums implements BaseEnum<String,String>15 
16     SUCCESS("request.success","请求成功"),18     FAILURE("request.failure","请求失败"20     OPERATION_SUCCESS("operation.success","操作成功"21 
22     OPERATION_FAILURE("operation.failure","操作失败"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     35 
 String desc;
37 
38     static Map<String,String> allMap = new HashMap<>();
39 
40     BaseEnums(String code,String desc) {
41         42         this.desc = desc;
44 
45     46         for(BaseEnums enums : BaseEnums.values()){
47             allMap.put(enums.code,enums.desc);
48         }
    @Override
52      String code() {
53         55 
56  String desc() {
58         59 60 
 String desc(String code) {
62          allMap.get(code);
63 64 
65 }
View Code

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

  3 
  4  org.apache.commons.lang3.StringUtils;
 org.apache.commons.lang3.time.DateUtils;
 java.text.ParseException;
 java.text.SimpleDateFormat;
 java.util.Date;
 10 
 12  * 日期时间工具类
 14  15  16   Dates {
 18 
 20      * 日期时间匹配格式
 21       22     interface Pattern {
 23          24          常规模式
 25          26          27          * yyyy-MM-dd
 28           29         String DATE = "yyyy-MM-dd" 30                  * 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          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" 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                  * yyyyMMdd
 76           77         String NONE_DATE = "yyyyMMdd"         * yyyyMMddHHmmss
 80           81         String NONE_DATETIME = "yyyyMMddHHmmss"         * yyyyMMddHHmm
 84           85         String NONE_DATETIME_MM = "yyyyMMddHHmm"         * yyyyMMddHHmmssSSS
 88           89         String NONE_DATETIME_SSS = "yyyyMMddHHmmssSSS" 90  91 
 92     final String DEFAULT_PATTERN = Pattern.DATETIME;
 93 
 94     final String[] PARSE_PATTERNS =  String[]{
            Pattern.DATE,1)"> 96             Pattern.DATETIME,1)"> 97             Pattern.DATETIME_MM,1)"> 98             Pattern.DATETIME_SSS,1)">            Pattern.SYS_DATE,1)">100             Pattern.SYS_DATETIME,1)">101             Pattern.SYS_DATETIME_MM,1)">102             Pattern.SYS_DATETIME_SSS
    };
106      * 格式化日期时间
108 @param date 日期时间
109 110  yyyy-MM-dd HH:mm:ss
111      112      String format(Date date) {
113          format(date,DEFAULT_PATTERN);
114 115 
116     117      * 格式化日期
118 119  date 日期(时间)
120 121  pattern 匹配模式 参考:{@link Dates.Pattern}
122 123  格式化后的字符串
124      125      String format(Date date,String pattern) {
126         if (date == ) {
127             128 129         pattern = StringUtils.isNotBlank(pattern) ? pattern : DEFAULT_PATTERN;
130         SimpleDateFormat sdf =  SimpleDateFormat(pattern);
131          sdf.format(date);
132 133 
134     135      * 解析日期
136 137  date 日期字符串
138 139  解析后的日期 默认格式:yyyy-MM-dd HH:mm:ss
140      141      Date parseDate(String date) {
142         if (StringUtils.isBlank(date)) {
143             144 145         try146              DateUtils.parseDate(date,PARSE_PATTERNS);
147         } catch (ParseException e) {
148             e.printStackTrace();
149 150         151 152 
153     154 155 156  date 日期
157 158  pattern 格式 参考:{159 160  解析后的日期,默认格式:yyyy-MM-dd HH:mm:ss
161      162      Date parseDate(String date,1)">163         164             165 166         String[] parsePatterns;
167         parsePatterns = StringUtils.isNotBlank(pattern) ?  String[]{pattern} : PARSE_PATTERNS;
168         169             170         } 171 172 173         174 175 
176 
177 
178 }
View Code

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

 com.google.common.base.Charsets;
 java.nio.charset.Charset;
 6 
 * 系统级常量类
12   Constants {
final String APP_NAME = "sunny"16 
17          * 系统编码
19      final Charset CHARSET = Charsets.UTF_8;
     * 标识:是/否、启用/禁用等
24       Flag {
26 
27         Integer YES = 128 
29         Integer NO = 030 32          * 操作类型
34       Operation {
         * 添加
38          39         String ADD = "add"         * 更新
42          43         String UPDATE = "update"         * 删除
46          47         String DELETE = "delete"49 
50          * 性别
52      53      Sex {
54         55          * 男
56          57         Integer MALE = 1         * 女
60          61         Integer FEMALE = 062 63 
64 }
View Code

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

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

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

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

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

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

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

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

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

 com.lyyzoo.core.exception;
 * 基础异常类
class BaseException extends RuntimeException {
long serialVersionUID = -997101946070796354L11 
     * 错误编码
14       BaseException() {}
18 
19      BaseException(String message) {
20         super(message);
22 
 BaseException(String code,String message) {
24         25         29         33         34 35 }
View Code

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

 * Service层异常
class ServiceException  BaseException {
long serialVersionUID = 6058294324031642376L ServiceException() {}
13 
14      ServiceException(String message) {
15          ServiceException(String code,1)">19         (code,message);
22 }
View Code

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

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

User:系统用户

 com.lyyzoo.core.system.dto;
 com.fasterxml.jackson.annotation.JsonFormat;
 com.lyyzoo.core.base.BaseDTO;
  7 
  9 
 * 系统用户
 15  @JsonInclude(JsonInclude.Include.NON_NULL)
class User  BaseDTO {
 18     long serialVersionUID = -7395431342743009038L 19 
     * 用户ID
 23      Long userId;
 25      * 用户名
 26       String username;
 29      * 密码
 30       String password;
 32          * 昵称
 34       String nickname;
 36          * 生日
 38       39     @JsonFormat(pattern = Dates.Pattern.DATE)
 Date birthday;
 41      42      * 性别:1-男/0-女
 43       Integer sex;
     * 是否启用:1/0
 47       Integer enabled;
 49 
 50      Long getUserId() {
 userId;
 52  53 
 54      setUserId(Long userId) {
this.userId = 57 
 String getUsername() {
 59          username;
 61 
 62      setUsername(String username) {
this.username = String getPassword() {
 password;
 68  69 
 setPassword(String password) {
this.password = 73 
 74      String getNickname() {
 75          nickname;
 76  77 
 78      setNickname(String nickname) {
 79         this.nickname = 81 
 Date getBirthday() {
 83          birthday;
 84  85 
 86      setBirthday(Date birthday) {
 87         this.birthday = 89 
 90      Integer getSex() {
 91          sex;
 92  setSex(Integer sex) {
 95         this.sex = 98      Integer getEnabled() {
 99          enabled;
101 
 setEnabled(Integer enabled) {
103         this.enabled =105 
106 }
View Code

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

 com.lyyzoo.core.system.controller;
 com.lyyzoo.core.base.BaseController;
 com.lyyzoo.core.base.BaseEnums;
 com.lyyzoo.core.system.dto.User;
 com.lyyzoo.core.util.Results;
 org.springframework.web.bind.annotation.PathVariable;
 org.springframework.web.bind.annotation.RequestMapping;
 org.springframework.web.bind.annotation.RestController;
 java.util.ArrayList;
15  java.util.List;
17  * 用户Controller
22  23 @RequestMapping("/sys/user")
24 @RestController
class UserController  BaseController {
static List<User> userList = new ArrayList<> 先静态模拟数据
31         User user1 =  User();
32         user1.setUserId(1L33         user1.setUsername("lufei"34         user1.setNickname("蒙奇D路飞"35         user1.setBirthday(Dates.parseDate("2000-05-05"));
36         user1.setSex(Constants.Sex.MALE);
        user1.setEnabled(Constants.Flag.YES);
        userList.add(user1);
40         User user2 = 41         user2.setUserId(2L42         user2.setUsername("nami"43         user2.setNickname("娜美"44         user2.setBirthday(Dates.parseDate("2000/7/3"        user2.setSex(Constants.Sex.FEMALE);
46         user2.setEnabled(Constants.Flag.YES);
        userList.add(user2);
50     @RequestMapping("/queryAll" Result queryAll(){
 Results.successWithData(userList,BaseEnums.SUCCESS.code(),BaseEnums.SUCCESS.description());
55     @RequestMapping("/queryOne/{userId}" Result queryOne(@PathVariable Long userId){
57         User user = (User u : userList){
59             if(u.getUserId().longValue() == userId){
60                 user = u;
61             }
 Results.successWithData(user);
65 }
View Code

② 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,按需选择接口。

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

 tk.mybatis.mapper.common.BaseMapper;
 tk.mybatis.mapper.common.ConditionMapper;
 tk.mybatis.mapper.common.IdsMapper;
 tk.mybatis.mapper.common.special.InsertListMapper;
 * BaseMapper
 * @name BaseMapper
15  interface Mapper<T> extends BaseMapper<T>,ConditionMapper<T>,IdsMapper<T>,InsertListMapper<T>18 }
View Code

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

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

4、添加分页插件PageHelper

参考地址:

  MyBatis 分页插件 - PageHelper

  分页插件使用方法

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

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

5、配置自动扫描Mapper

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

 com.lyyzoo.core.config;
 org.springframework.context.annotation.Bean;
 org.springframework.context.annotation.Configuration;
 5 
 tk.mybatis.spring.mapper.MapperScannerConfigurer;
 * MyBatis相关配置.
 bojiangzhou 2018-01-07
@Configuration
 MyBatisConfig {
     * Mapper扫描配置. 自动扫描将Mapper接口生成代理注入到Spring.
    @Bean
21      MapperScannerConfigurer mapperScannerConfigurer() {
22         MapperScannerConfigurer mapperScannerConfigurer =  MapperScannerConfigurer();
23          注意这里的扫描路径: 1.不要扫描到自定义的Mapper; 2.定义的路径不要扫描到tk.mybatis.mapper(如定义**.mapper).
 两个做法都会导致扫描到tk.mybatis的Mapper,就会产生重复定义的报错.
25         mapperScannerConfigurer.setBasePackage("**.lyyzoo.**.mapper"26          mapperScannerConfigurer;
29 }
View Code

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

6、定义基础Service

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

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

 * Service 基础通用接口
 * @name BaseService
 11  interface Service<T> 13 
 15      insert
 17          * 保存一个实体,null的属性也会保存,不会使用数据库默认值
 record
@return
    T insert(T record);
 24 
     * 批量插入,null的属性也会保存,不会使用数据库默认值
 recordList
 31     List<T> insert(List<T> recordList);
 33      34      * 保存一个实体,null的属性不会保存,会使用数据库默认值
    T insertSelective(T record);
     * 批量插入,null的属性不会保存,会使用数据库默认值
 44  45  46       47     List<T> insertSelective(List<T> 48 
 49      update
 51      53      * 根据主键更新实体全部字段,null值会被更新
 57       58     T update(T record);
 59 
 60      61      * 批量更新,根据主键更新实体全部字段,null值会被更新
 65       66     List<T> update(List<T> 67 
 68          * 根据主键更新属性不为null的值
 70     T updateSelective(T record);
 75 
 77      * 批量更新,根据主键更新属性不为null的值
 81       82     List<T> updateSelective(List<T> delete
 87          * 根据主键删除
 89  id id不能为空
 92      int delete(Long id);
 95          * 根据主键字符串进行删除,类中只有存在一个带有@Id注解的字段
 ids 类似1,2,3
 99      100      delete(String ids);
     * 根据主键删除多个实体,ID数组
105  ids 类似[1,3],不能为空
106      107      delete(Long[] ids);
     * 根据实体属性作为条件进行删除
112 114      115      delete(T record);
116 
     * 根据主键删除多个实体
122      int delete(List<T>126      insert or update or delete
127     128     129      * 根据实体的operate决定哪种操作. null的属性也会保存,不会使用数据库默认值
130 133      134     T persist(T record);
135 
136          * 批量操作.根据实体的operate决定哪种操作. null的属性也会保存,不会使用数据库默认值
141      142     List<T> persist(List<T>143 
144     145      * 根据实体的operate决定哪种操作. 根据主键更新属性不为null的值
146 147 149      150     T persistSelective(T record);
151 
152     153      * 批量操作.根据实体的operate决定哪种操作. 根据主键更新属性不为null的值
157      158     List<T> persistSelective(List<T>159 
160 
161      select
163     164          * 根据主键查询
167  id 不能为空
169      170     T get(Long id);
171 
172     173      * 根据实体中的属性进行查询,只能有一个返回值,有多个结果是抛出异常
175 177      178     T get(T record);
179 
180     181      * 根据字段和值查询 返回一个
182  key 不能为空
183  value 不能为空
185      186     T get(String key,Object value);
187 
188 
189     190      * 根据主键字符串进行查询
191  ids 如 "1,3,4"
193 194      195     List<T> select(String ids);
196 
197          * 根据实体中的属性值进行查询
199 200 202      203     List<T> select(T record);
204 
205          * 根据属性和值查询
207 208  key
209  value
211      212     List<T> select(String key,1)">213 
214     215      * 根据实体中的属性值进行分页查询
216 217  pageNum
219  pageSize
220 221      222     List<T> select(T record,1)">int pageNum,1)"> pageSize);
225      * 查询全部结果
227 228      229     List<T> selectAll();
230 
231     232      * 根据实体中的属性查询总数
233 235 236      237      count(T record);
238 
239 }
View Code

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

 com.github.pagehelper.PageHelper;
 com.lyyzoo.core.constants.Constants;
 com.lyyzoo.core.exception.UpdateFailedException;
 com.lyyzoo.core.util.Reflections;
 org.springframework.beans.factory.annotation.Autowired;
 org.springframework.transaction.annotation.Transactional;
 org.springframework.util.Assert;
 javax.annotation.PostConstruct;
 javax.persistence.Id;
 java.lang.reflect.Field;
 * 基础Service实现类
 bojiangzhou 2018-01-04
 21   22 abstract class BaseService<T> implements Service<T> 24     @Autowired
private Mapper<T> mapper;
 26 
private Class<T> entityClass;
 28 
 29     @SuppressWarnings("unchecked" 30     @PostConstruct
 init() {
 32         this.entityClass = Reflections.getClassGenericType(getClass());
 34 
 37      38     @Transactional(rollbackFor = Exception. T insert(T record) {
 40         mapper.insert(record);
 41          record;
 44     @Transactional(rollbackFor = Exception.public List<T> insert(List<T> recordList) {
        mapper.insertList(recordList);
 47          recordList;
 48  50     @Transactional(rollbackFor = Exception. T insertSelective(T record) {
        mapper.insertSelective(record);
 55 
 56     @Transactional(rollbackFor = Exception. 57     public List<T> insertSelective(List<T> 由于Mapper暂未提供Selective的批量插入,此处循环查询. 当然也可参考InsertListMapper自己实现.
(T record : recordList){
            mapper.insertSelective(record);
 64 
 65      67      68     @Transactional(rollbackFor = Exception. 69      T update(T record) {
int count = mapper.updateByPrimaryKey(record);
        checkUpdate(count,record);
 73  74 
 75     @Transactional(rollbackFor = Exception.public List<T> update(List<T> 77          Mapper暂未提供批量更新,此处循实现
 79                         checkUpdate(count,1)"> 81  85     @Transactional(rollbackFor = Exception. T updateSelective(T record) {
 mapper.updateByPrimaryKeySelective(record);
 89          92     @Transactional(rollbackFor = Exception.public List<T> updateSelective(List<T> 96             103     104     105     @Transactional(rollbackFor = Exception.106      delete(Long id) {
107          mapper.deleteByPrimaryKey(id);
110     @Transactional(rollbackFor = Exception. delete(Long[] ids) {
112         int count = 0(Long id : ids){
            mapper.deleteByPrimaryKey(id);
115             count++117          count;
119 
120     @Transactional(rollbackFor = Exception.121      delete(T record) {
122          mapper.delete(record);
125     @Transactional(rollbackFor = Exception.127         128                     mapper.delete(record);
130             count++132         134 
135      all operate. insert or update or delete
137     138     @Transactional(rollbackFor = Exception.139      T persist(T record) {
140         BaseDTO dto = (BaseDTO) record;
141         Assert.notNull(dto.get_operate(),"_operate not be null."switch (dto.get_operate()) {
case Constants.Operation.ADD:
                insert(record);
145                 break Constants.Operation.UPDATE:
                update(record);
148                 149              Constants.Operation.DELETE:
                delete(record);
151                 152             default:
153                 155         dto.set_operate(156         158 
159     @Transactional(rollbackFor = Exception.160     public List<T> persist(List<T>161         162             BaseDTO dto =163             Assert.notNull(dto.get_operate(),1)">165                                     insert(record);
167                     168                 169                     update(record);
170                     171                                     delete(record);
173                     174                 175                     177             dto.set_operate(182     @Transactional(rollbackFor = Exception.183      T persistSelective(T record) {
184         BaseDTO dto =185         Assert.notNull(dto.get_operate(),1)">186         187                             insertSelective(record);
189                 190                             updateSelective(record);
192                 193             194 195                 196             197                 199         201 
202     @Transactional(rollbackFor = Exception.203     public List<T> persistSelective(List<T>204         205             BaseDTO dto =206             Assert.notNull(dto.get_operate(),1)">207             208                                     insertSelective(record);
210                     211                 212                     updateSelective(record);
213                     214                 216                     217                 218                     225     226     227      T get(Long id) {
228         T entity = 230             entity = entityClass.newInstance();
231             Field idField = Reflections.getFieldByAnnotation(entityClass,Id.            idField.set(entity,id);
233         }  (Exception e) {
236 
 mapper.selectByPrimaryKey(entity);
 T get(T record) {
 mapper.selectOne(record);
 T get(String key,Object value) {
245         T entity = 246         247             entity =248             Field field = Reflections.getField(entityClass,key);
249             field.set(entity,value);
250         } 251 252 253 
254          mapper.selectOne(entity);
255 256 
257     public List<T> select(String ids) {
258          mapper.selectByIds(ids);
259 260 
261      select(T record) {
262 
263          mapper.select(record);
264 265 
266     267         T entity = 268         269             entity =270             Field field =271 272         } 273 275          mapper.select(entity);
276 277 
278     public List<T> select(T record,1)"> pageSize) {
279         PageHelper.startPage(pageNum,pageSize);
280         281 282 
283      selectAll() {
284          mapper.selectAll();
285 286 
287      count(T record) {
288          mapper.selectCount(record);
289 290 
291     292      * 检查乐观锁<br>
293      * 更新失败时,抛出 UpdateFailedException 异常
294 295  updateCount update,delete 操作返回的值
296  record 操作参数
297      298     protected void checkUpdate( updateCount,Object record) {
299         if (updateCount == 0 && record instanceof BaseDTO) {
300             BaseDTO baseDTO =301             if (baseDTO.getVersion() != 302                 throw  UpdateFailedException();
303 304 305 306 
307 }
View Code

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

 org.slf4j.Logger;
 org.slf4j.LoggerFactory;
  5 
 java.lang.reflect.Modifier;
 java.lang.reflect.ParameterizedType;
 java.lang.reflect.Type;
 * 反射工具类.
 bojiangzhou 2018-01-06
 17 
 Reflections {
static Logger logger = LoggerFactory.getLogger(Reflections. 21 
     * 通过反射,获得Class定义中声明的泛型参数的类型,注意泛型必须定义在父类处. 如无法找到,返回Object.class.
 clazz class类
 the 返回第一个声明的泛型类型. 如果没有,则返回Object.class
 28      static Class getClassGenericType(final Class clazz) {
 31         return getClassGenericType(clazz,0 32  index 获取第几个泛型参数的类型,默认从0开始,即第一个
 返回第index个泛型参数类型.
final Class clazz,1)"> index) {
 44         Type genType = clazz.getGenericSuperclass();
 45 
if (!(genType  ParameterizedType)) {
 47             return Object. 50         Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
 51 
if (index >= params.length || index < 0 53             logger.warn("Index: " + index + ",Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + params.length);
 54             if (!(params[index]  Class)) {
 57             logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter" 58              60 
 (Class) params[index];
 63 
 65      * 根据注解类型获取实体的Field
 66  entityClass 实体类型
 annotationClass 注解类型
 返回第一个有该注解类型的Field,如果没有则返回null.
 72       73     @SuppressWarnings("unchecked" Field getFieldByAnnotation(Class entityClass,Class annotationClass) {
 75         Field[] fields = entityClass.getDeclaredFields();
 76          (Field field : fields) {
 77             if (field.getAnnotation(annotationClass) !=                 makeAccessible(field);
 79                  field;
 86      * 获取实体的字段
 fieldName 字段名称
 该字段名称对应的字段,1)"> 93       Field getField(Class entityClass,String fieldName){
 96             Field field = entityClass.getDeclaredField(fieldName);
            makeAccessible(field);
 98              99         }  (NoSuchFieldException e) {
     * 改变private/protected的成员变量为public.
108       makeAccessible(Field field) {
if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
111             field.setAccessible(114 
115 }
View Code

7、获取AOP代理

Spring 只要引入aop则是默认开启事务的,一般我们只要在需要事务管理的地方加上@Transactional注解即可支持事务,一般我们会加在Service的类或者具体的增加、删除、更改的方法上。

我这里要说的是获取代理的问题。Service的事务管理是AOP实现的,AOP的实现用的是JDK动态代理或CGLIB动态代理。所以,如果你想在你的代理方法中以 this 调用当前接口的另一个方法,另一个方法的事务是不会起作用的。因为事务的方法是代理对象的,而 this 是当前类对象,不是一个代理对象,自然事务就不会起作用了。这是我在不久前的开发中遇到的实际问题,我自定义了一个注解,加在方法上,使用AspectJ来拦截该注解,却没拦截到,原因就是这个方法是被另一个方法以 this 的方式调用的,所以AOP不能起作用。

更详细的可参考:Spring AOP无法拦截内部方法调用

所以添加一个获取自身代理对象的接口,以方便获取代理对象来操作当前类方法。Service接口只需要继承该接口,T为接口本身即可,就可以通过self()获取自身的代理对象了。

 org.springframework.aop.framework.AopContext;
 * 获取代理对象本身.
 7  interface ProxySelf<T> 9          * 取得当前对象的代理.
 代理对象,如果未被代理,则抛出 IllegalStateException
13      14     @SuppressWarnings("unchecked" T self() {
 (T) AopContext.currentProxy();
18 }
View Code

还需要开启开启 exposeProxy = true,暴露代理对象,否则?AopContext.currentProxy() 会抛出异常。

8、数据持久化测试

① 实体映射

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

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

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

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

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

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

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

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

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

     com.lyyzoo.system.dto;
     javax.persistence.GeneratedValue;
     javax.persistence.GenerationType;
     javax.persistence.Table;
     * @name User
     22 @Table(name = "SYS_USER" 25 
        @Id
     30     @GeneratedValue(strategy = GenerationType.IDENTITY)
     47     @JsonFormat(pattern = 50  51       56     110     111         113 }

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

import javax.persistence.* 11 
 18   20 @Table(name = "SYS_USER" 28     @GeneratedValue(strategy = 29     @OrderBy("DESC" 33       41       45       46     @JsonFormat(pattern = 54       55      56 
113 
114 }
View Code

② 创建表结构

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

③ 创建UserMapper

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

 com.lyyzoo.core.system.mapper;
 com.lyyzoo.core.base.Mapper;
 * @name UserMapper
11  interface UserMapper extends Mapper<User>14 }
View Code

④ 创建UserService

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

 com.lyyzoo.core.system.service;
 com.lyyzoo.core.base.Service;
 * 用户Service接口
interface UserService extends Service<User>14 }
View Code

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

 com.lyyzoo.core.system.service.impl;
 org.springframework.stereotype.Service;
 com.lyyzoo.core.base.BaseService;
 com.lyyzoo.core.system.service.UserService;
 8 
 * 用户Service实现类
14  @Service
class UserServiceImpl extends BaseService<User>  UserService {
18 }
View Code

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

import org.springframework.web.bind.annotation.* javax.validation.Valid;
20  @RequestMapping
 UserService userService;
29     @PostMapping("/sys/user/queryAll"31         List<User> list = userService.selectAll();
 Results.successWithData(list,1)">35     @RequestMapping("/sys/user/queryOne/{userId}"37         User user = userService.get(userId);
38         40 
41     @PostMapping("/sys/user/save" Result save(@Valid @RequestBody User user){
43         user = userService.insertSelective(user);
47     @PostMapping("/sys/user/update"public Result update(@Valid @RequestBody List<User> user){
49         user = userService.persistSelective(user);
50         52 
53     @RequestMapping("/sys/user/delete"54      Result delete(User user){
        userService.delete(user);
 Results.success();
59     @RequestMapping("/sys/user/delete/{userId}" Result delete(@PathVariable Long userId){
        userService.delete(userId);
65 }
View Code

⑥ 测试结果

查询所有:

批量保存/修改:

9、代码生成器

使用代码生成器来生成基础的代码结构,生成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配置日志记录级别、日志输出文件目录等。

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

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

同时,将日志滚动输出到日志文件,保留历史记录。可通过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异常。

 org.springframework.http.HttpStatus;
 org.springframework.web.bind.annotation.ExceptionHandler;
 org.springframework.web.bind.annotation.RestControllerAdvice;
 org.springframework.web.servlet.NoHandlerFoundException;
 9 
 com.lyyzoo.core.constants.BaseEnums;
 com.lyyzoo.core.exception.AuthorityException;
 com.lyyzoo.core.exception.BaseException;
 com.lyyzoo.core.exception.ServiceException;
 * 全局异常处理
 bojiangzhou 2018-02-06
@RestControllerAdvice
 GlobalExceptionConfig {
final Logger logger = LoggerFactory.getLogger(GlobalExceptionConfig.     * 处理 ServiceException 异常
30      31     @ExceptionHandler(ServiceException. Result handleServiceException(ServiceException e){
33         Result result = Results.failure(e.getCode(),e.getMessage());
        result.setStatus(HttpStatus.BAD_REQUEST.value());
35         logger.info("ServiceException[code: {},message: {}]" result;
     * 处理 AuthorityException 异常
41      42     @ExceptionHandler(AuthorityException. Result handleAuthorityException(AuthorityException e){
44         Result result = Results.failure(BaseEnums.FORBIDDEN.code(),BaseEnums.FORBIDDEN.desc());
        result.setStatus(HttpStatus.FORBIDDEN.value());
46         logger.info("AuthorityException[code: {},1)">47              * 处理 NoHandlerFoundException 异常. <br/>
     * 需配置 [spring.mvc.throw-exception-if-no-handler-found=true]
     * 需配置 [spring.resources.add-mappings=false]
54      55     @ExceptionHandler(NoHandlerFoundException. Result handleNotFoundException(NoHandlerFoundException e){
57         Result result = Results.failure(BaseEnums.NOT_FOUND.code(),BaseEnums.NOT_FOUND.desc());
        result.setStatus(HttpStatus.NOT_FOUND.value());
        logger.info(e.getMessage());
60         62 
     * 处理 BaseException 异常
65      66     @ExceptionHandler(BaseException. Result handleBaseException(BaseException e){
68         Result result =69         result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
70         logger.error("BaseException[code: {},e.getMessage(),e);
     * 处理 Exception 异常
76      77     @ExceptionHandler(Exception. Result handleException(Exception e){
79         Result result = Results.failure(BaseEnums.ERROR.code(),BaseEnums.ERROR.desc());
81         logger.error(e.getMessage(),1)">82         83 84 
85 }
View Code

看上面的代码,@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插件:

 com.lyyzoo.core.plugins;
 net.sf.jsqlparser.expression.Expression;
 net.sf.jsqlparser.expression.LongValue;
 net.sf.jsqlparser.expression.operators.arithmetic.Addition;
 net.sf.jsqlparser.expression.operators.conditional.AndExpression;
 net.sf.jsqlparser.expression.operators.relational.EqualsTo;
 net.sf.jsqlparser.parser.CCJSqlParserUtil;
 net.sf.jsqlparser.schema.Column;
 net.sf.jsqlparser.statement.Statement;
 net.sf.jsqlparser.statement.update.Update;
 org.apache.ibatis.executor.statement.StatementHandler;
 org.apache.ibatis.mapping.BoundSql;
 org.apache.ibatis.mapping.MappedStatement;
 org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.* org.apache.ibatis.reflection.MetaObject;
 org.apache.ibatis.reflection.SystemMetaObject;
 java.lang.reflect.Proxy;
 java.sql.Connection;
 java.util.Properties;
 * 乐观锁:数据版本插件
 bojiangzhou 2018-02-10
 32  @Intercepts(
    @Signature(
 35         type = StatementHandler. 36         method = "prepare" 37         args = {Connection.class,Integer.}
    )
class VersionInterceptor  Interceptor {
final String VERSION_COLUMN_NAME = "version"final Logger logger = LoggerFactory.getLogger(VersionInterceptor.public Object intercept(Invocation invocation) throws Throwable {
 48          获取 StatementHandler,实际是 RoutingStatementHandler
 49         StatementHandler handler = (StatementHandler) processTarget(invocation.getTarget());
 包装原始对象,便于获取和设置属性
 51         MetaObject metaObject = SystemMetaObject.forObject(handler);
 MappedStatement 是对SQL更高层次的一个封装,这个对象包含了执行SQL所需的各种配置信息
 53         MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement" SQL类型
 55         SqlCommandType sqlType = ms.getSqlCommandType();
if(sqlType != SqlCommandType.UPDATE) {
 57              invocation.proceed();
 获取版本号
 60         Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_COLUMN_NAME);
if(originalVersion == null || Long.valueOf(originalVersion.toString()) <= 0){
 62              64          获取绑定的SQL
 65         BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql" 原始SQL
 67         String originalSql = boundSql.getSql();
 加入version的SQL
 69         originalSql = addVersionToSql(originalSql,originalVersion);
 修改 BoundSql
 71         metaObject.setValue("delegate.boundSql.sql" proceed() 可以执行被拦截对象真正的方法,该方法实际上执行了method.invoke(target,args)方法
     * Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象.
 target 被拦截的对象
 代理对象
 82       Object plugin(Object target) {
 85         return Plugin.wrap(target,1)"> 87 
 88          * 设置参数
 90       setProperties(Properties properties) {
 94  95 
     * 获取代理的原始对象
 target
101       Object processTarget(Object target) {
(Proxy.isProxyClass(target.getClass())) {
104             MetaObject mo = SystemMetaObject.forObject(target);
105             return processTarget(mo.getValue("h.target" target;
     * 为原SQL添加version
 originalSql 原SQL
 originalVersion 原版本号
115  加入version的SQL
116       String addVersionToSql(String originalSql,Object originalVersion){
118         {
119             Statement stmt = CCJSqlParserUtil.parse(originalSql);
120             if(!(stmt  Update)){
121                  originalSql;
123             Update update = (Update)stmt;
124             if(!contains(update)){
125                 buildVersionExpression(update);
126 127             Expression where = update.getWhere();
128             if(where != 129                 AndExpression and =  AndExpression(where,buildVersionEquals(originalVersion));
                update.setWhere(and);
131             }else                update.setWhere(buildVersionEquals(originalVersion));
134              stmt.toString();
135         }(Exception e){
            logger.error(e.getMessage(),1)">137             140 
 contains(Update update){
142         List<Column> columns = update.getColumns();
(Column column : columns){
144             (column.getColumnName().equalsIgnoreCase(VERSION_COLUMN_NAME)){
148         150 
151      buildVersionExpression(Update update){
152          列 version
153         Column versionColumn =  Column();
        versionColumn.setColumnName(VERSION_COLUMN_NAME);
        update.getColumns().add(versionColumn);
156 
157          值 version+1
158         Addition add =  Addition();
        add.setLeftExpression(versionColumn);
160         add.setRightExpression(new LongValue(1161         update.getExpressions().add(add);
162 163 
 Expression buildVersionEquals(Object originalVersion){
165         Column column =         column.setColumnName(VERSION_COLUMN_NAME);
167 
 条件 version = originalVersion
169         EqualsTo equal =  EqualsTo();
        equal.setLeftExpression(column);
171         equal.setRightExpression( LongValue(originalVersion.toString()));
172          equal;
174 
175 }
View Code

之后还需配置该插件,只需要在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中添加如下配置即可,大部分配置是默认配置,可更改。有详细的注释,比较容易理解。

####################################
# Druid
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# 初始化连接大小[0]
spring.datasource.druid.initial-size=1
# 最小空闲连接数[0]
spring.datasource.druid.min-idle=1
# 最大连接数[8]
spring.datasource.druid.max-active=20
# 配置获取连接等待超时的时间(毫秒)[-1]
spring.datasource.druid.max-wait=60000
# 查询超时时间(秒)
spring.datasource.druid.query-timeout=90
# 用来检测连接是否有效的sql,要求是一个查询语句
spring.datasource.druid.validation-query=SELECT 'x'
# 申请连接时检测连接可用性[false]
spring.datasource.druid.test-on-borrow=false
# 归还连接检测[false]
spring.datasource.druid.test-on-return=false
# 超时是否检测连接可用性[true]
spring.datasource.druid.test-while-idle=true
28 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接 (毫秒)
spring.datasource.druid.time-between-eviction-runs-millis=60000
#  配置一个连接在池中最小生存的时间(毫秒,默认30分钟)
31 spring.datasource.druid.min-evictable-idle-time-millis=300000
32 # 通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat;日志用的filter:log4j;防御sql注入的filter:wall
spring.datasource.druid.filters=stat,wall,slf4j
# 合并多个DruidDataSource的监控数据
35 spring.datasource.druid.use-global-data-source-stat=true
36 
# 是否缓存PreparedStatement. PSCache对支持游标的数据库性能提升巨大,比如说oracle.在mysql下建议关闭.
spring.datasource.druid.pool-prepared-statements=false
# 每个连接上PSCache的大小
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]
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
# 监控页面的用户名和密码
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
spring.datasource.druid.stat-view-servlet.reset-enable=false
# StatFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter]
spring.datasource.druid.filter.stat.db-type=mysql
#慢SQL记录
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=2000
# SQL合并
spring.datasource.druid.filter.stat.merge-sql=false
57 
# WallFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE-wallfilter]
spring.datasource.druid.filter.wall.enabled=true
60 spring.datasource.druid.filter.wall.db-type=mysql
spring.datasource.druid.filter.wall.config.delete-allow=false
62 spring.datasource.druid.filter.wall.config.drop-table-allow=false
View Code

之后启动项目在地址栏输入/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数据结构相关的操作,这样比较方便使用。

 com.lyyzoo.cache.redis;
 org.springframework.cache.annotation.EnableCaching;
 org.springframework.data.redis.cache.RedisCacheManager;
 org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.* org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
 org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
 com.fasterxml.jackson.annotation.JsonAutoDetect;
 com.fasterxml.jackson.annotation.PropertyAccessor;
 com.fasterxml.jackson.databind.ObjectMapper;
 * Redis配置.
 * 使用@EnableCaching开启声明式缓存支持. 之后就可以使用 @Cacheable/@CachePut/@CacheEvict 注解缓存数据.
 bojiangzhou 2018-02-11
24  @EnableCaching
 RedisConfig {
 RedisConnectionFactory redisConnectionFactory;
 Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder;
32 
33          * 覆盖默认配置 RedisTemplate,使用 String 类型作为key,设置key/value的序列化规则
35      37     @SuppressWarnings("unchecked"public RedisTemplate<String,Object> redisTemplate() {
39         RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>        redisTemplate.setConnectionFactory(redisConnectionFactory);
 使用 Jackson2JsonRedisSerialize 替换默认序列化
43         Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.44         ObjectMapper objectMapper = jackson2ObjectMapperBuilder.createXmlMapper().build();
        objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
48 
49          设置value的序列化规则和key的序列化规则
        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
 redisTemplate;
public HashOperations<String,String,Object> hashOperations(RedisTemplate<String,1)"> redisTemplate) {
61          redisTemplate.opsForHash();
65     public ValueOperations<String,String> valueOperations(RedisTemplate<String,1)"> redisTemplate.opsForValue();
67 public ListOperations<String,Object> listOperations(RedisTemplate<String,1)"> redisTemplate.opsForList();
74 75     public SetOperations<String,Object> setOperations(RedisTemplate<String,1)">76          redisTemplate.opsForSet();
77 78 
79 80     public ZSetOperations<String,Object> zSetOperations(RedisTemplate<String,1)">81          redisTemplate.opsForZSet();
82 83 
85      RedisCacheManager cacheManager() {
86         RedisCacheManager cacheManager =  RedisCacheManager(redisTemplate());
87         cacheManager.setUsePrefix(88          cacheManager;
89 90 
91 }
View Code

同时,使用@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 学习(二) —— 数据类型及操作

 org.springframework.beans.factory.annotation.Value;
 org.springframework.data.redis.connection.DataType;
 org.springframework.stereotype.Component;
 java.util.Collection;
 java.util.Set;
 java.util.concurrent.TimeUnit;
 java.util.stream.Collectors;
 java.util.stream.Stream;
 * Redis 操作工具
 bojiangzhou 2018-02-12
@Component
 RedisOperator {
private RedisTemplate<String,1)">private ValueOperations<String,1)"> valueOperator;
private HashOperations<String,1)"> hashOperator;
private ListOperations<String,1)"> listOperator;
private SetOperations<String,1)"> setOperator;
private ZSetOperations<String,1)"> zSetOperator;
 37 
     * 默认过期时长,单位:秒
 40      long DEFAULT_EXPIRE = 60 * 60 * 24 42 
/** 不设置过期时长 long NOT_EXPIRE = -1     * Redis的根操作路径
 48       49     @Value("${redis.root:sunny}" String category;
 RedisOperator setCategory(String category) {
this.category = category;
     * 获取Key的全路径
 key key
 full key
 62       63      String getFullKey(String key) {
this.category + ":" + key;
 66 
 ------------------------------------------------------------------------------
     * 判断key是否存在
     * <p>
     * <i>exists key</i>
 78       79      existsKey(String key) {
 80          redisTemplate.hasKey(getFullKey(key));
 82 
 83          * 判断key存储的值类型
     * <i>type key</i>
 DataType[string、list、set、zset、hash]
 91       DataType typeKey(String key){
 93          redisTemplate.type(getFullKey(key));
     * 重命名key. 如果newKey已经存在,则newKey的原值被覆盖
     * <i>rename oldKey newKey</i>
 oldKey oldKeys
 newKey newKey
104       renameKey(String oldKey,String newKey){
        redisTemplate.rename(getFullKey(oldKey),getFullKey(newKey));
     * newKey不存在时才重命名.
     * <i>renamenx oldKey newKey</i>
 oldKey oldKey
 修改成功返回true
118      119      renameKeyNx(String oldKey,1)">120          redisTemplate.renameIfAbsent(getFullKey(oldKey),1)">122 
124      * 删除key
     * <i>del key</i>
130      131      deleteKey(String key){
        redisTemplate.delete(key);
     * <i>del key1 key2 ...</i>
141  keys 可传入多个key
142      143      deleteKey(String ... keys){
144         Set<String> ks = Stream.of(keys).map(k -> getFullKey(k)).collect(Collectors.toSet());
        redisTemplate.delete(ks);
147 
148      keys key集合
155      156     void deleteKey(Collection<String> keys){
157         Set<String> ks = keys.stream().map(k ->     * 设置key的生命周期,单位秒
163      * <i>expire key seconds</i><br>
     * <i>pexpire key milliseconds</i>
 time 时间数
 timeUnit TimeUnit 时间单位
171      void expireKey(String key,1)">long time,TimeUnit timeUnit){
        redisTemplate.expire(key,time,timeUnit);
176     177      * 设置key在指定的日期过期
179      * <i>expireat key timestamp</i>
 date 指定日期
184      185      expireKeyAt(String key,Date date){
        redisTemplate.expireAt(key,date);
187      * 查询key的生命周期
     * <i>ttl key</i>
195 197  指定时间单位的时间数
198       getKeyExpire(String key,1)"> redisTemplate.getExpire(key,1)">204      * 将key设置为永久有效
     * <i>persist key</i>
210      211      persistKey(String key){
        redisTemplate.persist(key);
213 214 
 RedisTemplate
219       getRedisTemplate() {
 ValueOperations
227       getValueOperator() {
 HashOperations
235       getHashOperator() {
241  ListOperations
243       getListOperator() {
 SetOperations
251       getSetOperator() {
257  ZSetOperations
259       getZSetOperator() {
264 }
View Code

?

八、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资源:

 com.lyyzoo.swagger.config;
 org.springframework.context.annotation.PropertySource;
 org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 com.spring4all.swagger.EnableSwagger2Doc;
 bojiangzhou 2018-02-19
@EnableSwagger2Doc
16 @PropertySource(value = "classpath:application-swagger.properties"class SunnySwaggerConfig  WebMvcConfigurerAdapter {
     * 注入swagger资源文件
20       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/"29 }
View Code

3、使用

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

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

 com.lyyzoo.admin.system.controller;
 com.lyyzoo.admin.system.dto.Menu;
 com.lyyzoo.admin.system.service.MenuService;
 io.swagger.annotations.Api;
 io.swagger.annotations.ApiImplicitParam;
 io.swagger.annotations.ApiOperation;
 io.swagger.annotations.ApiParam;
17 @Api(tags = "菜单管理"class MenuController  MenuService service;
     * 查找单个用户
 menuId 菜单ID
 Result
31     @ApiOperation("查找单个用户"32     @ApiImplicitParam(name = "menuId",value = "菜单ID",paramType = "path"33     @GetMapping("/sys/menu/get/{menuId}" Result get(@PathVariable Long menuId){
35         Menu menu = service.selectById(menuId);
 Results.successWithData(menu);
     * 保存菜单
 menu 菜单
44      45     @ApiOperation("保存菜单"46     @PostMapping("/sys/menu/save"public Result save(@ApiParam(name = "menu",value = "菜单")@RequestBody Menu menu){
48         menu = service.save(menu);
51 
     * 删除菜单
57      58     @ApiOperation("删除菜单"59     @ApiImplicitParam(name = "menuId",1)">60     @PostMapping("/sys/menu/delete/{menuId}" Result delete(@PathVariable Long menuId){
        service.deleteById(menuId);
66 }
View Code

之后访问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。

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"modelVersion>4.0.0>com.lyyzoo 7     >sunny 8     >0.0.1-SNAPSHOTpackaging>pom>Sunnydescription>Lyyzoo Base Application development platformparent>org.springframework.boot>spring-boot-starter-parent>1.5.10.RELEASE18         relativePath20 
properties22         project.build.sourceEncodingproject.reporting.outputEncodingjava.version>1.8sunny.version27         springboot.versionmodulesmodule>sunny-starter>sunny-starter-core>sunny-starter-cache34         >sunny-starter-security>sunny-starter-admin>sunny-starter-swagger37     buildplugins41             plugin42                 43                 >spring-boot-maven-plugin44             45         47 
project>
View Code

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

 7          8          9         >com.lyyzoo.parent>jar>Sunny Parentdependencies core 22             >com.lyyzoo.core23             24             >${sunny.version} cache 28             >com.lyyzoo.cache29             30              security >com.lyyzoo.security35             36             37          admin 39         40             >com.lyyzoo.admin42             43          swagger 46             >com.lyyzoo.swagger47             48             57                 58             59         >
View Code

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

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

2、开发规范

?

十、结语

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

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

?

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

?

-----

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读