工厂类中使用ThreadLocal的陷阱

1. 背景

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

2. 初始实现LoggerFactory

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

1
2
3
4
5
6
7
8
9
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()*的返回值赋给类的某个成员变量使用”。如下所示:

1
2
3
4
5
6
7
public class Test {
private final ISessionLogger logger = LoggerFactory.getSessionLogger();

public void func() {
logger.log("anything");
}
}

4. 两种改进方案

  1. 增加中间代理类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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));
    }
    }
  2. 静态方法代态工厂类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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的常规使用场景不一致,两者混搭时,就容易出现非预期的结果。

评论