第一阶段:XML配置,在Spring1.x时代,使用Spring开发满眼都是xml配置的Bean,随着项目的扩大,咱们须要把xml配置文件分放到不一样的配置文件里,那时候须要频繁的在开发的类和配置文件之间切换。html
第二阶段:注解配置,在Spring2.x时代,Spring提供声明Bean的注解,大大减小了配置量。应用的基本配置用xml,业务配置用注解。前端
第三阶段:Java配置,从Spring3.x到如今,Spring提供了Java配置,使用Java配置可让你更理解你所配置的Bean。java
Spring Boot:使用“习惯优于配置”的理念让你的项目快速运行起来。使用Spring Boot很容易建立一个独立运行、准生产级别的基于Spring框架的项目,使用Spring Boot你能够不用或者只须要不多的Spring配置。mysql
下面就来使用Spring Boot一步步搭建一个先后端分离的应用开发框架,而且之后不断的去完善这个框架,往里面添加功能。后面以实战为主,不会介绍太多概念,取而代之的是详细的操做。git
开发平台:windowsgithub
开发工具:Intellij IDEA 2017.1web
JDK:Java 8redis
Maven:maven-3.3.9算法
服务器:tomcat 8.0spring
数据库: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相关的东西。
① 经过File > New > Project,新建工程,选择Spring Initializr,而后Next。
② 尽可能为本身的框架想个好点的名字,能够去申请个本身的域名。我这里项目名称为Sunny,项目路径为com.lyyzoo.sunny。
③ 这里先什么都不选,后面再去集成。注意个人Spring Boot版本为1.5.9。Next
④ 定义好工程的目录,用一个专用目录吧,不要在一个目录下和其它东西杂在一块儿。以后点击Finish。
上面说的这么详细,只有一个目的,从一个开始就作好规范。
⑤ 生成的项目结构以下,能够本身去看下pom.xml里的内容。
先建立一个core核心、cache缓存、security受权认证,其它的后面再集成进去。
跟上面同样的方式,在Sunny下建立sunny-starter-core、sunny-starter-cache、sunny-starter-security子模块。
这样分模块后,咱们之后须要哪一个模块就引入哪一个模块便可,若是哪一个模块不知足需求,还能够重写该模块。
最终的项目结构以下:
首先在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根目录下。
① 配置文件
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为咱们作了不少自动化的配置,搭建快速方便。
可是,正由于它为咱们作了不少事情,就有不少坑,有时候,出了问题,咱们可能很难找出问题所在,这时候,咱们可能就要考虑下是不是自动配置致使的,有可能配置冲突了,或者没有使用上自定义的配置等等。
core是项目的核心模块,结构初步规划以下:
base是项目的基础核心,定义一些基础类,如BaseController、BaseService等;
cache是缓存相关;
config是配置中心,模块全部的配置放到config里统一管理;
constants里定义系统的常量。
exception里封装一些基础的异常类;
system是系统模块;
util里则是一些通用工具类;
只需在pom.xml中加入spring-boot-starter-web的依赖便可。
以后,查看POM的依赖树(插件:Maven Helper),能够看到引入了starter、tomcat、web支持等。能够看出,Sping Boot内嵌了servlet容器,默认tomcat。
自动配置在WebMvcAutoConfiguration和WebMvcProperties里,可自行查看源码,通常咱们不需添加其余配置就能够启动这个web项目了。
在core中添加一些基础的功能支持。
① 首先引入一些经常使用的依赖库,主要是一些经常使用工具类,方便之后的开发。
<!-- ******************************* 经常使用依赖库 ********************************** --> <!-- 针对开发IO流功能的工具类库 --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>${commons.io.version}</version> </dependency> <!-- 文件上传 --> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>${commons.fileupload.version}</version> <exclusions> <exclusion> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> </exclusion> </exclusions> </dependency> <!-- 经常使用的集合操做,丰富的工具类 --> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>${commons.collections.version}</version> </dependency> <!-- 操做javabean的工具包 --> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>${commons.beanutils.version}</version> <exclusions> <exclusion> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> </exclusion> </exclusions> </dependency> <!-- 包含一些通用的编码解码算法. 如:MD五、SHA一、Base64等 --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>${commons.codec.version}</version> </dependency> <!-- 包含丰富的工具类如 StringUtils --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${commons.lang3.version}</version> </dependency> <!-- Guava工程包含了若干被Google的Java项目普遍依赖的核心库. 集合[collections] 、缓存[caching] 、原生类型支持[primitives support] 、 并发库[concurrency libraries] 、通用注解[common annotations] 、字符串处理[string processing] 、I/O 等等。 --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${guava.version}</version> </dependency>
版本号以下:
② 在base添加一个Result类,做为前端的返回对象,Controller的直接返回对象都是Result。
package com.lyyzoo.core.base; import com.fasterxml.jackson.annotation.JsonInclude; import java.io.Serializable; /** * 前端返回对象 * * @version 1.0 * @author bojiangzhou 2017-12-28 */ public class Result implements Serializable { private static final long serialVersionUID = 1430633339880116031L; /** * 成功与否标志 */ private boolean success = true; /** * 返回状态码,为空则默认200.前端须要拦截一些常见的状态码如40三、40四、500等 */ @JsonInclude(JsonInclude.Include.NON_NULL) private Integer status; /** * 编码,可用于前端处理多语言,不须要则不用返回编码 */ @JsonInclude(JsonInclude.Include.NON_NULL) private String code; /** * 相关消息 */ @JsonInclude(JsonInclude.Include.NON_NULL) private String msg; /** * 相关数据 */ @JsonInclude(JsonInclude.Include.NON_NULL) private Object data; public Result() {} public Result(boolean success) { this.success = success; } public Result(boolean success, Integer status) { this.success = success; this.status = status; } public Result(boolean success, String code, String msg){ this(success); this.code = code; this.msg = msg; } public Result(boolean success, Integer status, String code, String msg) { this.success = success; this.status = status; this.code = code; this.msg = msg; } public Result(boolean success, String code, String msg, Object data){ this(success); this.code = code; this.msg = msg; this.data = data; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
以后在util添加生成Result的工具类Results,用于快速方便的建立Result对象。
package com.lyyzoo.core.util; import com.lyyzoo.core.base.Result; /** * Result生成工具类 * * @version 1.0 * @author bojiangzhou 2017-12-28 */ public class Results { protected Results() {} public static Result newResult() { return new Result(); } public static Result newResult(boolean success) { return new Result(success); } // // 业务调用成功 // ---------------------------------------------------------------------------------------------------- public static Result success() { return new Result(); } public static Result success(String msg) { return new Result(true, null, msg); } public static Result success(String code, String msg) { return new Result(true, code, msg); } public static Result successWithStatus(Integer status) { return new Result(true, status); } public static Result successWithStatus(Integer status, String msg) { return new Result(true, status, null, msg); } public static Result successWithData(Object data) { return new Result(true, null, null, data); } public static Result successWithData(Object data, String msg) { return new Result(true, null, msg, data); } public static Result successWithData(Object data, String code, String msg) { return new Result(true, code, msg, data); } // // 业务调用失败 // ---------------------------------------------------------------------------------------------------- public static Result failure() { return new Result(false); } public static Result failure(String msg) { return new Result(false, null, msg); } public static Result failure(String code, String msg) { return new Result(false, code, msg); } public static Result failureWithStatus(Integer status) { return new Result(false, status); } public static Result failureWithStatus(Integer status, String msg) { return new Result(false, status, null, msg); } public static Result failureWithData(Object data) { return new Result(false, null, null, data); } public static Result failureWithData(Object data, String msg) { return new Result(false, null, msg, data); } public static Result failureWithData(Object data, String code, String msg) { return new Result(false, code, msg, data); } }
③ 在base添加BaseEnum<K, V>枚举接口,定义了获取值和描述的接口。
package com.lyyzoo.core.base; /** * 基础枚举接口 * * @version 1.0 * @author bojiangzhou 2017-12-31 */ public interface BaseEnum<K, V> { /** * 获取编码 * * @return 编码 */ K code(); /** * 获取描述 * * @return 描述 */ V desc(); }
而后在constants下定义一个基础枚举常量类,咱们把一些描述信息维护到枚举里面,尽可能不要在代码中直接出现魔法值(如一些编码、中文等),之后的枚举常量类也能够按照这种模式来写。
package com.lyyzoo.core.constants; import com.lyyzoo.core.base.BaseEnum; import java.util.HashMap; import java.util.Map; /** * 基础枚举值 * * @version 1.0 * @author bojiangzhou 2018-01-01 */ public enum BaseEnums implements BaseEnum<String, String> { SUCCESS("request.success", "请求成功"), FAILURE("request.failure", "请求失败"), OPERATION_SUCCESS("operation.success", "操做成功"), OPERATION_FAILURE("operation.failure", "操做失败"), ERROR("system.error", "系统异常"), NOT_FOUND("not_found", "请求资源不存在"), FORBIDDEN("forbidden", "无权限访问"), VERSION_NOT_MATCH("record_not_exists_or_version_not_match", "记录版本不存在或不匹配"), PARAMETER_NOT_NULL("parameter_not_be_null", "参数不能为空"); private String code; private String desc; private static Map<String, String> allMap = new HashMap<>(); BaseEnums(String code, String desc) { this.code = code; this.desc = desc; } static { for(BaseEnums enums : BaseEnums.values()){ allMap.put(enums.code, enums.desc); } } @Override public String code() { return code; } @Override public String desc() { return desc; } public String desc(String code) { return allMap.get(code); } }
④ 再添加一个经常使用的日期工具类对象,主要包含一些经常使用的日期时间格式化,后续可再继续往里面添加一些公共方法。
package com.lyyzoo.core.util; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * 日期时间工具类 * * @version 1.0 * @author bojiangzhou 2017-12-28 */ public class Dates { /** * 日期时间匹配格式 */ public interface Pattern { // // 常规模式 // ---------------------------------------------------------------------------------------------------- /** * yyyy-MM-dd */ String DATE = "yyyy-MM-dd"; /** * yyyy-MM-dd HH:mm:ss */ String DATETIME = "yyyy-MM-dd HH:mm:ss"; /** * yyyy-MM-dd HH:mm */ String DATETIME_MM = "yyyy-MM-dd HH:mm"; /** * yyyy-MM-dd HH:mm:ss.SSS */ String DATETIME_SSS = "yyyy-MM-dd HH:mm:ss.SSS"; /** * HH:mm */ String TIME = "HH:mm"; /** * HH:mm:ss */ String TIME_SS = "HH:mm:ss"; // // 系统时间格式 // ---------------------------------------------------------------------------------------------------- /** * yyyy/MM/dd */ String SYS_DATE = "yyyy/MM/dd"; /** * yyyy/MM/dd HH:mm:ss */ String SYS_DATETIME = "yyyy/MM/dd HH:mm:ss"; /** * yyyy/MM/dd HH:mm */ String SYS_DATETIME_MM = "yyyy/MM/dd HH:mm"; /** * yyyy/MM/dd HH:mm:ss.SSS */ String SYS_DATETIME_SSS = "yyyy/MM/dd HH:mm:ss.SSS"; // // 无链接符模式 // ---------------------------------------------------------------------------------------------------- /** * yyyyMMdd */ String NONE_DATE = "yyyyMMdd"; /** * yyyyMMddHHmmss */ String NONE_DATETIME = "yyyyMMddHHmmss"; /** * yyyyMMddHHmm */ String NONE_DATETIME_MM = "yyyyMMddHHmm"; /** * yyyyMMddHHmmssSSS */ String NONE_DATETIME_SSS = "yyyyMMddHHmmssSSS"; } public static final String DEFAULT_PATTERN = Pattern.DATETIME; public static final String[] PARSE_PATTERNS = new String[]{ Pattern.DATE, Pattern.DATETIME, Pattern.DATETIME_MM, Pattern.DATETIME_SSS, Pattern.SYS_DATE, Pattern.SYS_DATETIME, Pattern.SYS_DATETIME_MM, Pattern.SYS_DATETIME_SSS }; /** * 格式化日期时间 * * @param date 日期时间 * * @return yyyy-MM-dd HH:mm:ss */ public static String format(Date date) { return format(date, DEFAULT_PATTERN); } /** * 格式化日期 * * @param date 日期(时间) * * @param pattern 匹配模式 参考:{@link Dates.Pattern} * * @return 格式化后的字符串 */ public static String format(Date date, String pattern) { if (date == null) { return null; } pattern = StringUtils.isNotBlank(pattern) ? pattern : DEFAULT_PATTERN; SimpleDateFormat sdf = new SimpleDateFormat(pattern); return sdf.format(date); } /** * 解析日期 * * @param date 日期字符串 * * @return 解析后的日期 默认格式:yyyy-MM-dd HH:mm:ss */ public static Date parseDate(String date) { if (StringUtils.isBlank(date)) { return null; } try { return DateUtils.parseDate(date, PARSE_PATTERNS); } catch (ParseException e) { e.printStackTrace(); } return null; } /** * 解析日期 * * @param date 日期 * * @param pattern 格式 参考:{@link Dates.Pattern} * * @return 解析后的日期,默认格式:yyyy-MM-dd HH:mm:ss */ public static Date parseDate(String date, String pattern) { if (StringUtils.isBlank(date)) { return null; } String[] parsePatterns; parsePatterns = StringUtils.isNotBlank(pattern) ? new String[]{pattern} : PARSE_PATTERNS; try { return DateUtils.parseDate(date, parsePatterns); } catch (ParseException e) { e.printStackTrace(); } return null; } }
⑤ Constants定义系统级的通用常量。
package com.lyyzoo.core.constants; import com.google.common.base.Charsets; import java.nio.charset.Charset; /** * 系统级常量类 * * @version 1.0 * @author bojiangzhou 2017-12-28 */ public class Constants { public static final String APP_NAME = "sunny"; /** * 系统编码 */ public static final Charset CHARSET = Charsets.UTF_8; /** * 标识:是/否、启用/禁用等 */ public interface Flag { Integer YES = 1; Integer NO = 0; } /** * 操做类型 */ public interface Operation { /** * 添加 */ String ADD = "add"; /** * 更新 */ String UPDATE = "update"; /** * 删除 */ String DELETE = "delete"; } /** * 性别 */ public interface Sex { /** * 男 */ Integer MALE = 1; /** * 女 */ Integer FEMALE = 0; } }
⑥ 在base添加空的BaseController、BaseDTO、Service、Mapper,先定义好基础结构,后面再添加功能。
BaseDTO:标准的who字段、版本号、及10个扩展字段。
由于这里用到了@Transient注解,先引入java持久化包:
package com.lyyzoo.core.base; import com.fasterxml.jackson.annotation.*; import com.lyyzoo.core.Constants; import com.lyyzoo.core.util.Dates; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import javax.persistence.Transient; import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 基础实体类 * * @version 1.0 * @author bojiangzhou 2017-12-29 */ public class BaseDTO implements Serializable { private static final long serialVersionUID = -4287607489867805101L; public static final String FIELD_OPERATE = "operate"; public static final String FIELD_OBJECT_VERSION_NUMBER = "versionNumber"; public static final String FIELD_CREATE_BY = "createBy"; public static final String FIELD_CREATOR = "creator"; public static final String FIELD_CREATE_DATE = "createDate"; public static final String FIELD_UPDATE_BY = "updateBy"; public static final String FIELD_UPDATER = "updater"; public static final String FIELD_UPDATE_DATE = "updateDate"; /** * 操做类型,add/update/delete 参考:{@link Constants.Operation} */ @Transient private String _operate; /** * 数据版本号,每发生update则自增,用于实现乐观锁. */ private Long versionNumber; // // 下面是标准 WHO 字段 // ---------------------------------------------------------------------------------------------------- /** * 建立人用户名 */ @JsonInclude(JsonInclude.Include.NON_NULL) private Long createBy; /** * 建立人名称 */ @JsonInclude(JsonInclude.Include.NON_NULL) @Transient private String creator; /** * 建立时间 */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonFormat(pattern = Dates.DEFAULT_PATTERN) private Date createDate; /** * 更新人用户名 */ @JsonInclude(JsonInclude.Include.NON_NULL) private Long updateBy; /** * 更新人名称 */ @JsonInclude(JsonInclude.Include.NON_NULL) @Transient private String updater; /** * 更新时间 */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonFormat(pattern = Dates.DEFAULT_PATTERN) private Date updateDate; /** * 其它属性 */ @JsonIgnore @Transient protected Map<String, Object> innerMap = new HashMap<>(); // // 下面是扩展属性字段 // ---------------------------------------------------------------------------------------------------- @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute1; @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute2; @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute3; @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute4; @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute5; @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute6; @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute7; @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute8; @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute9; @JsonInclude(JsonInclude.Include.NON_NULL) private String attribute10; public String get_operate() { return _operate; } public void set_operate(String _operate) { this._operate = _operate; } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); } public String toJSONString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE); } public Long getVersionNumber() { return versionNumber; } public void setVersionNumber(Long versionNumber) { this.versionNumber = versionNumber; } public Long getCreateBy() { return createBy; } public void setCreateBy(Long createBy) { this.createBy = createBy; } public String getCreator() { return creator; } public void setCreator(String creator) { this.creator = creator; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public Long getUpdateBy() { return updateBy; } public void setUpdateBy(Long updateBy) { this.updateBy = updateBy; } public String getUpdater() { return updater; } public void setUpdater(String updater) { this.updater = updater; } public Date getUpdateDate() { return updateDate; } public void setUpdateDate(Date updateDate) { this.updateDate = updateDate; } @JsonAnyGetter public Object getAttribute(String key) { return innerMap.get(key); } @JsonAnySetter public void setAttribute(String key, Object obj) { innerMap.put(key, obj); } public String getAttribute1() { return attribute1; } public void setAttribute1(String attribute1) { this.attribute1 = attribute1; } public String getAttribute2() { return attribute2; } public void setAttribute2(String attribute2) { this.attribute2 = attribute2; } public String getAttribute3() { return attribute3; } public void setAttribute3(String attribute3) { this.attribute3 = attribute3; } public String getAttribute4() { return attribute4; } public void setAttribute4(String attribute4) { this.attribute4 = attribute4; } public String getAttribute5() { return attribute5; } public void setAttribute5(String attribute5) { this.attribute5 = attribute5; } public String getAttribute6() { return attribute6; } public void setAttribute6(String attribute6) { this.attribute6 = attribute6; } public String getAttribute7() { return attribute7; } public void setAttribute7(String attribute7) { this.attribute7 = attribute7; } public String getAttribute8() { return attribute8; } public void setAttribute8(String attribute8) { this.attribute8 = attribute8; } public String getAttribute9() { return attribute9; } public void setAttribute9(String attribute9) { this.attribute9 = attribute9; } public String getAttribute10() { return attribute10; } public void setAttribute10(String attribute10) { this.attribute10 = attribute10; } }
同时,重写了toString方法,增长了toJsonString方法,使得能够格式化输出DTO的数据:
直接打印DTO,输出的格式大概就是这个样子:
⑦ 在exception添加BaseException,定义一些基础异常类
基础异常类都继承自运行时异常类(RunntimeException),尽量把受检异常转化为非受检异常,更好的面向接口编程,提升代码的扩展性、稳定性。
BaseException:添加了一个错误编码,其它自定义的异常应当继承该类。
package com.lyyzoo.core.exception; /** * 基础异常类 * * @version 1.0 * @author bojiangzhou 2017-12-31 */ public class BaseException extends RuntimeException { private static final long serialVersionUID = -997101946070796354L; /** * 错误编码 */ protected String code; public BaseException() {} public BaseException(String message) { super(message); } public BaseException(String code, String message) { super(message); this.code = code; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } }
ServiceException:继承BaseException,Service层往Controller抛出的异常。
package com.lyyzoo.core.exception; /** * Service层异常 * * @version 1.0 * @author bojiangzhou 2017-12-31 */ public class ServiceException extends BaseException { private static final long serialVersionUID = 6058294324031642376L; public ServiceException() {} public ServiceException(String message) { super(message); } public ServiceException(String code, String message) { super(code, message); } }
① 在system模块下,再分红dto、controller、service、mapper、constants子包,之后一个模块功能开发就是这样一个基础结构。
User:系统用户
package com.lyyzoo.core.system.dto; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; import com.lyyzoo.core.base.BaseDTO; import com.lyyzoo.core.util.Dates; import java.util.Date; /** * 系统用户 * * @version 1.0 * @author bojiangzhou 2017-12-31 */ @JsonInclude(JsonInclude.Include.NON_NULL) public class User extends BaseDTO { private static final long serialVersionUID = -7395431342743009038L; /** * 用户ID */ private Long userId; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 昵称 */ private String nickname; /** * 生日 */ @JsonFormat(pattern = Dates.Pattern.DATE) private Date birthday; /** * 性别:1-男/0-女 */ private Integer sex; /** * 是否启用:1/0 */ private Integer enabled; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public Integer getSex() { return sex; } public void setSex(Integer sex) { this.sex = sex; } public Integer getEnabled() { return enabled; } public void setEnabled(Integer enabled) { this.enabled = enabled; } }
UserController:用户控制层;用@RestController注解,先后端分离,由于无需返回视图,采用Restful风格,直接返回数据。
package com.lyyzoo.core.system.controller; import com.lyyzoo.core.Constants; import com.lyyzoo.core.base.BaseController; import com.lyyzoo.core.base.BaseEnums; import com.lyyzoo.core.base.Result; import com.lyyzoo.core.system.dto.User; import com.lyyzoo.core.util.Dates; import com.lyyzoo.core.util.Results; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; /** * 用户Controller * * @version 1.0 * @author bojiangzhou 2017-12-31 */ @RequestMapping("/sys/user") @RestController public class UserController extends BaseController { private static List<User> userList = new ArrayList<>(); // 先静态模拟数据 static { User user1 = new User(); user1.setUserId(1L); user1.setUsername("lufei"); user1.setNickname("蒙奇D路飞"); user1.setBirthday(Dates.parseDate("2000-05-05")); user1.setSex(Constants.Sex.MALE); user1.setEnabled(Constants.Flag.YES); userList.add(user1); User user2 = new User(); user2.setUserId(2L); user2.setUsername("nami"); user2.setNickname("娜美"); user2.setBirthday(Dates.parseDate("2000/7/3")); user2.setSex(Constants.Sex.FEMALE); user2.setEnabled(Constants.Flag.YES); userList.add(user2); } @RequestMapping("/queryAll") public Result queryAll(){ return Results.successWithData(userList, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description()); } @RequestMapping("/queryOne/{userId}") public Result queryOne(@PathVariable Long userId){ User user = null; for(User u : userList){ if(u.getUserId().longValue() == userId){ user = u; } } return Results.successWithData(user); } }
② Postman请求:请求成功,基础的HTTP服务已经实现了。
添加spring-boot-starter-jdbc以支持JDBC访问数据库,而后添加MySql的JDBC驱动mysql-connector-java;
在application.properties里配置mysql的数据库驱动
以后在application-dev.properties里配置开发环境数据库的链接信息,添加以后,Springboot就会自动配置数据源了。
MyBatis官方为了方便Springboot集成MyBatis,专门提供了一个符合Springboot规范的starter项目,即mybatis-spring-boot-starter。
在application.properties里添加mybatis映射配置:
通用Mapper能够极大的简化开发,极其方便的进行单表的增删改查。
关于通用Mapper,参考网站地址:
以后,在core.base下建立自定义的Mapper,按需选择接口。
具体可参考:根据须要自定义接口
package com.lyyzoo.core.base; import tk.mybatis.mapper.common.BaseMapper; import tk.mybatis.mapper.common.ConditionMapper; import tk.mybatis.mapper.common.IdsMapper; import tk.mybatis.mapper.common.special.InsertListMapper; /** * * BaseMapper * * @name BaseMapper * @version 1.0 * @author bojiangzhou 2017-12-31 */ public interface Mapper<T> extends BaseMapper<T>, ConditionMapper<T>, IdsMapper<T>, InsertListMapper<T> { }
定义好基础Mapper后,就具备下图中的基本通用方法了。每一个实体类对应的*Mapper继承Mapper<T>来得到基本的增删改查的通用方法。
在application.properties里配置自定义的基础Mapper
参考地址:
分页插件配置,通常状况下,不须要作任何配置。
以后,咱们就能够在代码中使用 PageHelper.startPage(1, 10) 对紧随其后的一个查询进行分页查询,很是方便。
在config下建立MyBatisConfig配置文件,经过mapperScannerConfigurer方法配置自动扫描Mapper文件。
package com.lyyzoo.core.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import tk.mybatis.spring.mapper.MapperScannerConfigurer; /** * MyBatis相关配置. * * @version 1.0 * @author bojiangzhou 2018-01-07 */ @Configuration public class MyBatisConfig { /** * Mapper扫描配置. 自动扫描将Mapper接口生成代理注入到Spring. */ @Bean public static MapperScannerConfigurer mapperScannerConfigurer() { MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer(); // 注意这里的扫描路径: 1.不要扫描到自定义的Mapper; 2.定义的路径不要扫描到tk.mybatis.mapper(如定义**.mapper). // 两个作法都会致使扫描到tk.mybatis的Mapper,就会产生重复定义的报错. mapperScannerConfigurer.setBasePackage("**.lyyzoo.**.mapper"); return mapperScannerConfigurer; } }
注意这里的 MapperScannerConfigurer 是tk.mybatis.spring.mapper.MapperScannerConfigurer,而不是org.mybatis,不然使用通用Mapper的方法时会报相似下面的这种错误
通常来讲,咱们不能在Controller中直接访问Mapper,所以咱们须要加上Service,经过Service访问Mapper。
首先定义基础Service<T>接口,根据Mapper定义基本的增删改查接口方法。
package com.lyyzoo.core.base; import java.util.List; /** * Service 基础通用接口 * * @name BaseService * @version 1.0 * @author bojiangzhou 2017-12-31 */ public interface Service<T> { // // insert // ---------------------------------------------------------------------------------------------------- /** * 保存一个实体,null的属性也会保存,不会使用数据库默认值 * * @param record * @return */ T insert(T record); /** * 批量插入,null的属性也会保存,不会使用数据库默认值 * * @param recordList * @return */ List<T> insert(List<T> recordList); /** * 保存一个实体,null的属性不会保存,会使用数据库默认值 * * @param record * @return */ T insertSelective(T record); /** * 批量插入,null的属性不会保存,会使用数据库默认值 * * @param recordList * @return */ List<T> insertSelective(List<T> recordList); // // update // ---------------------------------------------------------------------------------------------------- /** * 根据主键更新实体所有字段,null值会被更新 * * @param record * @return */ T update(T record); /** * 批量更新,根据主键更新实体所有字段,null值会被更新 * * @param recordList * @return */ List<T> update(List<T> recordList); /** * 根据主键更新属性不为null的值 * * @param record * @return */ T updateSelective(T record); /** * 批量更新,根据主键更新属性不为null的值 * * @param recordList * @return */ List<T> updateSelective(List<T> recordList); // // delete // ---------------------------------------------------------------------------------------------------- /** * 根据主键删除 * * @param id id不能为空 * @return */ int delete(Long id); /** * 根据主键字符串进行删除,类中只有存在一个带有@Id注解的字段 * * @param ids 相似1,2,3 */ int delete(String ids); /** * 根据主键删除多个实体,ID数组 * * @param ids 相似[1,2,3],不能为空 */ int delete(Long[] ids); /** * 根据实体属性做为条件进行删除 * * @param record * @return */ int delete(T record); /** * 根据主键删除多个实体 * * @param recordList * @return */ int delete(List<T> recordList); // // insert or update or delete // ---------------------------------------------------------------------------------------------------- /** * 根据实体的operate决定哪一种操做. null的属性也会保存,不会使用数据库默认值 * * @param record * @return */ T persist(T record); /** * 批量操做.根据实体的operate决定哪一种操做. null的属性也会保存,不会使用数据库默认值 * * @param recordList * @return */ List<T> persist(List<T> recordList); /** * 根据实体的operate决定哪一种操做. 根据主键更新属性不为null的值 * * @param record * @return */ T persistSelective(T record); /** * 批量操做.根据实体的operate决定哪一种操做. 根据主键更新属性不为null的值 * * @param recordList * @return */ List<T> persistSelective(List<T> recordList); // // select // ---------------------------------------------------------------------------------------------------- /** * 根据主键查询 * * @param id 不能为空 * @return */ T get(Long id); /** * 根据实体中的属性进行查询,只能有一个返回值,有多个结果是抛出异常 * * @param record * @return */ T get(T record); /** * 根据字段和值查询 返回一个 * @param key 不能为空 * @param value 不能为空 * @return */ T get(String key, Object value); /** * 根据主键字符串进行查询 * * @param ids 如 "1,2,3,4" * @return */ List<T> select(String ids); /** * 根据实体中的属性值进行查询 * * @param record * @return */ List<T> select(T record); /** * 根据属性和值查询 * * @param key * @param value * @return */ List<T> select(String key, Object value); /** * 根据实体中的属性值进行分页查询 * * @param record * @param pageNum * @param pageSize * @return */ List<T> select(T record, int pageNum, int pageSize); /** * 查询所有结果 * * @return */ List<T> selectAll(); /** * 根据实体中的属性查询总数 * * @param record * @return */ int count(T record); }
而后是实现类BaseService,之后的开发中,Service接口实现Service<T>,Service实现类继承BaseService<T>。
package com.lyyzoo.core.base; import com.github.pagehelper.PageHelper; import com.lyyzoo.core.constants.Constants; import com.lyyzoo.core.exception.UpdateFailedException; import com.lyyzoo.core.util.Reflections; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import javax.annotation.PostConstruct; import javax.persistence.Id; import java.lang.reflect.Field; import java.util.List; /** * 基础Service实现类 * * @version 1.0 * @author bojiangzhou 2018-01-04 */ public abstract class BaseService<T> implements Service<T> { @Autowired private Mapper<T> mapper; private Class<T> entityClass; @SuppressWarnings("unchecked") @PostConstruct public void init() { this.entityClass = Reflections.getClassGenericType(getClass()); } // // insert // ---------------------------------------------------------------------------------------------------- @Transactional(rollbackFor = Exception.class) public T insert(T record) { mapper.insert(record); return record; } @Transactional(rollbackFor = Exception.class) public List<T> insert(List<T> recordList) { mapper.insertList(recordList); return recordList; } @Transactional(rollbackFor = Exception.class) public T insertSelective(T record) { mapper.insertSelective(record); return record; } @Transactional(rollbackFor = Exception.class) public List<T> insertSelective(List<T> recordList) { // 因为Mapper暂未提供Selective的批量插入,此处循环查询. 固然也可参考InsertListMapper本身实现. for(T record : recordList){ mapper.insertSelective(record); } return recordList; } // // update // ---------------------------------------------------------------------------------------------------- @Transactional(rollbackFor = Exception.class) public T update(T record) { int count = mapper.updateByPrimaryKey(record); checkUpdate(count, record); return record; } @Transactional(rollbackFor = Exception.class) public List<T> update(List<T> recordList) { // Mapper暂未提供批量更新,此处循实现 for(T record : recordList){ int count = mapper.updateByPrimaryKey(record); checkUpdate(count, record); } return recordList; } @Transactional(rollbackFor = Exception.class) public T updateSelective(T record) { int count = mapper.updateByPrimaryKeySelective(record); checkUpdate(count, record); return record; } @Transactional(rollbackFor = Exception.class) public List<T> updateSelective(List<T> recordList) { // Mapper暂未提供批量更新,此处循实现 for(T record : recordList){ int count = mapper.updateByPrimaryKeySelective(record); checkUpdate(count, record); } return recordList; } // // delete // ---------------------------------------------------------------------------------------------------- @Transactional(rollbackFor = Exception.class) public int delete(Long id) { return mapper.deleteByPrimaryKey(id); } @Transactional(rollbackFor = Exception.class) public int delete(Long[] ids) { int count = 0; for(Long id : ids){ mapper.deleteByPrimaryKey(id); count++; } return count; } @Transactional(rollbackFor = Exception.class) public int delete(T record) { return mapper.delete(record); } @Transactional(rollbackFor = Exception.class) public int delete(List<T> recordList) { int count = 0; for(T record : recordList){ mapper.delete(record); count++; } return count; } // // all operate. insert or update or delete // ---------------------------------------------------------------------------------------------------- @Transactional(rollbackFor = Exception.class) public T persist(T record) { BaseDTO dto = (BaseDTO) record; Assert.notNull(dto.get_operate(), "_operate not be null."); switch (dto.get_operate()) { case Constants.Operation.ADD: insert(record); break; case Constants.Operation.UPDATE: update(record); break; case Constants.Operation.DELETE: delete(record); break; default: break; } dto.set_operate(null); return record; } @Transactional(rollbackFor = Exception.class) public List<T> persist(List<T> recordList) { for(T record : recordList){ BaseDTO dto = (BaseDTO) record; Assert.notNull(dto.get_operate(), "_operate not be null."); switch (dto.get_operate()) { case Constants.Operation.ADD: insert(record); break; case Constants.Operation.UPDATE: update(record); break; case Constants.Operation.DELETE: delete(record); break; default: break; } dto.set_operate(null); } return recordList; } @Transactional(rollbackFor = Exception.class) public T persistSelective(T record) { BaseDTO dto = (BaseDTO) record; Assert.notNull(dto.get_operate(), "_operate not be null."); switch (dto.get_operate()) { case Constants.Operation.ADD: insertSelective(record); break; case Constants.Operation.UPDATE: updateSelective(record); break; case Constants.Operation.DELETE: delete(record); break; default: break; } return record; } @Transactional(rollbackFor = Exception.class) public List<T> persistSelective(List<T> recordList) { for(T record : recordList){ BaseDTO dto = (BaseDTO) record; Assert.notNull(dto.get_operate(), "_operate not be null."); switch (dto.get_operate()) { case Constants.Operation.ADD: insertSelective(record); break; case Constants.Operation.UPDATE: updateSelective(record); break; case Constants.Operation.DELETE: delete(record); break; default: break; } } return recordList; } // // select // ---------------------------------------------------------------------------------------------------- public T get(Long id) { T entity = null; try { entity = entityClass.newInstance(); Field idField = Reflections.getFieldByAnnotation(entityClass, Id.class); idField.set(entity, id); } catch (Exception e) { e.printStackTrace(); } return mapper.selectByPrimaryKey(entity); } public T get(T record) { return mapper.selectOne(record); } public T get(String key, Object value) { T entity = null; try { entity = entityClass.newInstance(); Field field = Reflections.getField(entityClass, key); field.set(entity, value); } catch (Exception e) { e.printStackTrace(); } return mapper.selectOne(entity); } public List<T> select(String ids) { return mapper.selectByIds(ids); } public List<T> select(T record) { return mapper.select(record); } public List<T> select(String key, Object value) { T entity = null; try { entity = entityClass.newInstance(); Field field = Reflections.getField(entityClass, key); field.set(entity, value); } catch (Exception e) { e.printStackTrace(); } return mapper.select(entity); } public List<T> select(T record, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); return mapper.select(record); } public List<T> selectAll() { return mapper.selectAll(); } public int count(T record) { return mapper.selectCount(record); } /** * 检查乐观锁<br> * 更新失败时,抛出 UpdateFailedException 异常 * * @param updateCount update,delete 操做返回的值 * @param record 操做参数 */ protected void checkUpdate(int updateCount, Object record) { if (updateCount == 0 && record instanceof BaseDTO) { BaseDTO baseDTO = (BaseDTO) record; if (baseDTO.getVersion() != null) { throw new UpdateFailedException(); } } } }
BaseService的实现用到了反射工具类Reflections:
package com.lyyzoo.core.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; /** * 反射工具类. * * @version 1.0 * @author bojiangzhou 2018-01-06 */ public abstract class Reflections { private static Logger logger = LoggerFactory.getLogger(Reflections.class); /** * 经过反射, 得到Class定义中声明的泛型参数的类型, 注意泛型必须定义在父类处. 如没法找到, 返回Object.class. * * @param clazz class类 * * @return the 返回第一个声明的泛型类型. 若是没有,则返回Object.class */ @SuppressWarnings("unchecked") public static Class getClassGenericType(final Class clazz) { return getClassGenericType(clazz, 0); } /** * 经过反射, 得到Class定义中声明的父类的泛型参数的类型. 如没法找到, 返回Object.class. * * @param clazz class类 * * @param index 获取第几个泛型参数的类型,默认从0开始,即第一个 * * @return 返回第index个泛型参数类型. */ public static Class getClassGenericType(final Class clazz, final int index) { Type genType = clazz.getGenericSuperclass(); if (!(genType instanceof ParameterizedType)) { return Object.class; } Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); if (index >= params.length || index < 0) { logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: " + params.length); return Object.class; } if (!(params[index] instanceof Class)) { logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter"); return Object.class; } return (Class) params[index]; } /** * 根据注解类型获取实体的Field * * @param entityClass 实体类型 * * @param annotationClass 注解类型 * * @return 返回第一个有该注解类型的Field,若是没有则返回null. */ @SuppressWarnings("unchecked") public static Field getFieldByAnnotation(Class entityClass, Class annotationClass) { Field[] fields = entityClass.getDeclaredFields(); for (Field field : fields) { if (field.getAnnotation(annotationClass) != null) { makeAccessible(field); return field; } } return null; } /** * 获取实体的字段 * * @param entityClass 实体类型 * * @param fieldName 字段名称 * * @return 该字段名称对应的字段,若是没有则返回null. */ public static Field getField(Class entityClass, String fieldName){ try { Field field = entityClass.getDeclaredField(fieldName); makeAccessible(field); return field; } catch (NoSuchFieldException e) { e.printStackTrace(); } return null; } /** * 改变private/protected的成员变量为public. */ public static void makeAccessible(Field field) { if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) { field.setAccessible(true); } } }
Spring 只要引入aop则是默认开启事务的,通常咱们只要在须要事务管理的地方加上@Transactional注解便可支持事务,通常咱们会加在Service的类或者具体的增长、删除、更改的方法上。
我这里要说的是获取代理的问题。Service的事务管理是AOP实现的,AOP的实现用的是JDK动态代理或CGLIB动态代理。因此,若是你想在你的代理方法中以 this 调用当前接口的另外一个方法,另外一个方法的事务是不会起做用的。由于事务的方法是代理对象的,而 this 是当前类对象,不是一个代理对象,天然事务就不会起做用了。这是我在不久前的开发中遇到的实际问题,我自定义了一个注解,加在方法上,使用AspectJ来拦截该注解,却没拦截到,缘由就是这个方法是被另外一个方法以 this 的方式调用的,因此AOP不能起做用。
更详细的可参考:Spring AOP没法拦截内部方法调用
因此添加一个获取自身代理对象的接口,以方便获取代理对象来操做当前类方法。Service接口只须要继承该接口,T为接口自己便可,就能够经过self()获取自身的代理对象了。
package com.lyyzoo.core.base; import org.springframework.aop.framework.AopContext; /** * 获取代理对象自己. */ public interface ProxySelf<T> { /** * 取得当前对象的代理. * * @return 代理对象,若是未被代理,则抛出 IllegalStateException */ @SuppressWarnings("unchecked") default T self() { return (T) AopContext.currentProxy(); } }
还须要开启开启 exposeProxy = true,暴露代理对象,不然 AopContext.currentProxy() 会抛出异常。
① 实体映射
实体类按照以下规则和数据库表进行转换,注解所有是JPA中的注解:
表名默认使用类名,驼峰转下划线(只对大写字母进行处理),如UserInfo默认对应的表名为user_info
表名可使@Table(name = "tableName")进行指定,对不符合第一条默认规则的能够经过这种方式指定表名。
字段默认和@Column同样,都会做为表字段,表字段默认为Java对象的Field名字驼峰转下划线形式。
可使用@Column(name = "fieldName")指定不符合第3条规则的字段名。
使用@Transient注解能够忽略字段,添加该注解的字段不会做为表字段使用,注意,若是没有与表关联,必定要用@Transient标注。
建议必定是有一个@Id注解做为主键的字段,能够有多个@Id注解的字段做为联合主键。
默认状况下,实体类中若是不存在包含@Id注解的字段,全部的字段都会做为主键字段进行使用(这种效率极低)。
因为基本类型,如int做为实体类字段时会有默认值0,并且没法消除,因此实体类中建议不要使用基本类型。
User实体主要加了@Table注解,映射表名;而后在userId上标注主键注解;其它字段若是没加@Transient注解的默认都会做为表字段。
package com.lyyzoo.core.system.dto; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; import com.lyyzoo.core.base.BaseDTO; import com.lyyzoo.core.util.Dates; import javax.persistence.*; import java.util.Date; import java.util.List; /** * 系统用户 * * @name User * @version 1.0 * @author bojiangzhou 2017-12-31 */ @JsonInclude(JsonInclude.Include.NON_NULL) @Table(name = "SYS_USER") public class User extends BaseDTO { private static final long serialVersionUID = -7395431342743009038L; /** * 用户ID */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @OrderBy("DESC") private Long userId; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 昵称 */ private String nickname; /** * 生日 */ @JsonFormat(pattern = Dates.Pattern.DATE) private Date birthday; /** * 性别:1-男/0-女 */ private Integer sex; /** * 是否启用:1/0 */ private Integer enabled; public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public Integer getSex() { return sex; } public void setSex(Integer sex) { this.sex = sex; } public Integer getEnabled() { return enabled; } public void setEnabled(Integer enabled) { this.enabled = enabled; } }
② 建立表结构
CREATE TABLE `sys_user` ( `USER_ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '表ID,主键,供其余表作外键', `USERNAME` varchar(30) NOT NULL COMMENT '用户名', `PASSWORD` varchar(100) NOT NULL COMMENT '密码', `NICKNAME` varchar(30) NOT NULL COMMENT '用户名称', `BIRTHDAY` date DEFAULT NULL COMMENT '生日', `SEX` int(1) DEFAULT NULL COMMENT '性别:1-男;0-女', `ENABLED` int(1) NOT NULL DEFAULT '1' COMMENT '启用标识:1/0', `VERSION_NUMBER` int(11) NOT NULL DEFAULT '1' COMMENT '行版本号,用来处理锁', `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立时间', `CREATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '建立人', `UPDATE_BY` bigint(11) NOT NULL DEFAULT '-1' COMMENT '更新人', `UPDATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', `ATTRIBUTE1` varchar(150) DEFAULT NULL, `ATTRIBUTE2` varchar(150) DEFAULT NULL, `ATTRIBUTE3` varchar(150) DEFAULT NULL, `ATTRIBUTE4` varchar(150) DEFAULT NULL, `ATTRIBUTE5` varchar(150) DEFAULT NULL, `ATTRIBUTE6` varchar(150) DEFAULT NULL, `ATTRIBUTE7` varchar(150) DEFAULT NULL, `ATTRIBUTE8` varchar(150) DEFAULT NULL, `ATTRIBUTE9` varchar(150) DEFAULT NULL, `ATTRIBUTE10` varchar(150) DEFAULT NULL, PRIMARY KEY (`USER_ID`), UNIQUE KEY `USERNAME` (`USERNAME`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='系统用户';
③ 建立UserMapper
在system.mapper下建立UserMapper接口,继承Mapper<User>:
package com.lyyzoo.core.system.mapper; import com.lyyzoo.core.base.Mapper; import com.lyyzoo.core.system.dto.User; /** * * @name UserMapper * @version 1.0 * @author bojiangzhou 2018-01-06 */ public interface UserMapper extends Mapper<User> { }
④ 建立UserService
在system.service下建立UserService接口,只需继承Service<User>接口便可。
package com.lyyzoo.core.system.service; import com.lyyzoo.core.base.Service; import com.lyyzoo.core.system.dto.User; /** * 用户Service接口 * * @version 1.0 * @author bojiangzhou 2018-01-06 */ public interface UserService extends Service<User> { }
在system.service.impl下建立UserServiceImpl实现类,继承BaseService<User>类,实现UserService接口。同时加上@Service注解。
package com.lyyzoo.core.system.service.impl; import org.springframework.stereotype.Service; import com.lyyzoo.core.base.BaseService; import com.lyyzoo.core.system.dto.User; import com.lyyzoo.core.system.service.UserService; /** * 用户Service实现类 * * @version 1.0 * @author bojiangzhou 2018-01-06 */ @Service public class UserServiceImpl extends BaseService<User> implements UserService { }
⑤ 修改UserController,注入UserService,增长一些测试API
package com.lyyzoo.core.system.controller; import com.lyyzoo.core.base.BaseController; import com.lyyzoo.core.base.BaseEnums; import com.lyyzoo.core.base.Result; import com.lyyzoo.core.system.dto.User; import com.lyyzoo.core.system.service.UserService; import com.lyyzoo.core.util.Results; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; /** * 用户Controller * * @version 1.0 * @author bojiangzhou 2017-12-31 */ @RequestMapping @RestController public class UserController extends BaseController { @Autowired private UserService userService; @PostMapping("/sys/user/queryAll") public Result queryAll(){ List<User> list = userService.selectAll(); return Results.successWithData(list, BaseEnums.SUCCESS.code(), BaseEnums.SUCCESS.description()); } @RequestMapping("/sys/user/queryOne/{userId}") public Result queryOne(@PathVariable Long userId){ User user = userService.get(userId); return Results.successWithData(user); } @PostMapping("/sys/user/save") public Result save(@Valid @RequestBody User user){ user = userService.insertSelective(user); return Results.successWithData(user); } @PostMapping("/sys/user/update") public Result update(@Valid @RequestBody List<User> user){ user = userService.persistSelective(user); return Results.successWithData(user); } @RequestMapping("/sys/user/delete") public Result delete(User user){ userService.delete(user); return Results.success(); } @RequestMapping("/sys/user/delete/{userId}") public Result delete(@PathVariable Long userId){ userService.delete(userId); return Results.success(); } }
⑥ 测试结果
查询全部:
批量保存/修改:
使用代码生成器来生成基础的代码结构,生成DTO、XML等等。
MyBatis官方提供了代码生成器MyBatis Generator,但通常须要定制化。MyBatis Generator
我这里从网上找了一个使用起来比较方便的界面工具,可生成DTO、Mapper、Mapper.xml,生成以后还需作一些小调整。另须要本身建立对应的Service、Controller。以后有时间再从新定制化一个符合本项目的代码生成器。
在前面的测试中,会发现控制台输出的日志不怎么友好,有不少日志也没有输出,不便于查找排查问题。对于一个应用程序来讲日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。
先贴出一些参考资料:
Java有不少经常使用的日志框架,如Log4j、Log4j 二、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级别输出到控制台。
能够看到,只要集成了spring-boot-starter-web,就引入了spring-boot-starter-logging,即slf4j和logback。
其它的几个包:jcl-over-slf4j,代码直接调用common-logging会被桥接到slf4j;jul-to-slf4j,代码直接调用java.util.logging会被桥接到slf4j;log4j-over-slf4j,代码直接调用log4j会被桥接到slf4j。
还需引入janino,若是不加入这个包会报错。
在resources下添加logback.xml配置文件,Logback默认会查找classpath下的logback.xml文件。
具体配置以下,有较详细的注释,很容易看懂。能够经过application.properties配置日志记录级别、日志输出文件目录等。
<?xml version="1.0" encoding="UTF-8"?> <!-- 级别从高到低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL --> <!-- 日志输出规则 根据当前ROOT 级别,日志输出时,级别高于root默认的级别时 会输出 --> <!-- 如下 每一个配置的 filter 是过滤掉输出文件里面,会出现高级别文件,依然出现低级别的日志信息,经过filter 过滤只记录本级别的日志 --> <!-- scan 当此属性设置为true时,配置文件若是发生改变,将会被从新加载,默认值为true。 --> <!-- scanPeriod 设置监测配置文件是否有修改的时间间隔,若是没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 --> <!-- debug 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 --> <configuration debug="false" scan="false" scanPeriod="5 minutes"> <!-- 引入配置文件 --> <property resource="application.properties"/> <property resource="application-${app.env:-dev}.properties"/> <property name="app.name" value="${app.name:-sunny}"/> <property name="app.env" value="${app.env:-dev}"/> <!-- 日志记录级别 --> <property name="logback_level" value="${logback.level:-DEBUG}"/> <!-- 是否输出日志到文件 --> <property name="logback_rolling" value="${logback.rolling:-false}"/> <!-- 设置日志输出目录 --> <property name="logback_rolling_path" value="${logback.rolling.path:-/data/logs}"/> <!-- 日志文件最大大小 --> <property name="logback_max_file_size" value="${logback.max_file_size:-10MB}"/> <!-- 格式化输出:%d:表示日期,%thread:表示线程名,%-5level:级别从左显示5个字符宽度,%logger:日志输出者的名字(一般是所在类的全名),%L:输出代码中的行号,%msg:日志消息,%n:换行符 --> <property name="logback_pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger %L -| %msg%n"/> <if condition='p("logback_rolling").equals("true")'> <then> <!-- 滚动记录文件 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${logback_rolling_path}/${app.name}.log</file> <!-- rollingPolicy:当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名 --> <!-- TimeBasedRollingPolicy:最经常使用的滚动策略,它根据时间来制定滚动策略 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 --> <fileNamePattern>${logback_rolling_path}/${app.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <!-- 日志文件的保存期限为30天 --> <maxHistory>30</maxHistory> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <!-- maxFileSize:这是活动文件的大小,默认值是10MB --> <maxFileSize>${logback_max_file_size}</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern>${logback_pattern}</pattern> <charset>UTF-8</charset> </encoder> </appender> <root> <appender-ref ref="FILE"/> </root> </then> </if> <!-- 将日志打印到控制台 --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${logback_pattern}</pattern> </encoder> </appender> <root level="${logback_level}"> <appender-ref ref="CONSOLE"/> </root> <contextName>${app.name}</contextName> </configuration>
加入配置文件后,就能够看到控制台格式化后的日志输出,还能够看到具体代码行数等,比以前的友好多了。
同时,将日志滚动输出到日志文件,保留历史记录。可经过logback.rolling=false控制是否须要输出日志到文件。
配置好以后,就可使用Logger来输出日志了,使用起来也是很是方便。
* 能够看到引入的包是slf4j.Logger,代码里并无引用任何一个跟 Logback 相关的类,这即是使用 Slf4j的好处,在须要将日志框架切换为其它日志框架时,无需改动已有的代码。
* LoggerFactory 的 getLogger() 方法接收一个参数,以这个参数决定 logger 的名字,好比第二图中的日志输出。在为 logger 命名时,用类的全限定类名做为 logger name 是最好的策略,这样可以追踪到每一条日志消息的来源
* 能够看到,能够经过提供占位符,以参数化的方式打印日志,避免字符串拼接的没必要要损耗,也无需经过logger.isDebugEnabled()这种方式判断是否须要打印。
如今有一个问题,当日志级别设置到INFO级别后,只会输出INFO以上的日志,如INFO、WARN、ERROR,这没毛病,问题是,程序中抛出的异常堆栈(运行时异常)都没有打印了,不利于排查问题。
并且,在某些状况下,咱们在Service中想直接把异常往Controller抛出不作处理,但咱们不能直接把异常信息输出到客户端,这是很是不友好的。
因此,在config下建一个GlobalExceptionConfig做为全局统一异常处理。主要处理了自定义的ServiceException、AuthorityException、BaseException,以及系统的NoHandlerFoundException和Exception异常。
package com.lyyzoo.core.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.NoHandlerFoundException; import com.lyyzoo.core.base.Result; import com.lyyzoo.core.constants.BaseEnums; import com.lyyzoo.core.exception.AuthorityException; import com.lyyzoo.core.exception.BaseException; import com.lyyzoo.core.exception.ServiceException; import com.lyyzoo.core.util.Results; /** * 全局异常处理 * * @author bojiangzhou 2018-02-06 * @version 1.0 */ @RestControllerAdvice public class GlobalExceptionConfig { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionConfig.class); /** * 处理 ServiceException 异常 */ @ExceptionHandler(ServiceException.class) public Result handleServiceException(ServiceException e){ Result result = Results.failure(e.getCode(), e.getMessage()); result.setStatus(HttpStatus.BAD_REQUEST.value()); logger.info("ServiceException[code: {}, message: {}]", e.getCode(), e.getMessage()); return result; } /** * 处理 AuthorityException 异常 */ @ExceptionHandler(AuthorityException.class) public Result handleAuthorityException(AuthorityException e){ Result result = Results.failure(BaseEnums.FORBIDDEN.code(), BaseEnums.FORBIDDEN.desc()); result.setStatus(HttpStatus.FORBIDDEN.value()); logger.info("AuthorityException[code: {}, message: {}]", e.getCode(), e.getMessage()); return result; } /** * 处理 NoHandlerFoundException 异常. <br/> * 需配置 [spring.mvc.throw-exception-if-no-handler-found=true] * 需配置 [spring.resources.add-mappings=false] */ @ExceptionHandler(NoHandlerFoundException.class) public Result handleNotFoundException(NoHandlerFoundException e){ Result result = Results.failure(BaseEnums.NOT_FOUND.code(), BaseEnums.NOT_FOUND.desc()); result.setStatus(HttpStatus.NOT_FOUND.value()); logger.info(e.getMessage()); return result; } /** * 处理 BaseException 异常 */ @ExceptionHandler(BaseException.class) public Result handleBaseException(BaseException e){ Result result = Results.failure(e.getCode(), e.getMessage()); result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); logger.error("BaseException[code: {}, message: {}]", e.getCode(), e.getMessage(), e); return result; } /** * 处理 Exception 异常 */ @ExceptionHandler(Exception.class) public Result handleException(Exception e){ Result result = Results.failure(BaseEnums.ERROR.code(), BaseEnums.ERROR.desc()); result.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); logger.error(e.getMessage(), e); return result; } }
看上面的代码,@ControllAdvice(@RestControllerAdvice能够返回ResponseBody),可看作Controller加强器,能够在@ControllerAdvice做用类下添加@ExceptionHandler,@InitBinder,@ModelAttribute注解的方法来加强Controller,都会做用在被 @RequestMapping 注解的方法上。
使用@ExceptionHandler 拦截异常,咱们能够经过该注解实现自定义异常处理。在每一个处理方法中,封装Result,返回对应的消息及状态码等。
经过Logger打印对应级别的日志,也能够看到控制台及日志文件中有异常堆栈的输出了。注意除了BaseException、Exception,其它的都只是打印了简单信息,且为INFO级别。Exception是ERROR级别,且打印了堆栈信息。
NoHandlerFoundException 是404异常,这里注意要先关闭DispatcherServlet的NotFound默认异常处理。
测试以下:这种返回结果就比较友好了。
在并发修改同一条记录时,为避免更新丢失,须要加锁。要么在应用层加锁,要么在缓存层加锁,要么在数据库层使用乐观锁,使用version做为更新依据【强制】。 —— 《阿里巴巴Java开发手册》
乐观锁,基于数据版本(version)记录机制实现,为数据库表增长一个"version"字段。读取出数据时,将此版本号一同读出,以后更新时,对此版本号加一。提交数据时,提交的版本数据与数据库表对应记录的当前版本信息进行比对,若是提交的数据版本号大于数据库表当前版本号,则予以更新,不然认为是过时数据。
所以,这节就来处理BaseDTO中的"version"字段,经过增长一个mybatis插件来实现更新时版本号自动+1。
MyBatis 容许在己映射语句执行过程当中的某一点进行拦截调用。默认状况下, MyBatis 容许使用插件来拦截的接口和方法包括如下几个:
Executor (update 、query 、flushStatements 、commit 、rollback 、getTransaction 、close 、isClosed)
ParameterHandler (getParameterObject 、setParameters)
ResultSetHandler (handleResul tSets 、handleCursorResultSets、handleOutputParameters)
MyBatis 插件实现拦截器接口Interceptor,在实现类中对拦截对象和方法进行处理 。
setProperties:传递插件的参数,能够经过参数来改变插件的行为。
plugin:参数 target 就是要拦截的对象,做用就是给被拦截对象生成一个代理对象,并返回。
intercept:会覆盖所拦截对象的原方法,Invocation参数能够反射调度原来对象的方法,能够获取到不少有用的东西。
除了须要实现拦截器接口外,还须要给实现类配置拦截器签名。 使用 @Intercepts 和 @Signature 这两个注解来配置拦截器要拦截的接口的方法,接口方法对应的签名基本都是固定的。
@Intercepts 注解的属性是一个 @Signature 数组,能够在同 一个拦截器中同时拦截不一样的接口和方法。
@Signature 注解包含如下三个属性。
type:设置拦截的接口,可选值是前面提到的4个接口 。
method:设置拦截接口中的方法名, 可选值是前面4个接口对应的方法,须要和接口匹配 。
要实现版本号自动更新,咱们须要在SQL被执行前修改SQL,所以咱们须要拦截的就是 StatementHandler 接口的 prepare 方法,该方法会在数据库执行前被调用,优先于当前接口的其它方法而被执行。
在 core.plugin 包下新建一个VersionPlugin插件,实现Interceptor拦截器接口。
该接口方法签名以下:
在 interceptor 方法中对 UPDATE 类型的操做,修改原SQL,加入version,修改后的SQL相似下图,更新时就会自动将version+1。同时带上version条件,若是该版本号小于数据库记录版本号,则不会更新。
VersionInterceptor插件:
package com.lyyzoo.core.plugins; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.update.Update; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Proxy; import java.sql.Connection; import java.util.List; import java.util.Properties; /** * 乐观锁:数据版本插件 * * @version 1.0 * @author bojiangzhou 2018-02-10 */ @Intercepts( @Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} ) ) public class VersionInterceptor implements Interceptor { private static final String VERSION_COLUMN_NAME = "version"; private static final Logger logger = LoggerFactory.getLogger(VersionInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { // 获取 StatementHandler,实际是 RoutingStatementHandler StatementHandler handler = (StatementHandler) processTarget(invocation.getTarget()); // 包装原始对象,便于获取和设置属性 MetaObject metaObject = SystemMetaObject.forObject(handler); // MappedStatement 是对SQL更高层次的一个封装,这个对象包含了执行SQL所需的各类配置信息 MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); // SQL类型 SqlCommandType sqlType = ms.getSqlCommandType(); if(sqlType != SqlCommandType.UPDATE) { return invocation.proceed(); } // 获取版本号 Object originalVersion = metaObject.getValue("delegate.boundSql.parameterObject." + VERSION_COLUMN_NAME); if(originalVersion == null || Long.valueOf(originalVersion.toString()) <= 0){ return invocation.proceed(); } // 获取绑定的SQL BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql"); // 原始SQL String originalSql = boundSql.getSql(); // 加入version的SQL originalSql = addVersionToSql(originalSql, originalVersion); // 修改 BoundSql metaObject.setValue("delegate.boundSql.sql", originalSql); // proceed() 能够执行被拦截对象真正的方法,该方法实际上执行了method.invoke(target, args)方法 return invocation.proceed(); } /** * Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的状况下才会使用动态代理拦截目标对象. * * @param target 被拦截的对象 * @return 代理对象 */ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 设置参数 */ @Override public void setProperties(Properties properties) { } /** * 获取代理的原始对象 * * @param target * @return */ private static Object processTarget(Object target) { if(Proxy.isProxyClass(target.getClass())) { MetaObject mo = SystemMetaObject.forObject(target); return processTarget(mo.getValue("h.target")); } return target; } /** * 为原SQL添加version * * @param originalSql 原SQL * @param originalVersion 原版本号 * @return 加入version的SQL */ private String addVersionToSql(String originalSql, Object originalVersion){ try{ Statement stmt = CCJSqlParserUtil.parse(originalSql); if(!(stmt instanceof Update)){ return originalSql; } Update update = (Update)stmt; if(!contains(update)){ buildVersionExpression(update); } Expression where = update.getWhere(); if(where != null){ AndExpression and = new AndExpression(where, buildVersionEquals(originalVersion)); update.setWhere(and); }else{ update.setWhere(buildVersionEquals(originalVersion)); } return stmt.toString(); }catch(Exception e){ logger.error(e.getMessage(), e); return originalSql; } } private boolean contains(Update update){ List<Column> columns = update.getColumns(); for(Column column : columns){ if(column.getColumnName().equalsIgnoreCase(VERSION_COLUMN_NAME)){ return true; } } return false; } private void buildVersionExpression(Update update){ // 列 version Column versionColumn = new Column(); versionColumn.setColumnName(VERSION_COLUMN_NAME); update.getColumns().add(versionColumn); // 值 version+1 Addition add = new Addition(); add.setLeftExpression(versionColumn); add.setRightExpression(new LongValue(1)); update.getExpressions().add(add); } private Expression buildVersionEquals(Object originalVersion){ Column column = new Column(); column.setColumnName(VERSION_COLUMN_NAME); // 条件 version = originalVersion EqualsTo equal = new EqualsTo(); equal.setLeftExpression(column); equal.setRightExpression(new LongValue(originalVersion.toString())); return equal; } }
以后还需配置该插件,只须要在MyBatisConfig中加入该配置便可。
最后,若是版本不匹配,更新失败,须要往外抛出异常提醒,因此修改BaseService的update方法,增长检查更新是否失败。
最后,能不用插件尽可能不要用插件,由于它将修改MyBatis的底层设计。插件生成的是层层代理对象的责任链模式,经过反射方法运行,会有必定的性能消耗。
咱们也能够修改 tk.mapper 生成SQL的方法,加入version,这里经过插件方式实现乐观锁主要是不为了去修改 mapper 的底层源码,比较方便。
建立数据库链接是一个很耗时的操做,也很容易对数据库形成安全隐患。对数据库链接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响程序的性能指标。
数据库链接池负责分配、管理和释放数据库链接,它容许应用程序重复使用一个现有的数据库链接,而不是再从新创建一个;释放空闲时间超过最大空闲时间的数据库链接来避免由于没有释放数据库链接而引发的数据库链接遗漏。数据库链接池能明显提升对数据库操做的性能。
参考:
经常使用数据库链接池 (DBCP、c3p0、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审计、用户名密码加密、日志等等。
Druid配置到core模块下,只需在application.properties中添加以下配置便可,大部分配置是默认配置,可更改。有详细的注释,比较容易理解。
#################################### # Druid #################################### spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource # 初始化链接大小[0] spring.datasource.druid.initial-size=1 # 最小空闲链接数[0] spring.datasource.druid.min-idle=1 # 最大链接数[8] spring.datasource.druid.max-active=20 # 配置获取链接等待超时的时间(毫秒)[-1] spring.datasource.druid.max-wait=60000 # 查询超时时间(秒) spring.datasource.druid.query-timeout=90 # 用来检测链接是否有效的sql,要求是一个查询语句 spring.datasource.druid.validation-query=SELECT 'x' # 申请链接时检测链接可用性[false] spring.datasource.druid.test-on-borrow=false # 归还链接检测[false] spring.datasource.druid.test-on-return=false # 超时是否检测链接可用性[true] spring.datasource.druid.test-while-idle=true # 配置间隔多久才进行一次检测,检测须要关闭的空闲链接 (毫秒) spring.datasource.druid.time-between-eviction-runs-millis=60000 # 配置一个链接在池中最小生存的时间(毫秒,默认30分钟) spring.datasource.druid.min-evictable-idle-time-millis=300000 # 经过别名的方式配置扩展插件,经常使用的插件有:监控统计用的filter:stat;日志用的filter:log4j;防护sql注入的filter:wall spring.datasource.druid.filters=stat,wall,slf4j # 合并多个DruidDataSource的监控数据 spring.datasource.druid.use-global-data-source-stat=true # 是否缓存PreparedStatement. PSCache对支持游标的数据库性能提高巨大,好比说oracle.在mysql下建议关闭. spring.datasource.druid.pool-prepared-statements=false # 每一个链接上PSCache的大小 spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20 # StatViewServlet [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatViewServlet%E9%85%8D%E7%BD%AE] spring.datasource.druid.stat-view-servlet.enabled=true spring.datasource.druid.stat-view-servlet.url-pattern=/druid/* # 监控页面的用户名和密码 spring.datasource.druid.stat-view-servlet.login-username=admin spring.datasource.druid.stat-view-servlet.login-password=admin spring.datasource.druid.stat-view-servlet.reset-enable=false # StatFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_StatFilter] spring.datasource.druid.filter.stat.db-type=mysql #慢SQL记录 spring.datasource.druid.filter.stat.log-slow-sql=true spring.datasource.druid.filter.stat.slow-sql-millis=2000 # SQL合并 spring.datasource.druid.filter.stat.merge-sql=false # WallFilter [https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE-wallfilter] spring.datasource.druid.filter.wall.enabled=true spring.datasource.druid.filter.wall.db-type=mysql spring.datasource.druid.filter.wall.config.delete-allow=false spring.datasource.druid.filter.wall.config.drop-table-allow=false
以后启动项目在地址栏输入/druid/index.html并登陆就能够看到Druid监控页面:
对于现在的一个中小型系统来讲,至少也须要一个缓存来缓存热点数据,加快数据的访问数据,这里选用Redis作缓存数据库。在之后可使用Redis作分布式缓存、作Session共享等。
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等。
Redis 默认配置了 RedisTemplate 和 StringRedisTemplate ,其使用的序列化规则是 JdkSerializationRedisSerializer,缓存到redis后,数据都变成了下面这种样式,很是不易于阅读。
所以,从新配置RedisTemplate,使用 Jackson2JsonRedisSerializer 来序列化 Key 和 Value。同时,增长HashOperations、ValueOperations等Redis数据结构相关的操做,这样比较方便使用。
package com.lyyzoo.cache.redis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.*; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; /** * Redis配置. * * 使用@EnableCaching开启声明式缓存支持. 以后就可使用 @Cacheable/@CachePut/@CacheEvict 注解缓存数据. * * @author bojiangzhou 2018-02-11 * @version 1.0 */ @Configuration @EnableCaching public class RedisConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Autowired private Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder; /** * 覆盖默认配置 RedisTemplate,使用 String 类型做为key,设置key/value的序列化规则 */ @Bean @SuppressWarnings("unchecked") public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 使用 Jackson2JsonRedisSerialize 替换默认序列化 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = jackson2ObjectMapperBuilder.createXmlMapper(false).build(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 设置value的序列化规则和key的序列化规则 redisTemplate.setKeySerializer(jackson2JsonRedisSerializer); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) { return redisTemplate.opsForHash(); } @Bean public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) { return redisTemplate.opsForValue(); } @Bean public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) { return redisTemplate.opsForList(); } @Bean public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) { return redisTemplate.opsForSet(); } @Bean public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) { return redisTemplate.opsForZSet(); } @Bean public RedisCacheManager cacheManager() { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate()); cacheManager.setUsePrefix(true); return cacheManager; } }
同时,使用@EnableCaching开启声明式缓存支持,这样就可使用基于注解的缓存技术。注解缓存是一个对缓存使用的抽象,经过在代码中添加下面的一些注解,达到缓存的效果。
@Cacheable:在方法执行前Spring先查看缓存中是否有数据,若是有数据,则直接返回缓存数据;没有则调用方法并将方法返回值放进缓存。
@CachePut:将方法的返回值放到缓存中。
@CacheEvict:删除缓存中的数据。
Redis服务器相关的一些配置可在application.properties中进行配置:
添加一个Redis的统一操做工具,主要是对redis的经常使用数据类型操做类作了一个归集。
ValueOperations用于操做String类型,HashOperations用于操做hash数据,ListOperations操做List集合,SetOperations操做Set集合,ZSetOperations操做有序集合。
关于redis的key命令和数据类型可参考个人学习笔记:
package com.lyyzoo.cache.redis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.core.*; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.Date; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Redis 操做工具 * * @version 1.0 * @author bojiangzhou 2018-02-12 */ @Component public class RedisOperator { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private ValueOperations<String, String> valueOperator; @Autowired private HashOperations<String, String, Object> hashOperator; @Autowired private ListOperations<String, Object> listOperator; @Autowired private SetOperations<String, Object> setOperator; @Autowired private ZSetOperations<String, Object> zSetOperator; /** * 默认过时时长,单位:秒 */ public final static long DEFAULT_EXPIRE = 60 * 60 * 24; /** 不设置过时时长 */ public final static long NOT_EXPIRE = -1; /** * Redis的根操做路径 */ @Value("${redis.root:sunny}") private String category; public RedisOperator setCategory(String category) { this.category = category; return this; } /** * 获取Key的全路径 * * @param key key * @return full key */ public String getFullKey(String key) { return this.category + ":" + key; } // // key // ------------------------------------------------------------------------------ /** * 判断key是否存在 * * <p> * <i>exists key</i> * * @param key key */ public boolean existsKey(String key) { return redisTemplate.hasKey(getFullKey(key)); } /** * 判断key存储的值类型 * * <p> * <i>type key</i> * * @param key key * @return DataType[string、list、set、zset、hash] */ public DataType typeKey(String key){ return redisTemplate.type(getFullKey(key)); } /** * 重命名key. 若是newKey已经存在,则newKey的原值被覆盖 * * <p> * <i>rename oldKey newKey</i> * * @param oldKey oldKeys * @param newKey newKey */ public void renameKey(String oldKey, String newKey){ redisTemplate.rename(getFullKey(oldKey), getFullKey(newKey)); } /** * newKey不存在时才重命名. * * <p> * <i>renamenx oldKey newKey</i> * * @param oldKey oldKey * @param newKey newKey * @return 修改为功返回true */ public boolean renameKeyNx(String oldKey, String newKey){ return redisTemplate.renameIfAbsent(getFullKey(oldKey), getFullKey(newKey)); } /** * 删除key * * <p> * <i>del key</i> * * @param key key */ public void deleteKey(String key){ redisTemplate.delete(key); } /** * 删除key * * <p> * <i>del key1 key2 ...</i> * * @param keys 可传入多个key */ public void deleteKey(String ... keys){ Set<String> ks = Stream.of(keys).map(k -> getFullKey(k)).collect(Collectors.toSet()); redisTemplate.delete(ks); } /** * 删除key * * <p> * <i>del key1 key2 ...</i> * * @param keys key集合 */ public void deleteKey(Collection<String> keys){ Set<String> ks = keys.stream().map(k -> getFullKey(k)).collect(Collectors.toSet()); redisTemplate.delete(ks); } /** * 设置key的生命周期,单位秒 * * <p> * <i>expire key seconds</i><br> * <i>pexpire key milliseconds</i> * * @param key key * @param time 时间数 * @param timeUnit TimeUnit 时间单位 */ public void expireKey(String key, long time, TimeUnit timeUnit){ redisTemplate.expire(key, time, timeUnit); } /** * 设置key在指定的日期过时 * * <p> * <i>expireat key timestamp</i> * * @param key key * @param date 指定日期 */ public void expireKeyAt(String key, Date date){ redisTemplate.expireAt(key, date); } /** * 查询key的生命周期 * * <p> * <i>ttl key</i> * * @param key key * @param timeUnit TimeUnit 时间单位 * @return 指定时间单位的时间数 */ public long getKeyExpire(String key, TimeUnit timeUnit){ return redisTemplate.getExpire(key, timeUnit); } /** * 将key设置为永久有效 * * <p> * <i>persist key</i> * * @param key key */ public void persistKey(String key){ redisTemplate.persist(key); } /** * * @return RedisTemplate */ public RedisTemplate<String, Object> getRedisTemplate() { return redisTemplate; } /** * * @return ValueOperations */ public ValueOperations<String, String> getValueOperator() { return valueOperator; } /** * * @return HashOperations */ public HashOperations<String, String, Object> getHashOperator() { return hashOperator; } /** * * @return ListOperations */ public ListOperations<String, Object> getListOperator() { return listOperator; } /** * * @return SetOperations */ public SetOperations<String, Object> getSetOperator() { return setOperator; } /** * * @return ZSetOperations */ public ZSetOperations<String, Object> getZSetOperator() { return zSetOperator; } }
作先后端分离,前端和后端的惟一联系,变成了API接口;API文档变成了先后端开发人员联系的纽带,变得愈来愈重要,swagger就是一款让你更好的书写API文档的框架。
Swagger是一个简单又强大的能为你的Restful风格的Api生成文档的工具。在项目中集成这个工具,根据咱们本身的配置信息可以自动为咱们生成一个api文档展现页,能够在浏览器中直接访问查看项目中的接口信息,同时也能够测试每一个api接口。
我这里直接使用别人已经整合好的swagger-spring-boot-starter,快速方便。
参考:spring-boot-starter-swagger
新建一个sunny-starter-swagger模块,作到可插拔。
根据文档,通常只须要作些简单的配置便可:
但若是想要显示swagger-ui.html文档展现页,还必须注入swagger资源:
package com.lyyzoo.swagger.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import com.spring4all.swagger.EnableSwagger2Doc; /** * @version 1.0 * @author bojiangzhou 2018-02-19 */ @Configuration @EnableSwagger2Doc @PropertySource(value = "classpath:application-swagger.properties") public class SunnySwaggerConfig extends WebMvcConfigurerAdapter { /** * 注入swagger资源文件 */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("swagger-ui.html") .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); } }
通常只须要在Controller加上swagger的注解便可显示对应的文档信息,如@Api、@ApiOperation、@ApiParam等。
经常使用注解参考:swagger-api-annotations
package com.lyyzoo.admin.system.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import com.lyyzoo.admin.system.dto.Menu; import com.lyyzoo.admin.system.service.MenuService; import com.lyyzoo.core.base.BaseController; import com.lyyzoo.core.base.Result; import com.lyyzoo.core.util.Results; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @Api(tags = "菜单管理") @RequestMapping @RestController public class MenuController extends BaseController { @Autowired private MenuService service; /** * 查找单个用户 * * @param menuId 菜单ID * @return Result */ @ApiOperation("查找单个用户") @ApiImplicitParam(name = "menuId", value = "菜单ID", paramType = "path") @GetMapping("/sys/menu/get/{menuId}") public Result get(@PathVariable Long menuId){ Menu menu = service.selectById(menuId); return Results.successWithData(menu); } /** * 保存菜单 * * @param menu 菜单 * @return Result */ @ApiOperation("保存菜单") @PostMapping("/sys/menu/save") public Result save(@ApiParam(name = "menu", value = "菜单")@RequestBody Menu menu){ menu = service.save(menu); return Results.successWithData(menu); } /** * 删除菜单 * * @param menuId 菜单ID * @return Result */ @ApiOperation("删除菜单") @ApiImplicitParam(name = "menuId", value = "菜单ID", paramType = "path") @PostMapping("/sys/menu/delete/{menuId}") public Result delete(@PathVariable Long menuId){ service.deleteById(menuId); return Results.success(); } }
以后访问swagger-ui.html页面就能够看到API文档信息了。
若是不须要swagger,在配置文件中配置swagger.enabled=false,或移除sunny-starter-swagger的依赖便可。
到这里,项目最基础的一些功能就算完成了,但因为前期的一些设计不合理及未考虑周全等因素,对项目作一些调整。并参考《阿里巴巴Java开发手册》对代码作了一些优化。
目前项目分为5个模块:
最外层的Sunny做为聚合模块负责管理全部子模块,方便统一构建。而且继承 spring-boot-starter-parent ,其它子模块则继承该模块,方便统一管理 Spring Boot 及本项目的版本。这里已经把Spring Boot的版本升到 1.5.10.RELEASE。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lyyzoo</groupId>
<artifactId>sunny</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Sunny</name>
<description>Lyyzoo Base Application development platform</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<sunny.version>0.0.1-SNAPSHOT</sunny.version>
<springboot.version>1.5.10.RELEASE</springboot.version>
</properties>
<modules>
<module>sunny-starter</module>
<module>sunny-starter-core</module>
<module>sunny-starter-cache</module>
<module>sunny-starter-security</module>
<module>sunny-starter-admin</module>
<module>sunny-starter-swagger</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
sunny-starter 则引入了其他几个模块,在开发项目时,只须要继承或引入sunny-starter便可,而无需一个个引入各个模块。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lyyzoo</groupId>
<artifactId>sunny</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.lyyzoo.parent</groupId>
<artifactId>sunny-starter</artifactId>
<packaging>jar</packaging>
<name>sunny-starter</name>
<description>Sunny Parent</description>
<dependencies>
<!-- core -->
<dependency>
<groupId>com.lyyzoo.core</groupId>
<artifactId>sunny-starter-core</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- cache -->
<dependency>
<groupId>com.lyyzoo.cache</groupId>
<artifactId>sunny-starter-cache</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- security -->
<dependency>
<groupId>com.lyyzoo.security</groupId>
<artifactId>sunny-starter-security</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- admin -->
<dependency>
<groupId>com.lyyzoo.admin</groupId>
<artifactId>sunny-starter-admin</artifactId>
<version>${sunny.version}</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>com.lyyzoo.swagger</groupId>
<artifactId>sunny-starter-swagger</artifactId>
<version>${sunny.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
对于一个Spring Boot项目,应该只有一个入口,即 @SpringBootApplication 注解的类。经测试,其它的模块的配置文件application.properties的配置不会生效,应该是引用了入口模块的配置文件。
因此为了让各个模块的配置文件都能生效,只需使用 @PropertySource 引入该配置文件便可,每一个模块都如此。在主模块定义的配置会覆盖其它模块的配置。
到此,基础架构篇结束!学习了不少新东西,如Spring Boot、Mapper、Druid;有些知识也深刻地学习了,如MyBatis、Redis、日志框架、Maven等等。
在这期间,看完两本书,可参考:《MyBatis从入门到精通》、《JavaEE开发的颠覆者 Spring Boot实战》,另外,开发规范听从《阿里巴巴Java开发手册》,其它的参考资料都在文中有体现。
紧接着,后面会完成 sunny-starter-security 模块的开发,主要使用spring-security技术,开发用户登陆及权限控制等。