最近公司严格要求日志脱敏,对于敏感字段,诸如身份证号、手机号、银行卡号等用户信息进行掩码,保证日志中没有明文。
项目代码中打印日志的地方形如:
logger.info("idCard:{},phone:{},mobile:{},name:{}", idCard, phone, mobile, name);
相信很多javaer都是这么做的,现在要对日志进行掩码,怎么做?难道一行一行去改?
当然不行!
这种情况当然是…debug了!
第一次遇上这种问题,很多人可能手足无措,那么是时候祭出终极大招:DEBUG。
从logger.info()进去,发现调用了AbstractLogger.logIfEnabled()方法,继续往下走到AbstractLogger.logMessage(),这一方法有下面两行代码:
final Message msg = messageFactory.newMessage(message, p0, p1, p2, p3);
logMessageSafely(fqcn, level, marker, msg, msg.getThrowable());
首先看下AbstractLogger的私有属性messageFactory是怎么来的:
public AbstractLogger() {
this.name = getClass().getName();
this.messageFactory = createDefaultMessageFactory();
this.flowMessageFactory = createDefaultFlowMessageFactory();
}
public AbstractLogger(final String name) {
this(name, createDefaultMessageFactory());
}
public AbstractLogger(final String name, final MessageFactory messageFactory) {
this.name = name;
this.messageFactory = messageFactory == null ? createDefaultMessageFactory() : narrow(messageFactory);
this.flowMessageFactory = createDefaultFlowMessageFactory();
}
其三个构造方法,对于messageFactory都使用了createDefaultMessageFactory()方法
private static MessageFactory2 createDefaultMessageFactory() {
try {
final MessageFactory result = DEFAULT_MESSAGE_FACTORY_CLASS.newInstance();
return narrow(result);
} catch (final InstantiationException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
默认来自静态属性DEFAULT_MESSAGE_FACTORY_CLASS的实例:
public static final Class extends MessageFactory> DEFAULT_MESSAGE_FACTORY_CLASS =
createClassForProperty("log4j2.messageFactory", ReusableMessageFactory.class,
ParameterizedMessageFactory.class);
仔细看看这个静态方法:
private static Class extends MessageFactory> createClassForProperty(final String property,
final Class<ReusableMessageFactory> reusableParameterizedMessageFactoryClass,
final Class<ParameterizedMessageFactory> parameterizedMessageFactoryClass) {
try {
final String fallback = Constants.ENABLE_THREADLOCALS ? reusableParameterizedMessageFactoryClass.getName()
: parameterizedMessageFactoryClass.getName();
final String clsName = PropertiesUtil.getProperties().getStringProperty(property, fallback);
return LoaderUtil.loadClass(clsName).asSubclass(MessageFactory.class);
} catch (final Throwable t) {
return parameterizedMessageFactoryClass;
}
}
直接从系统属性log4j2.messageFactory获取了工厂类全名(真的如此吗?)
看到这里以为要大功告成了,目前看上去是需要实现工厂类的接口,再将其配置给log4j2.messageFactory属性即可。于是就有了工厂类如下:
public class DesensitizedParameterizedMessageFactory implements MessageFactory {
@Override
public Message newMessage(Object message) {
return null;
}
@Override
public Message newMessage(String message) {
return null;
}
@Override
public Message newMessage(String message, Object... params) {
return null;
}
}
为啥非要叫XXParameterizedMessageFactory,具体细节请自行debug。
现在工厂有了,产品呢?
回到AbstractLogger.logMessage()方法,工厂类messageFactory产生的产品是Message,打开看下,(肯定)是个接口。这里我们只看由ParameterizedMessageFactory产生的ParameterizedMessage。
关注其几个重要私有属性:
//消息模板,本例子中是idCard:{},phone:{},mobile:{},name:{}
private String messagePattern;
//参数数组,本例子中是数组{idCard, phone, mobile, name}
private transient Object[] argArray;
//格式化后的具体消息
private String formattedMessage;
//特殊处理异常对象
private transient Throwable throwable;
//这个很重要,是标识位{}在messagePattern中的位置
private int[] indices;
//当前indices的位置,表示使用到第几个标识位了
private int usedCount;
我们是希望在格式化的时候,就对敏感字段进行掩码,因此重点关注org.apache.logging.log4j.message.ParameterizedMessage#formatTo这个方法,debug进去后执行了org.apache.logging.log4j.message.ParameterFormatter#formatMessage2这个方法:
static void formatMessage2(final StringBuilder buffer, final String messagePattern,
final Object[] arguments, final int argCount, final int[] indices) {
if (messagePattern == null || arguments == null || argCount == 0) {
buffer.append(messagePattern);
return;
}
int previous = 0;
for (int i = 0; i < argCount; i++) {
buffer.append(messagePattern, previous, indices[i]);
previous = indices[i] + 2;
recursiveDeepToString(arguments[i], buffer, null);
}
buffer.append(messagePattern, previous, messagePattern.length());
}
如上,我们看到在这里对arguments数组进行了循环,将其值最终append到目标StringBuilder,如果我们在这里能判断模板包含了什么敏感字符串,就可以在这里使用不同的掩码方式对字段进行处理。
怎么获取当前是哪个字读在append了呢?
前面提到的indices这个时候就可以用了,那么我们可以从模板messagePattern截取出当前要append这段字符串:
String word = messagePattern.substring(previous, indices[i]);
为了能达到掩码的目的,我们加了一个枚举类来完成对不同字段进行掩码操作:
public enum DesensitizedWords {
idCard("idCard", 6, 3),
phone("phone", 3, 4),
mobile("mobile", 3, 4),
name("name", 0, 1),
;
private String word;
private int front;
private int tail;
DesensitizedWords(String word, int front, int tail) {
this.word = word;
this.front = front;
this.tail = tail;
}
public static String desensitize(String word, String val) {
for (DesensitizedWords item : DesensitizedWords.values()) {
if (word.contains(item.word)) {
return hide(val, item.front, item.tail, '*');
}
}
return val;
}
public static String hide(String src, int front, int tail, char replace) {
if (null == src)
return src;
int len = src.length();
if (front > len || tail > len) {
return src;
}
StringBuilder builder = new StringBuilder();
if (front > 0) {
builder.append(src.substring(0, front));
} else {
front = 0;
}
String tailStr = "";
if (tail > 0) {
tailStr = src.substring(src.length() - tail, src.length());
} else {
tail = 0;
}
int padding = len - front - tail;
if (padding > 0) {
for (int i = 0; i < padding; i++) {
builder.append(replace);
}
}
builder.append(tailStr);
return builder.toString();
}
}
然后将org.apache.logging.log4j.message.ParameterFormatter#formatMessage2方法重写(实际上由于ParameterFormatter是final并且是package可见的,因此我们将org.apache.logging.log4j.message.ParameterizedMessage整个类copy一份为DesensitizedParameterizedMessage,并且将ParameterFormatter作为DesensitizedParameterizedMessage的内部类,并重写formatMessage2方法如下):
static void formatMessage2(final StringBuilder buffer, final String messagePattern,
final Object[] arguments, final int argCount, final int[] indices) {
if (messagePattern == null || arguments == null || argCount == 0) {
buffer.append(messagePattern);
return;
}
int previous = 0;
for (int i = 0; i < argCount; i++) {
String word = messagePattern.substring(previous, indices[i]);
buffer.append(messagePattern, previous, indices[i]);
previous = indices[i] + 2;
StringBuilder builder = new StringBuilder();
recursiveDeepToString(arguments[i], builder, null);
buffer.append(DesensitizedWords.desensitize(word, builder.toString()));
}
buffer.append(messagePattern, previous, messagePattern.length());
}
通过上面的改写(copy),我们已经将最基本的类创建出来,现在要考虑如何使用他们,前文有讲到使用系统属性log4j2.messageFactory来配置工厂类,实际上并没有起作用,再次debug后发现,我们需要在resources下加一个log4j2.component.properties文件来配置这个属性才能生效,文件配置如下:
log4j2.messageFactory=com.X.Xx.DesensitizedParameterizedMessageFactory
记得将工厂类产生的产品改为DesensitizedParameterizedMessage哟。
测试用例
String idCard = "210002197812129527";
String name = "周星星";
String phone = "14700000000";
String mobile = "14700000000";
logger.info("明文数据:{},{},{},{}", idCard, phone, mobile, name);
logger.info("idCard:{},phone:{},mobile:{},name:{}", idCard, phone, mobile, name);
logger.error("error-idCard:{},error-phone:{},error-mobile:{},error-name:{}", idCard, phone, mobile, name);
logger.warn("warn-idCard:{},warn-phone:{},warn-mobile:{},warn-name:{}", idCard, phone, mobile, name);
logger.debug("debug-idCard:{},debug-phone:{},debug-mobile:{},debug-name:{}", idCard, phone, mobile, name);
效果如下
2017-12-06 10:48:39.490 [http-bio-8080-exec-1:563179] [DemoController.java:83] - [INFO] 明文数据:210002188012120359,14700000000,14700000000,周星星
2017-12-06 10:48:39.491 [http-bio-8080-exec-1:563180] [DemoController.java:84] - [INFO] idCard:210002*********359,phone:147****0000,mobile:147****0000,name:**星
2017-12-06 10:48:39.491 [http-bio-8080-exec-1:563180] [DemoController.java:85] - [ERROR] error-idCard:210002*********359,error-phone:147****0000,error-mobile:147****0000,error-name:**星
2017-12-06 10:48:39.492 [http-bio-8080-exec-1:563181] [DemoController.java:86] - [WARN] warn-idCard:210002*********359,warn-phone:147****0000,warn-mobile:147****0000,warn-name:**星
2017-12-06 10:48:39.493 [http-bio-8080-exec-1:563182] [DemoController.java:87] - [DEBUG] debug-idCard:210002*********359,debug-phone:147****0000,debug-mobile:147****0000,debug-name:**星
终于到了大家喜欢的环节(没有做裁剪,自行操作)。
DesensitizedParameterizedMessage.java
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.StringBuilderFormattable;
import java.text.SimpleDateFormat;
import java.util.*;
public class DesensitizedParameterizedMessage implements Message, StringBuilderFormattable {
// Should this be configurable?
private static final int DEFAULT_STRING_BUILDER_SIZE = 255;
/**
* Prefix for recursion.
*/
public static final String RECURSION_PREFIX = ParameterFormatter.RECURSION_PREFIX;
/**
* Suffix for recursion.
*/
public static final String RECURSION_SUFFIX = ParameterFormatter.RECURSION_SUFFIX;
/**
* Prefix for errors.
*/
public static final String ERROR_PREFIX = ParameterFormatter.ERROR_PREFIX;
/**
* Separator for errors.
*/
public static final String ERROR_SEPARATOR = ParameterFormatter.ERROR_SEPARATOR;
/**
* Separator for error messages.
*/
public static final String ERROR_MSG_SEPARATOR = ParameterFormatter.ERROR_MSG_SEPARATOR;
/**
* Suffix for errors.
*/
public static final String ERROR_SUFFIX = ParameterFormatter.ERROR_SUFFIX;
private static final long serialVersionUID = -665975803997290697L;
private static final int HASHVAL = 31;
// storing JDK classes in ThreadLocals does not cause memory leaks in web apps, so this is okay
private static ThreadLocal threadLocalStringBuilder = new ThreadLocal<>();
private String messagePattern;
private transient Object[] argArray;
private String formattedMessage;
private transient Throwable throwable;
private int[] indices;
private int usedCount;
public DesensitizedParameterizedMessage(final String messagePattern, final Object[] arguments, final Throwable throwable) {
this.argArray = arguments;
this.throwable = throwable;
init(messagePattern);
}
public DesensitizedParameterizedMessage(final String messagePattern, final Object... arguments) {
this.argArray = arguments;
init(messagePattern);
}
/**
* Constructor with a pattern and a single parameter.
*
* @param messagePattern The message pattern.
* @param arg The parameter.
*/
public DesensitizedParameterizedMessage(final String messagePattern, final Object arg) {
this(messagePattern, new Object[]{arg});
}
public DesensitizedParameterizedMessage(final String messagePattern, final Object arg0, final Object arg1) {
this(messagePattern, new Object[]{arg0, arg1});
}
private void init(final String messagePattern) {
this.messagePattern = messagePattern;
final int len = Math.max(1, messagePattern == null ? 0 : messagePattern.length() >> 1); // divide by 2
this.indices = new int[len]; // LOG4J2-1542 ensure non-zero array length
final int placeholders = ParameterFormatter.countArgumentPlaceholders2(messagePattern, indices);
initThrowable(argArray, placeholders);
this.usedCount = Math.min(placeholders, argArray == null ? 0 : argArray.length);
}
private void initThrowable(final Object[] params, final int usedParams) {
if (params != null) {
final int argCount = params.length;
if (usedParams < argCount && this.throwable == null && params[argCount - 1] instanceof Throwable) {
this.throwable = (Throwable) params[argCount - 1];
}
}
}
@Override
public String getFormat() {
return messagePattern;
}
@Override
public Object[] getParameters() {
return argArray;
}
@Override
public Throwable getThrowable() {
return throwable;
}
@Override
public String getFormattedMessage() {
if (formattedMessage == null) {
final StringBuilder buffer = getThreadLocalStringBuilder();
formatTo(buffer);
formattedMessage = buffer.toString();
}
return formattedMessage;
}
private static StringBuilder getThreadLocalStringBuilder() {
StringBuilder buffer = threadLocalStringBuilder.get();
if (buffer == null) {
buffer = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
threadLocalStringBuilder.set(buffer);
}
buffer.setLength(0);
return buffer;
}
@Override
public void formatTo(final StringBuilder buffer) {
if (formattedMessage != null) {
buffer.append(formattedMessage);
} else {
if (indices[0] < 0) {
ParameterFormatter.formatMessage(buffer, messagePattern, argArray, usedCount);
} else {
ParameterFormatter.formatMessage2(buffer, messagePattern, argArray, usedCount, indices);
}
}
}
public static String format(final String messagePattern, final Object[] arguments) {
return ParameterFormatter.format(messagePattern, arguments);
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final DesensitizedParameterizedMessage that = (DesensitizedParameterizedMessage) o;
if (messagePattern != null ? !messagePattern.equals(that.messagePattern) : that.messagePattern != null) {
return false;
}
if (!Arrays.equals(this.argArray, this.argArray)) {
return false;
}
//if (throwable != null ? !throwable.equals(that.throwable) : that.throwable != null) return false;
return true;
}
@Override
public int hashCode() {
int result = messagePattern != null ? messagePattern.hashCode() : 0;
result = HASHVAL * result + (argArray != null ? Arrays.hashCode(argArray) : 0);
return result;
}
public static int countArgumentPlaceholders(final String messagePattern) {
return ParameterFormatter.countArgumentPlaceholders(messagePattern);
}
public static String deepToString(final Object o) {
return ParameterFormatter.deepToString(o);
}
public static String identityToString(final Object obj) {
return ParameterFormatter.identityToString(obj);
}
@Override
public String toString() {
return "ParameterizedMessage[messagePattern=" + messagePattern + ", stringArgs=" +
Arrays.toString(argArray) + ", throwable=" + throwable + ']';
}
static class ParameterFormatter {
/**
* Prefix for recursion.
*/
static final String RECURSION_PREFIX = "[...";
/**
* Suffix for recursion.
*/
static final String RECURSION_SUFFIX = "...]";
/**
* Prefix for errors.
*/
static final String ERROR_PREFIX = "[!!!";
/**
* Separator for errors.
*/
static final String ERROR_SEPARATOR = "=>";
/**
* Separator for error messages.
*/
static final String ERROR_MSG_SEPARATOR = ":";
/**
* Suffix for errors.
*/
static final String ERROR_SUFFIX = "!!!]";
private static final char DELIM_START = '{';
private static final char DELIM_STOP = '}';
private static final char ESCAPE_CHAR = '\\';
private static ThreadLocal threadLocalSimpleDateFormat = new ThreadLocal<>();
private ParameterFormatter() {
}
static int countArgumentPlaceholders(final String messagePattern) {
if (messagePattern == null) {
return 0;
}
final int length = messagePattern.length();
int result = 0;
boolean isEscaped = false;
for (int i = 0; i < length - 1; i++) {
final char curChar = messagePattern.charAt(i);
if (curChar == ESCAPE_CHAR) {
isEscaped = !isEscaped;
} else if (curChar == DELIM_START) {
if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
result++;
i++;
}
isEscaped = false;
} else {
isEscaped = false;
}
}
return result;
}
static int countArgumentPlaceholders2(final String messagePattern, final int[] indices) {
if (messagePattern == null) {
return 0;
}
final int length = messagePattern.length();
int result = 0;
boolean isEscaped = false;
for (int i = 0; i < length - 1; i++) {
final char curChar = messagePattern.charAt(i);
if (curChar == ESCAPE_CHAR) {
isEscaped = !isEscaped;
indices[0] = -1; // escaping means fast path is not available...
result++;
} else if (curChar == DELIM_START) {
if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
indices[result] = i;
result++;
i++;
}
isEscaped = false;
} else {
isEscaped = false;
}
}
return result;
}
static int countArgumentPlaceholders3(final char[] messagePattern, final int length, final int[] indices) {
int result = 0;
boolean isEscaped = false;
for (int i = 0; i < length - 1; i++) {
final char curChar = messagePattern[i];
if (curChar == ESCAPE_CHAR) {
isEscaped = !isEscaped;
} else if (curChar == DELIM_START) {
if (!isEscaped && messagePattern[i + 1] == DELIM_STOP) {
indices[result] = i;
result++;
i++;
}
isEscaped = false;
} else {
isEscaped = false;
}
}
return result;
}
static String format(final String messagePattern, final Object[] arguments) {
final StringBuilder result = new StringBuilder();
final int argCount = arguments == null ? 0 : arguments.length;
formatMessage(result, messagePattern, arguments, argCount);
return result.toString();
}
static void formatMessage2(final StringBuilder buffer, final String messagePattern,
final Object[] arguments, final int argCount, final int[] indices) {
if (messagePattern == null || arguments == null || argCount == 0) {
buffer.append(messagePattern);
return;
}
int previous = 0;
for (int i = 0; i < argCount; i++) {
String word = messagePattern.substring(previous, indices[i]);
buffer.append(messagePattern, previous, indices[i]);
previous = indices[i] + 2;
StringBuilder builder = new StringBuilder();
recursiveDeepToString(arguments[i], builder, null);
buffer.append(DesensitizedWords.desensitize(word, builder.toString()));
}
buffer.append(messagePattern, previous, messagePattern.length());
}
static void formatMessage3(final StringBuilder buffer, final char[] messagePattern, final int patternLength,
final Object[] arguments, final int argCount, final int[] indices) {
if (messagePattern == null) {
return;
}
if (arguments == null || argCount == 0) {
buffer.append(messagePattern);
return;
}
int previous = 0;
for (int i = 0; i < argCount; i++) {
buffer.append(messagePattern, previous, indices[i]);
previous = indices[i] + 2;
recursiveDeepToString(arguments[i], buffer, null);
}
buffer.append(messagePattern, previous, patternLength);
}
static void formatMessage(final StringBuilder buffer, final String messagePattern,
final Object[] arguments, final int argCount) {
if (messagePattern == null || arguments == null || argCount == 0) {
buffer.append(messagePattern);
return;
}
int escapeCounter = 0;
int currentArgument = 0;
int i = 0;
final int len = messagePattern.length();
for (; i < len - 1; i++) { // last char is excluded from the loop
final char curChar = messagePattern.charAt(i);
if (curChar == ESCAPE_CHAR) {
escapeCounter++;
} else {
if (isDelimPair(curChar, messagePattern, i)) { // looks ahead one char
i++;
// write escaped escape chars
writeEscapedEscapeChars(escapeCounter, buffer);
if (isOdd(escapeCounter)) {
// i.e. escaped: write escaped escape chars
writeDelimPair(buffer);
} else {
// unescaped
writeArgOrDelimPair(arguments, argCount, currentArgument, buffer);
currentArgument++;
}
} else {
handleLiteralChar(buffer, escapeCounter, curChar);
}
escapeCounter = 0;
}
}
handleRemainingCharIfAny(messagePattern, len, buffer, escapeCounter, i);
}
private static boolean isDelimPair(final char curChar, final String messagePattern, final int curCharIndex) {
return curChar == DELIM_START && messagePattern.charAt(curCharIndex + 1) == DELIM_STOP;
}
private static void handleRemainingCharIfAny(final String messagePattern, final int len,
final StringBuilder buffer, final int escapeCounter, final int i) {
if (i == len - 1) {
final char curChar = messagePattern.charAt(i);
handleLastChar(buffer, escapeCounter, curChar);
}
}
private static void handleLastChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
if (curChar == ESCAPE_CHAR) {
writeUnescapedEscapeChars(escapeCounter + 1, buffer);
} else {
handleLiteralChar(buffer, escapeCounter, curChar);
}
}
private static void handleLiteralChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
writeUnescapedEscapeChars(escapeCounter, buffer);
buffer.append(curChar);
}
private static void writeDelimPair(final StringBuilder buffer) {
buffer.append(DELIM_START);
buffer.append(DELIM_STOP);
}
private static boolean isOdd(final int number) {
return (number & 1) == 1;
}
private static void writeEscapedEscapeChars(final int escapeCounter, final StringBuilder buffer) {
final int escapedEscapes = escapeCounter >> 1; // divide by two
writeUnescapedEscapeChars(escapedEscapes, buffer);
}
private static void writeUnescapedEscapeChars(int escapeCounter, final StringBuilder buffer) {
while (escapeCounter > 0) {
buffer.append(ESCAPE_CHAR);
escapeCounter--;
}
}
private static void writeArgOrDelimPair(final Object[] arguments, final int argCount, final int currentArgument,
final StringBuilder buffer) {
if (currentArgument < argCount) {
recursiveDeepToString(arguments[currentArgument], buffer, null);
} else {
writeDelimPair(buffer);
}
}
static String deepToString(final Object o) {
if (o == null) {
return null;
}
if (o instanceof String) {
return (String) o;
}
final StringBuilder str = new StringBuilder();
final Set dejaVu = new HashSet<>(); // that's actually a neat name ;)
recursiveDeepToString(o, str, dejaVu);
return str.toString();
}
private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set dejaVu) {
if (appendSpecialTypes(o, str)) {
return;
}
if (isMaybeRecursive(o)) {
appendPotentiallyRecursiveValue(o, str, dejaVu);
} else {
tryObjectToString(o, str);
}
}
private static boolean appendSpecialTypes(final Object o, final StringBuilder str) {
if (o == null || o instanceof String) {
str.append((String) o);
return true;
} else if (o instanceof CharSequence) {
str.append((CharSequence) o);
return true;
} else if (o instanceof StringBuilderFormattable) {
((StringBuilderFormattable) o).formatTo(str);
return true;
} else if (o instanceof Integer) { // LOG4J2-1415 unbox auto-boxed primitives to avoid calling toString()
str.append(((Integer) o).intValue());
return true;
} else if (o instanceof Long) {
str.append(((Long) o).longValue());
return true;
} else if (o instanceof Double) {
str.append(((Double) o).doubleValue());
return true;
} else if (o instanceof Boolean) {
str.append(((Boolean) o).booleanValue());
return true;
} else if (o instanceof Character) {
str.append(((Character) o).charValue());
return true;
} else if (o instanceof Short) {
str.append(((Short) o).shortValue());
return true;
} else if (o instanceof Float) {
str.append(((Float) o).floatValue());
return true;
}
return appendDate(o, str);
}
private static boolean appendDate(final Object o, final StringBuilder str) {
if (!(o instanceof Date)) {
return false;
}
final Date date = (Date) o;
final SimpleDateFormat format = getSimpleDateFormat();
str.append(format.format(date));
return true;
}
private static SimpleDateFormat getSimpleDateFormat() {
SimpleDateFormat result = threadLocalSimpleDateFormat.get();
if (result == null) {
result = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
threadLocalSimpleDateFormat.set(result);
}
return result;
}
private static boolean isMaybeRecursive(final Object o) {
return o.getClass().isArray() || o instanceof Map || o instanceof Collection;
}
private static void appendPotentiallyRecursiveValue(final Object o, final StringBuilder str,
final Set dejaVu) {
final Class> oClass = o.getClass();
if (oClass.isArray()) {
appendArray(o, str, dejaVu, oClass);
} else if (o instanceof Map) {
appendMap(o, str, dejaVu);
} else if (o instanceof Collection) {
appendCollection(o, str, dejaVu);
}
}
private static void appendArray(final Object o, final StringBuilder str, Set dejaVu,
final Class> oClass) {
if (oClass == byte[].class) {
str.append(Arrays.toString((byte[]) o));
} else if (oClass == short[].class) {
str.append(Arrays.toString((short[]) o));
} else if (oClass == int[].class) {
str.append(Arrays.toString((int[]) o));
} else if (oClass == long[].class) {
str.append(Arrays.toString((long[]) o));
} else if (oClass == float[].class) {
str.append(Arrays.toString((float[]) o));
} else if (oClass == double[].class) {
str.append(Arrays.toString((double[]) o));
} else if (oClass == boolean[].class) {
str.append(Arrays.toString((boolean[]) o));
} else if (oClass == char[].class) {
str.append(Arrays.toString((char[]) o));
} else {
if (dejaVu == null) {
dejaVu = new HashSet<>();
}
// special handling of container Object[]
final String id = identityToString(o);
if (dejaVu.contains(id)) {
str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
} else {
dejaVu.add(id);
final Object[] oArray = (Object[]) o;
str.append('[');
boolean first = true;
for (final Object current : oArray) {
if (first) {
first = false;
} else {
str.append(", ");
}
recursiveDeepToString(current, str, new HashSet<>(dejaVu));
}
str.append(']');
}
//str.append(Arrays.deepToString((Object[]) o));
}
}
private static void appendMap(final Object o, final StringBuilder str, Set dejaVu) {
// special handling of container Map
if (dejaVu == null) {
dejaVu = new HashSet<>();
}
final String id = identityToString(o);
if (dejaVu.contains(id)) {
str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
} else {
dejaVu.add(id);
final Map, ?> oMap = (Map, ?>) o;
str.append('{');
boolean isFirst = true;
for (final Object o1 : oMap.entrySet()) {
final Map.Entry, ?> current = (Map.Entry, ?>) o1;
if (isFirst) {
isFirst = false;
} else {
str.append(", ");
}
final Object key = current.getKey();
final Object value = current.getValue();
recursiveDeepToString(key, str, new HashSet<>(dejaVu));
str.append('=');
recursiveDeepToString(value, str, new HashSet<>(dejaVu));
}
str.append('}');
}
}
private static void appendCollection(final Object o, final StringBuilder str, Set dejaVu) {
// special handling of container Collection
if (dejaVu == null) {
dejaVu = new HashSet<>();
}
final String id = identityToString(o);
if (dejaVu.contains(id)) {
str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
} else {
dejaVu.add(id);
final Collection> oCol = (Collection>) o;
str.append('[');
boolean isFirst = true;
for (final Object anOCol : oCol) {
if (isFirst) {
isFirst = false;
} else {
str.append(", ");
}
recursiveDeepToString(anOCol, str, new HashSet<>(dejaVu));
}
str.append(']');
}
}
private static void tryObjectToString(final Object o, final StringBuilder str) {
// it's just some other Object, we can only use toString().
try {
str.append(o.toString());
} catch (final Throwable t) {
handleErrorInObjectToString(o, str, t);
}
}
private static void handleErrorInObjectToString(final Object o, final StringBuilder str, final Throwable t) {
str.append(ERROR_PREFIX);
str.append(identityToString(o));
str.append(ERROR_SEPARATOR);
final String msg = t.getMessage();
final String className = t.getClass().getName();
str.append(className);
if (!className.equals(msg)) {
str.append(ERROR_MSG_SEPARATOR);
str.append(msg);
}
str.append(ERROR_SUFFIX);
}
static String identityToString(final Object obj) {
if (obj == null) {
return null;
}
return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
}
}
}
DesensitizedParameterizedMessageFactory.java
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.MessageFactory;
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
public class DesensitizedParameterizedMessageFactory implements MessageFactory {
private ParameterizedMessageFactory messageFactory = ParameterizedMessageFactory.INSTANCE;
@Override
public Message newMessage(Object message) {
return messageFactory.newMessage(message);
}
@Override
public Message newMessage(String message) {
return new DesensitizedParameterizedMessage(message);
}
@Override
public Message newMessage(String message, Object... params) {
return new DesensitizedParameterizedMessage(message, params);
}
}
DesensitizedWords.java见上文
这次改动全部是在debug中完成的(太懒,没有截图),没有列举所有情况(谁知道你们打日志的姿势长啥样呢),可能有些坑在里面。
后来看了下官方文档,有说到一些扩展点(https://logging.apache.org/log4j/2.x/manual/extending.html),上面的改动可以看成是MessageFactory(https://logging.apache.org/log4j/2.x/manual/extending.html#MessageFactory)的扩展,目前我们使用ParameterizedMessageFactory还没有什么问题,其他的要各位看官自行操作了。
(第一次写技术文章,文笔不咋地,后面会慢慢改进的(:哈。)