本文将介绍在Spring框架下如何利用拦截器做日志记录,简化我们的日志处理,
1 首先我们需要在Spring-mvc.xml中注册这个拦截器,代码如下:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/*/*" /><!-- 需拦截的地址 -->
<mvc:exclude-mapping path="/login/loginIn" /><!-- 登录不拦截 -->
<bean class="com.zsq.cn.common.sys.interceptor.LogInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
2 新建一个日志类 (setter、getter方法就没有拷贝了,但是重写了setParams方法)
public class Log {
private static final long serialVersionUID = 1L;
private String type; // 日志类型(1:操作日志;2:错误日志)
private String title; // 日志标题
private String remoteAddr; // 操作用户的IP地址
private String requestUri; // 操作的URI
private String method; // 操作的方式
private String params; // 操作提交的数据
private String userAgent; // 操作用户代理信息
private String exception; // 异常信息
// 日志类型(1:接入日志;2:错误日志)
public static final String TYPE_ACCESS = "1";
public static final String TYPE_EXCEPTION = "2";
public void setParams(String params) {
this.params = params;
}
/**
* 设置请求参数
* @param paramMap
*/
@SuppressWarnings({ "unchecked", "deprecation" })
public void setParams(Map paramMap){
if(paramMap == null){
return;
}
StringBuilder sb = new StringBuilder();
for(Map.Entry<String, String[]> para : (((Map<String, String[]>) paramMap).entrySet())){
sb.append(para.getKey().endsWith("password") ?
para.getKey()+"="+"...":para.getKey()+"="+Arrays.toString(para.getValue())+"::");
}
setParams(sb.toString());
}
@Override
public String toString(){
return ReflectionToStringBuilder.toString(this);
}
}
3 新建一个拦截器类,即 1中bean的class类
这里需要说明下,这个类的最后一个方法的程序段
String title = (String)request.getAttribute(Constant.LOG_TITLE);
if(title != null){
LogUtils.saveNormalLog(request, handler, ex);
}
因为有很多请求我们是不需要做日志记录的,所以我判断了下 如果有日志信息,则进行日志的持久化操作。这里的Constant.LOG_TITLE一般写在Controller的方法里,我将会在第5个程序段中给出实例。
public class LogInterceptor implements HandlerInterceptor{
private Logger logger = LoggerFactory.getLogger(LogInterceptor.class);
//每次请求都会保留一个HttpServletRequest副本
public static ThreadLocal<HttpServletRequest> curentHttpRequest = new ThreadLocal<>();
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
curentHttpRequest.set(request);//设置本次请求的HttpServletRequest
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// System.out.println(LocalDate.now()+"访问视图"+modelAndView.getViewName());
if(modelAndView != null){
logger.info(LocalDate.now()+"访问视图"+modelAndView.getViewName());
}
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
String title = (String)request.getAttribute(Constant.LOG_TITLE);
if(title != null){
LogUtils.saveNormalLog(request, handler, ex);
}
}
}
4 新建一个日志工具类来处理日志
public class LogUtils {
/**
* 保存日志
* @param request
* @param handler
* @param ex
* @param title
*/
public static void saveLog(HttpServletRequest request, Object handler, Exception ex,String title) {
User user = SessionUtils.getSession(request, SessionNames.SESSION_USER);
if (user != null){
Log log = new Log();
log.setTitle(title);
log.setType(ex == null ? Log.TYPE_ACCESS : Log.TYPE_EXCEPTION);
log.setRemoteAddr(request.getRemoteAddr());
log.setUserAgent(request.getHeader("user-agent"));
log.setRequestUri(request.getRequestURI());
log.setParams(request.getParameterMap());
log.setMethod(request.getMethod());
// 异步保存日志
new Thread(new SaveLogThread(log)).start();
}
}
/**保存异常日志
* @param exceptionMessage
*/
public static void saveExceptionLog(String exceptionMessage,Exception ex){
HttpServletRequest request = LogInterceptor.curentHttpRequest.get();
String title = (String) request.getAttribute(Constant.LOG_TITLE);
saveLog(request,null,ex,title+"--"+exceptionMessage);
}
/**
* 正常保存日志
* @param request
* @param handler
* @param ex
*/
public static void saveNormalLog(HttpServletRequest request, Object handler, Exception ex) {
saveLog(request,handler,ex,(String)request.getAttribute(Constant.LOG_TITLE));
}
/**
* 单独开启一个线程持久化日志
*
*/
static class SaveLogThread implements Runnable{
private Log log;
public SaveLogThread(Log log){
this.log = log;
}
@Override
public void run() {
if(log != null){
//这里进行持久化操作
System.out.println("-----------请进行日志持久化操作,下面将输出日志信息--------------");
System.out.println(log.toString());
}
}
}
}
5 新建一个测试请求类
@Controller
@RequestMapping("test")
public class TestController {
@Autowired
private TestService testService;
@RequestMapping("test")
public String test(HttpServletRequest request,User user,Model model){
//注意只有这里request设置日志信息后,该请求才会持久化操作,因为我在3中的最后一个方法判断了如果没有日志信息则不持久化
request.setAttribute(Constant.LOG_TITLE, "测试");
User user2 = testService.getUser();
model.addAttribute("user", user2);
return "index/test";
}
}
但是需要说明的是,只要异常的地方,并且你想记录异常日志,调用saveExceptionLog(String exceptionMessage,Exception ex)方法即可,即使该请求的Controller没有设置
request.setAttribute(Constant.LOG_TITLE, "测试");异常日志也会持久化。
当然,如果每一次日志记录系统都开启一个线程的话,那将会很耗费系统资源的,因为单个线程的开启和销毁都是需要时间的,在这里我们需要对上面的实现方法进行改进,使用线程去做日志持久化记录。
2.1 首先在spring-context.xml中注册一个线程池的bean
<!-- 使用线程池管理线程 -->
<bean id="taskExecutor"
class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5" /> <!-- 线程池维护线程的最少数量 -->
<property name="keepAliveSeconds" value="300" /> <!-- 线程池维护线程所允许的空闲时间 -->
<property name="maxPoolSize" value="10" /> <!-- 线程池维护线程的最大数量 -->
<property name="queueCapacity" value="25" /> <!-- 线程池所使用的缓冲队列大小,不设置的话 默认为Integer.MAX_VALUE -->
</bean>
说明:你可以看到,注册bean的class 是spring提供的ThreadPoolTaskExecutor,我看了下这个类的源码如下
源码中,默认为某些属性赋了默认值,请注意上图中划红线的一个属性,ThreadPoolExecutor。这个类是jdk提供的java.util.concurrent.ThreadPoolExecutor,其实,spring的ThreadPoolTaskExecutor只是对ThreadPoolExecutor完成了一次封装,其最终的实现仍然是ThreadPoolExecutor这个类。到底ThreadPoolTaskExecutor是怎样封装ThreadPoolExecutor的呢?,我们就拿corePoolSize为例,在xml文件设置ThreadPoolTaskExecutor类的属性 :
<property name="corePoolSize" value="5" /> <!-- 线程池维护线程的最少数量 -->
在spring注册这个bean的时候,将会执行下面代码。这段代码也是ThreadPoolTaskExecutor的源码,ThreadPoolTaskExecutor中的每个属性都有这样的一段设置属性值得代码
public void setCorePoolSize(int corePoolSize) {
synchronized (this.poolSizeMonitor) {
this.corePoolSize = corePoolSize;
if (this.threadPoolExecutor != null) {
this.threadPoolExecutor.setCorePoolSize(corePoolSize);
}
}
}
2.3 新建一个上下文工具,用于运行时获取spring注册的bean,这个类需要在spring-context.xml文件中配置如下:
<bean id="ApplicationContextUtils" class="com.zsq.cn.common.sys.utils.ApplicationContextUtils" />
public class ApplicationContextUtils implements ApplicationContextAware,DisposableBean{
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void destroy() throws Exception {
this.applicationContext = null;
}
/**
* 根据类获取bean
* @param t
* @return
*/
public static <T> T getBean(Class<T> t){
if(null != applicationContext){
T object = null;
object = applicationContext.getBean(t);
return object;
}
return null;
}
/**
* 根据bean的名字获取bean
* @param beanName
* @return
*/
public static Object getBean(String beanName){
if(null != applicationContext){
return applicationContext.getBean(beanName);
}
return null;
}
}
2.2 新建一个线程池工具类,所有实现Runable接口的线程都可以用这个线程池
public class ThreadPoolUtils {
/**
* 获取线程池对象,其已经在 spring-context.xml中注册。
*/
private static ThreadPoolTaskExecutor tpte =
(ThreadPoolTaskExecutor) ApplicationContextUtils.getBean(ThreadPoolTaskExecutor.class);
public static void execute(Runnable task){
tpte.execute(task);
}
}
2.3 将 代码段 4 中的
//异步报错日志
new Thread(new SaveLog(log)).start()
这行代码改为
ThreadPoolUtils.execute(new LogTask(log));
这样,所有的日志服务都可以交给线程池去处理了。关于线程池这篇文章没有太深入研究原理,还有一些特性并没有提到。比如。任务缓存队列及排队策略、任务拒绝策略、线程池的关闭、线程池容量的动态调整。感兴趣的朋友可以参考下这篇博文,很详细地阐述了线程池的原理。
http://www.importnew.com/19011.html