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

 


Spring的简史

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

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

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

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

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

 

零、开发技术简介

开发平台:windows

开发工具:Intellij IDEA 2017.1

JDK:Java 8

Maven:maven-3.3.9

服务器:tomcat 8.0

数据库:MySQL 5.7

数据源:Druid1.1.6

缓存:Redis 3.2

日志框架:SLF4J+Logback

Spring Boot:1.5.9.RELEASE

ORM框架:MyBatis+通用Mapper

Spring Boot官方文档:Spring Boot Reference Guide

 

一、创建项目

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

1、创建工程

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

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

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

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

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

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

2、创建Starter

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

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

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

最终的项目结构如下:

3、启动项目

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

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

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

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

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

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

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

③ 替换默认的banner

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

4、Spring Boot 配置

① 配置文件

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

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

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

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

② starter pom

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

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

③ 自动配置

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

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

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

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

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

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

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

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

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

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

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

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

5、项目结构划分

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

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

    cache是缓存相关;

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

    constants里定义系统的常量。

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

 system是系统模块;

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

 

二、基础结构功能

1、web支持

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

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

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

2、基础功能

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

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

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

版本号如下:

② 在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对象。

  View Code

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

  View Code

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

  View Code

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

  View Code

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

  View Code

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

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

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

  View Code

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

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

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

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

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

  View Code

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

  View Code

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

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

User:系统用户

  View Code

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

  View Code

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

 

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

1、添加JDBC、配置数据源

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

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

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

2、集成MyBatis

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

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

3、添加MyBatis通用Mapper

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

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

  MyBatis通用Mapper

  MyBatis 相关工具

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

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

  View Code

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

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

4、添加分页插件PageHelper

参考地址:

  MyBatis 分页插件 - PageHelper

  分页插件使用方法

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

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

5、配置自动扫描Mapper

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

  View Code

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

6、定义基础Service

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

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

  View Code

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

  View Code

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

  View Code

7、获取AOP代理

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

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

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

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

  View Code

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

8、数据持久化测试

① 实体映射

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

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

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

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

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

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

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

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

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

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

  View Code

② 创建表结构

  View Code

③ 创建UserMapper

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

  View Code

④ 创建UserService

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

  View Code

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

  View Code

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

  View Code

⑥ 测试结果

查询所有:

批量保存/修改:

9、代码生成器

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

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

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

mybatis-generator界面工具

 

四、日志及全局异常处理

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

先贴出一些参考资料:

  logback 配置详解

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

  Java常用日志框架介绍

1、日志框架简介

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2、配置日志

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

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

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

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

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

  View Code

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

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

3、使用Logger

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

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

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

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

4、全局异常处理

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

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

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

  View Code

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

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

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

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

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

    

 

五、数据库乐观锁

1、乐观锁

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

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

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

2、MyBatis插件介绍

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

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

  • ParameterHandler (getParameterObject 、setParameters)

  • ResultSetHandler (handleResul tSets 、handleCursorResultSets、handleOutputParameters)

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

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

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

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

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

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

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

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

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

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

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

3、数据版本插件

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

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

该接口方法签名如下:

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

VersionInterceptor插件:

  View Code

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

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

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

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

 

六、Druid数据库连接池

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

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

参考:

  Druid常见问题集锦

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

1、Druid

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

2、配置

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

  View Code

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

 

七、Redis缓存

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

1、SpringBoot的缓存支持

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

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

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

首先引入Redis的依赖:

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

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

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

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

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

  View Code

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

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

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

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

 

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

3、Redis工具类

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

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

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

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

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

  View Code

 

八、Swagger支持API文档

1、Swagger

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

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

2、配置

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

参考:spring-boot-starter-swagger

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

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

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

  View Code

3、使用

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

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

  View Code

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

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

 

九、项目优化调整

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

1、项目结构

目前项目分为5个模块:

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

  View Code

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

  View Code

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

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

2、开发规范

 

十、结语

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

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

 

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

 

-----https://www.cnblogs.com/chiangchou/p/sunny-1.html

转载于:https://www.cnblogs.com/sjqq/p/10055446.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值