什么是AOP
AOP
(Aspect Oriented Programming,面向切面编程),可以在运行时动态地将代码切入到类中指定方法、指定位置上的一种技术。说白了,就是把 横切逻辑 从 业务逻辑 中抽离出来。哪些属于 横切逻辑 呢?比如,性能监控、日志、事务管理、权限控制等等这些在调用业务的之前或者之后需要额外处理的事物。
简单实现一个AOP审计日志功能
在Spring Boot项目中创建一个注解,方法类上加上了这个注解才会进这个切面
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.ls.im.core.common.log.EventOperateType;
/**
*
* @ClassName: AuditLog
* @Description: 自定义审计日志注解
* @author victor
* @date 2018年11月14日
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuditLog {
String note() default "";
EventOperateType operate() default EventOperateType.QUERY;
}
EventOperateType是一个枚举类
/**
*
* @ClassName: EventOperateType
* @Description: 事件操作类型
* @author victor
* @date 2018年11月14日
*
*/
public enum EventOperateType {
ADD("add"),
UPDATE("update"),
DELETE("delete"),
QUERY("query"),
DOWNLOAD("download");
private String name;
private EventOperateType(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
创建一个Aspect 切面定义类
/**
*
* @ClassName: AuditLogAspect
* @Description: 审计日志AOP切面定义
* @author victor
* @date 2018年11月14日
*
*/
@Aspect
@Component
public class AuditLogAspect {
@Autowired
private IAuditLogFeignClient auditLogFeignClient;
final static Logger logger = LoggerFactory.getLogger(AuditLogAspect.class);
ThreadLocal<Long> beginTime = new ThreadLocal<>();
/**
*
* @Title: aopPointCut
* @Description: 设置了注解@AuditLog才进入切点,接收AuditLog参数
* @param @param auditLog 参数
* @return void 返回类型
* @throws
*/
@Pointcut("@annotation(auditLog)")
public void aopPointCut(AuditLog auditLog) {
}
/**
*
* @Title: doBefore
* @Description: 进入切面前执行
* @param @param joinPoint
* @param @param auditLog 参数
* @return void 返回类型
* @throws
*/
@Before("aopPointCut(auditLog)")
public void doBefore(JoinPoint joinPoint, AuditLog auditLog) {
logger.info("@Before note={}", auditLog.note());
//记录开始时间
beginTime.set(System.currentTimeMillis());
}
/**
*
* @Title: doAfter
* @Description: 方法执行后执行
* @param @param auditLog 参数
* @return void 返回类型
* @throws
*/
@After("aopPointCut(auditLog)")
public void doAfter(AuditLog auditLog) {
logger.info("@After note={}", auditLog.note());
}
/**
*
* @Title: doAfterReturn
* @Description: 方法正常返回结果后执行
* @param @param joinPoint
* @param @param auditLog
* @param @param ret 参数
* @return void 返回类型
* @throws
*/
@AfterReturning(pointcut = "aopPointCut(auditLog)",returning = "ret")
public void doAfterReturn(JoinPoint joinPoint,AuditLog auditLog,Object ret) {
//执行的方法名称
logger.info("@AfterReturning");
logger.info("note={}", auditLog.note());
AuditLogVo auditLogVo = new AuditLogVo();
auditLogVo.setNote(auditLog.note());
auditLogVo.setOperate(auditLog.operate().toString());
// 记录请求到达时间
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// url
auditLogVo.setUrl(request.getRequestURI());
logger.info("url={}", auditLogVo.getUrl());
//prefix
String prefix = request.getHeader("x-forwarded-prefix");
prefix = prefix.replace("/", "");
auditLogVo.setPrefix(prefix);
logger.info("prefix={}", auditLogVo.getPrefix());
// ip
auditLogVo.setIp(getIp(request));
logger.info("ip={}", auditLogVo.getIp());
// 参数
Enumeration<String> paramter = request.getParameterNames();
Map<String, String> map = new HashMap<String, String>();
while (paramter.hasMoreElements()) {
String str = (String) paramter.nextElement();
map.put(str, request.getParameter(str));
}
auditLogVo.setParams(map.toString());
logger.info("params={}", map.toString());
//执行开始时间
auditLogVo.setBeginTime(new Date(beginTime.get()));
logger.info("执行开始时间={}",auditLogVo.getBeginTime());
//执行结束时间
long endTime = System.currentTimeMillis();
auditLogVo.setEndTime(new Date(endTime));
logger.info("执行结束时间={}",auditLogVo.getEndTime());
//执行花费时间,单位ms
auditLogVo.setExecutionTime(endTime - beginTime.get());;
logger.info("执行时间={}", auditLogVo.getExecutionTime());
//插入数据库操作
logger.info("审计日志插入数据库操作={}",result.isSuccessful());
}
/**
*
* @Title: doAfterThrowing
* @Description: 方法执行抛出异常执行
* @param @param ex
* @param @param auditLog 参数
* @return void 返回类型
* @throws
*/
@AfterThrowing(pointcut = "aopPointCut(auditLog)",throwing = "ex")
public void doAfterThrowing(JoinPoint joinPoint,AuditLog auditLog,Throwable ex) {
logger.info("@AfterThrowing");
logger.info("note={}", auditLog.note());
logger.info("operate={}", auditLog.operate());
String retValue = JSONArray.fromObject(ex).toString();
logger.info("目标方法中抛出了异常Throwable={}", retValue);
//插入数据库操作
}
/**
*
* @Title: getIp
* @Description: 通过request获取到反向代理前真实的ip
* @param @param request
* @param @return 参数
* @return String 返回类型
* @throws
*/
public String getIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = ip.indexOf(",");
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
}
ip = request.getHeader("X-Real-IP");
if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
return ip;
}
return request.getRemoteAddr();
}
}
这样定义完了以后就可以扫描到加上了注解@DauditLog的方法如:
@RequestMapping("/query95598wkst")
@AuditLog(note="95598申诉关联工单查询",operate=EventOperateType.QUERY)
public WrappedResult query95598wkst(@QueryRequestParam("params")RequestCondition queryCondition){
return null;
}
这样子设置后访问到这个Controller的时候就会被切刀我们定义的Aspect内容
内容解释
创建注解跟枚举类就不说明了。说明下Aspect类的一些内容
@aspect为类上面的注解——切面
@pointcut(…)——切入点。为此类内一个空方法上面的注解。可以把拦截的地址表达式表示为方法签名,利于使用起来方便。相当于声明了一个切入点表达式。在通知可以用这个方法来引用切入点。
@before@after@Around@AfterRunning@AfterThrowing——通知。具体在某些时刻去执行某些操作可以在这些注解的方法内实现。
三者在一块组成一个切面。
@pointcut(execution(...))用法
1)execution(* *(..)) 表示匹配所有方法
2)execution(public * com. savage.service.UserService.*(..)) 表示匹配com.savage.server.UserService中所有的公有方法
3)execution(* com.savage.server...(..)) 表示匹配com.savage.server包及其子包下的所有方法
除此外还有许多方法可以查看网上别人的案例 ,这篇文章里面使用的是@Pointcut("@annotation(auditLog)"),这个方法可以直接扫描到注解,有使用了auditLog注解的才会被扫描到。
上面的案例中使用了@Before@After@AfterRunning@AfterThrowing这四个通知,通过名字也知道。before是拦截方法执行前,After是之后,AfterRunning是方法执行正常返回结果,AfterThrowing是方法执行失败报错抛出异常了。上面案例中没使用的@Around是可以同时在所拦截方法的前后执行一段逻辑。下面给出@Around案例
*//**
* 统计方法耗时环绕通知
* @param joinPoint
*/
@Around("aopPointCut()")
public Object timeAround(ProceedingJoinPoint joinPoint) {
long startTime = 0;
long endTime;
long E_time = 0;
Object obj = null;
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//url
logger.info("url={}",request.getRequestURI());
//method
logger.info("method={}", request.getMethod());
//ip
logger.info("ip={}", getIp(request));
//类方法
logger.info("classMethod={}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
//参数
Enumeration<String> paramter = request.getParameterNames();
while (paramter.hasMoreElements()) {
String str = (String) paramter.nextElement();
logger.info(str + "={}", request.getParameter(str));
}
// 获取开始时间
startTime = System.currentTimeMillis();
// 获取返回结果集
obj = joinPoint.proceed(joinPoint.getArgs());
endTime = System.currentTimeMillis();
// 获取方法执行时间
E_time = endTime - startTime;
logger.info("执行开始时间:"+startTime);
logger.info("执行结束时间:"+endTime);
//打印返回结果信息
try {
WrappedResult result = (WrappedResult) obj;
logger.info("success:"+result.isSuccessful());
logger.info("resultValue:"+result.getResultValue());
} catch (Exception e) {
logger.info("return:"+obj);
}
} catch (Throwable t) {
// 当方法中报异常时,会抛出这里的异常,
endTime = System.currentTimeMillis();
E_time = endTime - startTime;
logger.info("执行开始时间:"+startTime);
logger.info("执行结束时间:"+endTime);
// 获取方法执行时间
logger.error("执行报错");
}
String classAndMethod = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
logger.info("执行 " + classAndMethod + " 耗时为:" + E_time + "ms");
return obj;
}
下面给出这个执行过程示例图
正常执行没有报错:
异常情况
执行到核心业务方法或者类时,会先执行AOP。在aop的逻辑内,先走@Around注解的方法。然后是@Before注解的方法,然后这两个都通过了,走核心代码,核心代码走完,无论核心有没有返回值,都会走@After方法。然后如果程序无异常,正常返回就走@AfterReturn,有异常就走@AfterThrowing。
动态修改注解内容
在开发中有时候会碰到这种情况,一个注解带上参数,但是在实际的代码运行过程中可能会发现这个参数需要动态修改为其他的,比如说新增和修改都放在同一个方法内,那么我们的注解刚好有个字段是事件类型(增、删、改、查),这种情况就需要实现动态修改注解内容。具体实现方式如下:
@AuditLog(note = "通过name查找菜单",operate = EventOperateType.QUERY)
@RequestMapping(value = "/sp/selMenu")
public WrappedResult selMenu(String cl_u_id,String name){
try {
// 获取当前线程的方法名
String threadName = Thread.currentThread().getStackTrace()[1].getMethodName();
// getMethod里面的参数对应updateUserInfo方法的参数,固定形式的,不可少
Method method = MenuController.class.getMethod(threadName, String.class, String.class);
// 调用修改方法
AuditLogUtil.alterAnnotationOn(method, EventOperateType.UPDATE);
} catch (Exception e) {
logger.info("动态修改审计日志信息失败");
}
try {
List<Map<String, Object>> mapList = menuService.desktopMenus(cl_u_id);
List<MenuIcons> list = menuService.selMenus(cl_u_id,name,mapList);
return WrappedResult.successWrapedResult(list);
} catch (Exception e) {
return WrappedResult.failedWrappedResult("查找菜单失败");
}
}
package com.ls.im.core.common.log;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AuditLogUtil {
final static Logger logger = LoggerFactory.getLogger(AuditLogUtil.class);
/**
*
* @Title: alterAnnotationOn
* @Description: 动态AuditLog修改注解的operate操作事件类型字段
* @param @param method
* @param @param type
* @param @throws Exception 参数
* @return void 返回类型
* @throws
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void alterAnnotationOn(Method method, EventOperateType type) throws Exception {
method.setAccessible(true);
logger.info("执行的方法={}",method.getName());
boolean methodHasAnno = method.isAnnotationPresent(AuditLog.class); // 是否指定类型的注释存在于此元素上
if (methodHasAnno) {
// 得到注解
AuditLog methodAnno = method.getAnnotation(AuditLog.class);
// 修改
InvocationHandler h = Proxy.getInvocationHandler(methodAnno);
// annotation注解的membervalues
Field hField = h.getClass().getDeclaredField("memberValues");
// 因为这个字段是 private final 修饰,所以要打开权限
hField.setAccessible(true);
// 获取 memberValues
Map<String, Object> memberValues = (Map) hField.get(h);
logger.info("修改操作事件类型由={}改为={}",methodAnno.operate(),type);
memberValues.put("operate", type);
}
}
}
这样就实现了修改注解的内容,注解AuditLog的operate就从query改成了update。但是实际上我们在Aspect类中会发现我们接收到的AuditLog没有改变,所以进行了以下方法就可以将数据变最新。
@AfterThrowing(pointcut = "aopPointCut(auditLog)",throwing = "ex")
public void doAfterThrowing(JoinPoint joinPoint,AuditLog auditLog,Throwable ex) {
try {
//尝试获取动态修改的注解AuditLog
auditLog = AuditLogAspect.getControllerMethodAuditLog(joinPoint);
} catch (Exception e1) {
logger.error("获取auditLog异常");
}
}
/**
*
* @Title: getControllerMethodAuditLog
* @Description: 获取注解中对方法的AuditLog信息,可以获取到反射动态修改后的AuditLog
* @param @param joinPoint
* @param @return
* @param @throws Exception 参数
* @return AuditLog 返回类型
* @throws
*/
@SuppressWarnings("rawtypes")
public static AuditLog getControllerMethodAuditLog(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();
AuditLog auditLog = null;
for (Method method : methods) {
if (method.getName().equals(methodName)) {
Class[] clazzs = method.getParameterTypes();
if (clazzs.length == arguments.length) {
auditLog = method.getAnnotation(AuditLog.class);
break;
}
}
}
return auditLog;
}
以上代码可以将反射动态修改后的AuditLog内容重新取出来。