写在前面
项目使用SpringMVC+MyBatis开发,Spring版本为:4.1.9.RELEASE。此项目为历史项目,现有需求为,增加记录系统管理员操作日志,现有实现的方式有很多种,比如AOP等等,本文使用拦截器+自定义注解实现。
拦截器介绍
Spring 的 Interceptor (拦截器)是通过 HandlerInterceptor 接口实现的。
4.1.9.RELEASE版本HandlerInterceptor文档地址https://docs.spring.io/spring-framework/docs/4.1.9.RELEASE/javadoc-api/org/springframework/web/servlet/HandlerInterceptor.html最新版本HandlerInterceptor文档地址https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/HandlerInterceptor.htmlSpring 的 handle mapping(处理器映射)机制包含handler interceptors(处理拦截器),当你需要需要对某一请求应用特定的功能时,拦截器会变得很有用。
自定义处理映射拦截器必须实现 org.springframework.web.servlet.HandlerInterceptor 接口。这个定义了三个方法:
/**
* 整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
*/
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object obj, Exception ex)
throws Exception {
}
/**
* 后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
*/
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse res, Object obj, ModelAndView mav)
throws Exception {
}
/**
* 预处理回调方法,实现处理器的预处理(如检查登陆),第三个参数为响应的处理器,自定义Controller
* 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
*/
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
}
根据本次需求,在自定义拦截器中preHandle(...)方法中处理即可。
如果项目中存在多个拦截器,那么Spring会根据配置的顺序,当preHandle(...)返回值为true时,自上而下顺序执行完所有拦截器。
自定义注解
根据官方文档,通俗的解释如下:
- 注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举类似。
- 注解用来修饰,类、方法、变量、参数、包。
- 注解不会对所修饰的代码产生直接的影响。
注解其实就是一种标记,可以在程序代码中的关键节点(类、方法、变量、参数、包)上打上这些标记,然后程序在编译时或运行时可以检测到这些标记从而执行一些特殊操作。因此可以得出自定义注解使用的基本流程:
定义注解——相当于定义标记;
配置注解——把标记打在需要用到的程序代码中;
解析注解——在编译期或运行时检测到标记,并进行特殊操作。
基本语法如下:
注解在Java中,与类、接口、枚举类似,因此其声明语法基本一致,只是所使用的关键字有所不同@interface。在底层实现上,所有定义的注解都会自动继承java.lang.annotation.Annotation接口。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemUserLog {
String value();
String remark() default "";
}
定义注解类型元素时需要注意如下几点:
- 访问修饰符必须为public,不写默认为public;
- 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组;
- 该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(后面使用会带来便利操作);
- ()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法;
- default代表默认值,值必须和第2点定义的类型一致;
- 如果没有默认值,代表后续使用注解时必须给该类型元素赋值。
@Target注解,是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的。源码枚举值如下:
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
}
@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。
注解的生命周期有三个阶段:1、Java源文件阶段;2、编译到class文件阶段;3、运行期阶段。同样使用了RetentionPolicy枚举类型定义了三个阶段:
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
- 如果一个注解被定义为RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读Java文件的人看到;
- 如果一个注解被定义为RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器可以在编译时根据注解做一些处理动作,但是运行时JVM(Java虚拟机)会忽略它,我们在运行期也不能读取到;
- 如果一个注解被定义为RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段,我们可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME;
- 在默认的情况下,自定义注解是使用的RetentionPolicy.CLASS。
@Documented注解,是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。
@Inherited注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。
在本次使用中,用到了一下三种注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
需求具体实现
1.首先自定义注解@SystemUserLog
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemUserLog {
String value();
String remark() default "";
}
value:记录当前接口功能
remark:记录其他杂项信息
2.实现自定义拦截器SystemUserOperateInterceptor,实现自HandlerInterceptor
import java.lang.reflect.Method;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.common.annotation.SystemUserLog;
import com.common.log.SystemUserOperateLog;
import com.entity.SystemUser;
public class SystemUserOperateInterceptor implements HandlerInterceptor {
//用户操作日志logger
private Logger systemUserLogger = SystemUserOperateLog.getLogger();
/**
* 整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
*/
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object obj, Exception ex)
throws Exception {
}
/**
* 后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
*/
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse res, Object obj, ModelAndView mav)
throws Exception {
}
/**
* 预处理回调方法,实现处理器的预处理(如检查登陆),第三个参数为响应的处理器,自定义Controller
* 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
*/
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
//判断对应method是否添加注解 @SystemUserLog
if(handler instanceof HandlerMethod){
HandlerMethod hm = (HandlerMethod) handler;
Method method = hm.getMethod();
boolean present = method.isAnnotationPresent(SystemUserLog.class);
if(present){
this.logSystemUserOperate(hm, req);
}
}
return true;
}
/**
* 记录用户操作日志
* @date 2021年12月28日下午3:41:34
* @param hm
* @param req
*/
private void logSystemUserOperate(HandlerMethod hm, HttpServletRequest req){
try {
Method method = hm.getMethod();
String methodName = method.getName();
String mappingValue = "";
String systemUserInfo = this.parseSystemUserInfo(req);
String remark = "";
String operateDes = "";
String param = "";
SystemUserLog userLog = method.getAnnotation(SystemUserLog.class);
if(userLog.value() != null){
operateDes = userLog.value();
}
if(userLog.remark() != null){
remark = userLog.remark();
}
Map<String, String[]> parameterMap = req.getParameterMap();
if(parameterMap != null){
JSONArray _paramArr = new JSONArray();
for (String key : parameterMap.keySet()) {
String _val = "";
String[] strings = parameterMap.get(key);
if(strings != null && strings.length > 0){
_val = StringUtils.join(strings, ",");
}
JSONObject p_json = new JSONObject();
p_json.put(key, _val);
_paramArr.put(p_json);
}
param = _paramArr.toString();
}
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String[] mappingValueArr = requestMapping.value();
if(mappingValueArr != null && mappingValueArr.length > 0){
mappingValue = StringUtils.join(mappingValueArr, ",");
}
String logFormat = "%s -> {methodName:%s, api:%s} -> des:%s, remarks:%s, Param -> %s";
String log = String.format(logFormat, systemUserInfo, methodName, mappingValue, operateDes, remark, param);
systemUserLogger.info(log);
} catch (Exception e) {}
}
/**
* 处理请求用户信息
* @date 2021年12月28日下午3:47:08
* @param req
* @return
*/
private String parseSystemUserInfo(HttpServletRequest req){
String info = "";
SystemUser systemUser = this.getSystemUser(req);
if(systemUser != null){
String name = systemUser.getReallyName();
if(name == null){
name = "";
}
Integer id = systemUser.getId();
info = String.format("{name:%s, id:%d}", name, id);
}
return info;
}
/**
* 通过cookie获取token,再根据token获取用户
* @date 2021年12月28日下午3:38:35
* @param req
* @return
*/
private SystemUser getSystemUser(HttpServletRequest req){
Object systemUserObj = req.getAttribute("systemUser");
if(systemUserObj instanceof SystemUser){
SystemUser systemUser = (SystemUser) systemUserObj;
return systemUser;
}
return null;
}
}
日志记录格式大致为:{登陆SystemUser信息}->{请求controller中方法名字,方法RequestMapping中映射字符串}->自定义注解value值+remark值 ->请求参数
拦截器中获取登陆SystemUser信息,使用了 req.getAttribute("systemUser"),是因为在FIlter中,对SystemUser的请求,做了过滤处理,并放入到request Attribute中。
同事需要在Spring的xml配置文件中,对当前自定义注解声明
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com..........interceptor.SystemUserOperateInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
截止目前,已经完成了自定义注解和自定义拦截器的处理,并且在拦截器中处理了日志的生成。
使用方法
在对应需要记录的SystemUser操作的请求接口中,添加@SystemUserLog注解,并对value和remark记性赋值即可。
@SystemUserLog(value="系统管理员测试", remark="方法无参,errorCode = 0 标识返回成功")
@RequestMapping(value="/systemUser/test")
public void testAnnotation(HttpServletRequest request,HttpServletResponse response,@RequestParam Map<String,String> map){
Integer errorCode = 2;
try{
//模拟程序处理
Thread.sleep(500L);
errorCode = 0;
JSONObject baseInfo = ResponseHelper.getBaseInfo(errorCode);
ResponseHelper.doReturn(baseInfo, response);
}catch(Exception e){
e.printStackTrace();
JSONObject baseInfo = ResponseHelper.getBaseInfo(2);
ResponseHelper.doReturn(baseInfo, response);
}
}
浏览器打开访问:http://127.0.0.1:8989/systemUser/test.do
即可在日志中记录到
2021-12-29 17:50:02,804 INFO [com........SystemUserOperateLog] - {name:admin, id:4} -> {methodName:testAnnotation, api:/systemUser/test} -> des:系统管理员测试, remarks:方法无参,errorCode = 0 标识返回成功, Param -> []
至此该需求已经实现。