需求:记录用户的某些重要的具体方法动作
实现:<采用spring的AOP切面思想,对需要监控记录的方法动作设置切点(自定义注解的方式),同时利用java的反射原理实现动态修改方法上的注解值>
一 AOP的基本概念
(1)Aspect(切面):通常是一个类,里面可以定义切入点和通知
(2)JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用
(3)Advice(通知):AOP在特定的切入点上执行的增强处理,有before,after,afterReturning,afterThrowing,around
(4)Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
(5)AOP代理:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类
概要:
框架:Spring + SpringMVC + Mybatis
pointcut切点:用的是Annotation自定义注解的方式,因为不需全部记录操作,只要某些关键的操作,并能够详细描述这个操作的内容,比如机构aa新增会员aaa,机构aa修改会员ccc的资料
需要的依赖(第三个):
org.aspectj
aspectjrt
1.8.9
org.aspectj
aspectjtools
1.8.9
org.aspectj
aspectjweaver
1.7.4
1、数据库设计一张专门记录操作日志的表
2、Annotation自定义注解类
package com.xxx.controller;
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;
/**
* 自定义注解 通过在controller类的方法上添加注解@AdminControllerLog表明哪些方法需要切面
* @author Uno
*@Documented:指明该注解可以用于生成doc
*@Inherited:该注解可以被自动继承
*@Retention:指明在什么级别显示该注解:
* RetentionPolicy.SOURCE 注解存在于源代码中,编译时会被抛弃
RetentionPolicy.CLASS 注解会被编译到class文件中,但是JVM会忽略
RetentionPolicy.RUNTIME JVM会读取注解,同时会保存到class文件中
@Target:指明该注解可以注解的程序范围
ElementType.TYPE 用于类,接口,枚举但不能是注解
ElementType.FIELD 作用于字段,包含枚举值
ElementType.METHOD 作用于方法,不包含构造方法
ElementType.PARAMETER 作用于方法的参数
ElementType.CONSTRUCTOR 作用于构造方法
ElementType.LOCAL_VERIABLE 作用于本地变量或者catch语句
ElementType.ANNOTATION_TYPE 作用于注解
ElementType.PACKAGE 作用于包
*/
@Target(ElementType.METHOD)//此注解只能作用于方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AdminControllerLog {
// 注解格式 例子@AdminControllerLog(description="修改会员资料")
String description() default ""; //默认值为空
}
3、切面类
package com.xxx.controller;
import java.lang.reflect.Method;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.xxx.b2b.model.organ.Organ;
import com.xxx.b2b.service.operateLog.LogService;
import com.xxx.b2b.utils.JsonUtils;
/**
* 切面类
*/
@Aspect // 标记为切面
@Component // spring扫描bean
public class OprateLogAspect {
// 注入Service用于把操作日志保存数据库
@Resource
private LogService logService;
// 常量
private static String ORGAN_IN_SESSION = "OrganInfo";
// Controller层切点
// 平常这里多用的是表达式 类似@Pointcut("execution(** com.xxx.controller.Performance.perform(..))")
@Pointcut("@annotation(com.qly.b2b.controller.AdminControllerLog)")
public void controllerAspect() {
}
/**
* 在目标方法正常完成后拦截记录Controller层用户的操作
*
* @param joinPoint
* 切点
*/
@AfterReturning("controllerAspect()")
public void doBefore(JoinPoint joinPoint) throws Exception {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
HttpSession session = request.getSession();
// 读取session中的用户
Organ organ = (Organ) session.getAttribute(ORGAN_IN_SESSION);
// 请求的IP
String ip = request.getRemoteAddr();
// 获取用户请求方法的参数并序列化为JSON格式字符串 用来记录入参 这里暂时用不到
String params = "";
if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0) {
for (int i = 0; i < joinPoint.getArgs().length; i++) {
params += (joinPoint.getArgs()[i]) + ";";
}
}
// 用户操作日志类
AdminLog adminLog = new AdminLog();
adminLog.setOrganId(organ.getOrganId());
// 方法描述
adminLog.setOperate(getControllerMethodDescription(joinPoint));
// 持久化到数据库
logService.insert(adminLog);
}
/**
* 获取注解中对方法的description描述信息 类似@AdminControllerLog (description="查询用户")
* 但这种description是写死的,后面我们需要利用反射实现动态修改
*/
public static String getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
// 类名
String targetName = joinPoint.getTarget().getClass().getName();
// 方法名
String methodName = joinPoint.getSignature().getName();
// 参数
Object[] arguments = joinPoint.getArgs();
// 切点类
Class targetClass = Class.forName(targetName);
// ps:getsDeclaredMethod会返回类所有声明的字段,包括private、protected、public,但是不包括 父类的
// getMethods():则会返回包括父类的所有的public方法,和getFields一样
Method[] methods = targetClass.getMethods();
String description = "";
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
// 获取description
description = method.getAnnotation(AdminControllerLog.class).description();
break;
}
}
}
return description;
}
}
4、Controller类
@Controller
@RequestMapping("/organ")
public class OrganController extends BaseController {
@Resource
private UserService userService;
@RequestMapping(value = "/updateUserInfo")
@AdminControllerLog(description="修改会员资料") // 切点标记 不作反射修改,就默认这个description值
public String updateUserInfo(@ModelAttribute("beanForm") User user, HttpServletRequest request, ModelMap modelMap)
throws Exception {
User u = new User();
u.setUserId(user.getUserId());
u.setUserName(user.getUserName());
u.setUserPwd(user.getUserPwd());
u.setWater(user.getWater());
u.setRemark(user.getRemark());
userService.updateUser(u);
try {
// 获取当前线程的方法名
String name = Thread.currentThread().getStackTrace()[1].getMethodName();
// getMethod里面的参数对应updateUserInfo方法的参数,固定形式的,不可少
Method method = OrganController.class.getMethod(name, User.class, HttpServletRequest.class,ModelMap.class);
// 描述信息
String altered = "修改会员" + user.getUserId() + "资料";
// 调用修改方法
alterAnnotationOn(method, altered);
} catch (Exception e) {
e.printStackTrace();
}
// 执行成功跳转
modelMap.put("returnMeg", "update");
return this.userManage("hy",new UserQuery(), modelMap, request);
}
/**
* 修改注解description
*
* @param method
* @param altered
* @throws Exception
*/
public static void alterAnnotationOn(Method method, String altered) throws Exception {
method.setAccessible(true);
System.out.println(method.getName());
boolean methodHasAnno = method.isAnnotationPresent(AdminControllerLog.class); // 是否指定类型的注释存在于此元素上
if (methodHasAnno) {
// 得到注解
AdminControllerLog methodAnno = method.getAnnotation(AdminControllerLog.class);
// 修改
InvocationHandler h = Proxy.getInvocationHandler(methodAnno);
// annotation注解的membervalues
Field hField = h.getClass().getDeclaredField("memberValues");
// 因为这个字段是 private final 修饰,所以要打开权限
hField.setAccessible(true);
// 获取 memberValues
Map memberValues = (Map) hField.get(h);
// 修改 description属性值
memberValues.put("description", altered);
}
}
5、在springmvc配置文件中加入切面声明
若有更好的解决方案,欢迎讨论!谢谢