#SpringAOP 记录操作日志
AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。
今天我们以主要讲解通过SpringAOP记录操作日志到数据库。
##实现思路
). 客户端发起请求 , 访问Controller中的insert方法
2). 在进入insert方法之前 , 被所定义的切面拦截 , 切入点表达式定义拦截Controller层中的方法
3). 由于需要记录controller中方法调用时请求参数名/值 , 以及返回值 , 操作人 等信息 , 在定义的通知中使用环绕通知类型进行日志记录;
4). 在定义的通知中获取请求参数信息 , 请求模块 , 请求的Controller类 , 方法等信息 ;
5). 放行原始方法的执行 , 并记录返回值 , 及其类型等信息 , 并将日志插入数据库
##所需依赖
创建一个工程, pom.xml 中添加如下依赖:
<dependencies>
<!-- 引入Spring的依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.2.4.RELEASE</version>
</dependency>
<!-- 引入Log4j的依赖 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- 引入servlet的依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjlib</artifactId>
<version>1.6.2</version>
</dependency>
<!-- 时间处理的工具 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.5</version>
</dependency>
<!-- json转换工具fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.31</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 配置tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port>
<path>/</path>
</configuration>
</plugin>
<!-- 配置编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<target>1.7</target>
<source>1.7</source>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
##定义日志信息对象
该对象主要封装的就是所要记录的日志信息
public class OperateLogMessage {
//操作的模块名
private String modelName;
//操作的模块值
private String modelValue;
//返回值
private String returnValue;
//返回值类型
private String returnClass;
//操作人
private String operateUser;
//操作时间
private String operateTime;
//参数值 , 键值对形式
private String paramAndValue;
//操作的类
private String operateClass;
//操作的方法
private String operateMethod;
public String getModelName() {
return modelName;
}
public void setModelName(String modelName) {
this.modelName = modelName;
}
public String getModelValue() {
return modelValue;
}
public void setModelValue(String modelValue) {
this.modelValue = modelValue;
}
public String getReturnValue() {
return returnValue;
}
public void setReturnValue(String returnValue) {
this.returnValue = returnValue;
}
public String getReturnClass() {
return returnClass;
}
public void setReturnClass(String returnClass) {
this.returnClass = returnClass;
}
public String getOperateUser() {
return operateUser;
}
public void setOperateUser(String operateUser) {
this.operateUser = operateUser;
}
public String getOperateTime() {
return operateTime;
}
public void setOperateTime(String operateTime) {
this.operateTime = operateTime;
}
public String getParamAndValue() {
return paramAndValue;
}
public void setParamAndValue(String paramAndValue) {
this.paramAndValue = paramAndValue;
}
public String getOperateClass() {
return operateClass;
}
public void setOperateClass(String operateClass) {
this.operateClass = operateClass;
}
public String getOperateMethod() {
return operateMethod;
}
public void setOperateMethod(String operateMethod) {
this.operateMethod = operateMethod;
}
}
##自定义注解
该注解主要用来起到标示以及记录方法形参的作用
@Inherited
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperateLog {
//模块名
String modelName();
//模块的值
String modelValue();
//请求参数
String[] params();
}
##定义业务处理类Controller
UserController是具体业务处理的类 , 将刚才声明的自定义注解作用在Controller的方法上, 用来自定义一些信息;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//在Controller的方法上声明 @OperateLog注解, 并通过注解的属性 , 记录操作的模块名, 模块值, 参数
@RequestMapping(value="insert")
@OperateLog(modelName="user" , modelValue="insert" , params={"name","age","gender"})
public String insert(String name , Integer age , String gender){
userService.insertUser(name , age , gender);
return "success";
}
@RequestMapping(value="update")
public String update(String name , Integer age , String gender){
userService.insertUser(name , age , gender);
return "success";
}
}
##定义通知
定义通知类 operateAdvice , 该通知类中定义了两个环绕通知 , 环绕通知所配置的切入点配置为拦截所有的 cn.itcast.controller 下的所有的类的所有方法 ;
@Component
@Aspect
public class OperateAdvice {
private static Logger log = Logger.getLogger(OperateAdvice.class);
/**
* 第一种方案 : 通过 execution 配置 @annotation 来实现注解的拦截
*
* @param pjp
* @param operateLog
* @return
* @throws Throwable
*/
@Around("execution(* cn.itcast.controller.*.*(..)) && @annotation(operateLog)")
public Object insertLogAround(ProceedingJoinPoint pjp , OperateLog operateLog) throws Throwable{
System.out.println(" *********************************** 方案一 [start] ****************************** ");
//定义日志记录对象
OperateLogMessage op = new OperateLogMessage();
//从注解中获取模块名 , 模块值 , 参数
String modelName = operateLog.modelName();
String modelValue = operateLog.modelValue();
String[] params = operateLog.params();
Map<String,String> paramMap = new HashMap<String, String>();
//获取request对象, 并通过request对象获取参数信息
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
if(params!=null){
for (String param : params) {
String paramValue = request.getParameter(param);
paramMap.put(param, paramValue);
}
}
//将获取的数据, 封装到模型对象中
op.setModelName(modelName);
op.setModelValue(modelValue);
op.setOperateTime(new DateTime().toString("yyyy-MM-dd HH:mm:ss"));
op.setOperateUser("10001");
//记录操作的方法名及所在的类
op.setOperateClass(pjp.getTarget().getClass().getName());
op.setOperateMethod(pjp.getSignature().getName());
//记录参数信息 , json格式的字符串
op.setParamAndValue(JsonUtils.obj2JsonString(paramMap));
//放行
Object object = pjp.proceed();
//记录返回值的类型 , 及返回值
op.setReturnClass(object.getClass().getName());
op.setReturnValue(object.toString());
//输出日志到日志文件 , 当然也可以在此处操作数据库, 将数据记录在数据库
log.info(JsonUtils.obj2JsonString(op));
System.out.println(" *********************************** 方案一 [end] ****************************** ");
//返回结果
return object;
}
/**
* 第二种方案 : 直接通过 切入点表达式进行拦截, 然后在内部进行判定.
*
* @param pjp
* @return
* @throws Throwable
*/
@Around("execution(* cn.itcast.controller.*.*(..))")
public Object insertLogAround2(ProceedingJoinPoint pjp) throws Throwable{
System.out.println(" *********************************** 方案二 [start] ****************************** ");
//定义日志记录对象
OperateLogMessage op = new OperateLogMessage();
Map<String,String> paramMap = new HashMap<String, String>();
//获取request对象, 并通过request对象获取参数信息
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
//获取所拦截的方法上的注解 @OperateLog
OperateLog opLog = getMethodOperateLog(pjp);
//如果获取到的注解不为null , 则代表该方法需要记录日志信息
if(opLog != null) {
op.setModelName(opLog.modelName());
op.setModelValue(opLog.modelValue());
//获取请求参数 , 并封装到Map中
String[] params = opLog.params();
if(params!=null){
for (String param : params) {
String paramValue = request.getParameter(param);
paramMap.put(param, paramValue);
}
}
//记录操作时间
op.setOperateTime(new DateTime().toString("yyyy-MM-dd HH:mm:ss"));
op.setOperateUser("10001"); // 操作人这里只是写死的,实际是要从session中获取用户
//记录操作的类, 已经操作的方法名
op.setOperateClass(pjp.getTarget().getClass().getName());
op.setOperateMethod(pjp.getSignature().getName());
//记录请求的参数
op.setParamAndValue(JsonUtils.obj2JsonString(paramMap));
//放行
Object object = pjp.proceed();
//记录返回值的类型 , 返回值
op.setReturnClass(object.getClass().getName());
op.setReturnValue(object.toString());
//输出日志到日志文件 , 当然也可以在此处操作数据库, 将数据记录在数据库
log.info(JsonUtils.obj2JsonString(op));
System.out.println(" *********************************** 方案二 [end] ****************************** ");
return object;
//如果没有获取到@OperateLog注解 , 代表不需要记录日志, 直接放行
}else{
Object object = pjp.proceed();
return object;
}
}
/**
* 获取拦截的方法上的 @OperateLog 注解
*
*/
private OperateLog getMethodOperateLog(JoinPoint joinPoint) throws Exception {
// 获取操作的类, 及方法名
String targetName = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
//获取请求参数
Object[] arguments = joinPoint.getArgs();
Class targetClass = Class.forName(targetName);
//获取拦截的类上的所有方法
Method[] methods = targetClass.getMethods();
OperateLog operateLog = null;
//获取方法上的注解
for(Method method : methods) {
if(method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if(clazzs.length == arguments.length) {
operateLog = method.getAnnotation(OperateLog.class);
break;
}
}
}
return operateLog;
}
}
方式一是通过切入点表达式中的@annotation(operateLog) 标识 , 获取到所拦截的@OperateLog注解 , 从而获取到注解中的属性值, 再通过request获取到请求的参数 , 方法执行的返回值, 以及返回值的类型 ;
方式二是通过反射获取到作用的有 @OperateLog注解的方法 , 若标识的有@OperateLog注解, 说明该方法的执行需要日志跟踪 。 从而获取到注解中的属性值, 再通过request获取到请求的参数 , 方法执行的返回值, 以及返回值的类型 ;
当然 , 这个入门程序中是直接将所有拿到的跟踪的日志信息 , 输出在了控制台 , 或日志文件中; 如果有需要, 也完全可以将日志信息记录在数据库表结构中 ;
##Spring的配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/s ... ing-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.2.xsd ">
<context:component-scan base-package="cn.itcast">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
##SpringMVC的配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/s ... ing-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
<!-- 配置报扫描 -->
<context:component-scan base-package="cn.itcast.controller"/>
<!-- 开启aop的自动代理 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- mvc注解驱动 -->
<mvc:annotation-driven/>
<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
##web.xml的配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>spring-aop-log</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<!-- 该监听器用来加载spring的配置文件, 初始化spring容器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- 该监听器用来将request对象放置在当前线程ThreadLocal中 , 便于在程序中直接获取request -->
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<!-- springmvc的前端控制器 -->
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
</web-app>
##通过Tomcat插件启动
执行tomcat7:run指令, 并访问controller
##测试结果
*********************************** 方案一 [start] ******************************
*********************************** 方案二 [start] ******************************
2018-03-31 21:00:04,292 [http-bio-8080-exec-1] ERROR [cn.itcast.aop.OperateAdvice] - {"modelName":"user","modelValue":"insert","operateClass":"cn.itcast.controller.UserController","operateMethod":"insert","operateTime":"2018-03-31 21:00:04","operateUser":"10001","paramAndValue":"{\"age\":\"20\",\"name\":\"zhangsan\",\"gender\":\"1\"}","returnClass":"java.lang.String","returnValue":"success"}
*********************************** 方案二 [end] ******************************
2018-03-31 21:00:04,293 [http-bio-8080-exec-1] ERROR [cn.itcast.aop.OperateAdvice] - {"modelName":"user","modelValue":"insert","operateClass":"cn.itcast.controller.UserController","operateMethod":"insert","operateTime":"2018-03-31 21:00:04","operateUser":"10001","paramAndValue":"{\"age\":\"20\",\"name\":\"zhangsan\",\"gender\":\"1\"}","returnClass":"java.lang.String","returnValue":"success"}
*********************************** 方案一 [end] ******************************
输出的日志信息中, 可以详细的看到当前操作的模块, 操作人, 操作时间 , 传递参数名, 传递参数值, 返回值, 返回值类型 等等信息 ;
这些日志记录下来 , 在后期就可以进行有效的日志跟踪 , 系统监控 , 当然这只是AOP其中的一个常用的用法而已 , AOP还用一些其他的用途 , 等待着大家继续探究。
|
|