Spring日志记录+线程池

本文将介绍在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




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值