最近公司正在开始平台化,需要统一打印日志格式。由于领导对于我的认可,把这件事情交给我来做。所谓统一日志格式主要是做以下几件事:
- more business log:细粒度的业务日志文件区分(Dao、Manage、Service、Web)
- method digest log:方法级别的日志摘要(对应业务日志的摘要日志)
- a call chain profile log:当前系统一次整个完整调用的层级概要图
这种不仅可以在排查问题更加方便,同时也可以通过监控方法摘要日志文件当某个方法调用时间达到设置的阈值时还可以进行报警。
摘要日志的打印原理是通过自定义注解,通过 Spring Aspect 切面对注解定义的方法进行增强。但是问题就卡在 dao 层的日志打印,Mybatis 调用数据库的接口在集成 Spring 生成的 Bean 对象无法代理。通过 debug 跟踪 Spring AOP 的源码发现这个接口对应的 Mapper 的 Spring bean Class 对象是 MapperFactoryBean 而不是对象的 BankInfoMapper。关于为什么是 MapperFactoryBean,不清的朋友可以看我之前的一篇文章 – MyBatis Spring 集成源码解析
Spring AOP 的原理是拿到 Spring bean 定义的所有的增强通知的列表。然后遍历这些通知,看哪个通知适用于当前需要创建的 Spring Bean。如果适用就添加到当前 bean 创建的动态代理对象的通知列表的属性中。当方法执行时就遍历代理对象的通知列表实现对对象功能的增强。
但是定义的切面表达式是 BankInfoMapper 所在的包,肯定与 MapperFactoryBean 所在的包不同。所以不能对 BankInfoMapper 这个对象进行代理。虽然可以把包定义到 MapperFactoryBean 的目录,但是与其它日志打印冲突。 所有只有别想他法:
如果上看到上面提到的文章就可以知道。在 Spring 集成 Mybatis 进行接口调用的时候,是通过 MapperFactoryBean 来对 Mybatis 接口进行创建对象的。那么能不能对 MapperFactoryBean 创建的对象进行代理呢。通过观察可以看到在 Mybatis 的 MapperScan 注解中,我们可以对 MapperFactoryBean 进行自定义。
那么问题就可以解决了,解题思路如下:我们可能通过继承 MapperFactoryBean ,然后对于重写它的 getObject 方法。MapperFactoryBean 创建的对象进行增强就可以了。
下面就是继承类,实现对 BankInfoMapper 进行增强。
DigestLogMapperFactoryBean.java
public class DigestLogMapperFactoryBean<T> extends MapperFactoryBean<T> {
public DigestLogMapperFactoryBean(){}
public DigestLogMapperFactoryBean(Class<T> mapperInterface){
super(mapperInterface);
}
@Override
public T getObject() throws Exception {
Class<?> mapperInterfaceClazz = getObjectType();
T mapperProxy = super.getObject();
return (T) new JdkDynamicDigestLogProxy(mapperProxy, mapperInterfaceClazz).getProxy();
}
}
下面就是增强类,对 Mybatis 的接口调用进行增强。
JdkDynamicDigestLogProxy.class
public class JdkDynamicDigestLogProxy implements InvocationHandler {
private static final Logger logger = LoggerFactory.getLogger(JdkDynamicDigestLogProxy.class);
/**
* 是否打印方法参数
*/
private boolean printArguments = false;
/**
* 是否打印方法结果
*/
private boolean printResults = false;
private Object object;
private Class<?> interfaceClazz;
public JdkDynamicDigestLogProxy(Object object, Class<?> interfaceClazz) {
this.interfaceClazz = interfaceClazz;
this.object = object;
}
public Object getProxy() {
return getProxy(ClassUtils.getDefaultClassLoader());
}
public Object getProxy(ClassLoader classLoader) {
return Proxy.newProxyInstance(classLoader, new Class[]{interfaceClazz}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object retValue = null;
String result = "N";
long beginTime = System.currentTimeMillis();
try {
retValue = method.invoke(object, args);
result = "Y";
return retValue;
} finally {
long endTime = System.currentTimeMillis();
logger.info("[({}.{},{},{}ms)({})({})]", interfaceClazz.getSimpleName(), method.getName(), result,
endTime - beginTime, getArgumentsString(args), getResultsString(retValue));
}
}
private String getMessage(Method method) {
return interfaceClazz.getSimpleName() + "." + method.getName();
}
/**
* 获取日志中使用的方法参数
*/
private String getArgumentsString(Object[] args) {
String arguments = "-";
if (printArguments) {
arguments = StringUtils.join(args, ",");
}
return arguments;
}
/**
* 获取结果内容
*/
private Object getResultsString(Object retValue) {
if (printResults) {
return retValue == null ? "null" : retValue.toString();
} else {
return "-";
}
}
}
最后就是在 MapperScan 添加上这个继承类(DigestLogMapperFactoryBean)就行了 :