关注公众号【1024个为什么】,及时接收最新推送文章!
本篇文章是对之前的两篇文章的一个总结、补充。
之前的解决方案是基于 fastjson + logback,本次补充了 fastjson + log4j。
至于其他的 json 序列化框架、日志框架,大家可以继续深究,重点是解决问题的思路。
背景
我们在打日志的过程有 4 个重复低效的场景
1、对象都是 json 序列化之后,再把数据交给日志框架
2、Date 类型默认会序列化为 时间戳,排查问题十分不友好
3、日志里的内容需要脱敏时,需要在数据交给日志框架前完成脱敏处理
4、每一个日志内容都要写占位符,万一忘记了,还要重新上线
目标
不再关心 json 序列化!
不再关心 Date 类型的序列化!
不再关心脱敏!
不再关心占位符!
一切都由底层自动完成。
解决思路
要想解决这个问题,必须要搞懂 json 和 log 框架的运行原理。
依托日志框架的扩展能力,把序列化、脱敏的工作交由日志框架完成。
常规流程
我们的解决流程
所以首先要搞明白如何扩展日志框架,拦截住日志数据。
log4j2
它的扩展点是 MessageFactory,详见官网:
https://logging.apache.org/log4j/2.x/manual/extending.html#MessageFactory
具体实现是:
(1)自定义一个 MessageFactory,自己实现序列化 toString() 的逻辑。
(2)修改配置, 指定我们自定义的 MessageFactory。(log4j2.messageFactory=com.log.CustomParameterizedMessageFactory)
public class CustomParameterizedMessageFactory extends AbstractMessageFactory {
static {
// 这里指定 fastjson 序列化时 Date 类型的格式
SerializeConfig.getGlobalInstance().put(Date.class, new SimpleDateFormatSerializer("yyyy-MM-dd HH:mm:ss"));
// 这里指定 fastjson 序列化时的 Filter,用于数据脱敏(遇到 Person 类就会自动脱敏)
// 有多个类需要脱敏的,依次添加即可;无需脱敏的,可以删除此行代码
SerializeConfig.getGlobalInstance().addFilter(Person.class, DataMaskFilter.instance());
}
public CustomParameterizedMessageFactory() {
super();
}
@Override
public Message newMessage(String s, Object... objects) {
List<String> objStr = new ArrayList<>();
for (int i = 0; i < objects.length; i++) {
// 连占位符都可以由底层实现,上游再也不用担心忘写占位符了
s = s + ", param" + i + "={}";
Object o = objects[i];
if (o instanceof String || o instanceof CharSequence
|| o instanceof Character || o instanceof Short
|| o instanceof Integer || o instanceof Long
|| o instanceof Double || o instanceof Float
|| o instanceof Boolean) {
// 基础数据类型,直接转 String
objStr.add(String.valueOf(o));
}else {
// 其他包装类通过 json 序列化为 String
objStr.add(JSON.toJSONString(o));
}
}
return new ParameterizedMessage(s, objStr.toArray());
}
}
注意:
某些版本是不支持修改默认 MessageFactory 的,比如:
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.2</version>
是否支持,看这段代码就行 org.apache.logging.log4j.spi.AbstractLogger
这个版本里面是写死的,如果想要用自定义的 MessageFactory ,只能在 getLogger() 的时候,指定 MessageFactory 。
private static Logger logger = LogManager.getLogger(TestLog4j.class CustomParameterizedMessageFactory.class);
支持修改的代码是下面这个样子的:
logback
它的扩展点是 ch.qos.logback.classic.pattern.MessageConverter。
(1)自定义一个 MessageConverter,实现自己实现序列化 toString() 的逻辑。
(2)修改配置,指定我们自定义的 MessageConverter。
<conversionRule conversionWord="msg" converterClass="com.log.CustomConverter "/>
public class CustomConverter extends MessageConverter {
static {
// 这里指定 fastjson 序列化时 Date 类型的格式
SerializeConfig.getGlobalInstance().put(Date.class, new SimpleDateFormatSerializer("yyyy-MM-dd HH:mm:ss"));
// 这里指定 fastjson 序列化时的 Filter,用于数据脱敏(遇到 Person 类就会自动脱敏)
// 有多个类需要脱敏的,依次添加即可;无需脱敏的,可以删除此行代码
SerializeConfig.getGlobalInstance().addFilter(Person.class, DataMaskFilter.instance());
}
@Override
public String convert(ILoggingEvent event){
try {
return toJsonString(event);
}catch (Exception e){
return super.convert(event);
}
}
private String toJsonString(ILoggingEvent event){
try {
List<String> objStr = new ArrayList<>();
String msg = event.getMessage();
Object[] argumentArray = event.getArgumentArray();
for (int i = 0; i < argumentArray.length; i++) {
msg = msg + ", param" + i + "={}";
Object o = argumentArray[i];
if (o instanceof String || o instanceof CharSequence
|| o instanceof Character || o instanceof Short
|| o instanceof Integer || o instanceof Long
|| o instanceof Double || o instanceof Float
|| o instanceof Boolean) {
// 基础数据类型,直接转 String
objStr.add(String.valueOf(o));
}else {
// 其他包装类通过 json 序列化为 String
objStr.add(JSON.toJSONString(o));
}
}
return MessageFormatter.arrayFormat(msg, objStr.toArray()).getMessage();
} catch (Exception e) {
return event.getMessage();
}
}
}
脱敏实现
通过 fastjson 的扩展点 com.alibaba.fastjson.serializer.ValueFilter,通过对 value 内容的修改,从而实现脱敏效果。
(1)自定义一个 ValueFilter,自己实现 process() 替换 value 的逻辑。
(2)修改配置, 把我们自定义的 ValueFilter 和 要脱敏的类,添加到全局的序列化配置中。
SerializeConfig.getGlobalInstance().addFilter(Person.class, DataMaskFilter.instance());
SerializeConfig.getGlobalInstance().addFilter(Xxxclass, DataMaskFilter.instance());
...
import com.alibaba.fastjson.serializer.ValueFilter;
import java.util.Arrays;
import java.util.List;
public class DataMaskFilter implements ValueFilter {
public static DataMaskFilter instance(){
return new DataMaskFilter();
}
@Override
public Object process(Object object, String name, Object value) {
for (DataMaskRuleEnum rule : DataMaskRuleEnum.values()) {
List<String> fields = Arrays.asList(rule.fieldName.split("\\|"));
if(fields.contains(name.toUpperCase())){
value = value.toString().replaceAll(rule.regular, rule.result);
}
}
return value;
}
}
public enum DataMaskRuleEnum {
ID_CARD("身份证号脱敏", "IDCARD", "^(\\d{4})\\d+(\\d{4})$", "$1****$2"),
PHONE("手机号脱敏", "PHONE|BANKPHONE", "^(\\d{3})\\d+(\\d{4})$", "$1****$2"),
BANK_CARD("银行卡号脱敏", "CARDNUM", "^(\\d{4})\\d+(\\d{4})$", "$1****$2"),
NAME("姓名脱敏", "NAME|REALNAME|WORKERNAME", "^([\\u4E00-\\u9FA5]{1,3})([\\u4E00-\\u9FA5])$", "**$2");
DataMaskRuleEnum(String description, String fieldName, String regular, String result) {
this.description = description;
this.fieldName = fieldName;
this.regular = regular;
this.result = result;
}
/**
* 脱敏规则描述
*/
public String description;
/**
* 要脱敏的属性名
*/
public String fieldName;
/**
* 脱敏规则
*/
public String regular;
/**
* 脱敏结果
*/
public String result;
}
最终效果
甚至还可以在底层处理集合对象,
大集合只打印出集合的条数,
避免打印全部内容,造成大日志文件。
原创不易,多多关注,一键三连,感谢支持!