Spring开发时Logger自动注入
起因是在每一个类中我都得写上下面的代码:
private Logger logger = LoggerFactory(this.getClass().getSimpleName());
虽然不是很长,但是我就是不想写,每次写这个我都会很烦。开发中有很多属性都是这样。
然后,我就想到,如何将这一过程自动化?我在看黑马程序员的网课时,那个老师是通过继承一个基类来实现的,但是这个操作很昂贵啊,占用了这个类唯一一次的继承机会。
接口实现
第一个想到的办法就是使用接口,一个类可以实现多个接口,在接口中提供默认方法来返回Logger
public interface LogOwnerComponent {
default Logger logger() {
return logger(this.getClass().getName());
}
default Logger logger(String name) {
return LoggerFactory.getLogger(name);
}
}
上面的接口给了两个方法,无参的返回以当前类名为名字的Logger
,有参的指定一个名字。
接口实现的问题:
- 调用者总得实现
LogOwnerComponent
接口,并且在使用到logger
时要调用方法,很难看:@Controller public class MyController implements LogOwnerComponent{ @RequestMapping("/") public String test() { logger().info("xxx"); } }
- 每次都调用
LoggerFactory.getLogger
创建一个新的Logger,我不知道该方法是否对于名字相同的Logger有缓存,但是这种写法总归不太讲究。
InstantiationAwareBeanPostProcessor
第二个想到的办法就是实现一个BeanPostProcessor
,然后在某些规则下自动给类注入一个Logger
// 该BeanPostProcessor在Bean对象实例化后检测Bean中是否有一个
// 标注了`@Passed`注解的未赋值的`org.slf4j.Logger`类型的属性
// 如果有,就为这个属性注入值
@Component
public class LoggerInjectBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
//
private Logger logger = LoggerFactory.getLogger(LoggerInjectBeanPostProcessor.class);
@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
// 获取该Bean的所有属性
Arrays.stream(bean.getClass().getDeclaredFields())
// 检测该属性是否能设置成Logger.class的实例
.filter(f -> f.getType().isAssignableFrom(Logger.class))
// 检测该属性上是否具有@Passed注解
.filter(f -> f.getAnnotation(Passed.class) != null)
// 检测该属性是否尚未被设置
.filter(f -> {
f.setAccessible(true);
try {
System.out.println("already set check "+f);
return f.get(bean) == null;
} catch (IllegalAccessException e) {
return false;
}
})
// 尝试设置属性
.forEach(f -> {
logger.debug("Found there's a Logger field annotated with `@Passed`. Bean [" + beanName + "] field name: [" + f.getName() + "].");
// 以Passed的value作为logger的name,如果没有就用类全限定名
Passed passed = f.getAnnotation(Passed.class);
String loggerName = passed.value();
if (null == loggerName || loggerName.equals("")) {
loggerName = bean.getClass().getName();
}
try {
f.set(bean, LoggerFactory.getLogger(loggerName));
} catch (IllegalAccessException e) {
logger.warn("Inject logger field! In bean [" + beanName + "], field is [" + f.getName() + "]");
}
});
return true;
}
}
@Passed
注解是自己提供的一个注解:
/**
* 在本项目中,一些BeanPostProcessor会在合适的时机检测Bean中是否具有某些属性,并且如果上面有`@Passed`注解,代表该Bean期望这个属性由外界初始化并传入
*
* 一个典型的例子就是`LoggerInjectBeanPostProcessor`会检测bean中是否有一个标注了`@Passed`注解的`org.slf4j.Logger`的属性,如果有,就自动帮它初始化
*
* 就像上面的例子,项目中总有些东西的初始化很繁琐,充满模式化,但又不得不做,`@Passed`注解就是为了解决这个问题
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Passed {
/**
* 对于不同类型的属性,value的作用可能不一样,具体请看对应的BeanPostProcessor上的注释
* @return
*/
String value() default "";
}
然后,对于项目中的所有被Spring的ApplicationContext所管理的组件,只需要这样,即可在它实例化后获得一个Logger:
@Passed
private Logger log;
你还可以给Logger指定名字:
@Passed("customLoggerName")
private Logger log;
该方法的问题:
- 这应该不算个问题:Logger的注入在实例化后阶段,有些基础ApplicationContext会在初始化阶段对所有未满足简单属性进行自动赋值,此时我们的Logger已经在实例化阶段被满足,所以如果有的用户期待在初始化阶段它还会被赋值,这个行为不会发生。不过我们的方法中必须得有
@Passed
注解,而且用户一般不会混用这两种行为,而且在常用的高级ApplicationContext中,这个未满足简单属性自动赋值的行为都被关闭了,所以不太会造成问题