自定义注解实现数据序列化时进行数据脱敏(基于springboot默认jackjson)、消息转换器HttpMessageConverter
消息转换器 HttpMessageConverter
消息转化器的作用
将请求报文转化为Java对象
将Java对象转化为响应报文
消息转换器接口
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ?
getSupportedMediaTypes() : Collections.emptyList());
}
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
转换器加载流程
消息转换器是在项目启动的时候通过WebMvcConfigurationSupport进行加载,当getMessageConverters()被调用的时候会通过configureMessageConverters()、addDefaultHttpMessageConverters()和extendMessageConverters()三个方法进行初始话消息转换器。生成的消息转换器放在List<HttpMessageConverter<?>> messageConverters集合中
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList();
// 加载委托给WebMvcConfigurer类型的Bean
this.configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
// 加载默认的转换器
this.addDefaultHttpMessageConverters(this.messageConverters);
}
// 加载扩展消息转换器
this.extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
自定义消息转换器参考下面
(1)直接注入Bean 的方式替换
(2)实现 WebMvcConfigurer#extendMessageConverters 接口方法
fastjson与jackjson
问题
在springboot中使用fastjson的@jsonField无效
原因:在springboot默认有json(jackjson)解析工具,所以使用fastjson不会生效
解决方案替换默认的解析工具(笔者不推荐,这里根据自己项目决定)
fastjson替换默认的jackjson
第一种方法bean方法
package com.anson.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class WebConfig {
/**
* @Author anson
* @Description 配置消息转换器
* @Date: 2019-12-8 11:23:33
* @version: 1.0
* new HttpMessageConverters(true, converters);
* 一定要设为true才能替换否则不会替换
* @return 返回一个消息转换的bean
*/
@Bean
public HttpMessageConverters fastJsonMessageConverters() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
//需要定义一个convert转换消息的对象;
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//添加fastJson的配置信息;
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
//全局时间配置
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
fastJsonConfig.setCharset(Charset.forName("UTF-8"));
//处理中文乱码问题
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
//在convert中添加配置信息.
fastConverter.setSupportedMediaTypes(fastMediaTypes);
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(0, fastConverter);
return new HttpMessageConverters(converters);
}
}
第二种方法实现webMvcConfigurer
@Configuration
public class WebConfigure implements WebMvcConfigurer{
/**
* 配置消息转换器
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//需要定义一个convert转换消息的对象;
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//添加fastJson的配置信息;
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
//全局时间配置
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
fastJsonConfig.setCharset(Charset.forName("UTF-8"));
//处理中文乱码问题
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
//在convert中添加配置信息.
fastConverter.setSupportedMediaTypes(fastMediaTypes);
fastConverter.setFastJsonConfig(fastJsonConfig);
converters.add(0,fastConverter);
}
}
jackjson注解实现原理
参考:https://blog.csdn.net/qq_18515155/article/details/128891441?spm=1001.2014.3001.5501
这里以Jackson中常用的@JsonFormat注解为例,目标就是将Company对象序列化。
public class Company {
/**
* 开张时间
*/
@JsonFormat(pattern = "yyyy-MM-dd")
private Date openDate;
public Date getOpenDate() {
return openDate;
}
public void setOpenDate(Date openDate) {
this.openDate = openDate;
}
}
public class Test {
public static void main(String[] args) {
Company company = new Company();
company.setOpenDate(new Date());
ObjectMapper objectMapper = new ObjectMapper();
String s;
try {
s = objectMapper.writeValueAsString(company);
System.out.println(s);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
执行结果如下:
{"openDate":"2023-02-05"}
处理过程简述:
寻找合适的序列化器
序列化器二次处理(主要用于传递属性元信息)
执行序列化
详细如下:
一·寻找合适的序列化器
(1)根据当前数据类型从缓存中获取对应的序列化器
(2)如果获取不到,则创建序列化器
(3)根据Company上的注解获取序列化器(Company上没有注解)
(4)根据当前的注解内省器获取能够处理 步骤3 拿到的注解的序列化器(默认的注解内省器是JacksonAnnotationIntrospector,对于注解@JsonSerialize则返回注解指定的序列化器,对于注解JsonRawValue则返回RawSerializer)
(5)如果获取不到,再根据是否实现了JsonSerializable接口或者标注了 @JsonValue注解获取对应的序列化器(当前Company都没有)
(6)如果没有,就只能创建通用的对象处理器BeanSerialzer
(7)在创建BeanSerialzer之前,需要先去找到对象的属性元信息
(8)为对象的属性(Compnay.openDate)找到合适的序列化器(类似上面的步骤,一步一步找,这里最终没有找对于openDate属性的序列化器)
(9)把属性元信息设置到对象元信息里,然后new一个BeanSerialzer并返回
(10)拿到返回的序列化器以后,如果该序列化器实现了ContextualSerializer接口,则调用它的createContextual方法进行二次处理,并返回序列化器
二.序列化器二次处理(主要用于传递属性元信息)
步骤10即为序列化器二次处理:当找到合适的序列化器以后,还需要根据是否这个序列化器实现了ContextualSerializer接口,该接口声明了方法JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
该接口可以拿到属性的元信息,如属性上的注解信息,根据元信息可以做一些二次处理,然后再返回序列化器,再用这个返回的序列化器做序列化处理。
三.执行序列化
(1)序列化对象
(2)序列化对象的字段
(3)动态获取能处理字段的序列化器
(4)检索内置的序列化器集合
(5)根据类型从内置的序列化器集合获取对应的序列化器,即根据openDate的类型Date.class获取到DateSerializer
(6)序列化器二次处理
(7)设置需要传递到序列化器中的上下文信息
(8)将openDate上的JsonFormat注解的pattern字段值传递到DateSerializer,用来在序列化openDate时知道应该转为哪种时间格式
(9)调用DateSerializer.serialze进行字段序列化
jackjson实现数据脱敏
脱敏即是对数据的部分信息用脱敏符号(*)处理。
自定义注解:
package com.chinatower.platform.awh.framework;
import com.chinatower.platform.awh.framework.enums.SentitiveFieldEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SensitiveField {
SentitiveFieldEnum type();
}
枚举类:
package com.chinatower.platform.awh.framework.enums;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
public enum SentitiveFieldEnum {
/**
* 身份证号
*/
ID_CARD,
/**
* 姓名
*/
NAME,
/**
* 手机号
*/
PHONE,
}
工具类:
package com.chinatower.platform.awh.framework.config;
import cn.hutool.core.util.StrUtil;
import org.apache.commons.lang3.StringUtils;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
public class SensitiveDataUtils {
public static String encryptCard(String card){
String left = StringUtils.left(card, 1);
return StringUtils.rightPad(left,card.length(),"*");
}
public static String encryptPhone(String card){
String left = StringUtils.left(card, 2);
return StringUtils.rightPad(left,card.length(),"*");
}
public static String encryptName(String card){
String left = StringUtils.left(card, 3);
return StringUtils.rightPad(left,card.length(),"*");
}
}
自定义处理注解的序列化器
package com.chinatower.platform.awh.framework.config;
import com.chinatower.platform.awh.framework.SensitiveField;
import com.chinatower.platform.awh.framework.enums.SentitiveFieldEnum;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import java.io.IOException;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
public class SensitiveFieldSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SentitiveFieldEnum eum ;
public SensitiveFieldSerializer(){}
public SensitiveFieldSerializer(SentitiveFieldEnum enu){this.eum = enu;}
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
SentitiveFieldEnum sentitiveFieldEnum = eum;
if (sentitiveFieldEnum == null){
return;
}
switch (sentitiveFieldEnum){
case ID_CARD:
s=SensitiveDataUtils.encryptCard(s);
break;
case NAME:
s=SensitiveDataUtils.encryptName(s);
break;
case PHONE:
s=SensitiveDataUtils.encryptPhone(s);
break;
default:break;
}
jsonGenerator.writeObject(s);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
//获取注解
System.out.println(1);
if (beanProperty != null){
SensitiveField annotation = beanProperty.getAnnotation(SensitiveField.class);
SentitiveFieldEnum type = annotation.type();
return new SensitiveFieldSerializer(type);
}
//返回处理枚举
System.out.println(2);
return serializerProvider.findNullValueSerializer(null);
}
}
自定义注解内省器
package com.chinatower.platform.awh.framework;
import com.chinatower.platform.awh.framework.config.SensitiveFieldSerializer;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
public class SensitiveFieldAnnotationIntrospector extends NopAnnotationIntrospector {
@Override
public Object findSerializer(Annotated am) {
SensitiveField annotation = am.getAnnotation(SensitiveField.class);
if (annotation !=null){
return SensitiveFieldSerializer.class;
}
return super.findSerializer(am);
}
}
Jackson序列化配置
package com.chinatower.platform.awh.framework.config;
import com.chinatower.platform.awh.framework.SensitiveFieldAnnotationIntrospector;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
/**
* @Description :
* @Date : 2023/5/23
* @Author :
*/
@Configuration
public class JacksonConfig {
@Bean
public SensitiveFieldAnnotationIntrospector sensitiveFieldAnnotationIntrospector(){
return new SensitiveFieldAnnotationIntrospector();
}
@Bean
public SensitiveFieldSerializer sensitiveFieldSerializer(){
return new SensitiveFieldSerializer();
}
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder, SensitiveFieldAnnotationIntrospector introspector){
ObjectMapper mapper = builder.createXmlMapper(false).build();
AnnotationIntrospector annotationIntrospector = mapper.getSerializationConfig().getAnnotationIntrospector();
AnnotationIntrospector pair = AnnotationIntrospector.pair(annotationIntrospector, introspector);
mapper.setAnnotationIntrospector(pair);
return mapper;
}
}
springboot的bean是单例模式
测试
/ 定义一个对象,字段标有注解
@Data
public class User {
@SensitiveField(type = SensitiveFieldTypeEnum.NAME)
private String name;
@SensitiveField(type = SensitiveFieldTypeEnum.PHONE)
private String phone;
private Integer age;
}
@GetMapping("/get")
public User get() {
User user = new User();
user.setAge(20);
user.setName("徐小莫");
user.setPhone("18211843612");
return user;
}
执行结果:
{
"name": "***",
"phone": "182****3672",
"age": 20
}
代码分析:
(1)如何找到处理自定义注解的序列化器
就要通过注解內省器找到合适的序列化器。
所以我们自定义了一个注解內省器,并把它加入到了当前Jackson注解內省器集合里:
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
// 将自定义注解內省器加入到Jackson注解内省器集合里,AnnotationIntrospector是双向链表结构
AnnotationIntrospector pair = AnnotationIntrospectorPair.pair(annotationIntrospector, introspector);
objectMapper.setAnnotationIntrospector(pair);
(2)**如何不影响已有的Jackson序列化配置 **
实际的SpringBoot项目中,对于Controller的返回数据,经常会在配置文件或者配置类进行一些Jackson的自定义配置。
如果针对当前的自定义注解序列化器新建一个ObjectMapper:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
SensitiveFieldSerializer sensitiveFieldSerializer = new SensitiveFieldSerializer();
simpleModule.addSerializer(String.class, sensitiveFieldSerializer);
objectMapper.registerModule(simpleModule);
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
AnnotationIntrospector newIntro = AnnotationIntrospectorPair.pair(annotationIntrospector,new SensitiveFieldAnnotationIntrospector());
objectMapper.setAnnotationIntrospector(newIntro);
那这个ObjectMapper不会包含原来的那些配置,因为他是一个新的对象。
要保证原来的配置生效,就需要对SpringBoot自动配置的ObjectMapper动手脚。
通过分析JacksonAutoConfiguration.class这个自动配置类
@Bean
JacksonAutoConfiguration.Jackson2ObjectMapperBuilderCustomizerConfiguration.StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(ApplicationContext applicationContext, JacksonProperties jacksonProperties) {
return new JacksonAutoConfiguration.Jackson2ObjectMapperBuilderCustomizerConfiguration.StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);
}
@Bean
@Scope("prototype")
//这个注解是多例模式
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext, List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
this.customize(builder, customizers);
return builder;
}
@Bean
@Primary
@ConditionalOnMissingBean
// 这个注解代表没有自定义的ObjectMqpper bean对象时才会在这里创建
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
发现SpringBoot会将配置信息封装成Jackson2ObjectMapperBuilder,进而去创建ObjectMapper对象。
所以需要通过注入已有的Jackson2ObjectMapperBuilder对象来创建我们自己的ObjectMapper,再将自定义注解内省器加入到它的內省器集合,使我们的自定义注解序列化器生效:
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder, SensitiveFieldAnnotationIntrospector introspector) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
AnnotationIntrospector annotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector();
// 将自定义注解內省器加入到Jackson注解内省器集合里,AnnotationIntrospector是双向链表结构
AnnotationIntrospector pair = AnnotationIntrospectorPair.pair(annotationIntrospector, introspector);
objectMapper.setAnnotationIntrospector(pair);
return objectMapper;
}