5

我遇到了一个问题,即我的生产系统中出现异常,但我真的没有关于谁导致它们的好信息。该人的用户名作为变量存储在他们的 tomcat 会话中,显然我可以在我的doPostordoGet方法中访问它,但除非我将该信息作为参数传递给我的每个业务对象,否则我无权访问会话. 出于显而易见的原因,我想将用户名添加到日志消息中,这样我就知道发生了什么。

所以我的解决方案是做这样的事情

public class ExceptionUtil {
    private ExceptionUtil() { } // no instantiation
    private static final ThreadLocal<String> local = new ThreadLocal<String>();
    public static void set(String user) { local.set(user); }
    public static String get() { return local.get(); }
}

然后在我的帖子/获取中,我可以做到这一点

String username = request.getSession().getAttribute("username");
ExceptionUtil.set(username);

然后在我的例外情况下,我可能会这样做(人为的,不好的做法示例)

catch(SQLException e) {
    logger.error(ExceptionUtil.get() + " did something dumb in sql", e);
    throw e;
}

我关心的唯一问题是 Tomcat 将如何管理我的线程。如果他们保留线程怎么办?他们会坚持吗?ThreadLocal 值是否也会持续存在?如果我将整个 Session 存储在 ThreadLocal 中而不仅仅是一个字符串,那将是一个严重的内存泄漏可能性。这也意味着如果有人忘记重新设置(或忘记在完成后清除)用户名/会话在一个持续存在多个请求的线程上,那里可能有陈旧的数据。

称我为愤世嫉俗者,但我不想依赖程序员(甚至,尤其是我自己!)不要忘记为程序的正确性做事。如果我可以对我的代码进行白痴验证,我愿意。这意味着更好地了解 Tomcat 将如何使用线程。

因此,单句形式的问题:

如果我在 Tomcat (7.0.27) 上运行的 web 应用程序中使用 ThreadLocal,我是否会冒 Thread 被用于多个请求的风险,并且来自先前请求的数据被持久化?

我应该指出,即使他们没有回答“Tomcat/ThreadLocal 恶作剧”的确切问题,我也愿意接受其他解决方案,这些解决方案允许我优雅地访问会话变量以进行日志记录。我也愿意就我的解决方案的潜在缺陷发表评论。我有一个业务问题要解决,而且我不接受任何一种解决方案。我只想知道是谁一直在我的产品系统上造成异常:)

4

2 回答 2

4

是的,tomcat 使用 ThreadPool 概念,这意味着线程正在被重用,因此正如您所建议的“您的线程本地保留值”,

我建议的替代方案可能是

  1. 完成后清理线程,在视图控制器的某个地方

  2. 编写一个请求过滤器并在过滤器开始时清理所有内容并推送新值,并将其分配给您服务器上的每个 url 模式

对于您所遵循的方法,而不是将某些值保存在类中,将请求存储在 Thread Local 中,然后使用该请求使用自制的 util 类从会话中提取值,该类接受请求,然后返回您想要的值,这样您在线程中保存会话并获取值,但请确保您每次都添加新鲜并在完成后清理请求(为此使用第二个选项)。

于 2012-08-10T23:37:33.880 回答
1

您不需要重新发明轮子,日志系统会为您完成。

如果logback/log4j是您的记录器实现,那么 Mapped Diagnostic Context( MDC ) 绝对是您的答案。MDC 在逻辑上类似于 ThreadLocal,但它更好:

  1. MDC 透明地处理线程安全和同步

  2. 子线程自动继承其父线程的映射诊断上下文的副本。所以即使你使用多线程来处理请求,它仍然可以。

因此,像这样在 servlet 过滤器中设置 MDC,以实现您的目标:

  public void doFilter(ServletRequest request, ServletResponse response,
    FilterChain chain) throws IOException, ServletException {

    boolean successfulRegistration = false;

    HttpServletRequest req = (HttpServletRequest) request;    
    Principal principal = req.getUserPrincipal();
    // Please note that we could have also used a cookie to 
    // retrieve the user name

    if (principal != null) {
      String username = principal.getName();
      successfulRegistration = registerUsername(username);
    } 

    try {
      chain.doFilter(request, response);
    } finally {
      if (successfulRegistration) {
        MDC.remove(USER_KEY);
      }
    }
  }

  private boolean registerUsername(String username) {
    if (username != null && username.trim().length() > 0) {
      MDC.put(USER_KEY, username);
      return true;
    }
    return false;
  }

然后在您的日志配置中,在您的模式布局中添加 %X{USER_KEY} 以使用您在 MDC 中设置的值。

logback中,有开箱即用的过滤器MDCInsertingServletFilter可以记录更多信息,例如 remoteHost/requestUrl 等,对于记录非常有用的信息。

查看 MDC http://logback.qos.ch/manual/mdc.html上的 logback 文档

于 2015-09-10T09:48:45.583 回答