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;
}
在菜单模块SysMenuServiceImpl的findObjects上加上**@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
在SysDeptServiceImpl的findObjects加上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;
}
在SysDeptServiceImpl的updateObject也加上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
在SysMenuServiceImpl的findObjects加上key
@RequiredCache(key="menuData")
@Override
public List<Map<String, Object>> findObjects() {
return sysMenuDao.findObjects();
}
在SysMenuServiceImpl的updateObject也加上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.获取注解中的参数
将SysCacheAspect中doAround
@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);
}
部门模块点击删除
页面显示删除成功,但再次点击部门管理发现数据还在
原因是数据从缓存中获取,没有清缓存
在SysDeptServiceImpl的deleteObject方法上加上
@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);
}
}
倒着遍历
若是将SysLogAspect的around的调用目标方法改一下
//调用目标方法
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);
}
在SysLogAspect的around中实现日志的保存
@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.在SysUserServiceImpl的findPageObjects上加上**@RequiredLog(operation = “用户查询”),在validById上加上@RequiredLog(operation = “禁用启用”)**
c.在SysLogAspect的saveLog中添加获取操作名称代码
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);
}