基於SpringBoot搭建應用開發框架(一) —— 基礎架構


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</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>
View Code

版本號如下:

② 在base添加一個Result類,作為前端的返回對象,Controller的直接返回對象都是Result。

  1 package com.lyyzoo.core.base;
  2 
  3 import com.fasterxml.jackson.annotation.JsonInclude;
  4 
  5 import 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     private boolean success = true;
 20     /**
 21      * 返回狀態碼,為空則默認200.前端需要攔截一些常見的狀態碼如403、404、500等
 22      */
 23     @JsonInclude(JsonInclude.Include.NON_NULL)
 24     private Integer status;
 25     /**
 26      * 編碼,可用於前端處理多語言,不需要則不用返回編碼
 27      */
 28     @JsonInclude(JsonInclude.Include.NON_NULL)
 29     private String code;
 30     /**
 31      * 相關消息
 32      */
 33     @JsonInclude(JsonInclude.Include.NON_NULL)
 34     private String msg;
 35     /**
 36      * 相關數據
 37      */
 38     @JsonInclude(JsonInclude.Include.NON_NULL)
 39     private Object data;
 40 
 41 
 42     public Result() {}
 43 
 44     public Result(boolean success) {
 45         this.success = success;
 46     }
 47 
 48     public Result(boolean success, Integer status) {
 49         this.success = success;
 50         this.status = status;
 51     }
 52 
 53     public Result(boolean success, String code, String msg){
 54         this(success);
 55         this.code = code;
 56         this.msg = msg;
 57     }
 58 
 59     public Result(boolean success, Integer status, String code, String msg) {
 60         this.success = success;
 61         this.status = status;
 62         this.code = code;
 63         this.msg = msg;
 64     }
 65 
 66     public Result(boolean success, String code, String msg, Object data){
 67         this(success);
 68         this.code = code;
 69         this.msg = msg;
 70         this.data = data;
 71     }
 72 
 73     public boolean isSuccess() {
 74         return success;
 75     }
 76 
 77     public void setSuccess(boolean success) {
 78         this.success = success;
 79     }
 80 
 81     public Integer getStatus() {
 82         return status;
 83     }
 84 
 85     public void setStatus(Integer status) {
 86         this.status = status;
 87     }
 88 
 89     public String getCode() {
 90         return code;
 91     }
 92 
 93     public void setCode(String code) {
 94         this.code = code;
 95     }
 96 
 97     public String getMsg() {
 98         return msg;
 99     }
100 
101     public void setMsg(String msg) {
102         this.msg = msg;
103     }
104 
105     public Object getData() {
106         return data;
107     }
108 
109     public void setData(Object data) {
110         this.data = data;
111     }
112 }
View Code

之后在util添加生成Result的工具類Results,用於快速方便的創建Result對象。

 1 package com.lyyzoo.core.util;
 2 
 3 import com.lyyzoo.core.base.Result;
 4 
 5 /**
 6  * Result生成工具類
 7  *
 8  * @version 1.0
 9  * @author bojiangzhou 2017-12-28
10  */
11 public class Results {
12 
13     protected Results() {}
14 
15     public static Result newResult() {
16         return new Result();
17 
18     }
19 
20     public static Result newResult(boolean success) {
21         return new Result(success);
22     }
23 
24     //
25     // 業務調用成功
26     // ----------------------------------------------------------------------------------------------------
27     public static Result success() {
28         return new Result();
29     }
30 
31     public static Result success(String msg) {
32         return new Result(true, null, msg);
33     }
34 
35     public static Result success(String code, String msg) {
36         return new Result(true, code, msg);
37     }
38 
39     public static Result successWithStatus(Integer status) {
40         return new Result(true, status);
41     }
42 
43     public static Result successWithStatus(Integer status, String msg) {
44         return new Result(true, status, null, msg);
45     }
46 
47     public static Result successWithData(Object data) {
48         return new Result(true, null, null, data);
49     }
50 
51     public static Result successWithData(Object data, String msg) {
52         return new Result(true, null, msg, data);
53     }
54 
55     public static Result successWithData(Object data, String code, String msg) {
56         return new Result(true, code, msg, data);
57     }
58 
59     //
60     // 業務調用失敗
61     // ----------------------------------------------------------------------------------------------------
62     public static Result failure() {
63         return new Result(false);
64     }
65 
66     public static Result failure(String msg) {
67         return new Result(false, null, msg);
68     }
69 
70     public static Result failure(String code, String msg) {
71         return new Result(false, code, msg);
72     }
73 
74     public static Result failureWithStatus(Integer status) {
75         return new Result(false, status);
76     }
77 
78     public static Result failureWithStatus(Integer status, String msg) {
79         return new Result(false, status, null, msg);
80     }
81 
82     public static Result failureWithData(Object data) {
83         return new Result(false, null, null, data);
84     }
85 
86     public static Result failureWithData(Object data, String msg) {
87         return new Result(false, null, msg, data);
88     }
89 
90     public static Result failureWithData(Object data, String code, String msg) {
91         return new Result(false, code, msg, data);
92     }
93 
94 }
View Code

③ 在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 }
View Code

然后在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 }
View Code

④ 再添加一個常用的日期工具類對象,主要包含一些常用的日期時間格式化,后續可再繼續往里面添加一些公共方法。

  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 }
View Code

⑤ 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 }
View Code

⑥ 在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 }
View Code

同時,重寫了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 }
View Code

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 }
View Code

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 }
View Code

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 }
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,按需選擇接口。

具體可參考:根據需要自定義接口

 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 }
View Code

定義好基礎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 }
View Code

注意這里的 MapperScannerConfigurertk.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 }
View Code

然后是實現類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.Constants;
  5 import com.lyyzoo.core.exception.UpdateFailedException;
  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  * @version 1.0
 20  * @author bojiangzhou 2018-01-04
 21  */
 22 public abstract class BaseService<T> implements Service<T> {
 23 
 24     @Autowired
 25     private Mapper<T> mapper;
 26 
 27     private Class<T> entityClass;
 28 
 29     @SuppressWarnings("unchecked")
 30     @PostConstruct
 31     public void init() {
 32         this.entityClass = Reflections.getClassGenericType(getClass());
 33     }
 34 
 35     //
 36     // insert
 37     // ----------------------------------------------------------------------------------------------------
 38     @Transactional(rollbackFor = Exception.class)
 39     public T insert(T record) {
 40         mapper.insert(record);
 41         return record;
 42     }
 43 
 44     @Transactional(rollbackFor = Exception.class)
 45     public List<T> insert(List<T> recordList) {
 46         mapper.insertList(recordList);
 47         return recordList;
 48     }
 49 
 50     @Transactional(rollbackFor = Exception.class)
 51     public T insertSelective(T record) {
 52         mapper.insertSelective(record);
 53         return record;
 54     }
 55 
 56     @Transactional(rollbackFor = Exception.class)
 57     public List<T> insertSelective(List<T> recordList) {
 58         // 由於Mapper暫未提供Selective的批量插入,此處循環查詢. 當然也可參考InsertListMapper自己實現.
 59         for(T record : recordList){
 60             mapper.insertSelective(record);
 61         }
 62         return recordList;
 63     }
 64 
 65     //
 66     // update
 67     // ----------------------------------------------------------------------------------------------------
 68     @Transactional(rollbackFor = Exception.class)
 69     public T update(T record) {
 70         int count = mapper.updateByPrimaryKey(record);
 71         checkUpdate(count, record);
 72         return record;
 73     }
 74 
 75     @Transactional(rollbackFor = Exception.class)
 76     public List<T> update(List<T> recordList) {
 77         // Mapper暫未提供批量更新,此處循實現
 78         for(T record : recordList){
 79             int count = mapper.updateByPrimaryKey(record);
 80             checkUpdate(count, record);
 81         }
 82         return recordList;
 83     }
 84 
 85     @Transactional(rollbackFor = Exception.class)
 86     public T updateSelective(T record) {
 87         int count = mapper.updateByPrimaryKeySelective(record);
 88         checkUpdate(count, record);
 89         return record;
 90     }
 91 
 92     @Transactional(rollbackFor = Exception.class)
 93     public List<T> updateSelective(List<T> recordList) {
 94         // Mapper暫未提供批量更新,此處循實現
 95         for(T record : recordList){
 96             int count = mapper.updateByPrimaryKeySelective(record);
 97             checkUpdate(count, record);
 98         }
 99         return recordList;
100     }
101 
102     //
103     // delete
104     // ----------------------------------------------------------------------------------------------------
105     @Transactional(rollbackFor = Exception.class)
106     public int delete(Long id) {
107         return mapper.deleteByPrimaryKey(id);
108     }
109 
110     @Transactional(rollbackFor = Exception.class)
111     public int delete(Long[] ids) {
112         int count = 0;
113         for(Long id : ids){
114             mapper.deleteByPrimaryKey(id);
115             count++;
116         }
117         return count;
118     }
119 
120     @Transactional(rollbackFor = Exception.class)
121     public int delete(T record) {
122         return mapper.delete(record);
123     }
124 
125     @Transactional(rollbackFor = Exception.class)
126     public int delete(List<T> recordList) {
127         int count = 0;
128         for(T record : recordList){
129             mapper.delete(record);
130             count++;
131         }
132         return count;
133     }
134 
135     //
136     // all operate. insert or update or delete
137     // ----------------------------------------------------------------------------------------------------
138     @Transactional(rollbackFor = Exception.class)
139     public T persist(T record) {
140         BaseDTO dto = (BaseDTO) record;
141         Assert.notNull(dto.get_operate(), "_operate not be null.");
142         switch (dto.get_operate()) {
143             case Constants.Operation.ADD:
144                 insert(record);
145                 break;
146             case Constants.Operation.UPDATE:
147                 update(record);
148                 break;
149             case Constants.Operation.DELETE:
150                 delete(record);
151                 break;
152             default:
153                 break;
154         }
155         dto.set_operate(null);
156         return record;
157     }
158 
159     @Transactional(rollbackFor = Exception.class)
160     public List<T> persist(List<T> recordList) {
161         for(T record : recordList){
162             BaseDTO dto = (BaseDTO) record;
163             Assert.notNull(dto.get_operate(), "_operate not be null.");
164             switch (dto.get_operate()) {
165                 case Constants.Operation.ADD:
166                     insert(record);
167                     break;
168                 case Constants.Operation.UPDATE:
169                     update(record);
170                     break;
171                 case Constants.Operation.DELETE:
172                     delete(record);
173                     break;
174                 default:
175                     break;
176             }
177             dto.set_operate(null);
178         }
179         return recordList;
180     }
181 
182     @Transactional(rollbackFor = Exception.class)
183     public T persistSelective(T record) {
184         BaseDTO dto = (BaseDTO) record;
185         Assert.notNull(dto.get_operate(), "_operate not be null.");
186         switch (dto.get_operate()) {
187             case Constants.Operation.ADD:
188                 insertSelective(record);
189                 break;
190             case Constants.Operation.UPDATE:
191                 updateSelective(record);
192                 break;
193             case Constants.Operation.DELETE:
194                 delete(record);
195                 break;
196             default:
197                 break;
198         }
199         return record;
200     }
201 
202     @Transactional(rollbackFor = Exception.class)
203     public List<T> persistSelective(List<T> recordList) {
204         for(T record : recordList){
205             BaseDTO dto = (BaseDTO) record;
206             Assert.notNull(dto.get_operate(), "_operate not be null.");
207             switch (dto.get_operate()) {
208                 case Constants.Operation.ADD:
209                     insertSelective(record);
210                     break;
211                 case Constants.Operation.UPDATE:
212                     updateSelective(record);
213                     break;
214                 case Constants.Operation.DELETE:
215                     delete(record);
216                     break;
217                 default:
218                     break;
219             }
220         }
221         return recordList;
222     }
223 
224     //
225     // select
226     // ----------------------------------------------------------------------------------------------------
227     public T get(Long id) {
228         T entity = null;
229         try {
230             entity = entityClass.newInstance();
231             Field idField = Reflections.getFieldByAnnotation(entityClass, Id.class);
232             idField.set(entity, id);
233         } catch (Exception e) {
234             e.printStackTrace();
235         }
236 
237         return mapper.selectByPrimaryKey(entity);
238     }
239 
240     public T get(T record) {
241         return mapper.selectOne(record);
242     }
243 
244     public T get(String key, Object value) {
245         T entity = null;
246         try {
247             entity = entityClass.newInstance();
248             Field field = Reflections.getField(entityClass, key);
249             field.set(entity, value);
250         } catch (Exception e) {
251             e.printStackTrace();
252         }
253 
254         return mapper.selectOne(entity);
255     }
256 
257     public List<T> select(String ids) {
258         return mapper.selectByIds(ids);
259     }
260 
261     public List<T> select(T record) {
262 
263         return mapper.select(record);
264     }
265 
266     public List<T> select(String key, Object value) {
267         T entity = null;
268         try {
269             entity = entityClass.newInstance();
270             Field field = Reflections.getField(entityClass, key);
271             field.set(entity, value);
272         } catch (Exception e) {
273             e.printStackTrace();
274         }
275         return mapper.select(entity);
276     }
277 
278     public List<T> select(T record, int pageNum, int pageSize) {
279         PageHelper.startPage(pageNum, pageSize);
280         return mapper.select(record);
281     }
282 
283     public List<T> selectAll() {
284         return mapper.selectAll();
285     }
286 
287     public int count(T record) {
288         return mapper.selectCount(record);
289     }
290 
291     /**
292      * 檢查樂觀鎖<br>
293      * 更新失敗時,拋出 UpdateFailedException 異常
294      *
295      * @param updateCount update,delete 操作返回的值
296      * @param record 操作參數
297      */
298     protected void checkUpdate(int updateCount, Object record) {
299         if (updateCount == 0 && record instanceof BaseDTO) {
300             BaseDTO baseDTO = (BaseDTO) record;
301             if (baseDTO.getVersion() != null) {
302                 throw new UpdateFailedException();
303             }
304         }
305     }
306 
307 }
View Code

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 }
View Code

7、獲取AOP代理

Spring 只要引入aop則是默認開啟事務的,一般我們只要在需要事務管理的地方加上@Transactional注解即可支持事務,一般我們會加在Service的類或者具體的增加、刪除、更改的方法上。

我這里要說的是獲取代理的問題。Service的事務管理是AOP實現的,AOP的實現用的是JDK動態代理或CGLIB動態代理。所以,如果你想在你的代理方法中以 this 調用當前接口的另一個方法,另一個方法的事務是不會起作用的。因為事務的方法是代理對象的,而 this 是當前類對象,不是一個代理對象,自然事務就不會起作用了。這是我在不久前的開發中遇到的實際問題,我自定義了一個注解,加在方法上,使用AspectJ來攔截該注解,卻沒攔截到,原因就是這個方法是被另一個方法以 this 的方式調用的,所以AOP不能起作用。

更詳細的可參考:Spring AOP無法攔截內部方法調用

所以添加一個獲取自身代理對象的接口,以方便獲取代理對象來操作當前類方法。Service接口只需要繼承該接口,T為接口本身即可,就可以通過self()獲取自身的代理對象了。

 1 package com.lyyzoo.core.base;
 2 
 3 import org.springframework.aop.framework.AopContext;
 4 
 5 /**
 6  * 獲取代理對象本身.
 7  */
 8 public interface ProxySelf<T> {
 9     /**
10      * 取得當前對象的代理.
11      * 
12      * @return 代理對象,如果未被代理,則拋出 IllegalStateException
13      */
14     @SuppressWarnings("unchecked")
15     default T self() {
16         return (T) AopContext.currentProxy();
17     }
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,而且無法消除,所以實體類中建議不要使用基本類型。

     1 package com.lyyzoo.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.GeneratedValue;  9 import javax.persistence.GenerationType;  10 import javax.persistence.Id;  11 import javax.persistence.Table;  12 import java.util.Date;  13 
     14 /**
     15  * 系統用戶  16  *  17  * @name User  18  * @version 1.0  19  * @author bojiangzhou 2017-12-31  20  */
     21 @JsonInclude(JsonInclude.Include.NON_NULL)  22 @Table(name = "SYS_USER")  23 public class User extends BaseDTO {  24     private static final long serialVersionUID = -7395431342743009038L;  25 
     26     /**
     27  * 用戶ID  28      */
     29  @Id  30     @GeneratedValue(strategy = GenerationType.IDENTITY)  31     private Long userId;  32     /**
     33  * 用戶名  34      */
     35     private String username;  36     /**
     37  * 密碼  38      */
     39     private String password;  40     /**
     41  * 昵稱  42      */
     43     private String nickname;  44     /**
     45  * 生日  46      */
     47     @JsonFormat(pattern = Dates.Pattern.DATE)  48     private Date birthday;  49     /**
     50  * 性別:1-男/0-女  51      */
     52     private Integer sex;  53     /**
     54  * 是否啟用:1/0  55      */
     56     private Integer enabled;  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 }

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 }
View Code

② 創建表結構

 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='系統用戶';
View Code

③ 創建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 }
View Code

④ 創建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 }
View Code

在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 }
View Code

⑤ 修改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 }
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配置日志記錄級別、日志輸出文件目錄等。

 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>
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異常。

 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 }
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插件:

  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 }
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中添加如下配置即可,大部分配置是默認配置,可更改。有詳細的注釋,比較容易理解。

 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
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數據結構相關的操作,這樣比較方便使用。

 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 }
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 學習(二) —— 數據類型及操作

  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 }
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資源:

 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 }
View Code

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 }
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。

 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>
View Code

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>
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技術,開發用戶登錄及權限控制等。

 

-----


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM