动吧项目AOP加强

2 篇文章 0 订阅

1.key写死

使用缓存可能造成脏读,这种现象在生活中也比较常见,如12306订票预定时查询显示有票,订票时查询显示没票,原因是预定查询走的是缓存,订票查询访问的是数据库。

下面我们在代码中演示一下,当前只有部门模块加了缓存(目前代码中key是写死的)

@Around("doCache()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
		
		System.out.println("Get Data from Cache");
		Object result = cache.get("dataKey");
		if(result!=null) return result;
		result = jp.proceed();
		System.out.println("Put Data from Cache");
		cache.put("dataKey", result);
		return result;
}

在菜单模块SysMenuServiceImplfindObjects上加上**@RequiredCache()**

@RequiredCache()
@Override
public List<Map<String, Object>> findObjects() {
		return sysMenuDao.findObjects();
}

菜单模块第一次查询走数据库,将数据查询出来放入缓存
在这里插入图片描述
菜单模块第2次查询走缓存
在这里插入图片描述
但是再点部门管理发现数据不对
在这里插入图片描述
原因是直接从缓存中获取

2.将RequiredCache的key写活

分析:动态获取key
RequiredCache中加入key,默认为空

public @interface RequiredCache {
	String key() default "";
}

ClearCache中加入key,默认为空

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClearCache {
	String key() default "";
}
  • 部门模块key
    SysDeptServiceImplfindObjects加上key
@RequiredCache(key="deptData")
@Override
public List<Map<String, Object>> findObjects() {
		List<Map<String, Object>> list=
		sysDeptDao.findObjects();
		if(list==null||list.size()==0)
		throw new ServiceException("没有部门信息");
		return list;
}

SysDeptServiceImplupdateObject也加上key

@ClearCache(key="deptData")
@Override
public int updateObject(SysDept entity) {
		//1.合法验证
		if(entity==null)
		throw new ServiceException("保存对象不能为空");
		if(StringUtils.isEmpty(entity.getName()))
		throw new ServiceException("部门不能为空");
		int rows;
		//2.更新数据
		try{
		rows=sysDeptDao.updateObject(entity);
		}catch(Exception e){
		e.printStackTrace();
		throw new ServiceException("更新失败");
		}
		//3.返回数据
		return rows;
}
  • 菜单模块key
    SysMenuServiceImplfindObjects加上key
@RequiredCache(key="menuData")
@Override
public List<Map<String, Object>> findObjects() {
		return sysMenuDao.findObjects();
}

SysMenuServiceImplupdateObject也加上key

@ClearCache(key="menuData")
@Override
public int updateObject(SysMenu entity) {
		//1.参数校验
		if(entity==null)
			throw new IllegalArgumentException("保存对象不能为空");
		//..
		//2.保存菜单对象
		int rows=sysMenuDao.updateObject(entity);
		if(rows==0)throw new ServiceException("记录已经不存在");
		return rows;
}

获取key思路分析:
a.获取方法
b.获取方法上的注解
c.获取注解中的参数
SysCacheAspectdoAround

@Around("doCache()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
	    Object key = "dataKey";
		MethodSignature ms = (MethodSignature)jp.getSignature();
		Method method = ms.getMethod();
		System.out.println("method =" + method);


		System.out.println("Get Data from Cache");
		Object result = cache.get(key);
		if(result!=null) return result;
		result = jp.proceed();
		System.out.println("Put Data from Cache");
		cache.put(key, result);
		return result;
}

结果为接口中的抽象方法(当前配置文件中aop的代理方式为jdk代理)
在这里插入图片描述
将代理方式改为cglib代理

#spring
spring:
  datasource:
    url: jdbc:mysql:///jtsys?serverTimezone=GMT%2B8&characterEncoding=utf8
    username: root
    password: root
  thymeleaf:
    prefix: classpath:/templates/pages/
  aop:
    proxy-target-class: true

代码一行没动,只是代理方式的不同,结果为实现类的方法
在这里插入图片描述
代码还是不太灵活,那如何写活呢?我们用反射获取目标方法将doAround代码稍微改改

@Around("doCache()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
	    Object key = "dataKey";
	    //获取目标对象
		Class<?> targetClass = jp.getTarget().getClass();
		MethodSignature ms = (MethodSignature)jp.getSignature();
		//此方法会根据代理方式的不同返回不同的结果,jdk代理返回接口中的抽象方法,cglib代理返回实现类的方法
		//Method method = ms.getMethod();
		Method targetMethod = targetClass.getDeclaredMethod(ms.getName(),ms.getParameterTypes());
		System.out.println("targetMethod = " + targetMethod);
		System.out.println("Get Data from Cache");
		Object result = cache.get(key);
		if(result!=null) return result;
		result = jp.proceed();
		System.out.println("Put Data from Cache");
		cache.put(key, result);
		return result;
}

此时无论代理方式是何种代理,返回结果均为实现类的方法,接着获取注解,从而获取注解中的参数

@Around("doCache()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
	    //1.获取目标方法对象
		Class<?> targetClass = jp.getTarget().getClass();
		MethodSignature ms = (MethodSignature)jp.getSignature();
		//此方法会根据代理方式的不同返回不同的结果,jdk代理返回接口中的抽象方法,cglib代理返回实现类的方法
		//Method method = ms.getMethod();
		Method targetMethod = targetClass.getDeclaredMethod(ms.getName(),ms.getParameterTypes());
		System.out.println("targetMethod = " + targetMethod);
		//2.获取目标方法对象上的注解
		RequiredCache requiredCache = targetMethod.getAnnotation(RequiredCache.class);
		//3.获取目标方法对象上的注解中的参数
		Object key = requiredCache.key();
		System.out.println("key = " + key);
		System.out.println("Get Data from Cache");
		Object result = cache.get(key);
		if(result!=null) return result;
		result = jp.proceed();
		System.out.println("Put Data from Cache");
		cache.put(key, result);
		return result;
}

在这里插入图片描述

3.同样的方式,把ClearCache的key也写活

@AfterReturning("doClear()")
public void doAfterReturning(ProceedingJoinPoint jp) {
		//cache.clear();
		//cache.remove(key);
}

启动报错
在这里插入图片描述
原因是ProceedingJoinPoint只适用于环绕通知
ProceedingJoinPoint改为JoinPoint

@AfterReturning("doClear()")
public void doAfterReturning(JoinPoint jp) throws NoSuchMethodException {
		//cache.clear();
		//1.获取目标方法对象
		Class<?> targetClass = jp.getTarget().getClass();
		//获取方法签名
		MethodSignature ms = (MethodSignature)jp.getSignature();
		Method targetMethod = targetClass.getDeclaredMethod(ms.getName(),ms.getParameterTypes());

		//2.获取目标方法对象上的注解
		ClearCache clearCache = targetMethod.getAnnotation(ClearCache.class);

		//3.获取目标方法对象上的注解中的参数
		Object key = clearCache.key();
		System.out.println("doAfterReturning.cache.key = " + key);
		cache.remove(key);
}

部门模块点击删除
在这里插入图片描述
页面显示删除成功,但再次点击部门管理发现数据还在
在这里插入图片描述
原因是数据从缓存中获取,没有清缓存
在这里插入图片描述
SysDeptServiceImpldeleteObject方法上加上

@ClearCache(key="depeData") 

点击删除后再次查看部门管理,数据已删除

4.切面优先级设置及实现

存在多个切面时,如何保证切面的执行顺序?切面的优先级需要借助**@Order**注解进行描述,数字越小优先级越高,默认优先级比较低。例如:
定义日志切面并指定优先级

@Order(1)
@Aspect
@Component
@Slf4j
public class SysLogAspect {
	...
}

定义缓存切面并指定优先级

@Order(2)
@Component
@Aspect
public class SysCacheAspect {
	....
}

当存在多个切面作用于同一目标对象方法时,这些切面会构成一个切面链,类似于过滤器链、拦截器链,其执行分析如下:
在这里插入图片描述
SysLogAspect上加上@Order(1),并在around方法中加上1行输出

System.out.println("SysLogAspect.around");

SysTimeAspect上加上@Order(2),并在around方法中加上输出

@Around("doTime()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
		System.out.println("SysTimeAspect.around.before");
		try {
			Object result = jp.proceed();
			System.out.println("SysTimeAspect.around.after");
			return result;
		} catch (Exception e) {
			System.out.println("SysTimeAspect.around.exception");
			throw e;
		}
}

点击用户管理,结果如下:
在这里插入图片描述
结论:当存在多个切面作用于同一目标对象方法时,优先级高的先执行后结束,有点先进后出的意味
这种玩法其实很常见,如SpringMVC的拦截器链DispatchServlet(idea中进入类的快捷键ctrl+shift+alt+n)的doDispatch(idea类中查找指定方法快捷键ctrl+f12)对拦截器的处理
在这里插入图片描述
再进入applyPreHandle方法源码

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
            HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
            if (!interceptor.preHandle(request, response, this.handler)) {
                this.triggerAfterCompletion(request, response, (Exception)null);
                return false;
            }
        }

        return true;
    }

所有的拦截器放入list或数组中(如录播中就是放入数组中,这是之后的版本对源码有修改),顺着遍历
再进入applyPostHandle方法源码

 void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
        for(int i = this.interceptorList.size() - 1; i >= 0; --i) {
            HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
            interceptor.postHandle(request, response, this.handler, mv);
        }

  }

倒着遍历
若是将SysLogAspectaround的调用目标方法改一下

//调用目标方法
Object result = null;//jp.proceed();//调用本切面中其他通知或下一个切面通知或目标方法

再点击用户管理,发现没有用户
在这里插入图片描述
原因是没有执行目标方法,从控制台可以看出只有log切面执行,时间切面没执行
在这里插入图片描述
环绕通知是所有通知中最重要的,around方法略显苛刻
在这里插入图片描述
a.方法返回值为Object,执行目标方法的返回值object会返回给调用方
b.参数为ProceedingJoinPoint,ProceedingJoinPoint也只能用于环绕通知
c.抛出异常,此异常环绕通知也建议抛出,可以先处理再抛出

5.AOP中关键对象与术语总结

Spring基于AspectJ框架实现AOP设计的关键对象概览如下图所示
在这里插入图片描述
切面对象(Aspect):封装了扩展业务逻辑的对象,在spring中可以使用@AspectJ描述.
切入点(Pointcut): 定义了切入扩展业务逻辑的一些方法的集合(哪些方法运行时切入扩展业务),一般会通过表达式进行相关定义,一个切面中可以定义多个切入点的定义.

//切入点表达式大体分2大类
a.粗粒度切入点表达式定义(不能精确到具体方法),例如bean,within表达式
1.bean("bean的名字")
bean(sysUserServiceImpl),sysUserServiceImpl类中所有方法集合为切入点
bean(*ServiceImpl),以ServiceImpl结尾的所有方法集合为切入点

2.within (“包名.类型”) 表达式案例分析
within(com.pj.service.SysUserServiceImpl),SysUserServiceImpl类中所有方法集合为切入点
within(com.pj.service.*),com.pj.service包下所有类中的方法集合为切入点
within(com.pj.service…*),com.pj.service包以及子包中所有类中方法的集合为切入点

b.细粒度切入点表达式定义(可以精确到具体方法),例如execution,@annotation表达式粗粒切入点表达式
1.execution(“返回值类全名.方法名(参数列表)) 表达式案例分析
execution(intcom.cy.pj.service.SysUserService.validById(Integer,Integer))
execution(com.cy.pj.service….*())
2.@annotation(“注解的类全名”)表达式案例分析
@annotation(com.annotation.RequiredCache),RequiredCache注解描述的方法为缓存切入点方法
@annotation(com.annotation.RequiredLog),由RequiredLog注解描述的方法为日志切入点方法

连接点(JoinPoint):切入点方法集合中封装了某个正在执行的目标方法信息的对象,可以通过此对象获取具体的目标方法信息,甚至去调用目标方法.
通知(Advice):切面(Aspect)内部封装扩展业务逻辑的具体方法对象,一个切面中可以有多个通知(例如@Around).

日志模块添加功能的实现
SysLogDao中新增添加方法insertObject

/**
 * 日志的新增操作
 */
int insertObject(SysLog entity);

SysLogMapper中写插入语句

<insert id="insertObject">
		insert into sys_logs
		(username,operation,method,params,time,ip,createdTime)
		values
		(#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})
</insert>

SysLogService中新增添加方法saveObject,方法返回值为void,不要定义成其他,因为此方法后面会进行异步调用,返回值定义成其他类型还得处理

void saveObject(SysLog entity);

SysLogServiceImpl添加方法实现

@Override
public void saveObject(SysLog entity) {
		String name = Thread.currentThread().getName();
		System.out.println("SysLogServiceImpl-->"+name);
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		sysLogDao.insertObject(entity);
}

SysLogAspectaround中实现日志的保存

@Around("doPointCut()")
public Object around(ProceedingJoinPoint jp) throws Throwable{
		try {
			System.out.println("SysLogAspect.around");
			//记录方法执行时的开始时间
			long t1 = System.currentTimeMillis();
			//调用目标方法
			Object result = jp.proceed();//调用本切面中其他通知或下一个切面通知或目标方法
			//记录方法执行的结束时间及总时长
			long t2 = System.currentTimeMillis();
			log.info("SysLogAspect method execut time {}",(t2-t1));
			//将用户的正常行为以日志的形式写入到数据库中
			saveLog(jp,t2-t1);
			return result;
		} catch (Exception e) {
			//出现异常时还要输出错误日志
			log.error("error {}",e.getMessage());
			throw e;
		}
}

SysLogAspect中注入SysLogService

@Autowired
private SysLogService sysLogService;

saveLog方法的实现

private void saveLog(ProceedingJoinPoint jp ,Long time)  {
		//1.获取用户的行为数据
		//获取目标方法所在的类的类型
		Class<?> targetCls = jp.getTarget().getClass();
		//类名
		String targetClsName = targetCls.getName();
		MethodSignature ms =(MethodSignature)jp.getSignature();
		//获取操作名
		String operation ="operation";
		//获取目标类中的方法名称
		String targetClsMethodName = targetClsName + "." + ms.getName();
		//获取调用目标方法时传递的参数
	    String params = Arrays.toString(jp.getArgs());
		//2.封装用户的行为数据
		SysLog entity = new SysLog();
		entity.setUsername("admin");//暂时写死
		entity.setCreatedTime(new Date());
		entity.setIp(IPUtils.getIpAddr());
		entity.setMethod(targetClsMethodName);
		entity.setOperation(operation);
		entity.setTime(time);
		entity.setParams(params);
		//3.保存用户行为数据(写入到数据库中)
		sysLogService.saveObject(entity);
	}

在这里插入图片描述
如何获取操作名称?
a.自定义注解RequiredLog

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
	String operation() default "operation";
}

b.在SysUserServiceImplfindPageObjects上加上**@RequiredLog(operation = “用户查询”),在validById上加上@RequiredLog(operation = “禁用启用”)**

c.在SysLogAspectsaveLog中添加获取操作名称代码

private void saveLog(ProceedingJoinPoint jp ,Long time) throws NoSuchMethodException, SecurityException, JsonProcessingException {
		//1.获取用户的行为数据
		//获取目标方法所在的类的类型
		Class<?> targetCls = jp.getTarget().getClass();
		//类名
		String targetClsName = targetCls.getName();
		MethodSignature ms =(MethodSignature)jp.getSignature();
		//获取操作名
		Method targetMethod = targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
		RequiredLog requiredLog = targetMethod.getAnnotation(RequiredLog.class);
		String operation ="operation";
		if(requiredLog != null) operation = requiredLog.operation();
		//获取目标类中的方法名称
		String targetClsMethodName = targetClsName + "." + ms.getName();
		//获取调用目标方法时传递的参数
		//String params = Arrays.toString(jp.getArgs());
		String params = new ObjectMapper().writeValueAsString(jp.getArgs());
		//2.封装用户的行为数据
		SysLog entity = new SysLog();
		entity.setUsername("admin");//暂时写死
		entity.setCreatedTime(new Date());
		entity.setIp(IPUtils.getIpAddr());
		entity.setMethod(targetClsMethodName);
		entity.setOperation(operation);
		entity.setTime(time);
		entity.setParams(params);
		//3.保存用户行为数据(写入到数据库中)
		sysLogService.saveObject(entity);
	}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值