工厂类中使用ThreadLocal的陷阱

240 阅读1分钟

1. 背景

由于EDI已有的日志结构比较混乱,多个人都写了自己的LoggerHelper工具类。近期的工作主要是写一个新的日志框架,通过SPI方式加载Appender的实现,并替换掉之前的日志内容。

2. 初始实现LoggerFactory

在实现日志框架时,我写了一个LoggerFactory,代码如下:

public class LoggerFactory {
    private static IAppender appender = AppenderFactory.getAppender();
    private static final ThreadLocal<ISessionLogger> loggerThreadLocal =
            ThreadLocal.withInitial(() -> new SessionLoggerImpl(appender));

    public static ISessionLogger getSessionLogger() {
        return loggerThreadLocal.get();
    }  
}

3. ThreadLocal的使用

写完让阳哥review后,阳哥说这个存在很大隐患:“使用这个类的人,大概率会像使用Log4j一样——把*LoggerFactory.getSessionLogger()*的返回值赋给类的某个成员变量使用”。如下所示:

public class Test {
    private final ISessionLogger logger = LoggerFactory.getSessionLogger();
  
    public void func() {
        logger.log("anything");
    }
}

4. 两种改进方案

  1. 增加中间代理类
public class LoggerFactory {
    private static final IAppender appender = AppenderFactory.getAppender();
    private static final ThreadLocal<ISessionLogger> loggerThreadLocal =
            ThreadLocal.withInitial(() -> new SessionLoggerImpl(appender));

    public static ISessionLogger getSessionLogger() {
        return (ISessionLogger) Proxy.newProxyInstance(EdiLoggerFactory.class.getClassLoader(),
                new Class[]{ISessionLogger.class}, 
               (proxy, method, args) -> method.invoke(loggerThreadLocal.get(), args));
    }
}
  1. 静态方法代态工厂类
public class Logger {
    private static IAppender appender = AppenderFactory.getAppender();
    private static final ThreadLocal<ISessionLogger> loggerThreadLocal =
            ThreadLocal.withInitial(() -> new SessionLoggerImpl(appender));

    public static void log(String info) {
        loggerThreadLocal.get().log(info);
    }    
}

5. 总结

工厂方法中使用ThreadLocal时需要注意: 1)工厂类获取的实例一般会赋值给成员变量,来供该类的所有方法使用; 2)获取ThreadLocal实例一般赋值给方法内的局部变量,来获取当前线程ThreadLocalMap中的实例; 3)由于工厂类和ThreadLocal的常规使用场景不一致,两者混搭时,就容易出现非预期的结果。