Loading

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,有参的指定一个名字。

接口实现的问题:

  1. 调用者总得实现LogOwnerComponent接口,并且在使用到logger时要调用方法,很难看:
    @Controller
    public class MyController implements LogOwnerComponent{
        @RequestMapping("/")
        public String test() {
            logger().info("xxx");
        }
    }
    
  2. 每次都调用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;

该方法的问题:

  1. 这应该不算个问题:Logger的注入在实例化后阶段,有些基础ApplicationContext会在初始化阶段对所有未满足简单属性进行自动赋值,此时我们的Logger已经在实例化阶段被满足,所以如果有的用户期待在初始化阶段它还会被赋值,这个行为不会发生。不过我们的方法中必须得有@Passed注解,而且用户一般不会混用这两种行为,而且在常用的高级ApplicationContext中,这个未满足简单属性自动赋值的行为都被关闭了,所以不太会造成问题
posted @ 2022-07-29 10:50  yudoge  阅读(385)  评论(0编辑  收藏  举报