1. Logback的设计目标之一是审查和调试复杂的分布式应用程序。真实世界的多数分布式系统需要同时处理多个客户端。在一个典型的多线程方式实现的分布式系统里, 不同的线程
处理不同的客户端。区分不同客户端的记录输出的一个可行的但不好的方法是为每个客户端都创建新的、独立的logger。但是这种技术使logger的数量增多且大大增加了管理开销。
2. 一个轻量的技术是为客户端的每个记录请求添加唯一戳(uniquely stamp)。Logback在SLJ4J里使用了这种技术的一种变体: 映射诊断环境(MDC)。
3. 为了给每个请求添加唯一戳, 可以把用户信息放进MDC。MDC类的重要部分如下:
4. MDC类只有静态方法, 开发者可以把信息放进一个诊断环境, 之后用其他logback组件获取这些信息。MDC是基于每个线程进行管理的。子线程自动继承其父的映射诊断环境
的一个副本。典型地, 当开始为新的客户端请求服务时, 开发者会向MDC里插入恰当的环境信息, 比如客户端id、客户端IP地址、请求参数等等。Logback组件会自动在每个记录
条目里包含这些信息。
5. MDC简单使用例子
5.1. 新建一个名为SampleMDC的Java项目, 同时添加相关jar包。
5.2. 新建SampleMDC.java
package com.fj.smdc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class SampleMDC {
private static final Logger logger = LoggerFactory.getLogger(SampleMDC.class);
public static void main(String[] args) {
MDC.put("first", "错误");
MDC.put("last", "警告");
logger.error("MDC简单使用错误信息。");
logger.warn("MDC简单使用警告信息。");
MDC.put("first", "信息");
MDC.put("last", "测试");
logger.info("MDC简单使用信息。");
logger.debug("MDC简单使用测试信息。");
}
}
5.3. 新建logback.xml
5.4. 运行项目
6. 映射诊断环境在客户端-服务器模式下最有奇效。典型情况是, 多个客户端被服务器的多个线程所处理。虽然MDC里的方法都是静态的, 但MDC是基于每个线程进行管理的, 这允许服务器的每个线程都有自己独立的MDC戳。MDC的操作, 比如put()和get(), 只作用于当前线程和当前线程的子线程, 不影响其他线程里的MDC。由于MDC的信息是基于每个线程进行管理的, 因此每个线程都有自己的MDC副本。所以在使用MDC时, 开发者不需要操心线程安全或同步, 因为MDC透明地、安全地处理了这些问题。
7. 客户端-服务器例子
7.1. 新建一个名为ClientServerMDC的Java项目, 同时添加相关jar包。
7.2. 在src目录下创建logback.xml
7.3. 新建一个MyServer.java
package com.fj.csmdc;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class MyServer {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(9999);
System.out.println("server start...");
int tnum = 0;
while(true) {
Socket socket = server.accept();
new Thread(new ServerHandler(socket), "t" + (++tnum)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(server != null) {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
7.4. 新建一个ServerHandler.java
package com.fj.csmdc;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class ServerHandler implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(ServerHandler.class);
private Socket socket;
public ServerHandler( Socket socket) {
this.socket = socket;
}
@Override
public void run() {
if(socket == null) {
logger.error("socket为空。");
return;
}
MDC.put("client", socket.getRemoteSocketAddress().toString());
try {
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
while(socket != null && (!socket.isClosed()) && (line = br.readLine()) != null) {
logger.info(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(socket != null && (!socket.isClosed())) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
7.5. 新建一个MyClient.java
package com.fj.csmdc;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class MyClient {
public static void main(String[] args) {
try {
Socket socket = new Socket("192.168.25.1", 9999);
System.out.println("client start...");
new Thread(new ClienWriter(socket)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ClienWriter implements Runnable{
private Socket socket;
public ClienWriter(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
while(socket != null && (!socket.isClosed())) {
System.out.println("请输入内容(不允许为空), 输入886退出程序: ");
line = br.readLine();
if(line == null || line.length() <= 0) {
continue;
}
if(line.equals("886")) {
break;
}
bw.write(line);
bw.newLine();
bw.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("client close socket writer " + System.currentTimeMillis());
if(socket != null && (!socket.isClosed())) {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
7.6. 运行MyServer.java
7.7. 运行MyClient.java
7.8. 在MyClient运行控制台输入"你好哇", 然后按下回车键
7.9. 查看运行的MyServer控制台
7.10. 再运行一个MyClient.java客户端
7.11. 在第二个MyClient运行控制台输入"很高兴见到你", 然后按下回车键
7.12. 查看运行的MyServer控制台
8. MDCInsertingServletFilter
8.1. 我们已经看到, 在处理多个客户端时, MDC非常有用。在一个管理用户验证的web程序里, 可以在MDC里设置用户名, 然后在用户注销登录时从MDC删除用户名。不幸的是, 上面的方法不是始终可靠的。由于MDC 是基于每个线程对数据进行管理的, 所以服务器循环使用线程时会导致错误地使用MDC里的信息。
8.2. 在处理请求时, 为保证MDC里的信息始终正确, 一个可行的方法是, 在处理流程的开头存储用户名, 然后再这个处理流程的尾部移除用户名。这种情况可以用一个servlet过滤器。
8.3. 在servlet过滤器的doFilter()方法里, 我们可以从请求取得相关的用户数据, 然后存储在MDC。后续的其他过滤器和servlet将自动从先前存储的MDC里受益。最终, 当我们的servlet 过滤器重新得到控制权后, 就有机会清除MDC数据。就是在过滤器链完成后, 该过滤器从MDC移除用户信息。
8.4. 在web程序里, 经常需要取得http请求的主机名、请求uri和user-agent。MDCInsertingServletFilter把这些数据放入MDC, 所用的键如下:
8.5. 新建一个名为FilterMDC的动态Web工程, 同时添加相关jar包。
8.6. 在src目录下创建logback.xml
8.7. 编写index.html
8.8. 编写MyServlet.java
package com.fj.fmdc;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(MyServlet.class);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String userName = req.getParameter("userName");
String password = req.getParameter("password");
if(userName != null && password != null) {
logger.debug("userName = " + userName + ", password = " + password);
resp.getWriter().write("login success.");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
8.9. 配置web.xml
8.10. 运行项目, 使用浏览器访问
8.11. 点击提交, 控制台打印
8.12. 浏览器输出
9. 子线程访问MDC副本
9.1. 在初始(主线程)上调用MDC.getCopyOfContextMap()。当子线程运行时, 它的第一个动作就应该是调用MDC.setContextMapValues(), 为子线程关联初始MDC值的副本。
9.2. 更改我们的SampleMDC.java
package com.fj.smdc;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class SampleMDC {
private static final Logger logger = LoggerFactory.getLogger(SampleMDC.class);
public static void main(String[] args) {
MDC.put("first", "错误");
MDC.put("last", "警告");
logger.error("MDC简单使用错误信息。");
logger.warn("MDC简单使用警告信息。");
MDC.put("first", "信息");
Map<String, String> contextMap = MDC.getCopyOfContextMap();
new Thread(new Runnable() {
@Override
public void run() {
MDC.setContextMap(contextMap);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("MDC简单使用信息。");
logger.debug("MDC简单使用测试信息。");
}
}).start();
MDC.put("last", "测试");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
9.3. 运行程序