# Reference Guide 2.0.x

# Part1 : 简介

Boot Plus 是基于spring boot 的增强,但并未修改SpringBoot已有的功能,也就是说完全可以兼容使用SpringBoot的项目。可以任意使用其中一个模块的增强,而不必引入所有模块。让spring boot的使用者更好地关注于业务。

主要增强以下几个方面:

  • dynamic datasource based on aop
  • cache: spring cache enhancement
  • web: json serialize enhancement, cros , validation enhancement
  • log management online
  • swagger(api online)
  • security integration: more simple, support JWT & Stateful Token

# Part2 : 新版本特性

从2.0.0版本开始,做了一些模块上的重构:

  • 移除了mybatis-jpa及相关的generator,单独维护至mybatis-jpa-parent
  • 同时也升级了spring boot 版本号至 2.3.12.RELEASE ,与 spring cloud Hoxton.SR12 的版本号保持一致
  • swaager移除了文档导出

# Part3 : 起步

# 3.1 系统要求

Boot Plus 1.3.x 至少要求java1.8,Spring Boot 2.3.12.RELEASE.

# 3.2 Maven 依赖

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alilitech</groupId>
            <artifactId>boot-plus-dependencies</artifactId>
            <version>2.0.x</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

# 3.3 开发第一个应用

# 3.3.1 创建pom

<?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.example</groupId>
    <artifactId>myproject</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.alilitech</groupId>
            <artifactId>boot-plus-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alilitech</groupId>
            <artifactId>boot-plus-web</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alilitech</groupId>
                <artifactId>boot-plus-dependencies</artifactId>
                <version>1.3.x</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

你可以通过IDE指定启动类:com.AppStart来启动项目,也可以通过自定义启动类启动项目。

# 3.3.2 创建一个可执行的jar

需要添加 spring-boot-maven-plugin 至 pom.xml :

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <mainClass>com.AppStart</mainClass>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

至此,你已成功搭建和部署一个项目了。

# Part4 : boot-plus-core

提供了基于spring的依赖(没有web),可用于项目只执行一次就结束进程的(比如云原生的定时任务)。提供了以下功能:

# 启动类:com.AppStart

启动时更优雅地打印更多信息,包括本地地址,当前IP地址等。

# 工具类:BeanUtils

更高效的深度拷贝。父类属性,组合属性都可一并拷贝。常用于Entity与DTO之间的对象拷贝,提高开发效率。

特性

  • 源对象可以继承,可以组合。父类的属性、组合对象的属性可以一并拷贝至目标对象,无需二次赋值
  • 基于属性反射+Getter/Setter方法进行拷贝(理论上可以在拷贝的时候重写源对象属性的Getter方法从而转换成需要的值,但不建议这么做)
  • 反射比较耗时,第一次反射后,启用了缓存,避免频繁反射带来的开销
  • 忽略源对象的属性。如果源对象的属性(包括组合对象)可能会有属性重复,但实现拷贝时只需要一个属性,如果不忽略某些属性,可能会造成目标对象属性值的覆盖。(下面会详解如何忽略)

注意

源对象和目标对象属性名称和属性类型要完全一致,否则无法拷贝,造成值丢失

如何忽略源对象属性

比如有个组合类如下

public class User {
    String userId;
    
    String name;
    
    Date createdTime;
    
    UserInfo userInfo;
    
    // getter & setter
    
}

public class UserInfo {
    String userInfoId;
    
    String userId;
    
    Date createdTime;
    
    // getter & setter
    
}

在用工具类拷贝的时候,UserInfo的createdTime可以会覆盖User对象的createdTime。

想要忽略UserInfocreatedTime,可以如下使用

// simleName.property,注意大小写
BeanUtils.copyPropertiesDeep(User, UserDTO.class, "UserInfo.createdTime");

如果想忽略UsercreatedTime,可以如下使用

// 源对象最外层的类名可以省略
BeanUtils.copyPropertiesDeep(User, UserDTO.class, "createdTime");
// 或
BeanUtils.copyPropertiesDeep(User, UserDTO.class, "User.createdTime");

# Part5 boot-plus-routing-datasource

动态数据源是指在项目里可以配置多个数据源,并且可以在不同的地方指定使用不同的数据源。

此多数据源的实现未依赖于任何一个持久化的框架,所以一般于用于多库多service。想在同一个事务里操作多个数据源暂时无法实现。

# 5.1 配置多数据源

主数据源还和原来的方式一样,当未指定数据源或找不到指定的数据源时使用主数据源。主数据源的标识是default。 其它数据源标识要以ds开头,否则无法识别。配置方式和spring boot的方式一样。

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test
    username: test
    password: MyNewPass4!
    ds-second:
      url: jdbc:mysql://127.0.0.1:3306/test1
      username: test
      password: MyNewPass4! 

代码里动态使用数据源的方式采用注解,一般在service层。比如:

@DynamicSource("ds-second")
public void exeService() {
    
}

# 5.2 加密密码解析

部分在配置文件里的密码是加密的,但在连数据的时候需要明文。所以提供了EncryptPropertyResolver来解析。

@Component
public class MyEncryptPropertyResolver implements EncryptPropertyResolver {
    @Override
    public boolean supportResolve(String key, String value) {
        return key.equals("spring.datasource.ds-second.password");
    }

    @Override
    public String resolve(String value) {
        return value;
    }
}

# 5.3 动态添加/移除数据源

代码里可以动态地创建数据源,并添加到多数据源里。

我们可以拿到数据源对象,判断是否是DefaultDynamicDataSource此类,可以调用此类的addDataSource方法。一般用于数据源实时动态修改切换。

//add datasource
defaultDynamicDataSource.addDataSource(datasourceName, datasourceUrl, datasourceUsername, datasourcePassword);

//remove datasource
defaultDynamicDataSource.remove(datasourceName);

# Part6 boot-plus-web

# 6.1 跨域支持

跨域只需要在application.yml里配置:

mvc:
  cors:
    enabled: true              
    path : "/**"               
    allowedOrigins: "*"        
    allowedMethods: "*"
    allowCredentials: true
    maxAge: 3600
    exposedHeaders: 
      - message
  • enabled: 是否启用跨域
  • path: 哪些URL可以跨域
  • allowedOrigins: 哪些域名可以请求跨域,逗号隔开
  • allowedMethods: 哪些方法可以跨域 POST, GET, PUT, DELETE, OPTIONS
  • allowCredentials
  • maxAge
  • exposedHeaders:跨域时哪些头部信息返回

# 6.2 Jackson序列化扩展

# 6.2.1 null值处理

对于部分null值,前端展示不友好,又不能直接去掉,若在业务里处理量大且麻烦,通过配置或注解可以解决此问题。

mvc:
  json:
    defaultNull: true
    defaultNullValue: "-"
  • defaultNull: 是否空字段(null)返回默认值,默认值如下:

    null字段类型 默认值
    String ""
    Array []
    Object {}
    Date ""
    Double 0.0
    Integer 0
    Map {}
    BigDecimal 0.0
  • defaultNullValue: 若配置了这个,则全部用此代替空字段(null)的返回默认值

以上都是对json的全局配置,我们对空值的默认值可以部分定义,如定义在某个需要序列化的类上:

@NullFormat(defaultNull = true, defaultNullValue = "-")

TIP

所有对null值默认值的处理不能对map等非常规bean起作用。如果是部分定义,只对当前类有效,其聚合的类无效。

若是对于同一个对象需要在不同线程里实现不同的效果,比如查看详情和修改详情对于null值处理不一样,使用如下:

DefaultNullContextHolder.set(false);   //关闭此次请求/线程对null值序列化的处理

# 6.2.2 自定义序列化

自定义序列化的方式有很多,比如我们使用jackson自带的注解可解决自定义序列化:

@JsonSerialize(using = MyJsonSerializer.class)
String name;

但是我们有很多种格式,比如有些值在转换成亿,有些值要转化成百万,有些要加币种前缀,有些要加单位后缀, 这些如果组合起来,jackson自带的序列化有点力不从心,为此我们扩展了jackson的自定义序列化。如:

@SerializerConvert(convertClasses = MySerializerConverter.class)
@SerializerFormat(newTarget = true, post = "万元", pre = "$")
private Integer s7 = 19000000;
  • @SerializerConvert

    表示数据转换,可定义多个计算与转换,依次执行。比如@SerializerConvert(convertClasses = {MySerializerConverter.class, MySerializerConverter2.class}),类似于一种链式计算。

  • @SerializerFormat

    表示数据格式化,将计算后的数据格式化成最终需要展示的格式。

    TIP

    两个注解可以任意搭配使用,可使用其中一个或多个

    新生成的key,可以如下配置来定义目标属性的key,用{} 表示占位符

    mvc:
      json:
      	target-filed-key-format: "{}Name"
    

# 6.2.3 数字格式化

业务上有很多逻辑运算,运算完了会产生结果,往往不同类型(如int、double)的数据返回的格式不一样,但实际上展示的时候需要统一保留两位或统一格式化。使用@NumberFormat注解:

@NumberFormat(pattern = "#,###,##0.00")
private BigDecimal amount;
  • pattern: 格式化格式,若使用了此样式,通过DecimalFormat.format(pattern)去格式化

  • scale: 保留位数,默认是2

  • round: 取舍模式,默认4舍五入,参考BigDecimal里的常量

  • 其它注解属性是和@SerializerFormat 一致,所以相当于用一个注解代替了两个注解,减少注解的个数

# 6.2.4 字典序列化

字典在数据库里表示不同的含义,从数据库里查出的是字典的key,与显示的值有一一对应关系。在实际展示给用户时必须是用户能理解的含义。以往的解决方式是通过在业务里单独处理或数据库查询的时候关联查询,若有新的字典含义可能还需要改代码,给项目带来了风险与不便。

Boot Plus提供了解决方法:

  • 定义字典和字典值。实现DictCollector并暴露给spring,DictCollector是字典收集器,可以收集所有的字典。可以定义多个字典收集器。

    有两个方法,一个方法是默认的,也是没有国际化的字典,一个是提供国际化的。

    • findDictAndValues

      没有国际化的字典收集方法,参考代码

      ResourceBundleCollection resourceBundleCollection = ResourceBundleCollection.ResourceBundleCollectionBuilder.newBuilder().build();
      
      Sting dictKey = ...;
      Map<String, Object> dictValueMap = ...;
      
      resourceBundleCollection.putResourceBundle(dictKey, dictValueMap);
      
    • findLocaleDictAndValues

      国际化的字典收集方法

    • // 开启国际化,默认为false
      mvc.json.enableLocale=true
      
  • 使用@DicFormat注解。无需添加其它字段即可实现字段值的显示。

    • dictKey: 字典key,默认为当前属性的名称
    • disableCache 禁用缓存(暂未实现)
    • 其它注解属性是和@SerializerFormat 一致,所以相当于用一个注解代替了两个注解,减少注解的个数

注意

字典序列化默认采取了缓存策略,若字典key未找到,会从相应的收集器或全部收集器再收集一次,故请在各个收集器里做好缓存策略,避免大面积将查询打到数据库。(已优化:对于同一个线程下若发现未匹配到相应的字典或字典值,不会重复收集)

注意

以上对json的扩展是基于jackson(spring mvc默认序列化)的,若在项目里未使用jackson序列化,无法扩展。

# 6.3 流处理

springmvc为我们提供了很好的文件上传的支持。但文件返回未实现。为些我们提供了更方便的文件下载和文件查看。

  • 文件下载:
@GetMapping("boot/fileDownload")
public ResponseEntity<FileDownloadStreamingResponseBody> fileDownload() {
    return new FileDownloadStreamingResponseBody(new File("xx.pdf"))
        .fileName("xxx.pdf")
        .mediaType(MediaType.APPLICATION_PDF)
        .toResponseEntity();
}
  • 文件查看:
@GetMapping("boot/fileView")
public ResponseEntity<FileViewStreamingResponseBody> fileView() {
    return new FileViewStreamingResponseBody(new File("xx.pdf"))
            .toResponseEntity();
}

# 6.4 参数校验

# 6.4.1 校验

校验是指对客户端请求的参数格式或逻辑校验,根据校验校验结果返回给客户端的不同信息。校验分两种:

  • 手工校验

    手工校验是指在代码里的业务校验。校验不通过可以抛出校验异常。如:throws new ValidateException("名称已存在")

  • 自动校验

    在自动映射的字段上,加上相关的注解,从而实现自动校验。如:@NotEmpty(message="名称不能为空")

# 6.4.2 自定义校验处理器

​ 通过实现com.alilitech.web.validate.ValidateHandler接口,并实现处理验证异常。此处理器只能有一个,并暴露给spring。

# 6.5 系统异常处理

框架提供了全局默认异常处理:打印异常信息,在头部信息里返回服务器内部错误

也可以自定义异常处理,实现com.alilitech.web.exception.ExceptionHandler接口,并交给spring管理。

# 6.6 基于请求的ThreadLocal管理

由于线程都是线程池,如果用了ThreadLocal没有清除,有可能会导致错误的结果。我们可以将我们要统一管理的ThreadLocal放置到ThreadLocalContainer中:ThreadLocalContainer.getInstance().addThreadLocal(...)

我们请求结束后会统一清除相关的值的设置

# Part7 boot-plus-log

集成了spring-boot-starter-actuator。

# 7.1 对Controller进行切面控制

logging:
  aspect: true

可以控制开启切面是否开启,同时提供了切面控制扩展,可以打印日志或记录日志。

@Service
public class LogOptService implements LogExtension {
    @Override
    public void beforeEnter(Signature signature, Object[] objects) {
        //....
    }
    @Override
    public Object afterExit(Signature signature, Object result) {
        //....
    }
}

# 7.2 管理日志级别

生产环境或测试环境有时候需要debug日志,等调试完后又关闭。访问/log.html在线管理日志级别。

注意

加了安全控制需要给相关人员赋于权限。相关的url包括GET /management/logsPUT /management/logs

# 7.3 基于线程的日志打印

// 开启基于线程的日志打印,这样同一个请求就可以快速鉴定出来。默认为true
logging.enableThreadLocal=true

# Part8 boot-plus-swagger

实现了在线API,主要实现以下功能:

  • 集成swaggger
  • 统一授权配置
  • 每个接口添加参数配置
  • 简单导出API
  • 对Pageable进行参数改写
  • 在线UI(/api.html)

可以配置swagger相关配置,如:

swagger:
  title: 接口文档
  description: 这是在线生成的API文档
  version: V1.0
  termsOfServiceUrl:
  contactName : David
  contactUrl: 
  contactEmail: 
  license:
  licenseUrl:
  defaultIncludePattern: 
    - "/api/.*"
  apiHost: localhost:8080                      #ui上测试操作的时候访问的实际url
  global:                                      #每个接口添加参数,在线API里,每个接口都会体现
    - name: Authorization
      description: 授权
      type: string
      parameterType: header
      required: false
  authorized:                                  #统一处理授权
    - name: Authorization
      in: header
  authorizedIncludePattern:
    - "/api/.*"

defaultIncludePattern: 哪些url的API会在在线文档里显示

apiHost:在线测试api时实际访问的API。由于代理映射问题,有时候访问html的地址和访问API的地址是不一致的

global:每个接口添加参数,在线API里,每个接口都会体现。数组,可配置多个。

authorized:统一授权,会在需要授权的API上加锁,显示需要授权。数组,可配置多个。

authorizedIncludePattern: 哪个URL需要授权,逗号隔开

TIP

在实际开发过程中如果有安全控制,也要将以下url分配相关权限:GET /swagger-resources/**, GET /swagger-ui/**, GET /v2/**

# Part9 boot-plus-security

集成了spring scurity,但由于spring scurity比较复杂,使用起来比较繁琐,故做了一些减法,对常用的保留,对不常用的暂时去除。若需要其它功能的,请自行集成。

此次集成主要实现了以下特性:

  • 可快速实现授权与鉴权,无需关注复杂的各种过滤器
  • 集成JWT
  • 实现Stateful Token(有状态Token)
  • 支持多因素认证
  • 支持本地缓存和分布式缓存一键切换

TIP

注意:由于security模块引入spring cache,所以一定要配置cache名称:

spring:
cache:
 cache-names:
 	- security

# 9.1 JWT

在Spring Scurity基本上实现无状态的JWT Token。主要实现以下功能:

  • 分离授权和鉴权
  • 用户可自定义扩展生成Token,校验Token
  • 用户可自定义扩展登陆成功,登陆失败,登出成功,根据登陆key查用户
  • 用户可自定义解析拿到Token,解析根据请求(uri)拿到资源(resource)对应的角色
  • 用户可自定义鉴权失败的返回
  • 登出Token黑名单功能(利用缓存,用户可自定义缓存)
  • 自动刷新Token(由于是无状态的,所以在Token快失效时,返回一个新的Token)

对于其它配置可以通过以下配置来实现

security:
  token:
    type: JWT
    ignorePatterns: 
      - pattern: "/*.ico"
        method: GET
      - pattern: "/css/**"
      - pattern: "/fonts/**"
    permitAllPatterns: 
    permitAllUserNames: 
      - admin
    bizUserClassName: 
    authenticationPrefix: "/authentication"
    jwt:
      secret:                    # 加密串
      timeoutMin:                # Token超时, 单位:分钟
      refreshSeconds:            # Token还有多久失败时刷新Token, 单位:秒

type:目前支持JWT,ST

ignorePatterns: 哪些url不需要授权和鉴权,这些url拿不到上下文

permitAllPatterns:哪些url,所有用户都有权限

permitAllUserNames: 哪些用户有全部url的权限

authenticationPrefix: 认证(登录、登出)uri前缀

bizUserClassName: 需要存储的业务用户类全路径名,默认是BizUser.class.getName()

jwt.secret: 加密串

jwt.timeoutMin: Token超时, 单位:分钟

jwt.refreshSeconds: Token还有多久失败时刷新Token, 单位:秒

# 9.2 Stateful Token(有状态token)

在spring Scurity 基础上实现了有状态的token, token对应的用户信息在缓存里存储。

配置如下:

security:
  token:
    type: ST
    ignorePatterns: 
      - pattern: "/*.ico"
        method: GET
      - pattern: "/css/**"
      - pattern: "/fonts/**"
    permitAllPatterns: 
    permitAllUserNames: 
      - admin
    bizUserClassName: 
    authenticationPrefix: "/authentication"

TIP

两种风格只需要切换配置即可。

# 9.3 如何开发

  • 登录url

    ${authenticationPrefix}/login

  • 登出uri

    ${authenticationPrefix}/logout

  • 开发者扩展类ExtensibleSecurity

    用户用这个一个类可实现自定义认证授权与鉴权部分

    • validTokenExtension: 校验token扩展,可自定义校验Token扩展,也可以刷新缓存期限

    • loginSuccess: 登录成功处理

    • loginFailure: 登录失败处理

    • logoutSuccess: 登出成功处理

    • loadUserByUsername:

      为用户名和密码方式的认证提供方法。

      根据用户名加载信息,包括用户名,密码。若需要鉴权的用户则需要加入角色信息

      if(maxAuth) {
          BizUser bizUser = new BizUser(user.getUserName(), user.getPassword(), new ArrayList<>());
          return bizUser;
      } else {
          List<String> roleCodes = ....
        BizUser bizUser = new BizUser(user.getUserName(), user.getPassword(), roleCodes);
          return bizUser;
      }
      
    • resolveToken 根据请求解析Token

    • obtainResource 根据request(资源)获得关联的角色信息,以验证此用户是否有权限

    • authorizationFailure 鉴权失败处理

    • addVirtualFilterDefinitions

      我们可能有这样的需求,一个认证请求会同时验证好多数据,比如登陆时附带验证码的时候,我们需要先验证验证码,再验证用户名和密码(多因素认证,不是多次认证)。我们可以定义多个VirtualFilterDefinition,如:

      virtualFilterDefinitions.add(VirtualFilterDefinition.get().supportedPredicate((servletRequest, servletResponse) -> {
          		// 此filter是否支持验证判断
                  AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher("/authentication/login", "POST");
                  RequestMatcher.MatchResult matcher = requestMatcher.matcher((HttpServletRequest) servletRequest);
                  return matcher.isMatch();
              }).authenticationFunction((servletRequest, servletResponse) -> {
          		// 如何验证,验证失败时可以抛出AuthenticationException
                  String code = servletRequest.getParameter("code");
                  if("1".equals(code)) {
                      return null;
                  }
                  throw new UsernameNotFoundException("验证码错误");
              }).endAuthentication((authentication, servletRequest, servletResponse) -> {	
          		// 此filter认证成功后,是否结束整个流程的认证
                  return false;
              }).alias("验证码验证"));  //别名,在日志里会看到经过了哪些filter
      

      TIP

      这些定义的filter会在用户名和密码认证的filter之前。用户名和密码的认证filter,只支持post请求。

      TIP

      VirtualFilterDefinition 提供了构建Authentication的方法

  • authenticationExtension 授权原始框架扩展,默认情况下实现以下功能

    • 开启跨域支持
    • session管理禁用
    • csrf 禁用
  • authorizationExtension 鉴权原始框架扩展,默认情况下实现以下功能

    • 开启跨域支持
    • session管理禁用
    • csrf 禁用
    • anonymous 禁用
    • securityContext 禁用
    • requestCache 禁用

TIP

对于authenticationExtensionauthorizationExtension原始默认实现功能不符合要求,可完全覆盖默认实现

# Part10 boot-plus-cache

spring cache为我们提供了多个缓存的抽象,我们只要调用cacheManager就可以操作不同的缓存,但并未提供缓存的tti、ttl的抽象。本模块致力于提供缓存的tti、ttl抽象。目前支持caffeine和redis。这样我们在本地开发的时候可以使用caffeine,不需要依赖redis。上生产可以切换成基于redis分布式缓存。

操作缓存的类CacheTemplate