Appearance
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接口全部封装到了接口中, 而抽象类则对具体的接口控制器进行了组合(实现了多个接口).
- 接口
- BaseController : 最基础的控制器, 只有一个常用的返回方法
- DeleteController : 封装了删除接口
- PageController : 封装了查询条件
- PoiController : 封装了导入导出接口
- QueryController : 封装了单体查询、列表查询、分页查询
- SaveController : 封装了保存接口
- UpdateController : 封装了修改接口
- 抽象类
- SuperCacheController : 组合了CURD导入导出等所有接口Controller, 且有缓存
- SuperController : 组合了CURD导入导出等所有接口Controller, 但没用缓存
- SuperNoPoiController : 组合了CURD等所有接口Controller.
- SuperSimpleController : 没有任何接口的抽象类.
业务Controller可以根据自身情况,选择继承 抽象Controller
还是 按需实现 接口Controller
.