Skip to content

SpringBoot全局配置

说明

这个模块主要封装了SpringBoot相关的配置和工具类.

BaseConfig

默认的Mvc配置类,用于Spring、SpringMVC的全局配置

  • ObjectMapper

为了统一后端返回数据到前端时,数据格式不规则、长整型数据精度丢失等问题,本项目配置了全局ObjectMapper类实现son序列化和反序列化时。

提示

  • 序列化:Controller层接口返回值 转成 json 格式的过程
  • 反序列化:前端请求通过json格式提交参数到 Controller 层的过程
  • 配置全局ObjectMapper类后,会和yml配置文件中 spring.jackson.xxx 的配置产生冲突,所以请勿在yml中重复配置
java
//在BaseConfig类配置了全局的 ObjectMapper 实例, 
//并调整了objectMapper的默认序列化和反序列化规则。
@Bean
@Primary
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnMissingBean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper objectMapper = builder.createXmlMapper(false).build();

    objectMapper
     // 设置当前位置
    .setLocale(Locale.CHINA)
    // 去掉默认的时间戳格式
    .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
    // 时区
    .setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()))
    // Date参数日期格式
    .setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT, Locale.CHINA))
    // 该特性决定parser是否允许JSON字符串包含非引号控制字符(值小于32的ASCII字符,包含制表符和换行符)。 如果该属性关闭,则如果遇到这些字符,则会抛出异常。JSON标准说明书要求所有控制符必须使用引号,因此这是一个非标准的特性
    .configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true)
    // 忽略不能转义的字符
    .configure(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature(), true)
    // 在使用spring boot + jpa/hibernate,如果实体字段上加有FetchType.LAZY,并使用jackson序列化为json串时,会遇到SerializationFeature.FAIL_ON_EMPTY_BEANS异常
    .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
    // 忽略未知字段
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    // 单引号处理
    .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
    // 注册自定义模块
    objectMapper.registerModule(new acuityJacksonModule())
    .findAndRegisterModules();
}

public class acuityJacksonModule extends SimpleModule {

    public acuityJacksonModule() {
        super();
				// 定义LocalDateTime、LocalDate、LocalTime 反序列化规则
        this.addDeserializer(LocalDateTime.class, acuityLocalDateTimeDeserializer.INSTANCE);
        this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        
				// 定义LocalDateTime、LocalDate、LocalTime 序列化规则
        this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
      	
      	// 定义Long、BigInteger、BigDecimal的序列化规则
        this.addSerializer(Long.class, ToStringSerializer.instance);
        this.addSerializer(Long.TYPE, ToStringSerializer.instance);
        this.addSerializer(BigInteger.class, ToStringSerializer.instance);
        this.addSerializer(BigDecimal.class, ToStringSerializer.instance);
    }
}
//在BaseConfig类配置了全局的 ObjectMapper 实例, 
//并调整了objectMapper的默认序列化和反序列化规则。
@Bean
@Primary
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnMissingBean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper objectMapper = builder.createXmlMapper(false).build();

    objectMapper
     // 设置当前位置
    .setLocale(Locale.CHINA)
    // 去掉默认的时间戳格式
    .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
    // 时区
    .setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()))
    // Date参数日期格式
    .setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT, Locale.CHINA))
    // 该特性决定parser是否允许JSON字符串包含非引号控制字符(值小于32的ASCII字符,包含制表符和换行符)。 如果该属性关闭,则如果遇到这些字符,则会抛出异常。JSON标准说明书要求所有控制符必须使用引号,因此这是一个非标准的特性
    .configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true)
    // 忽略不能转义的字符
    .configure(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature(), true)
    // 在使用spring boot + jpa/hibernate,如果实体字段上加有FetchType.LAZY,并使用jackson序列化为json串时,会遇到SerializationFeature.FAIL_ON_EMPTY_BEANS异常
    .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
    // 忽略未知字段
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    // 单引号处理
    .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
    // 注册自定义模块
    objectMapper.registerModule(new acuityJacksonModule())
    .findAndRegisterModules();
}

public class acuityJacksonModule extends SimpleModule {

    public acuityJacksonModule() {
        super();
				// 定义LocalDateTime、LocalDate、LocalTime 反序列化规则
        this.addDeserializer(LocalDateTime.class, acuityLocalDateTimeDeserializer.INSTANCE);
        this.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        this.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        
				// 定义LocalDateTime、LocalDate、LocalTime 序列化规则
        this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        this.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
      	
      	// 定义Long、BigInteger、BigDecimal的序列化规则
        this.addSerializer(Long.class, ToStringSerializer.instance);
        this.addSerializer(Long.TYPE, ToStringSerializer.instance);
        this.addSerializer(BigInteger.class, ToStringSerializer.instance);
        this.addSerializer(BigDecimal.class, ToStringSerializer.instance);
    }
}

序列化配置项

按照上面的代码进行配置后,会将入参和返回值按下面的格式接收和返回。

  • Long -> String

返回值字段是Long类型,返回给前端的是字符串格式。

解决问题

解决前端接收Long类型的值,精度丢失问题。

java
public acuityJacksonModule() {
    this.addSerializer(Long.class, ToStringSerializer.instance);
    this.addSerializer(Long.TYPE, ToStringSerializer.instance);
}
public acuityJacksonModule() {
    this.addSerializer(Long.class, ToStringSerializer.instance);
    this.addSerializer(Long.TYPE, ToStringSerializer.instance);
}

序列化结果

json
{
    "id": "29984253320102284"
}
{
    "id": "29984253320102284"
}
java
public class User {
  	private Long id;
}

public class UserController {
  	public User get() {
      return new User().setId(29984253320102284L);
    }
}
public class User {
  	private Long id;
}

public class UserController {
  	public User get() {
      return new User().setId(29984253320102284L);
    }
}
  • BigInteger -> String

返回值字段是BigInteger类型,返回给前端的是字符串格式。

解决问题

解决前端接收BigInteger类型的值,精度丢失问题。

java
public acuityJacksonModule() {
    this.addSerializer(BigInteger.class, ToStringSerializer.instance);
}
public acuityJacksonModule() {
    this.addSerializer(BigInteger.class, ToStringSerializer.instance);
}

序列化结果

json
{
    "id": "29984253320102284"
}
{
    "id": "29984253320102284"
}
java
public class User {
  	private BigInteger id;
}
public class UserController {
  	public User get() {
      return new User().setId(new BigInteger(29984253320102284L));
    }
}
public class User {
  	private BigInteger id;
}
public class UserController {
  	public User get() {
      return new User().setId(new BigInteger(29984253320102284L));
    }
}
  • BigDecimal -> String

返回值字段是BigDecimal类型,返回给前端的是字符串格式。

解决问题

解决前端接收BigDecimal类型的值,精度丢失问题。

java
public acuityJacksonModule() {
    this.addSerializer(BigDecimal.class, ToStringSerializer.instance);
}
public acuityJacksonModule() {
    this.addSerializer(BigDecimal.class, ToStringSerializer.instance);
}

序列化结果

json
{
    "id": "123.23"
}
{
    "id": "123.23"
}
java
public class User {
  	private BigDecimal id;
}

public class UserController {
  	public User get() {
      return new User().setId(new BigDecimal(123.23));
    }
}
public class User {
  	private BigDecimal id;
}

public class UserController {
  	public User get() {
      return new User().setId(new BigDecimal(123.23));
    }
}
  • Date -> String

返回值字段是Date类型,返回给前端的是字符串格式。

解决问题

统一日期参数的格式

java
public class BaseConfig {
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
  	  objectMapper
      // Date参数日期格式
      .setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT, Locale.CHINA))
      // ...
    }
}
public class BaseConfig {
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
  	  objectMapper
      // Date参数日期格式
      .setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT, Locale.CHINA))
      // ...
    }
}

序列化结果

json
{
    "date": "yyyy-MM-dd HH:mm:ss"  // 这里的格式由上面的 DEFAULT_DATE_TIME_FORMAT 决定
}
{
    "date": "yyyy-MM-dd HH:mm:ss"  // 这里的格式由上面的 DEFAULT_DATE_TIME_FORMAT 决定
}
java
public class User {
  	private Date date;
}

public class UserController {
  	public User get() {
      return new User().setDate(new Date());
    }
}
public class User {
  	private Date date;
}

public class UserController {
  	public User get() {
      return new User().setDate(new Date());
    }
}
  • LocalDateTime -> String

返回值字段是LocalDateTime类型,返回给前端的是字符串格式。

解决问题

统一日期参数的格式

java
public acuityJacksonModule() {
    this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
}
public acuityJacksonModule() {
    this.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
}

序列化结果:Controller层接口返回参数中,LocalDateTime类型的字段,会转成字符串。

json
{
    "date": "yyyy-MM-dd HH:mm:ss"   
}
{
    "date": "yyyy-MM-dd HH:mm:ss"   
}
java
public class User {
  	private LocalDateTime date;
}

public class UserController {
  	public User get() {
      return new User().setDate(LocalDateTime.now());
    }
}
public class User {
  	private LocalDateTime date;
}

public class UserController {
  	public User get() {
      return new User().setDate(LocalDateTime.now());
    }
}
  • LocalDate -> String

返回值字段是LocalDate类型,返回给前端的是字符串格式。

解决问题

统一日期参数的格式

java

序列化结果:Controller层接口返回参数中,LocalDateTime类型的字段,会转成字符串。

json
{
    "date": "yyyy-MM-dd"  
}
{
    "date": "yyyy-MM-dd"  
}
java
public class User {
  	private LocalDate date;
}

public class UserController {
  	public User get() {
      return new User().setDate(LocalDate.now());
    }
}
public class User {
  	private LocalDate date;
}

public class UserController {
  	public User get() {
      return new User().setDate(LocalDate.now());
    }
}
  • LocalTime -> String

返回值字段是LocalTime类型,返回给前端的是字符串格式。

解决问题

统一日期参数的格式

java
public acuityJacksonModule() {
    this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
}
public acuityJacksonModule() {
    this.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
}

序列化结果

json
{
    "date": "HH:mm:ss"  
}
{
    "date": "HH:mm:ss"  
}
java
public class User {
  	private LocalTime date;
}

public class UserController {
  	public User get() {
      return new User().setDate(LocalTime.now());
    }
}
public class User {
  	private LocalTime date;
}

public class UserController {
  	public User get() {
      return new User().setDate(LocalTime.now());
    }
}

反序列化配置项

反序列化跟序列化刚好相反,是将前端传递的json数据转换为JavaBean的过程。

  • String -> Long 字符串类型的长整型数字,会转换为Long
  • String -> BaseEnum 符合枚举值的字符串,可以转换为BaseEnum
  • String -> LocalDate, 支持前端传递格式:

yyyy-MM-dd: 如:2021-11-11

  • String -> LocalDateTime, 支持前端传递格式:

yyyy-MM-dd: 如:2021-11-11 yyyy/MM/dd: 如:2021/11/11 yyyy年MM月dd日: 如:2021年11月11日 yyyy-MM-dd HH:mm:ss: 如:2021-11-11 11:11:11 yyyy/MM/dd HH:mm:ss: 如:2021/11/11 11:11:11 yyyy年MM月dd日HH时mm分ss秒: 如:2021年11月11日11时11分11秒

  • String -> LocalTime

支持前端传递格式: HH:mm:ss

代码使用 Converter

[Converter]

通过配置全局的Converter,解决Controller层方法入参为日期类型且通过@RequestParam接收参数时,日期格式的转换规则。

java
  /**
   * 解决 @RequestParam(value = "date") Date date
   * date 类型参数 格式问题
   */
  @Bean
  public Converter<String, Date> dateConvert() {
      return new String2DateConverter();
  }

  /**
   * 解决 @RequestParam(value = "time") LocalDate time
   */
  @Bean
  public Converter<String, LocalDate> localDateConverter() {
      return new String2LocalDateConverter();
  }

  /**
   * 解决 @RequestParam(value = "time") LocalTime time
   */
  @Bean
  public Converter<String, LocalTime> localTimeConverter() {
      return new String2LocalTimeConverter();
  }

  /**
   * 解决 @RequestParam(value = "time") LocalDateTime time
   */
  @Bean
  public Converter<String, LocalDateTime> localDateTimeConverter() {
      return new String2LocalDateTimeConverter();
  }
  /**
   * 解决 @RequestParam(value = "date") Date date
   * date 类型参数 格式问题
   */
  @Bean
  public Converter<String, Date> dateConvert() {
      return new String2DateConverter();
  }

  /**
   * 解决 @RequestParam(value = "time") LocalDate time
   */
  @Bean
  public Converter<String, LocalDate> localDateConverter() {
      return new String2LocalDateConverter();
  }

  /**
   * 解决 @RequestParam(value = "time") LocalTime time
   */
  @Bean
  public Converter<String, LocalTime> localTimeConverter() {
      return new String2LocalTimeConverter();
  }

  /**
   * 解决 @RequestParam(value = "time") LocalDateTime time
   */
  @Bean
  public Converter<String, LocalDateTime> localDateTimeConverter() {
      return new String2LocalDateTimeConverter();
  }
[ HeaderThreadLocalInterceptor]
java
		// 用于启用 HeaderThreadLocalInterceptor
		@Bean
    @ConditionalOnClass
    @ConditionalOnProperty(prefix = Constants.PROJECT_PREFIX + ".webmvc", name = "header", havingValue = "true", matchIfMissing = true)
    public GlobalMvcConfigurer getGlobalMvcConfigurer() {
        return new GlobalMvcConfigurer();
    }
		// 用于启用 HeaderThreadLocalInterceptor
		@Bean
    @ConditionalOnClass
    @ConditionalOnProperty(prefix = Constants.PROJECT_PREFIX + ".webmvc", name = "header", havingValue = "true", matchIfMissing = true)
    public GlobalMvcConfigurer getGlobalMvcConfigurer() {
        return new GlobalMvcConfigurer();
    }
yaml
acuity:
  webmvc:
    header: true
acuity:
  webmvc:
    header: true

全局异常处理 AbstractGlobalExceptionHandler

AbstractGlobalExceptionHandler 全局异常处理,拦截到指定的异常后,统一使用R对象返回错误信息。

全局响应体包装 AbstractGlobalResponseBodyAdvice

AbstractGlobalResponseBodyAdvice 全局响应体包装-若系统启用了AbstractGlobalResponseBodyAdvice类,系统会将Controller层所有方法的返回值自动包装为R对象。该注解用于局部禁用此功能。

java
public class AbstractGlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        // 类上如果被 IgnoreResponseBodyAdvice 标识就不拦截
        if (methodParameter.getDeclaringClass().isAnnotationPresent(IgnoreResponseBodyAdvice.class)) {
            return false;
        }

        // 方法上被标注也不拦截
        return !Objects.requireNonNull(methodParameter.getMethod()).isAnnotationPresent(IgnoreResponseBodyAdvice.class);
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (o == null) {
            return null;
        }
        if (o instanceof R) {
            return o;
        }
        return R.success(o);
    }
}
public class AbstractGlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        // 类上如果被 IgnoreResponseBodyAdvice 标识就不拦截
        if (methodParameter.getDeclaringClass().isAnnotationPresent(IgnoreResponseBodyAdvice.class)) {
            return false;
        }

        // 方法上被标注也不拦截
        return !Objects.requireNonNull(methodParameter.getMethod()).isAnnotationPresent(IgnoreResponseBodyAdvice.class);
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (o == null) {
            return null;
        }
        if (o instanceof R) {
            return o;
        }
        return R.success(o);
    }
}
java
public class CaptchaController {
  	/* 
  	 * 若服务通过配置,启用了AbstractGlobalResponseBodyAdvice类,默认情况下Controller层所有的方法都会按照 R 的格式返回
  	 */
    @GetMapping(value = "/captcha", produces = "image/png")
    @IgnoreResponseBodyAdvice
    public void captcha(@RequestParam(value = "key") String key, HttpServletResponse response) throws IOException {
        this.captchaService.createImg(key, response);
    }
  
  	// 虽然方法只返回了User对象,但前端接收到的实际上是 R.success(user)
  	@GetMapping(value = "/test")
    public User test() throws IOException {
        return new User().setId(1);
    }
}
public class CaptchaController {
  	/* 
  	 * 若服务通过配置,启用了AbstractGlobalResponseBodyAdvice类,默认情况下Controller层所有的方法都会按照 R 的格式返回
  	 */
    @GetMapping(value = "/captcha", produces = "image/png")
    @IgnoreResponseBodyAdvice
    public void captcha(@RequestParam(value = "key") String key, HttpServletResponse response) throws IOException {
        this.captchaService.createImg(key, response);
    }
  
  	// 虽然方法只返回了User对象,但前端接收到的实际上是 R.success(user)
  	@GetMapping(value = "/test")
    public User test() throws IOException {
        return new User().setId(1);
    }
}
json
{
	"code": 0,  // 状态码  0表示请求成功 其他请求失败
	"data": {   //  将controller层的返回值包装到data字段
	},
	"errorMsg": "", //错误消息
	"extra": {}, // 扩展数据
	"isSuccess": true,  // 是否请求成功
	"msg": "",  // 响应消息
	"path": "",  // 访问失败时的请求路径
	"timestamp": 0  // 后端响应时的时间戳
}
{
	"code": 0,  // 状态码  0表示请求成功 其他请求失败
	"data": {   //  将controller层的返回值包装到data字段
	},
	"errorMsg": "", //错误消息
	"extra": {}, // 扩展数据
	"isSuccess": true,  // 是否请求成功
	"msg": "",  // 响应消息
	"path": "",  // 访问失败时的请求路径
	"timestamp": 0  // 后端响应时的时间戳
}

请求头拦截器 HeaderThreadLocalInterceptor

HeaderThreadLocalInterceptor 读取请求头中的参数, 放到LocalThread中

读取请求头中的参数, 放到ContextUtil中

  • PATH_HEADER:前端页面的路径
  • TENANT_ID_HEADER: 当前用户的租户ID
  • TENANT_BASE_POOL_NAME_HEADER: 接下来的SQL,需要访问那个租户base库
  • USER_ID_HEADER:当前用户的用户ID
  • EMPLOYEE_ID_HEADER:当前用户在某个租户下的员工ID
  • APPLICATION_ID_HEADER:当前请求的应用ID
  • CURRENT_COMPANY_ID_HEADER:当前用户所属的机构ID
  • CURRENT_TOP_COMPANY_ID_HEADER:当前用户所属的顶级机构ID
  • CURRENT_DEPT_ID_HEADER:当前用户所属部门ID
  • CLIENT_ID_HEADER:客户端ID
  • TRACE_ID_HEADER:日志链路Id
  • TOKEN_HEADER:当前请求的Token

UndertowServerFactoryCustomizer : Undertow 全局配置

WebUtils : Web 工具类

Controller的用法

  • 已迁至 acuity-mvc
  • 这里强调一下Controller的用法, 我提供的控制器有接口、也有抽象类, 将常用的CRUD接口全部封装到了接口中, 而抽象类则对具体的接口控制器进行了组合(实现了多个接口).
  1. 接口
  • BaseController : 最基础的控制器, 只有一个常用的返回方法
  • DeleteController : 封装了删除接口
  • PageController : 封装了查询条件
  • PoiController : 封装了导入导出接口
  • QueryController : 封装了单体查询、列表查询、分页查询
  • SaveController : 封装了保存接口
  • UpdateController : 封装了修改接口
  1. 抽象类
  • SuperCacheController : 组合了CURD导入导出等所有接口Controller, 且有缓存
  • SuperController : 组合了CURD导入导出等所有接口Controller, 但没用缓存
  • SuperNoPoiController : 组合了CURD等所有接口Controller.
  • SuperSimpleController : 没有任何接口的抽象类.

业务Controller可以根据自身情况,选择继承 抽象Controller 还是 按需实现 接口Controller.

欢迎使用天源云Saas快速开发系统