前提:不修改之前打印日志的代码(否则工作量太大),支持并发
服务改造为多租户服务,日志打印在一起不能区分是哪个租户请求的,在不改造项目之前打印日志代码的前提下,根据请求打印日志到不同文件。
一般项目中日志打印:
private static final Logger LOG = LoggerFactory.getLogger(xxx.class);
LOG.error("error");
LOG.info("info");
logback的主要对象
1.LoggerContext logback的核心对象,加载配置文件,存储loggerList……是log back的核心容器
2.Logger 这个Logger为logback的logger 它实现了slf4j的Logger对象,除了实现了所有的logger方法外,我们动态配置日志输出源,也需要里面的 addAppender(),detachAppender()方法
3.Appender 日志输出的最终实现, doAppend(E e)方法,最终实现了日志的输出,如果自定义appender 需要最终实现该接口。
注:以上对象并非logback全部核心对象,对于今天的日志动态输出,仅仅涉及到以上核心对象。
logback默认加载 classpath:logback.xml 配置,打印日志到文件中原理是通过 Appender 接口来实现不同的打印,logback原理请看:https://blog.csdn.net/qq_26462567/category_9307914.html
打印日志到文件中使用的 RollingFileAppender ,LOG.info("info");跟踪源码可以发现最后会调用当前Logger引用的Appender.doAppend 方法:
AppenderAttachableImpl:
所以我们重写doAppend
public class DynamicRollingFileAppender<E> extends RollingFileAppender<E>{ /** * 日志打印会调用此方法,进行复写,判断租户,根据租户打印到不同日志文件 * @param eventObject */ public void doAppend(E eventObject) { String tenantType = RequestContext.getContext().getTenantType(); if(StringUtils.isBlank(tenantType)){ return; // throw new IllegalArgumentException("当前请求未找到租户类型"); } // this.getName() 是在logback.xml中配置的<appender name="appenderName" class="com.zh.log.core.logback.DynamicRollingFileAppender"> // 只打印当前租户的Append,RollingFileAppender追加器以租户类型标识开头的执行追加 if(this.getName().startsWith(tenantType)){ super.doAppend(eventObject); } } }
RequestContext 是当前线程文本类,存储当前线程中的一些变量以及租户信息,
TenantType是枚举类,定义租户信息,读者自行定义,这里不提供了。
public class RequestContext { private static final String TENANT_TYPE = "TENANT_TYPE"; private Map<String, Object> values = new HashMap<String, Object>(); private static final ThreadLocal<RequestContext> LOCAL = new ThreadLocal<RequestContext>() { @Override protected RequestContext initialValue() { return new RequestContext(); } }; public static RequestContext getContext() { return LOCAL.get(); } public static void clearContext() { LOCAL.remove(); LOCAL.set(new RequestContext()); } public Object get(String key) { return values.get(key); } public void remove(String key) { values.remove(key); } public RequestContext set(String key, Object value) { if (value == null) { values.remove(key); } else { values.put(key, value); } return this; } /** * 设置数据源 * @param value * @return */ public RequestContext setDataSource(String value) { if (value == null) { values.remove(TENANT_TYPE); } else { values.put(TENANT_TYPE, value); } return this; } /** * 设置数据源 * @param value * @return */ public RequestContext setTenantType(String value) { if (value == null) { values.remove(TENANT_TYPE); } else { values.put(TENANT_TYPE, value); } return this; } /** * Get current DataSource * * @return data source name */ public String getTenantType() { return (String) values.get(TENANT_TYPE); } /** * Get current DataSource * * @return data source name */ public String getRedisPre() { if(StringUtils.isBlank(this.getTenantType())){ throw new IllegalArgumentException("当前请求未找到租户类型"); } return TenantType.getRedisPre(this.getTenantType()); } /** * Get current DataSource * * @return data source name */ public String getDataSource() { return (String) values.get(TENANT_TYPE); } /** * Clear current DataSource * * @return data source name */ public void clearDataSource() { remove(TENANT_TYPE); } }
利用 Filter 过滤器在请求进入后,根据请求域名来判断租户(每个租户的域名请求是不同的),并且把租户保存到 RequestContext 中,供当前后续代码获取。
public class SwitchTenantFilter implements Filter { /** 日志 */ private static Logger logger = LoggerFactory.getLogger(SwitchTenantFilter.class); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String host = request.getHeader("host"); // 租户1访问 if(host.contains("test1")){ RequestContext.getContext().setDataSource("test1"); } // 租户2访问 if(host.contains("test2")){ RequestContext.getContext().setDataSource("test2"); } String dataSource = RequestContext.getContext().getDataSource(); if(StringUtils.isBlank(dataSource)){ logger.error("访问host:{},切换数据源失败!!!", host); filterChain.doFilter(request, response); return; } logger.info("访问host:{},切换数据源成功,数据源key:{}", host, RequestContext.getContext().getDataSource()); filterChain.doFilter(request, response); // 清除 ThreadLocal 变量 } @Override public void init(FilterConfig filterConfig) throws ServletException { // TODO Auto-generated method stub } @Override public void destroy() { // TODO Auto-generated method stub } }
在Configuration类中实例化这个Filter Bean,配置拦截
// ===SwitchTenantFilter 切换租户,在WebAccessLogFilter之前加载=== @Bean public FilterRegistrationBean switchDataSourceFilterRegistrationBean() { FilterRegistrationBean registration = new FilterRegistrationBean(); SwitchTenantFilter switchTenantFilter = new SwitchTenantFilter(); registration.setFilter(switchTenantFilter); registration.addUrlPatterns("/*"); registration.setName("switchDataSourceFilter"); registration.setOrder(12); registration.setDispatcherTypes(DispatcherType.REQUEST); return registration; } // ===SwitchTenantFilter===
最后配置logback.xml,将自定义的Appender配置进去,每个租户配置一个Appender,配置打印路径以及文件大小切换策略,把所有Appender在root中引用,打印会一层一层往上调用,最后会调用到root根logger,调用doAppend方法。
logback.xml:
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="true"> <property name="test1PATH" value="./logs/test1/" /> <property name="test2PATH" value="./logs/test2/" /> <property name="APPNAME" value="test" /> <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t${APPNAME}\t%level\t%thread\t%logger{0}\t%logger{5}:%line\t%userName\t%clientIp:%clientPort\t%message%n</pattern> </encoder> </appender> <appender name="test1Appender" class="com.test1.test.core.logback.DynamicRollingFileAppender"> <file>${test1PATH}test1.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${test1PATH}test1-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>10MB</maxFileSize> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t%level\t%thread\t%logger{0}\t%logger{5}:%line\t%userName\t%clientIp:%clientPort\t%message%n</pattern> </encoder> </appender> <appender name="test2Appender" class="com.test1.test.core.logback.DynamicRollingFileAppender"> <file>${test2PATH}test2.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${test2PATH}test2-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>10MB</maxFileSize> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t%level\t%thread\t%logger{0}\t%logger{5}:%line\t%userName\t%clientIp:%clientPort\t%message%n</pattern> </encoder> </appender> <appender name="test1MysqlAppender" class="com.test1.test.core.logback.DynamicRollingFileAppender"> <file>${test1PATH}test1-mysql.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${test1PATH}test1-mysql-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>10MB</maxFileSize> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t%thread\t%userName\t%clientIp\t%message%n</pattern> </encoder> </appender> <appender name="test2MysqlAppender" class="com.test1.test.core.logback.DynamicRollingFileAppender"> <file>${test2PATH}test2-mysql.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${test2PATH}test2-mysql-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>10MB</maxFileSize> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}\t%serialNo\t%thread\t%userName\t%clientIp\t%message%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="test1Appender" /> <appender-ref ref="test2Appender" /> <appender-ref ref="consoleAppender" /> </root> </configuration>
参考: