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>
版本號如下:
② 在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 }
之后在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 }
③ 在base添加BaseEnum<K, V>枚舉接口,定義了獲取值和描述的接口。

1 package com.lyyzoo.core.base; 2 3 /** 4 * 基礎枚舉接口 5 * 6 * @version 1.0 7 * @author bojiangzhou 2017-12-31 8 */ 9 public interface BaseEnum<K, V> { 10 11 /** 12 * 獲取編碼 13 * 14 * @return 編碼 15 */ 16 K code(); 17 18 /** 19 * 獲取描述 20 * 21 * @return 描述 22 */ 23 V desc(); 24 25 }
然后在constants下定義一個基礎枚舉常量類,我們把一些描述信息維護到枚舉里面,盡量不要在代碼中直接出現魔法值(如一些編碼、中文等),以后的枚舉常量類也可以按照這種模式來寫。

1 package com.lyyzoo.core.constants; 2 3 import com.lyyzoo.core.base.BaseEnum; 4 5 import java.util.HashMap; 6 import java.util.Map; 7 8 /** 9 * 基礎枚舉值 10 * 11 * @version 1.0 12 * @author bojiangzhou 2018-01-01 13 */ 14 public enum BaseEnums implements BaseEnum<String, String> { 15 16 SUCCESS("request.success", "請求成功"), 17 18 FAILURE("request.failure", "請求失敗"), 19 20 OPERATION_SUCCESS("operation.success", "操作成功"), 21 22 OPERATION_FAILURE("operation.failure", "操作失敗"), 23 24 ERROR("system.error", "系統異常"), 25 26 NOT_FOUND("not_found", "請求資源不存在"), 27 28 FORBIDDEN("forbidden", "無權限訪問"), 29 30 VERSION_NOT_MATCH("record_not_exists_or_version_not_match", "記錄版本不存在或不匹配"), 31 32 PARAMETER_NOT_NULL("parameter_not_be_null", "參數不能為空"); 33 34 private String code; 35 36 private String desc; 37 38 private static Map<String, String> allMap = new HashMap<>(); 39 40 BaseEnums(String code, String desc) { 41 this.code = code; 42 this.desc = desc; 43 } 44 45 static { 46 for(BaseEnums enums : BaseEnums.values()){ 47 allMap.put(enums.code, enums.desc); 48 } 49 } 50 51 @Override 52 public String code() { 53 return code; 54 } 55 56 @Override 57 public String desc() { 58 return desc; 59 } 60 61 public String desc(String code) { 62 return allMap.get(code); 63 } 64 65 }
④ 再添加一個常用的日期工具類對象,主要包含一些常用的日期時間格式化,后續可再繼續往里面添加一些公共方法。

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

1 package com.lyyzoo.core.constants; 2 3 import com.google.common.base.Charsets; 4 5 import java.nio.charset.Charset; 6 7 /** 8 * 系統級常量類 9 * 10 * @version 1.0 11 * @author bojiangzhou 2017-12-28 12 */ 13 public class Constants { 14 15 public static final String APP_NAME = "sunny"; 16 17 /** 18 * 系統編碼 19 */ 20 public static final Charset CHARSET = Charsets.UTF_8; 21 22 /** 23 * 標識:是/否、啟用/禁用等 24 */ 25 public interface Flag { 26 27 Integer YES = 1; 28 29 Integer NO = 0; 30 } 31 32 /** 33 * 操作類型 34 */ 35 public interface Operation { 36 /** 37 * 添加 38 */ 39 String ADD = "add"; 40 /** 41 * 更新 42 */ 43 String UPDATE = "update"; 44 /** 45 * 刪除 46 */ 47 String DELETE = "delete"; 48 } 49 50 /** 51 * 性別 52 */ 53 public interface Sex { 54 /** 55 * 男 56 */ 57 Integer MALE = 1; 58 /** 59 * 女 60 */ 61 Integer FEMALE = 0; 62 } 63 64 }
⑥ 在base添加空的BaseController、BaseDTO、Service、Mapper,先定義好基礎結構,后面再添加功能。
BaseDTO:標准的who字段、版本號、及10個擴展字段。
因為這里用到了@Transient注解,先引入java持久化包:

1 package com.lyyzoo.core.base; 2 3 import com.fasterxml.jackson.annotation.*; 4 import com.lyyzoo.core.Constants; 5 import com.lyyzoo.core.util.Dates; 6 import org.apache.commons.lang3.builder.ToStringBuilder; 7 import org.apache.commons.lang3.builder.ToStringStyle; 8 9 import javax.persistence.Transient; 10 import java.io.Serializable; 11 import java.util.Date; 12 import java.util.HashMap; 13 import java.util.Map; 14 15 /** 16 * 基礎實體類 17 * 18 * @version 1.0 19 * @author bojiangzhou 2017-12-29 20 */ 21 public class BaseDTO implements Serializable { 22 private static final long serialVersionUID = -4287607489867805101L; 23 24 public static final String FIELD_OPERATE = "operate"; 25 public static final String FIELD_OBJECT_VERSION_NUMBER = "versionNumber"; 26 public static final String FIELD_CREATE_BY = "createBy"; 27 public static final String FIELD_CREATOR = "creator"; 28 public static final String FIELD_CREATE_DATE = "createDate"; 29 public static final String FIELD_UPDATE_BY = "updateBy"; 30 public static final String FIELD_UPDATER = "updater"; 31 public static final String FIELD_UPDATE_DATE = "updateDate"; 32 33 34 /** 35 * 操作類型,add/update/delete 參考:{@link Constants.Operation} 36 */ 37 @Transient 38 private String _operate; 39 40 /** 41 * 數據版本號,每發生update則自增,用於實現樂觀鎖. 42 */ 43 private Long versionNumber; 44 45 // 46 // 下面是標准 WHO 字段 47 // ---------------------------------------------------------------------------------------------------- 48 /** 49 * 創建人用戶名 50 */ 51 @JsonInclude(JsonInclude.Include.NON_NULL) 52 private Long createBy; 53 /** 54 * 創建人名稱 55 */ 56 @JsonInclude(JsonInclude.Include.NON_NULL) 57 @Transient 58 private String creator; 59 /** 60 * 創建時間 61 */ 62 @JsonInclude(JsonInclude.Include.NON_NULL) 63 @JsonFormat(pattern = Dates.DEFAULT_PATTERN) 64 private Date createDate; 65 66 /** 67 * 更新人用戶名 68 */ 69 @JsonInclude(JsonInclude.Include.NON_NULL) 70 private Long updateBy; 71 /** 72 * 更新人名稱 73 */ 74 @JsonInclude(JsonInclude.Include.NON_NULL) 75 @Transient 76 private String updater; 77 /** 78 * 更新時間 79 */ 80 @JsonInclude(JsonInclude.Include.NON_NULL) 81 @JsonFormat(pattern = Dates.DEFAULT_PATTERN) 82 private Date updateDate; 83 84 /** 85 * 其它屬性 86 */ 87 @JsonIgnore 88 @Transient 89 protected Map<String, Object> innerMap = new HashMap<>(); 90 91 // 92 // 下面是擴展屬性字段 93 // ---------------------------------------------------------------------------------------------------- 94 95 @JsonInclude(JsonInclude.Include.NON_NULL) 96 private String attribute1; 97 98 @JsonInclude(JsonInclude.Include.NON_NULL) 99 private String attribute2; 100 101 @JsonInclude(JsonInclude.Include.NON_NULL) 102 private String attribute3; 103 104 @JsonInclude(JsonInclude.Include.NON_NULL) 105 private String attribute4; 106 107 @JsonInclude(JsonInclude.Include.NON_NULL) 108 private String attribute5; 109 110 @JsonInclude(JsonInclude.Include.NON_NULL) 111 private String attribute6; 112 113 @JsonInclude(JsonInclude.Include.NON_NULL) 114 private String attribute7; 115 116 @JsonInclude(JsonInclude.Include.NON_NULL) 117 private String attribute8; 118 119 @JsonInclude(JsonInclude.Include.NON_NULL) 120 private String attribute9; 121 122 @JsonInclude(JsonInclude.Include.NON_NULL) 123 private String attribute10; 124 125 public String get_operate() { 126 return _operate; 127 } 128 129 public void set_operate(String _operate) { 130 this._operate = _operate; 131 } 132 133 @Override 134 public String toString() { 135 return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); 136 } 137 138 public String toJSONString() { 139 return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); 140 } 141 142 public Long getVersionNumber() { 143 return versionNumber; 144 } 145 146 public void setVersionNumber(Long versionNumber) { 147 this.versionNumber = versionNumber; 148 } 149 150 public Long getCreateBy() { 151 return createBy; 152 } 153 154 public void setCreateBy(Long createBy) { 155 this.createBy = createBy; 156 } 157 158 public String getCreator() { 159 return creator; 160 } 161 162 public void setCreator(String creator) { 163 this.creator = creator; 164 } 165 166 public Date getCreateDate() { 167 return createDate; 168 } 169 170 public void setCreateDate(Date createDate) { 171 this.createDate = createDate; 172 } 173 174 public Long getUpdateBy() { 175 return updateBy; 176 } 177 178 public void setUpdateBy(Long updateBy) { 179 this.updateBy = updateBy; 180 } 181 182 public String getUpdater() { 183 return updater; 184 } 185 186 public void setUpdater(String updater) { 187 this.updater = updater; 188 } 189 190 public Date getUpdateDate() { 191 return updateDate; 192 } 193 194 public void setUpdateDate(Date updateDate) { 195 this.updateDate = updateDate; 196 } 197 198 @JsonAnyGetter 199 public Object getAttribute(String key) { 200 return innerMap.get(key); 201 } 202 203 @JsonAnySetter 204 public void setAttribute(String key, Object obj) { 205 innerMap.put(key, obj); 206 } 207 208 public String getAttribute1() { 209 return attribute1; 210 } 211 212 public void setAttribute1(String attribute1) { 213 this.attribute1 = attribute1; 214 } 215 216 public String getAttribute2() { 217 return attribute2; 218 } 219 220 public void setAttribute2(String attribute2) { 221 this.attribute2 = attribute2; 222 } 223 224 public String getAttribute3() { 225 return attribute3; 226 } 227 228 public void setAttribute3(String attribute3) { 229 this.attribute3 = attribute3; 230 } 231 232 public String getAttribute4() { 233 return attribute4; 234 } 235 236 public void setAttribute4(String attribute4) { 237 this.attribute4 = attribute4; 238 } 239 240 public String getAttribute5() { 241 return attribute5; 242 } 243 244 public void setAttribute5(String attribute5) { 245 this.attribute5 = attribute5; 246 } 247 248 public String getAttribute6() { 249 return attribute6; 250 } 251 252 public void setAttribute6(String attribute6) { 253 this.attribute6 = attribute6; 254 } 255 256 public String getAttribute7() { 257 return attribute7; 258 } 259 260 public void setAttribute7(String attribute7) { 261 this.attribute7 = attribute7; 262 } 263 264 public String getAttribute8() { 265 return attribute8; 266 } 267 268 public void setAttribute8(String attribute8) { 269 this.attribute8 = attribute8; 270 } 271 272 public String getAttribute9() { 273 return attribute9; 274 } 275 276 public void setAttribute9(String attribute9) { 277 this.attribute9 = attribute9; 278 } 279 280 public String getAttribute10() { 281 return attribute10; 282 } 283 284 public void setAttribute10(String attribute10) { 285 this.attribute10 = attribute10; 286 } 287 288 }
同時,重寫了toString方法,增加了toJsonString方法,使得可以格式化輸出DTO的數據:
直接打印DTO,輸出的格式大概就是這個樣子:
⑦ 在exception添加BaseException,定義一些基礎異常類
基礎異常類都繼承自運行時異常類(RunntimeException),盡可能把受檢異常轉化為非受檢異常,更好的面向接口編程,提高代碼的擴展性、穩定性。
BaseException:添加了一個錯誤編碼,其它自定義的異常應當繼承該類。

1 package com.lyyzoo.core.exception; 2 3 /** 4 * 基礎異常類 5 * 6 * @version 1.0 7 * @author bojiangzhou 2017-12-31 8 */ 9 public class BaseException extends RuntimeException { 10 private static final long serialVersionUID = -997101946070796354L; 11 12 /** 13 * 錯誤編碼 14 */ 15 protected String code; 16 17 public BaseException() {} 18 19 public BaseException(String message) { 20 super(message); 21 } 22 23 public BaseException(String code, String message) { 24 super(message); 25 this.code = code; 26 } 27 28 public String getCode() { 29 return code; 30 } 31 32 public void setCode(String code) { 33 this.code = code; 34 } 35 }
ServiceException:繼承BaseException,Service層往Controller拋出的異常。

1 package com.lyyzoo.core.exception; 2 3 /** 4 * Service層異常 5 * 6 * @version 1.0 7 * @author bojiangzhou 2017-12-31 8 */ 9 public class ServiceException extends BaseException { 10 private static final long serialVersionUID = 6058294324031642376L; 11 12 public ServiceException() {} 13 14 public ServiceException(String message) { 15 super(message); 16 } 17 18 public ServiceException(String code, String message) { 19 super(code, message); 20 } 21 22 }
3、添加系統用戶功能,使用Postman測試接口
① 在system模塊下,再分成dto、controller、service、mapper、constants子包,以后一個模塊功能開發就是這樣一個基礎結構。
User:系統用戶

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

1 package com.lyyzoo.core.system.controller; 2 3 import com.lyyzoo.core.Constants; 4 import com.lyyzoo.core.base.BaseController; 5 import com.lyyzoo.core.base.BaseEnums; 6 import com.lyyzoo.core.base.Result; 7 import com.lyyzoo.core.system.dto.User; 8 import com.lyyzoo.core.util.Dates; 9 import com.lyyzoo.core.util.Results; 10 import org.springframework.web.bind.annotation.PathVariable; 11 import org.springframework.web.bind.annotation.RequestMapping; 12 import org.springframework.web.bind.annotation.RestController; 13 14 import java.util.ArrayList; 15 import java.util.List; 16 17 /** 18 * 用戶Controller 19 * 20 * @version 1.0 21 * @author bojiangzhou 2017-12-31 22 */ 23 @RequestMapping("/sys/user") 24 @RestController 25 public class UserController extends BaseController { 26 27 private static List<User> userList = new ArrayList<>(); 28 29 // 先靜態模擬數據 30 static { 31 User user1 = new User(); 32 user1.setUserId(1L); 33 user1.setUsername("lufei"); 34 user1.setNickname("蒙奇D路飛"); 35 user1.setBirthday(Dates.parseDate("2000-05-05")); 36 user1.setSex(Constants.Sex.MALE); 37 user1.setEnabled(Constants.Flag.YES); 38 userList.add(user1); 39 40 User user2 = new User(); 41 user2.setUserId(2L); 42 user2.setUsername("nami"); 43 user2.setNickname("娜美"); 44 user2.setBirthday(Dates.parseDate("2000/7/3")); 45 user2.setSex(Constants.Sex.FEMALE); 46 user2.setEnabled(Constants.Flag.YES); 47 userList.add(user2); 48 } 49 50 @RequestMapping("/queryAll") 51 public Result queryAll(){ 52 return Results.successWithData(userList, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description()); 53 } 54 55 @RequestMapping("/queryOne/{userId}") 56 public Result queryOne(@PathVariable Long userId){ 57 User user = null; 58 for(User u : userList){ 59 if(u.getUserId().longValue() == userId){ 60 user = u; 61 } 62 } 63 return Results.successWithData(user); 64 } 65 }
② Postman請求:請求成功,基礎的HTTP服務已經實現了。
三、集成MyBatis,實現基礎Mapper和Service
1、添加JDBC、配置數據源
添加spring-boot-starter-jdbc以支持JDBC訪問數據庫,然后添加MySql的JDBC驅動mysql-connector-java;
在application.properties里配置mysql的數據庫驅動
之后在application-dev.properties里配置開發環境數據庫的連接信息,添加之后,Springboot就會自動配置數據源了。
2、集成MyBatis
MyBatis官方為了方便Springboot集成MyBatis,專門提供了一個符合Springboot規范的starter項目,即mybatis-spring-boot-starter。
在application.properties里添加mybatis映射配置:
3、添加MyBatis通用Mapper
通用Mapper可以極大的簡化開發,極其方便的進行單表的增刪改查。
關於通用Mapper,參考網站地址:
之后,在core.base下創建自定義的Mapper,按需選擇接口。
具體可參考:根據需要自定義接口

1 package com.lyyzoo.core.base; 2
3 import tk.mybatis.mapper.common.BaseMapper; 4 import tk.mybatis.mapper.common.ConditionMapper; 5 import tk.mybatis.mapper.common.IdsMapper; 6 import tk.mybatis.mapper.common.special.InsertListMapper; 7
8 /**
9 * 10 * BaseMapper 11 * 12 * @name BaseMapper 13 * @version 1.0 14 * @author bojiangzhou 2017-12-31 15 */
16 public interface Mapper<T> extends BaseMapper<T>, ConditionMapper<T>, IdsMapper<T>, InsertListMapper<T> { 17
18 }
定義好基礎Mapper后,就具有下圖中的基本通用方法了。每個實體類對應的*Mapper繼承Mapper<T>來獲得基本的增刪改查的通用方法。
在application.properties里配置自定義的基礎Mapper
4、添加分頁插件PageHelper
參考地址:
分頁插件配置,一般情況下,不需要做任何配置。
之后,我們就可以在代碼中使用 PageHelper.startPage(1, 10) 對緊隨其后的一個查詢進行分頁查詢,非常方便。
5、配置自動掃描Mapper
在config下創建MyBatisConfig配置文件,通過mapperScannerConfigurer方法配置自動掃描Mapper文件。

1 package com.lyyzoo.core.config; 2 3 import org.springframework.context.annotation.Bean; 4 import org.springframework.context.annotation.Configuration; 5 6 import tk.mybatis.spring.mapper.MapperScannerConfigurer; 7 8 /** 9 * MyBatis相關配置. 10 * 11 * @version 1.0 12 * @author bojiangzhou 2018-01-07 13 */ 14 @Configuration 15 public class MyBatisConfig { 16 17 /** 18 * Mapper掃描配置. 自動掃描將Mapper接口生成代理注入到Spring. 19 */ 20 @Bean 21 public static MapperScannerConfigurer mapperScannerConfigurer() { 22 MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); 23 // 注意這里的掃描路徑: 1.不要掃描到自定義的Mapper; 2.定義的路徑不要掃描到tk.mybatis.mapper(如定義**.mapper). 24 // 兩個做法都會導致掃描到tk.mybatis的Mapper,就會產生重復定義的報錯. 25 mapperScannerConfigurer.setBasePackage("**.lyyzoo.**.mapper"); 26 return mapperScannerConfigurer; 27 } 28 29 }
注意這里的 MapperScannerConfigurer 是tk.mybatis.spring.mapper.MapperScannerConfigurer,而不是org.mybatis,否則使用通用Mapper的方法時會報類似下面的這種錯誤
6、定義基礎Service
一般來說,我們不能在Controller中直接訪問Mapper,因此我們需要加上Service,通過Service訪問Mapper。
首先定義基礎Service<T>接口,根據Mapper定義基本的增刪改查接口方法。

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

1 package com.lyyzoo.core.base; 2 3 import com.github.pagehelper.PageHelper; 4 import com.lyyzoo.core.constants.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 }
BaseService的實現用到了反射工具類Reflections:

1 package com.lyyzoo.core.util; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 6 import java.lang.reflect.Field; 7 import java.lang.reflect.Modifier; 8 import java.lang.reflect.ParameterizedType; 9 import java.lang.reflect.Type; 10 11 /** 12 * 反射工具類. 13 * 14 * @version 1.0 15 * @author bojiangzhou 2018-01-06 16 */ 17 18 public abstract class Reflections { 19 20 private static Logger logger = LoggerFactory.getLogger(Reflections.class); 21 22 /** 23 * 通過反射, 獲得Class定義中聲明的泛型參數的類型, 注意泛型必須定義在父類處. 如無法找到, 返回Object.class. 24 * 25 * @param clazz class類 26 * 27 * @return the 返回第一個聲明的泛型類型. 如果沒有,則返回Object.class 28 */ 29 @SuppressWarnings("unchecked") 30 public static Class getClassGenericType(final Class clazz) { 31 return getClassGenericType(clazz, 0); 32 } 33 34 /** 35 * 通過反射, 獲得Class定義中聲明的父類的泛型參數的類型. 如無法找到, 返回Object.class. 36 * 37 * @param clazz class類 38 * 39 * @param index 獲取第幾個泛型參數的類型,默認從0開始,即第一個 40 * 41 * @return 返回第index個泛型參數類型. 42 */ 43 public static Class getClassGenericType(final Class clazz, final int index) { 44 Type genType = clazz.getGenericSuperclass(); 45 46 if (!(genType instanceof ParameterizedType)) { 47 return Object.class; 48 } 49 50 Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); 51 52 if (index >= params.length || index < 0) { 53 logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + params.length); 54 return Object.class; 55 } 56 if (!(params[index] instanceof Class)) { 57 logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); 58 return Object.class; 59 } 60 61 return (Class) params[index]; 62 } 63 64 /** 65 * 根據注解類型獲取實體的Field 66 * 67 * @param entityClass 實體類型 68 * 69 * @param annotationClass 注解類型 70 * 71 * @return 返回第一個有該注解類型的Field,如果沒有則返回null. 72 */ 73 @SuppressWarnings("unchecked") 74 public static Field getFieldByAnnotation(Class entityClass, Class annotationClass) { 75 Field[] fields = entityClass.getDeclaredFields(); 76 for (Field field : fields) { 77 if (field.getAnnotation(annotationClass) != null) { 78 makeAccessible(field); 79 return field; 80 } 81 } 82 return null; 83 } 84 85 /** 86 * 獲取實體的字段 87 * 88 * @param entityClass 實體類型 89 * 90 * @param fieldName 字段名稱 91 * 92 * @return 該字段名稱對應的字段,如果沒有則返回null. 93 */ 94 public static Field getField(Class entityClass, String fieldName){ 95 try { 96 Field field = entityClass.getDeclaredField(fieldName); 97 makeAccessible(field); 98 return field; 99 } catch (NoSuchFieldException e) { 100 e.printStackTrace(); 101 } 102 return null; 103 } 104 105 106 /** 107 * 改變private/protected的成員變量為public. 108 */ 109 public static void makeAccessible(Field field) { 110 if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) { 111 field.setAccessible(true); 112 } 113 } 114 115 }
7、獲取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 }
還需要開啟開啟 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 }
② 創建表結構

1 CREATE TABLE `sys_user` ( 2 `USER_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '表ID,主鍵,供其他表做外鍵', 3 `USERNAME` varchar(30) NOT NULL COMMENT '用戶名', 4 `PASSWORD` varchar(100) NOT NULL COMMENT '密碼', 5 `NICKNAME` varchar(30) NOT NULL COMMENT '用戶名稱', 6 `BIRTHDAY` date DEFAULT NULL COMMENT '生日', 7 `SEX` int(1) DEFAULT NULL COMMENT '性別:1-男;0-女', 8 `ENABLED` int(1) NOT NULL DEFAULT '1' COMMENT '啟用標識:1/0', 9 `VERSION_NUMBER` int(11) NOT NULL DEFAULT '1' COMMENT '行版本號,用來處理鎖', 10 `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間', 11 `CREATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '創建人', 12 `UPDATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '更新人', 13 `UPDATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新時間', 14 `ATTRIBUTE1` varchar(150) DEFAULT NULL, 15 `ATTRIBUTE2` varchar(150) DEFAULT NULL, 16 `ATTRIBUTE3` varchar(150) DEFAULT NULL, 17 `ATTRIBUTE4` varchar(150) DEFAULT NULL, 18 `ATTRIBUTE5` varchar(150) DEFAULT NULL, 19 `ATTRIBUTE6` varchar(150) DEFAULT NULL, 20 `ATTRIBUTE7` varchar(150) DEFAULT NULL, 21 `ATTRIBUTE8` varchar(150) DEFAULT NULL, 22 `ATTRIBUTE9` varchar(150) DEFAULT NULL, 23 `ATTRIBUTE10` varchar(150) DEFAULT NULL, 24 PRIMARY KEY (`USER_ID`), 25 UNIQUE KEY `USERNAME` (`USERNAME`) 26 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='系統用戶';
③ 創建UserMapper
在system.mapper下創建UserMapper接口,繼承Mapper<User>:

1 package com.lyyzoo.core.system.mapper; 2 3 import com.lyyzoo.core.base.Mapper; 4 import com.lyyzoo.core.system.dto.User; 5 6 /** 7 * 8 * @name UserMapper 9 * @version 1.0 10 * @author bojiangzhou 2018-01-06 11 */ 12 public interface UserMapper extends Mapper<User> { 13 14 }
④ 創建UserService
在system.service下創建UserService接口,只需繼承Service<User>接口即可。

1 package com.lyyzoo.core.system.service; 2 3 import com.lyyzoo.core.base.Service; 4 import com.lyyzoo.core.system.dto.User; 5 6 /** 7 * 用戶Service接口 8 * 9 * @version 1.0 10 * @author bojiangzhou 2018-01-06 11 */ 12 public interface UserService extends Service<User> { 13 14 }
在system.service.impl下創建UserServiceImpl實現類,繼承BaseService<User>類,實現UserService接口。同時加上@Service注解。

1 package com.lyyzoo.core.system.service.impl; 2 3 import org.springframework.stereotype.Service; 4 5 import com.lyyzoo.core.base.BaseService; 6 import com.lyyzoo.core.system.dto.User; 7 import com.lyyzoo.core.system.service.UserService; 8 9 /** 10 * 用戶Service實現類 11 * 12 * @version 1.0 13 * @author bojiangzhou 2018-01-06 14 */ 15 @Service 16 public class UserServiceImpl extends BaseService<User> implements UserService { 17 18 }
⑤ 修改UserController,注入UserService,增加一些測試API

1 package com.lyyzoo.core.system.controller; 2 3 import com.lyyzoo.core.base.BaseController; 4 import com.lyyzoo.core.base.BaseEnums; 5 import com.lyyzoo.core.base.Result; 6 import com.lyyzoo.core.system.dto.User; 7 import com.lyyzoo.core.system.service.UserService; 8 import com.lyyzoo.core.util.Results; 9 import org.springframework.beans.factory.annotation.Autowired; 10 import org.springframework.web.bind.annotation.*; 11 12 import javax.validation.Valid; 13 import java.util.List; 14 15 /** 16 * 用戶Controller 17 * 18 * @version 1.0 19 * @author bojiangzhou 2017-12-31 20 */ 21 @RequestMapping 22 @RestController 23 public class UserController extends BaseController { 24 25 @Autowired 26 private UserService userService; 27 28 29 @PostMapping("/sys/user/queryAll") 30 public Result queryAll(){ 31 List<User> list = userService.selectAll(); 32 return Results.successWithData(list, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description()); 33 } 34 35 @RequestMapping("/sys/user/queryOne/{userId}") 36 public Result queryOne(@PathVariable Long userId){ 37 User user = userService.get(userId); 38 return Results.successWithData(user); 39 } 40 41 @PostMapping("/sys/user/save") 42 public Result save(@Valid @RequestBody User user){ 43 user = userService.insertSelective(user); 44 return Results.successWithData(user); 45 } 46 47 @PostMapping("/sys/user/update") 48 public Result update(@Valid @RequestBody List<User> user){ 49 user = userService.persistSelective(user); 50 return Results.successWithData(user); 51 } 52 53 @RequestMapping("/sys/user/delete") 54 public Result delete(User user){ 55 userService.delete(user); 56 return Results.success(); 57 } 58 59 @RequestMapping("/sys/user/delete/{userId}") 60 public Result delete(@PathVariable Long userId){ 61 userService.delete(userId); 62 return Results.success(); 63 } 64 65 }
⑥ 測試結果
查詢所有:
批量保存/修改:
9、代碼生成器
使用代碼生成器來生成基礎的代碼結構,生成DTO、XML等等。
MyBatis官方提供了代碼生成器MyBatis Generator,但一般需要定制化。MyBatis Generator
我這里從網上找了一個使用起來比較方便的界面工具,可生成DTO、Mapper、Mapper.xml,生成之后還需做一些小調整。另需要自己創建對應的Service、Controller。之后有時間再重新定制化一個符合本項目的代碼生成器。
四、日志及全局異常處理
在前面的測試中,會發現控制台輸出的日志不怎么友好,有很多日志也沒有輸出,不便於查找排查問題。對於一個應用程序來說日志記錄是必不可少的一部分。線上問題追蹤,基於日志的業務邏輯統計分析等都離不日志。
先貼出一些參考資料:
1、日志框架簡介
Java有很多常用的日志框架,如Log4j、Log4j 2、Commons Logging、Slf4j、Logback等。有時候你可能會感覺有點混亂,下面簡單介紹下。
-
Log4j:Apache Log4j是一個基於Java的日志記錄工具,是Apache軟件基金會的一個項目。
-
Log4j 2:Apache Log4j 2是apache開發的一款Log4j的升級產品。
-
Commons Logging:Apache基金會所屬的項目,是一套Java日志接口。
-
Slf4j:類似於Commons Logging,是一套簡易Java日志門面,本身並無日志的實現。(Simple Logging Facade for Java,縮寫Slf4j)。
-
Logback:一套日志組件的實現(slf4j陣營)。
Commons Logging和Slf4j是日志門面,提供一個統一的高層接口,為各種loging API提供一個簡單統一的接口。log4j和Logback則是具體的日志實現方案。可以簡單的理解為接口與接口的實現,調用者只需要關注接口而無需關注具體的實現,做到解耦。
比較常用的組合使用方式是Slf4j與Logback組合使用,Commons Logging與Log4j組合使用。
基於下面的一些優點,選用Slf4j+Logback的日志框架:
-
更快的執行速度,Logback重寫了內部的實現,在一些關鍵執行路徑上性能提升10倍以上。而且logback不僅性能提升了,初始化內存加載也更小了
-
自動清除舊的日志歸檔文件,通過設置TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 屬性,你就可以控制日志歸檔文件的最大數量
-
Logback擁有遠比log4j更豐富的過濾能力,可以不用降低日志級別而記錄低級別中的日志。
-
Logback必須配合Slf4j使用。由於Logback和Slf4j是同一個作者,其兼容性不言而喻。
-
默認情況下,Spring Boot會用Logback來記錄日志,並用INFO級別輸出到控制台。
2、配置日志
可以看到,只要集成了spring-boot-starter-web,就引入了spring-boot-starter-logging,即slf4j和logback。
其它的幾個包:jcl-over-slf4j,代碼直接調用common-logging會被橋接到slf4j;jul-to-slf4j,代碼直接調用java.util.logging會被橋接到slf4j;log4j-over-slf4j,代碼直接調用log4j會被橋接到slf4j。
還需引入janino,如果不加入這個包會報錯。
在resources下添加logback.xml配置文件,Logback默認會查找classpath下的logback.xml文件。
具體配置如下,有較詳細的注釋,很容易看懂。可以通過application.properties配置日志記錄級別、日志輸出文件目錄等。

1 <?xml version="1.0" encoding="UTF-8"?>
2
3 <!-- 級別從高到低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL -->
4 <!-- 日志輸出規則 根據當前ROOT 級別,日志輸出時,級別高於root默認的級別時 會輸出 -->
5 <!-- 以下 每個配置的 filter 是過濾掉輸出文件里面,會出現高級別文件,依然出現低級別的日志信息,通過filter 過濾只記錄本級別的日志 -->
6 <!-- scan 當此屬性設置為true時,配置文件如果發生改變,將會被重新加載,默認值為true。 -->
7 <!-- scanPeriod 設置監測配置文件是否有修改的時間間隔,如果沒有給出時間單位,默認單位是毫秒。當scan為true時,此屬性生效。默認的時間間隔為1分鍾。 -->
8 <!-- debug 當此屬性設置為true時,將打印出logback內部日志信息,實時查看logback運行狀態。默認值為false。 -->
9 <configuration debug="false" scan="false" scanPeriod="5 minutes">
10
11 <!-- 引入配置文件 -->
12 <property resource="application.properties"/>
13 <property resource="application-${app.env:-dev}.properties"/>
14
15 <property name="app.name" value="${app.name:-sunny}"/>
16 <property name="app.env" value="${app.env:-dev}"/>
17
18 <!-- 日志記錄級別 -->
19 <property name="logback_level" value="${logback.level:-DEBUG}"/>
20 <!-- 是否輸出日志到文件 -->
21 <property name="logback_rolling" value="${logback.rolling:-false}"/>
22 <!-- 設置日志輸出目錄 -->
23 <property name="logback_rolling_path" value="${logback.rolling.path:-/data/logs}"/>
24 <!-- 日志文件最大大小 -->
25 <property name="logback_max_file_size" value="${logback.max_file_size:-10MB}"/>
26 <!-- 格式化輸出:%d:表示日期,%thread:表示線程名,%-5level:級別從左顯示5個字符寬度,%logger:日志輸出者的名字(通常是所在類的全名),%L:輸出代碼中的行號,%msg:日志消息,%n:換行符 -->
27 <property name="logback_pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger %L -| %msg%n"/>
28
29
30 <if condition='p("logback_rolling").equals("true")'>
31 <then>
32 <!-- 滾動記錄文件 -->
33 <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
34 <file>${logback_rolling_path}/${app.name}.log</file>
35 <!-- rollingPolicy:當發生滾動時,決定RollingFileAppender的行為,涉及文件移動和重命名 -->
36 <!-- TimeBasedRollingPolicy:最常用的滾動策略,它根據時間來制定滾動策略 -->
37 <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
38 <!-- 活動文件的名字會根據fileNamePattern的值,每隔一段時間改變一次 -->
39 <fileNamePattern>${logback_rolling_path}/${app.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
40
41 <!-- 日志文件的保存期限為30天 -->
42 <maxHistory>30</maxHistory>
43
44 <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
45 <!-- maxFileSize:這是活動文件的大小,默認值是10MB -->
46 <maxFileSize>${logback_max_file_size}</maxFileSize>
47 </timeBasedFileNamingAndTriggeringPolicy>
48 </rollingPolicy>
49 <encoder>
50 <pattern>${logback_pattern}</pattern>
51 <charset>UTF-8</charset>
52 </encoder>
53 </appender>
54
55 <root>
56 <appender-ref ref="FILE"/>
57 </root>
58 </then>
59 </if>
60
61
62 <!-- 將日志打印到控制台 -->
63 <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
64 <encoder>
65 <pattern>${logback_pattern}</pattern>
66 </encoder>
67 </appender>
68
69 <root level="${logback_level}">
70 <appender-ref ref="CONSOLE"/>
71 </root>
72
73 <contextName>${app.name}</contextName>
74
75 </configuration>
加入配置文件后,就可以看到控制台格式化后的日志輸出,還可以看到具體代碼行數等,比之前的友好多了。
同時,將日志滾動輸出到日志文件,保留歷史記錄。可通過logback.rolling=false控制是否需要輸出日志到文件。
3、使用Logger
配置好之后,就可以使用Logger來輸出日志了,使用起來也是非常方便。
* 可以看到引入的包是slf4j.Logger,代碼里並沒有引用任何一個跟 Logback 相關的類,這便是使用 Slf4j的好處,在需要將日志框架切換為其它日志框架時,無需改動已有的代碼。
* LoggerFactory 的 getLogger() 方法接收一個參數,以這個參數決定 logger 的名字,比如第二圖中的日志輸出。在為 logger 命名時,用類的全限定類名作為 logger name 是最好的策略,這樣能夠追蹤到每一條日志消息的來源
* 可以看到,可以通過提供占位符,以參數化的方式打印日志,避免字符串拼接的不必要損耗,也無需通過logger.isDebugEnabled()這種方式判斷是否需要打印。
4、全局異常處理
現在有一個問題,當日志級別設置到INFO級別后,只會輸出INFO以上的日志,如INFO、WARN、ERROR,這沒毛病,問題是,程序中拋出的異常堆棧(運行時異常)都沒有打印了,不利於排查問題。
而且,在某些情況下,我們在Service中想直接把異常往Controller拋出不做處理,但我們不能直接把異常信息輸出到客戶端,這是非常不友好的。
所以,在config下建一個GlobalExceptionConfig作為全局統一異常處理。主要處理了自定義的ServiceException、AuthorityException、BaseException,以及系統的NoHandlerFoundException和Exception異常。

1 package com.lyyzoo.core.config; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 import org.springframework.http.HttpStatus; 6 import org.springframework.web.bind.annotation.ExceptionHandler; 7 import org.springframework.web.bind.annotation.RestControllerAdvice; 8 import org.springframework.web.servlet.NoHandlerFoundException; 9 10 import com.lyyzoo.core.base.Result; 11 import com.lyyzoo.core.constants.BaseEnums; 12 import com.lyyzoo.core.exception.AuthorityException; 13 import com.lyyzoo.core.exception.BaseException; 14 import com.lyyzoo.core.exception.ServiceException; 15 import com.lyyzoo.core.util.Results; 16 17 /** 18 * 全局異常處理 19 * 20 * @author bojiangzhou 2018-02-06 21 * @version 1.0 22 */ 23 @RestControllerAdvice 24 public class GlobalExceptionConfig { 25 26 private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionConfig.class); 27 28 /** 29 * 處理 ServiceException 異常 30 */ 31 @ExceptionHandler(ServiceException.class) 32 public Result handleServiceException(ServiceException e){ 33 Result result = Results.failure(e.getCode(), e.getMessage()); 34 result.setStatus(HttpStatus.BAD_REQUEST.value()); 35 logger.info("ServiceException[code: {}, message: {}]", e.getCode(), e.getMessage()); 36 return result; 37 } 38 39 /** 40 * 處理 AuthorityException 異常 41 */ 42 @ExceptionHandler(AuthorityException.class) 43 public Result handleAuthorityException(AuthorityException e){ 44 Result result = Results.failure(BaseEnums.FORBIDDEN.code(), BaseEnums.FORBIDDEN.desc()); 45 result.setStatus(HttpStatus.FORBIDDEN.value()); 46 logger.info("AuthorityException[code: {}, message: {}]", e.getCode(), e.getMessage()); 47 return result; 48 } 49 50 /** 51 * 處理 NoHandlerFoundException 異常. <br/> 52 * 需配置 [spring.mvc.throw-exception-if-no-handler-found=true] 53 * 需配置 [spring.resources.add-mappings=false] 54 */ 55 @ExceptionHandler(NoHandlerFoundException.class) 56 public Result handleNotFoundException(NoHandlerFoundException e){ 57 Result result = Results.failure(BaseEnums.NOT_FOUND.code(), BaseEnums.NOT_FOUND.desc()); 58 result.setStatus(HttpStatus.NOT_FOUND.value()); 59 logger.info(e.getMessage()); 60 return result; 61 } 62 63 /** 64 * 處理 BaseException 異常 65 */ 66 @ExceptionHandler(BaseException.class) 67 public Result handleBaseException(BaseException e){ 68 Result result = Results.failure(e.getCode(), e.getMessage()); 69 result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); 70 logger.error("BaseException[code: {}, message: {}]", e.getCode(), e.getMessage(), e); 71 return result; 72 } 73 74 /** 75 * 處理 Exception 異常 76 */ 77 @ExceptionHandler(Exception.class) 78 public Result handleException(Exception e){ 79 Result result = Results.failure(BaseEnums.ERROR.code(), BaseEnums.ERROR.desc()); 80 result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); 81 logger.error(e.getMessage(), e); 82 return result; 83 } 84 85 }
看上面的代碼,@ControllAdvice(@RestControllerAdvice可以返回ResponseBody),可看做Controller增強器,可以在@ControllerAdvice作用類下添加@ExceptionHandler,@InitBinder,@ModelAttribute注解的方法來增強Controller,都會作用在被 @RequestMapping 注解的方法上。
使用@ExceptionHandler 攔截異常,我們可以通過該注解實現自定義異常處理。在每個處理方法中,封裝Result,返回對應的消息及狀態碼等。
通過Logger打印對應級別的日志,也可以看到控制台及日志文件中有異常堆棧的輸出了。注意除了BaseException、Exception,其它的都只是打印了簡單信息,且為INFO級別。Exception是ERROR級別,且打印了堆棧信息。
NoHandlerFoundException 是404異常,這里注意要先關閉DispatcherServlet的NotFound默認異常處理。
測試如下:這種返回結果就比較友好了。
五、數據庫樂觀鎖
1、樂觀鎖
在並發修改同一條記錄時,為避免更新丟失,需要加鎖。要么在應用層加鎖,要么在緩存層加鎖,要么在數據庫層使用樂觀鎖,使用version作為更新依據【強制】。 —— 《阿里巴巴Java開發手冊》
樂觀鎖,基於數據版本(version)記錄機制實現,為數據庫表增加一個"version"字段。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。提交數據時,提交的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。
因此,這節就來處理BaseDTO中的"version"字段,通過增加一個mybatis插件來實現更新時版本號自動+1。
2、MyBatis插件介紹
MyBatis 允許在己映射語句執行過程中的某一點進行攔截調用。默認情況下, MyBatis 允許使用插件來攔截的接口和方法包括以下幾個:
-
Executor (update 、query 、flushStatements 、commit 、rollback 、getTransaction 、close 、isClosed)
-
ParameterHandler (getParameterObject 、setParameters)
-
ResultSetHandler (handleResul tSets 、handleCursorResultSets、handleOutputParameters)
- StatementHandler (prepare 、parameterize 、batch update 、query)
MyBatis 插件實現攔截器接口Interceptor,在實現類中對攔截對象和方法進行處理 。
-
setProperties:傳遞插件的參數,可以通過參數來改變插件的行為。
-
plugin:參數 target 就是要攔截的對象,作用就是給被攔截對象生成一個代理對象,並返回。
-
intercept:會覆蓋所攔截對象的原方法,Invocation參數可以反射調度原來對象的方法,可以獲取到很多有用的東西。
除了需要實現攔截器接口外,還需要給實現類配置攔截器簽名。 使用 @Intercepts 和 @Signature 這兩個注解來配置攔截器要攔截的接口的方法,接口方法對應的簽名基本都是固定的。
@Intercepts 注解的屬性是一個 @Signature 數組,可以在同 一個攔截器中同時攔截不同的接口和方法。
@Signature 注解包含以下三個屬性。
-
type:設置攔截的接口,可選值是前面提到的4個接口 。
-
method:設置攔截接口中的方法名, 可選值是前面4個接口對應的方法,需要和接口匹配 。
- args:設置攔截方法的參數類型數組,通過方法名和參數類型可以確定唯一一個方法 。
3、數據版本插件
要實現版本號自動更新,我們需要在SQL被執行前修改SQL,因此我們需要攔截的就是 StatementHandler 接口的 prepare 方法,該方法會在數據庫執行前被調用,優先於當前接口的其它方法而被執行。
在 core.plugin 包下新建一個VersionPlugin插件,實現Interceptor攔截器接口。
該接口方法簽名如下:
在 interceptor 方法中對 UPDATE 類型的操作,修改原SQL,加入version,修改后的SQL類似下圖,更新時就會自動將version+1。同時帶上version條件,如果該版本號小於數據庫記錄版本號,則不會更新。
VersionInterceptor插件:

1 package com.lyyzoo.core.plugins; 2 3 import net.sf.jsqlparser.expression.Expression; 4 import net.sf.jsqlparser.expression.LongValue; 5 import net.sf.jsqlparser.expression.operators.arithmetic.Addition; 6 import net.sf.jsqlparser.expression.operators.conditional.AndExpression; 7 import net.sf.jsqlparser.expression.operators.relational.EqualsTo; 8 import net.sf.jsqlparser.parser.CCJSqlParserUtil; 9 import net.sf.jsqlparser.schema.Column; 10 import net.sf.jsqlparser.statement.Statement; 11 import net.sf.jsqlparser.statement.update.Update; 12 import org.apache.ibatis.executor.statement.StatementHandler; 13 import org.apache.ibatis.mapping.BoundSql; 14 import org.apache.ibatis.mapping.MappedStatement; 15 import org.apache.ibatis.mapping.SqlCommandType; 16 import org.apache.ibatis.plugin.*; 17 import org.apache.ibatis.reflection.MetaObject; 18 import org.apache.ibatis.reflection.SystemMetaObject; 19 import org.slf4j.Logger; 20 import org.slf4j.LoggerFactory; 21 22 import java.lang.reflect.Proxy; 23 import java.sql.Connection; 24 import java.util.List; 25 import java.util.Properties; 26 27 /** 28 * 樂觀鎖:數據版本插件 29 * 30 * @version 1.0 31 * @author bojiangzhou 2018-02-10 32 */ 33 @Intercepts( 34 @Signature( 35 type = StatementHandler.class, 36 method = "prepare", 37 args = {Connection.class, Integer.class} 38 ) 39 ) 40 public class VersionInterceptor implements Interceptor { 41 42 private static final String VERSION_COLUMN_NAME = "version"; 43 44 private static final Logger logger = LoggerFactory.getLogger(VersionInterceptor.class); 45 46 @Override 47 public Object intercept(Invocation invocation) throws Throwable { 48 // 獲取 StatementHandler,實際是 RoutingStatementHandler 49 StatementHandler handler = (StatementHandler) processTarget(invocation.getTarget()); 50 // 包裝原始對象,便於獲取和設置屬性 51 MetaObject metaObject = SystemMetaObject.forObject(handler); 52 // MappedStatement 是對SQL更高層次的一個封裝,這個對象包含了執行SQL所需的各種配置信息 53 MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); 54 // SQL類型 55 SqlCommandType sqlType = ms.getSqlCommandType(); 56 if(sqlType != SqlCommandType.UPDATE) { 57 return invocation.proceed(); 58 } 59 // 獲取版本號 60 Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_COLUMN_NAME); 61 if(originalVersion == null || Long.valueOf(originalVersion.toString()) <= 0){ 62 return invocation.proceed(); 63 } 64 // 獲取綁定的SQL 65 BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); 66 // 原始SQL 67 String originalSql = boundSql.getSql(); 68 // 加入version的SQL 69 originalSql = addVersionToSql(originalSql, originalVersion); 70 // 修改 BoundSql 71 metaObject.setValue("delegate.boundSql.sql", originalSql); 72 73 // proceed() 可以執行被攔截對象真正的方法,該方法實際上執行了method.invoke(target, args)方法 74 return invocation.proceed(); 75 } 76 77 /** 78 * Plugin.wrap 方法會自動判斷攔截器的簽名和被攔截對象的接口是否匹配,只有匹配的情況下才會使用動態代理攔截目標對象. 79 * 80 * @param target 被攔截的對象 81 * @return 代理對象 82 */ 83 @Override 84 public Object plugin(Object target) { 85 return Plugin.wrap(target, this); 86 } 87 88 /** 89 * 設置參數 90 */ 91 @Override 92 public void setProperties(Properties properties) { 93 94 } 95 96 /** 97 * 獲取代理的原始對象 98 * 99 * @param target 100 * @return 101 */ 102 private static Object processTarget(Object target) { 103 if(Proxy.isProxyClass(target.getClass())) { 104 MetaObject mo = SystemMetaObject.forObject(target); 105 return processTarget(mo.getValue("h.target")); 106 } 107 return target; 108 } 109 110 /** 111 * 為原SQL添加version 112 * 113 * @param originalSql 原SQL 114 * @param originalVersion 原版本號 115 * @return 加入version的SQL 116 */ 117 private String addVersionToSql(String originalSql, Object originalVersion){ 118 try{ 119 Statement stmt = CCJSqlParserUtil.parse(originalSql); 120 if(!(stmt instanceof Update)){ 121 return originalSql; 122 } 123 Update update = (Update)stmt; 124 if(!contains(update)){ 125 buildVersionExpression(update); 126 } 127 Expression where = update.getWhere(); 128 if(where != null){ 129 AndExpression and = new AndExpression(where, buildVersionEquals(originalVersion)); 130 update.setWhere(and); 131 }else{ 132 update.setWhere(buildVersionEquals(originalVersion)); 133 } 134 return stmt.toString(); 135 }catch(Exception e){ 136 logger.error(e.getMessage(), e); 137 return originalSql; 138 } 139 } 140 141 private boolean contains(Update update){ 142 List<Column> columns = update.getColumns(); 143 for(Column column : columns){ 144 if(column.getColumnName().equalsIgnoreCase(VERSION_COLUMN_NAME)){ 145 return true; 146 } 147 } 148 return false; 149 } 150 151 private void buildVersionExpression(Update update){ 152 // 列 version 153 Column versionColumn = new Column(); 154 versionColumn.setColumnName(VERSION_COLUMN_NAME); 155 update.getColumns().add(versionColumn); 156 157 // 值 version+1 158 Addition add = new Addition(); 159 add.setLeftExpression(versionColumn); 160 add.setRightExpression(new LongValue(1)); 161 update.getExpressions().add(add); 162 } 163 164 private Expression buildVersionEquals(Object originalVersion){ 165 Column column = new Column(); 166 column.setColumnName(VERSION_COLUMN_NAME); 167 168 // 條件 version = originalVersion 169 EqualsTo equal = new EqualsTo(); 170 equal.setLeftExpression(column); 171 equal.setRightExpression(new LongValue(originalVersion.toString())); 172 return equal; 173 } 174 175 }
之后還需配置該插件,只需要在MyBatisConfig中加入該配置即可。
最后,如果版本不匹配,更新失敗,需要往外拋出異常提醒,所以修改BaseService的update方法,增加檢查更新是否失敗。
最后,能不用插件盡量不要用插件,因為它將修改MyBatis的底層設計。插件生成的是層層代理對象的責任鏈模式,通過反射方法運行,會有一定的性能消耗。
我們也可以修改 tk.mapper 生成SQL的方法,加入version,這里通過插件方式實現樂觀鎖主要是不為了去修改 mapper 的底層源碼,比較方便。
六、Druid數據庫連接池
創建數據庫連接是一個很耗時的操作,也很容易對數據庫造成安全隱患。對數據庫連接的管理能顯著影響到整個應用程序的伸縮性和健壯性,影響程序的性能指標。
數據庫連接池負責分配、管理和釋放數據庫連接,它允許應用程序重復使用一個現有的數據庫連接,而不是再重新建立一個;釋放空閑時間超過最大空閑時間的數據庫連接來避免因為沒有釋放數據庫連接而引起的數據庫連接遺漏。數據庫連接池能明顯提高對數據庫操作的性能。
參考:
常用數據庫連接池 (DBCP、c3p0、Druid) 配置說明
1、Druid
Druid首先是一個數據庫連接池,但它不僅僅是一個數據庫連接池,它還包含一個ProxyDriver,一系列內置的JDBC組件庫,一個SQLParser。Druid支持所有JDBC兼容的數據庫,包括Oracle、MySql、Derby、Postgresql、SQLServer、H2等等。 Druid針對Oracle和MySql做了特別優化,比如Oracle的PSCache內存占用優化,MySql的ping檢測優化。Druid在監控、可擴展性、穩定性和性能方面都有明顯的優勢。Druid提供了Filter-Chain模式的擴展API,可以自己編寫Filter攔截JDBC中的任何方法,可以在上面做任何事情,比如說性能監控、SQL審計、用戶名密碼加密、日志等等。
2、配置
Druid配置到core模塊下,只需在application.properties中添加如下配置即可,大部分配置是默認配置,可更改。有詳細的注釋,比較容易理解。

1 #################################### 2 # Druid 3 #################################### 4 spring.datasource.driver-class-name=com.mysql.jdbc.Driver 5 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource 6 7 # 初始化連接大小[0] 8 spring.datasource.druid.initial-size=1 9 # 最小空閑連接數[0] 10 spring.datasource.druid.min-idle=1 11 # 最大連接數[8] 12 spring.datasource.druid.max-active=20 13 14 # 配置獲取連接等待超時的時間(毫秒)[-1] 15 spring.datasource.druid.max-wait=60000 16 # 查詢超時時間(秒) 17 spring.datasource.druid.query-timeout=90 18 19 # 用來檢測連接是否有效的sql,要求是一個查詢語句 20 spring.datasource.druid.validation-query=SELECT 'x' 21 # 申請連接時檢測連接可用性[false] 22 spring.datasource.druid.test-on-borrow=false 23 # 歸還連接檢測[false] 24 spring.datasource.druid.test-on-return=false 25 # 超時是否檢測連接可用性[true] 26 spring.datasource.druid.test-while-idle=true 27 28 # 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接 (毫秒) 29 spring.datasource.druid.time-between-eviction-runs-millis=60000 30 # 配置一個連接在池中最小生存的時間(毫秒,默認30分鍾) 31 spring.datasource.druid.min-evictable-idle-time-millis=300000 32 # 通過別名的方式配置擴展插件,常用的插件有:監控統計用的filter:stat;日志用的filter:log4j;防御sql注入的filter:wall 33 spring.datasource.druid.filters=stat,wall,slf4j 34 # 合並多個DruidDataSource的監控數據 35 spring.datasource.druid.use-global-data-source-stat=true 36 37 # 是否緩存PreparedStatement. PSCache對支持游標的數據庫性能提升巨大,比如說oracle.在mysql下建議關閉. 38 spring.datasource.druid.pool-prepared-statements=false 39 # 每個連接上PSCache的大小 40 spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20 41 42 # StatViewServlet [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE] 43 spring.datasource.druid.stat-view-servlet.enabled=true 44 spring.datasource.druid.stat-view-servlet.url-pattern=/druid/* 45 # 監控頁面的用戶名和密碼 46 spring.datasource.druid.stat-view-servlet.login-username=admin 47 spring.datasource.druid.stat-view-servlet.login-password=admin 48 spring.datasource.druid.stat-view-servlet.reset-enable=false 49 50 # StatFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter] 51 spring.datasource.druid.filter.stat.db-type=mysql 52 #慢SQL記錄 53 spring.datasource.druid.filter.stat.log-slow-sql=true 54 spring.datasource.druid.filter.stat.slow-sql-millis=2000 55 # SQL合並 56 spring.datasource.druid.filter.stat.merge-sql=false 57 58 # WallFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE-wallfilter] 59 spring.datasource.druid.filter.wall.enabled=true 60 spring.datasource.druid.filter.wall.db-type=mysql 61 spring.datasource.druid.filter.wall.config.delete-allow=false 62 spring.datasource.druid.filter.wall.config.drop-table-allow=false
之后啟動項目在地址欄輸入/druid/index.html並登錄就可以看到Druid監控頁面:
七、Redis緩存
對於如今的一個中小型系統來說,至少也需要一個緩存來緩存熱點數據,加快數據的訪問數據,這里選用Redis做緩存數據庫。在以后可以使用Redis做分布式緩存、做Session共享等。
1、SpringBoot的緩存支持
Spring定義了org.springframework.cache.CacheManager和org.springframework.cache.Cache接口來統一不同的緩存技術。CacheManager是Spring提供的各種緩存技術抽象接口,Cache接口包含緩存的各種操作。
針對不同的緩存技術,需要實現不同的CacheManager,Redis緩存則提供了RedisCacheManager的實現。
我將redis緩存功能放到sunny-starter-cache模塊下,cache模塊下可以有多種緩存技術,同時,對於其它項目來說,緩存是可插拔的,想用緩存直接引入cache模塊即可。
首先引入Redis的依賴:
SpringBoot已經默認為我們自動配置了多個CacheManager的實現,在autoconfigure.cache包下。在Spring Boot 環境下,使用緩存技術只需在項目中導入相關的依賴包即可。
在 RedisCacheConfiguration 里配置了默認的 CacheManager;SpringBoot提供了默認的redis配置,RedisAutoConfiguration 是Redis的自動化配置,比如創建連接池、初始化RedisTemplate等。
2、Redis 配置及聲明式緩存支持
Redis 默認配置了 RedisTemplate 和 StringRedisTemplate ,其使用的序列化規則是 JdkSerializationRedisSerializer,緩存到redis后,數據都變成了下面這種樣式,非常不易於閱讀。
因此,重新配置RedisTemplate,使用 Jackson2JsonRedisSerializer 來序列化 Key 和 Value。同時,增加HashOperations、ValueOperations等Redis數據結構相關的操作,這樣比較方便使用。

1 package com.lyyzoo.cache.redis; 2
3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.cache.annotation.EnableCaching; 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Configuration; 7 import org.springframework.data.redis.cache.RedisCacheManager; 8 import org.springframework.data.redis.connection.RedisConnectionFactory; 9 import org.springframework.data.redis.core.*; 10 import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 11 import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 12
13 import com.fasterxml.jackson.annotation.JsonAutoDetect; 14 import com.fasterxml.jackson.annotation.PropertyAccessor; 15 import com.fasterxml.jackson.databind.ObjectMapper; 16
17 /**
18 * Redis配置. 19 * 20 * 使用@EnableCaching開啟聲明式緩存支持. 之后就可以使用 @Cacheable/@CachePut/@CacheEvict 注解緩存數據. 21 * 22 * @author bojiangzhou 2018-02-11 23 * @version 1.0 24 */
25 @Configuration 26 @EnableCaching 27 public class RedisConfig { 28 @Autowired 29 private RedisConnectionFactory redisConnectionFactory; 30 @Autowired 31 private Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder; 32
33 /**
34 * 覆蓋默認配置 RedisTemplate,使用 String 類型作為key,設置key/value的序列化規則 35 */
36 @Bean 37 @SuppressWarnings("unchecked") 38 public RedisTemplate<String, Object> redisTemplate() { 39 RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); 40 redisTemplate.setConnectionFactory(redisConnectionFactory); 41
42 // 使用 Jackson2JsonRedisSerialize 替換默認序列化
43 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); 44 ObjectMapper objectMapper = jackson2ObjectMapperBuilder.createXmlMapper(false).build(); 45 objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 46 objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 47 jackson2JsonRedisSerializer.setObjectMapper(objectMapper); 48
49 // 設置value的序列化規則和key的序列化規則
50 redisTemplate.setKeySerializer(jackson2JsonRedisSerializer); 51 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); 52 redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer); 53 redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); 54 redisTemplate.afterPropertiesSet(); 55
56 return redisTemplate; 57 } 58
59 @Bean 60 public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) { 61 return redisTemplate.opsForHash(); 62 } 63
64 @Bean 65 public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) { 66 return redisTemplate.opsForValue(); 67 } 68
69 @Bean 70 public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) { 71 return redisTemplate.opsForList(); 72 } 73
74 @Bean 75 public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) { 76 return redisTemplate.opsForSet(); 77 } 78
79 @Bean 80 public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) { 81 return redisTemplate.opsForZSet(); 82 } 83
84 @Bean 85 public RedisCacheManager cacheManager() { 86 RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate()); 87 cacheManager.setUsePrefix(true); 88 return cacheManager; 89 } 90
91 }
同時,使用@EnableCaching開啟聲明式緩存支持,這樣就可以使用基於注解的緩存技術。注解緩存是一個對緩存使用的抽象,通過在代碼中添加下面的一些注解,達到緩存的效果。
-
@Cacheable:在方法執行前Spring先查看緩存中是否有數據,如果有數據,則直接返回緩存數據;沒有則調用方法並將方法返回值放進緩存。
-
@CachePut:將方法的返回值放到緩存中。
-
@CacheEvict:刪除緩存中的數據。
Redis服務器相關的一些配置可在application.properties中進行配置:
3、Redis工具類
添加一個Redis的統一操作工具,主要是對redis的常用數據類型操作類做了一個歸集。
ValueOperations用於操作String類型,HashOperations用於操作hash數據,ListOperations操作List集合,SetOperations操作Set集合,ZSetOperations操作有序集合。
關於redis的key命令和數據類型可參考我的學習筆記:

1 package com.lyyzoo.cache.redis; 2 3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.beans.factory.annotation.Value; 5 import org.springframework.data.redis.connection.DataType; 6 import org.springframework.data.redis.core.*; 7 import org.springframework.stereotype.Component; 8 9 import java.util.Collection; 10 import java.util.Date; 11 import java.util.Set; 12 import java.util.concurrent.TimeUnit; 13 import java.util.stream.Collectors; 14 import java.util.stream.Stream; 15 16 /** 17 * Redis 操作工具 18 * 19 * @version 1.0 20 * @author bojiangzhou 2018-02-12 21 */ 22 @Component 23 public class RedisOperator { 24 25 @Autowired 26 private RedisTemplate<String, Object> redisTemplate; 27 @Autowired 28 private ValueOperations<String, String> valueOperator; 29 @Autowired 30 private HashOperations<String, String, Object> hashOperator; 31 @Autowired 32 private ListOperations<String, Object> listOperator; 33 @Autowired 34 private SetOperations<String, Object> setOperator; 35 @Autowired 36 private ZSetOperations<String, Object> zSetOperator; 37 38 /** 39 * 默認過期時長,單位:秒 40 */ 41 public final static long DEFAULT_EXPIRE = 60 * 60 * 24; 42 43 /** 不設置過期時長 */ 44 public final static long NOT_EXPIRE = -1; 45 46 /** 47 * Redis的根操作路徑 48 */ 49 @Value("${redis.root:sunny}") 50 private String category; 51 52 public RedisOperator setCategory(String category) { 53 this.category = category; 54 return this; 55 } 56 57 /** 58 * 獲取Key的全路徑 59 * 60 * @param key key 61 * @return full key 62 */ 63 public String getFullKey(String key) { 64 return this.category + ":" + key; 65 } 66 67 68 // 69 // key 70 // ------------------------------------------------------------------------------ 71 /** 72 * 判斷key是否存在 73 * 74 * <p> 75 * <i>exists key</i> 76 * 77 * @param key key 78 */ 79 public boolean existsKey(String key) { 80 return redisTemplate.hasKey(getFullKey(key)); 81 } 82 83 /** 84 * 判斷key存儲的值類型 85 * 86 * <p> 87 * <i>type key</i> 88 * 89 * @param key key 90 * @return DataType[string、list、set、zset、hash] 91 */ 92 public DataType typeKey(String key){ 93 return redisTemplate.type(getFullKey(key)); 94 } 95 96 /** 97 * 重命名key. 如果newKey已經存在,則newKey的原值被覆蓋 98 * 99 * <p> 100 * <i>rename oldKey newKey</i> 101 * 102 * @param oldKey oldKeys 103 * @param newKey newKey 104 */ 105 public void renameKey(String oldKey, String newKey){ 106 redisTemplate.rename(getFullKey(oldKey), getFullKey(newKey)); 107 } 108 109 /** 110 * newKey不存在時才重命名. 111 * 112 * <p> 113 * <i>renamenx oldKey newKey</i> 114 * 115 * @param oldKey oldKey 116 * @param newKey newKey 117 * @return 修改成功返回true 118 */ 119 public boolean renameKeyNx(String oldKey, String newKey){ 120 return redisTemplate.renameIfAbsent(getFullKey(oldKey), getFullKey(newKey)); 121 } 122 123 /** 124 * 刪除key 125 * 126 * <p> 127 * <i>del key</i> 128 * 129 * @param key key 130 */ 131 public void deleteKey(String key){ 132 redisTemplate.delete(key); 133 } 134 135 /** 136 * 刪除key 137 * 138 * <p> 139 * <i>del key1 key2 ...</i> 140 * 141 * @param keys 可傳入多個key 142 */ 143 public void deleteKey(String ... keys){ 144 Set<String> ks = Stream.of(keys).map(k -> getFullKey(k)).collect(Collectors.toSet()); 145 redisTemplate.delete(ks); 146 } 147 148 /** 149 * 刪除key 150 * 151 * <p> 152 * <i>del key1 key2 ...</i> 153 * 154 * @param keys key集合 155 */ 156 public void deleteKey(Collection<String> keys){ 157 Set<String> ks = keys.stream().map(k -> getFullKey(k)).collect(Collectors.toSet()); 158 redisTemplate.delete(ks); 159 } 160 161 /** 162 * 設置key的生命周期,單位秒 163 * 164 * <p> 165 * <i>expire key seconds</i><br> 166 * <i>pexpire key milliseconds</i> 167 * 168 * @param key key 169 * @param time 時間數 170 * @param timeUnit TimeUnit 時間單位 171 */ 172 public void expireKey(String key, long time, TimeUnit timeUnit){ 173 redisTemplate.expire(key, time, timeUnit); 174 } 175 176 /** 177 * 設置key在指定的日期過期 178 * 179 * <p> 180 * <i>expireat key timestamp</i> 181 * 182 * @param key key 183 * @param date 指定日期 184 */ 185 public void expireKeyAt(String key, Date date){ 186 redisTemplate.expireAt(key, date); 187 } 188 189 /** 190 * 查詢key的生命周期 191 * 192 * <p> 193 * <i>ttl key</i> 194 * 195 * @param key key 196 * @param timeUnit TimeUnit 時間單位 197 * @return 指定時間單位的時間數 198 */ 199 public long getKeyExpire(String key, TimeUnit timeUnit){ 200 return redisTemplate.getExpire(key, timeUnit); 201 } 202 203 /** 204 * 將key設置為永久有效 205 * 206 * <p> 207 * <i>persist key</i> 208 * 209 * @param key key 210 */ 211 public void persistKey(String key){ 212 redisTemplate.persist(key); 213 } 214 215 216 /** 217 * 218 * @return RedisTemplate 219 */ 220 public RedisTemplate<String, Object> getRedisTemplate() { 221 return redisTemplate; 222 } 223 224 /** 225 * 226 * @return ValueOperations 227 */ 228 public ValueOperations<String, String> getValueOperator() { 229 return valueOperator; 230 } 231 232 /** 233 * 234 * @return HashOperations 235 */ 236 public HashOperations<String, String, Object> getHashOperator() { 237 return hashOperator; 238 } 239 240 /** 241 * 242 * @return ListOperations 243 */ 244 public ListOperations<String, Object> getListOperator() { 245 return listOperator; 246 } 247 248 /** 249 * 250 * @return SetOperations 251 */ 252 public SetOperations<String, Object> getSetOperator() { 253 return setOperator; 254 } 255 256 /** 257 * 258 * @return ZSetOperations 259 */ 260 public ZSetOperations<String, Object> getZSetOperator() { 261 return zSetOperator; 262 } 263 264 }
八、Swagger支持API文檔
1、Swagger
做前后端分離,前端和后端的唯一聯系,變成了API接口;API文檔變成了前后端開發人員聯系的紐帶,變得越來越重要,swagger就是一款讓你更好的書寫API文檔的框架。
Swagger是一個簡單又強大的能為你的Restful風格的Api生成文檔的工具。在項目中集成這個工具,根據我們自己的配置信息能夠自動為我們生成一個api文檔展示頁,可以在瀏覽器中直接訪問查看項目中的接口信息,同時也可以測試每個api接口。
2、配置
我這里直接使用別人已經整合好的swagger-spring-boot-starter,快速方便。
參考:spring-boot-starter-swagger
新建一個sunny-starter-swagger模塊,做到可插拔。
根據文檔,一般只需要做些簡單的配置即可:
但如果想要顯示swagger-ui.html文檔展示頁,還必須注入swagger資源:

1 package com.lyyzoo.swagger.config; 2 3 import org.springframework.context.annotation.Configuration; 4 import org.springframework.context.annotation.PropertySource; 5 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 6 import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 7 8 import com.spring4all.swagger.EnableSwagger2Doc; 9 10 /** 11 * @version 1.0 12 * @author bojiangzhou 2018-02-19 13 */ 14 @Configuration 15 @EnableSwagger2Doc 16 @PropertySource(value = "classpath:application-swagger.properties") 17 public class SunnySwaggerConfig extends WebMvcConfigurerAdapter { 18 /** 19 * 注入swagger資源文件 20 */ 21 @Override 22 public void addResourceHandlers(ResourceHandlerRegistry registry) { 23 registry.addResourceHandler("swagger-ui.html") 24 .addResourceLocations("classpath:/META-INF/resources/"); 25 registry.addResourceHandler("/webjars/**") 26 .addResourceLocations("classpath:/META-INF/resources/webjars/"); 27 } 28 29 }
3、使用
一般只需要在Controller加上swagger的注解即可顯示對應的文檔信息,如@Api、@ApiOperation、@ApiParam等。
常用注解參考:swagger-api-annotations

1 package com.lyyzoo.admin.system.controller; 2
3 import org.springframework.beans.factory.annotation.Autowired; 4 import org.springframework.web.bind.annotation.*; 5
6 import com.lyyzoo.admin.system.dto.Menu; 7 import com.lyyzoo.admin.system.service.MenuService; 8 import com.lyyzoo.core.base.BaseController; 9 import com.lyyzoo.core.base.Result; 10 import com.lyyzoo.core.util.Results; 11
12 import io.swagger.annotations.Api; 13 import io.swagger.annotations.ApiImplicitParam; 14 import io.swagger.annotations.ApiOperation; 15 import io.swagger.annotations.ApiParam; 16
17 @Api(tags = "菜單管理") 18 @RequestMapping 19 @RestController 20 public class MenuController extends BaseController { 21
22 @Autowired 23 private MenuService service; 24
25 /**
26 * 查找單個用戶 27 * 28 * @param menuId 菜單ID 29 * @return Result 30 */
31 @ApiOperation("查找單個用戶") 32 @ApiImplicitParam(name = "menuId", value = "菜單ID", paramType = "path") 33 @GetMapping("/sys/menu/get/{menuId}") 34 public Result get(@PathVariable Long menuId){ 35 Menu menu = service.selectById(menuId); 36 return Results.successWithData(menu); 37 } 38
39 /**
40 * 保存菜單 41 * 42 * @param menu 菜單 43 * @return Result 44 */
45 @ApiOperation("保存菜單") 46 @PostMapping("/sys/menu/save") 47 public Result save(@ApiParam(name = "menu", value = "菜單")@RequestBody Menu menu){ 48 menu = service.save(menu); 49 return Results.successWithData(menu); 50 } 51
52 /**
53 * 刪除菜單 54 * 55 * @param menuId 菜單ID 56 * @return Result 57 */
58 @ApiOperation("刪除菜單") 59 @ApiImplicitParam(name = "menuId", value = "菜單ID", paramType = "path") 60 @PostMapping("/sys/menu/delete/{menuId}") 61 public Result delete(@PathVariable Long menuId){ 62 service.deleteById(menuId); 63 return Results.success(); 64 } 65
66 }
之后訪問swagger-ui.html頁面就可以看到API文檔信息了。
如果不需要swagger,在配置文件中配置swagger.enabled=false,或移除sunny-starter-swagger的依賴即可。
九、項目優化調整
到這里,項目最基礎的一些功能就算完成了,但由於前期的一些設計不合理及未考慮周全等因素,對項目做一些調整。並參考《阿里巴巴Java開發手冊》對代碼做了一些優化。
1、項目結構
目前項目分為5個模塊:
最外層的Sunny作為聚合模塊負責管理所有子模塊,方便統一構建。並且繼承 spring-boot-starter-parent ,其它子模塊則繼承該模塊,方便統一管理 Spring Boot 及本項目的版本。這里已經把Spring Boot的版本升到 1.5.10.RELEASE。

1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 6 <groupId>com.lyyzoo</groupId> 7 <artifactId>sunny</artifactId> 8 <version>0.0.1-SNAPSHOT</version> 9 <packaging>pom</packaging> 10 11 <name>Sunny</name> 12 <description>Lyyzoo Base Application development platform</description> 13 14 <parent> 15 <groupId>org.springframework.boot</groupId> 16 <artifactId>spring-boot-starter-parent</artifactId> 17 <version>1.5.10.RELEASE</version> 18 <relativePath/> 19 </parent> 20 21 <properties> 22 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 23 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 24 <java.version>1.8</java.version> 25 26 <sunny.version>0.0.1-SNAPSHOT</sunny.version> 27 <springboot.version>1.5.10.RELEASE</springboot.version> 28 </properties> 29 30 <modules> 31 <module>sunny-starter</module> 32 <module>sunny-starter-core</module> 33 <module>sunny-starter-cache</module> 34 <module>sunny-starter-security</module> 35 <module>sunny-starter-admin</module> 36 <module>sunny-starter-swagger</module> 37 </modules> 38 39 <build> 40 <plugins> 41 <plugin> 42 <groupId>org.springframework.boot</groupId> 43 <artifactId>spring-boot-maven-plugin</artifactId> 44 </plugin> 45 </plugins> 46 </build> 47 48 </project>
sunny-starter 則引入了其余幾個模塊,在開發項目時,只需要繼承或引入sunny-starter即可,而無需一個個引入各個模塊。

1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 6 <parent> 7 <groupId>com.lyyzoo</groupId> 8 <artifactId>sunny</artifactId> 9 <version>0.0.1-SNAPSHOT</version> 10 </parent> 11 12 <groupId>com.lyyzoo.parent</groupId> 13 <artifactId>sunny-starter</artifactId> 14 <packaging>jar</packaging> 15 16 <name>sunny-starter</name> 17 <description>Sunny Parent</description> 18 19 <dependencies> 20 <!-- core --> 21 <dependency> 22 <groupId>com.lyyzoo.core</groupId> 23 <artifactId>sunny-starter-core</artifactId> 24 <version>${sunny.version}</version> 25 </dependency> 26 <!-- cache --> 27 <dependency> 28 <groupId>com.lyyzoo.cache</groupId> 29 <artifactId>sunny-starter-cache</artifactId> 30 <version>${sunny.version}</version> 31 </dependency> 32 <!-- security --> 33 <dependency> 34 <groupId>com.lyyzoo.security</groupId> 35 <artifactId>sunny-starter-security</artifactId> 36 <version>${sunny.version}</version> 37 </dependency> 38 <!-- admin --> 39 <dependency> 40 <groupId>com.lyyzoo.admin</groupId> 41 <artifactId>sunny-starter-admin</artifactId> 42 <version>${sunny.version}</version> 43 </dependency> 44 <!-- swagger --> 45 <dependency> 46 <groupId>com.lyyzoo.swagger</groupId> 47 <artifactId>sunny-starter-swagger</artifactId> 48 <version>${sunny.version}</version> 49 </dependency> 50 51 </dependencies> 52 53 <build> 54 <plugins> 55 <plugin> 56 <groupId>org.springframework.boot</groupId> 57 <artifactId>spring-boot-maven-plugin</artifactId> 58 </plugin> 59 </plugins> 60 </build> 61 62 63 </project>
對於一個Spring Boot項目,應該只有一個入口,即 @SpringBootApplication 注解的類。經測試,其它的模塊的配置文件application.properties的配置不會生效,應該是引用了入口模塊的配置文件。
所以為了讓各個模塊的配置文件都能生效,只需使用 @PropertySource 引入該配置文件即可,每個模塊都如此。在主模塊定義的配置會覆蓋其它模塊的配置。
2、開發規范
十、結語
到此,基礎架構篇結束!學習了很多新東西,如Spring Boot、Mapper、Druid;有些知識也深入地學習了,如MyBatis、Redis、日志框架、Maven等等。
在這期間,看完兩本書,可參考:《MyBatis從入門到精通》、《JavaEE開發的顛覆者 Spring Boot實戰》,另外,開發規范遵從《阿里巴巴Java開發手冊》,其它的參考資料都在文中有體現。
緊接着,后面會完成 sunny-starter-security 模塊的開發,主要使用spring-security技術,開發用戶登錄及權限控制等。
-----