1、AOP概述
我们先来举个例子
@SpringBootTest public class TliasTest { @Autowired private DeptService deptService; @Test void proxy(){ DeptService proxyInstance = (DeptService) Proxy.newProxyInstance(deptService.getClass().getClassLoader(), deptService.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long begin = System.currentTimeMillis(); Object result = method.invoke(deptService, args); long end = System.currentTimeMillis(); System.out.println(method.getName() + "方法耗时:" + (end - begin)); return result; } }); proxyInstance.findAll(); } }
每个需要统计耗时的对象都要写一次代理方法吗?也比较繁琐…
概念描述
AOP (Aspect Orient Programming),面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。
springAOP,旨在管理bean对象的过程中底层使用动态代理机制,对特定的方法进行功能增强。
AOP的出现是为了对程序解耦,减少系统的重复代码,提高可拓展性和可维护性。
应用场景:
- 记录系统的操作日志
- 权限控制
- 事务管理
- 缓存
- 等等
相关术语:
-
通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
-
连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
-
切入点(PointCut): 可以插入增强处理的连接点。
-
切面(Aspect): 切面是通知和切入点的结合。
-
织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象(代理对象),这个过程就是织入。
-
目标对象(Target):通知所应用的对象(被代理对象)
2、AOP使用
2.1 使用入门
步骤一:起步依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
步骤二:开启aop支持(省略)
@SpringBootApplication
//开启AOP支持 引入起步依赖后省略即可
@EnableAspectJAutoProxy
public class TliasApplication {
public static void main(String[] args) {
SpringApplication.run(TliasApplication.class, args);
}
}
步骤三:定义切面
@Component
@Aspect //声明该bean是一个切面bean 找到切入点+添加通知(增强动作)
@Slf4j
public class ZhiFuAspect {}
// "execution(* com.heima.tlias.service.impl.DeptServiceImpl.*(..))" 找到切入点
// @Before做通知增强 buildLu() 增强的功能
@Before("execution(* com.heima.tlias.service.impl.DeptServiceImpl.*(..))")
public void buildLu(){
log.info("开始修路....");
}
@AfterReturning("execution(* com.heima.tlias.service.impl.DeptServiceImpl.*(..))")
public void helpMai(){
log.info("帮助卖 销售....");
}
}
2.2 观察被增强后的bean对象
在使用切面之前:
deptService对应的bean对象是:
在使用切面之后:
会生成并使用一个代理对象:
2.2 切入点表达式
常用的切入点表达式有:
1)匹配方法
execution(modifier? ret-type declaring-type?name-pattern(param-pattern) throws-pattern?)
execution(* com.dsn.tlias.service..*(..))
注意:
com.heima.tlias.service.impl.DeptServiceImpl.*(…)
com.*(…) 方法前面一个点,前面的字符串是类!
com…*(…) 方法前面2个点,前面的字符串是包!
2)匹配注解
对方法上有该注解的做增强
@annotation(注解的全限定类名)
@Component
@Aspect
@Slf4j
public class GetMappingAspect {
@Autowired
private HttpServletRequest request;
@Before("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void isGetMapping(){
String method = request.getMethod();
log.info("请求方式是" + method);
}
}
3)多种组合起来
可以通过&&
、||
、!
来对切入点进行拼接
4)抽取公共使用
@Component
@Aspect //声明该bean是一个切面bean 找到切入点+添加通知(增强动作)
@Slf4j
public class ZhiFuAspect {
@Pointcut("execution(* com.heima.tlias.service.impl.DeptServiceImpl.*(..))")
public void pt(){};
@Before("pt()")
public void buildLu(){
log.info("开始修路....");
}
@AfterReturning("pt()")
public void helpMai(){
log.info("帮助卖 销售....");
}
}
2.3 通知类型
注解 | 说明 |
---|---|
@Before | 前置通知,在连接点方法前调用 |
@Around | 环绕通知,它将覆盖原有方法,可以想象成前置+原方法+后置 |
@After | 后置通知,在连接点方法后调用 类比finally |
@AfterReturning | 返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常 |
@AfterThrowing | 异常通知,当连接点方法异常时调用 |
环绕通知比较常用:
@Component
@Aspect //声明该bean是一个切面bean 找到切入点+添加通知(增强动作)
@Slf4j
@Order(1)
public class RuntimeAroundAspect {
@Around("com.heima.tlias.aspectj.ZhiFuAspect.pt()")
public Object around2Time(ProceedingJoinPoint joinPoint) {
//前置通知
log.info("前置通知位置:从我这走 买路财,放行");
//方法执行 放行
Object result = null;
try {
result = joinPoint.proceed();
//后置通知
log.info("后置通知位置:放行走完了");
return result;
} catch (Throwable e) {
//异常通知
log.info("异常通知位置:"+ e.getMessage());
return Result.error("有异常");
} finally {
//最终通知
log.info("终于走完了.....");
}
}
}
2.3.1 (单个切面类中)五大通知执行顺序
不同版本的Spring是有一定差异的,使用时候要注意
- Spring 4
- 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕返回 ==> 环绕最终 ==> @After ==> @AfterReturning
- 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> 环绕异常 ==> 环绕最终 ==> @After ==> @AfterThrowing
- Spring 5.28
- 正常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterReturning ==> @After ==> 环绕返回 ==> 环绕最终
- 异常情况:环绕前置 ==> @Before ==> 目标方法执行 ==> @AfterThrowing ==> @After ==> 环绕异常 ==> 环绕最终
2.3.2 多个切面类的执行顺序
1)默认情况下:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
2)@Order可指定执行顺序
@Component
@Aspect
@Order(1) //切面类的执行顺序
public class 某切面类 {
}
@Order(1) 注意作用在AOP切面执行顺序上!
3.3.3 注意区分
这些过滤器、拦截器、切面并不是我们在代码中手动调用的,所以需要大家在脑海中强行构建他们的执行顺序。
AOP切面位置:
3、应用场景演示
权限控制
控制某资源能被某用户访问。保护服务接口,会加在controller层。
**需求:**emp表中很多的用户,都可以登录系统,登录后可以任意访问资源。做到,普通员工只能查看员工信息并修改;管理层可以操作部门、员工。
考虑2个方向:
1)用户如何给他这些访问权限—如何为用户定义权限?
规定,员工资源都需要一个权限名:“emps”;部门资源访问都需要“depts”。
姓张的都是老板(emps、depts),其他的都是小员工(“emps”);登录时赋予权限!
2)如何判断用户访问某资源时,是否有所需权限?
2.1)为资源加"锁"
2.2)进行判断,登录用户有没有权限打开“锁”。
在执行方法之前,写个判断:拿到用户的令牌、解析令牌中的权限字符串(emps|depts),还要拿到方法上的锁所需的权限(depts),如果用户拥有的字符串包含方法所需的,可以访问方法,否则,提示无权限!
赋权
@PostMapping("/login")
public Result login(@RequestBody Map<String,String> loginMap){
Emp loginEmp = empService.login(loginMap);
if(loginEmp != null){
Map<String,Object> claims = new HashMap<>();
claims.put("userId",loginEmp.getId());
// 赋权:把属于你的字符串交给你
if(loginEmp.getName().startsWith("张")){
claims.put("lockKey","depts|emps");
}else{
claims.put("lockKey","emps");
}
String jwt = jwtUtils.generateToken(claims);
return Result.success(jwt);
} else
// return Result.error("NOT_LOGIN");
throw new CustomerException("用户登陆失败");
}
资源加锁
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
//@Inherited
public @interface SecurityLock {
String value() default "";
}
@GetMapping("/{id}")
@SecurityLock("depts")
public Result findById(@PathVariable("id")Integer id){
// 返回值返回的是一个集合的时候 1)先写了mapper 2)没有认真看文档
Dept depts = deptService.findById(id);
return Result.success(depts);
}
权限判断
@Component
@Aspect
@Slf4j
public class AuthAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private JwtUtils jwtUtils;
@Pointcut("@annotation(com.***.***.comons.anno.SecurityLock)")
private void authPt(){};
@Around("authPt()")
public Object preAuth(ProceedingJoinPoint joinPoint){
// 1.获取登录用户令牌
String token = request.getHeader("token");
if(StringUtils.isBlank(token)){
//返回错误信息
return Result.error("NOT_LOGIN");
}
// 2.解析用户令牌中的权限数据
Claims claims = jwtUtils.parseToken(token);
if(claims == null){
//返回错误信息
return Result.error("NOT_LOGIN");
}
String lockKey = (String) claims.get("lockKey");
// 3.得到joinPoint对应的方法上的注解中 所需权限
Object[] args = joinPoint.getArgs();
Object target = joinPoint.getTarget();//获得切入点方法所属的目标对象
String name = joinPoint.getSignature().getName();//获取切入点对象方法的方法名
// 强行转换为方法签名对象
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 得到切入点方法对象
Method method = signature.getMethod();
// 获取方法对象上的注解SecurityLock
SecurityLock securityLock = method.getDeclaredAnnotation(SecurityLock.class);
String needKey = securityLock.value();
// 4.比较是否包含
if(lockKey.contains(needKey)){
// 放行
Object result = null;
try {
result = joinPoint.proceed();
return result;
} catch (Throwable e) {
e.printStackTrace();
return Result.error("服务端异常");
}
}else{
return Result.error("无权访问",403);
}
}
}
4、spring基于AOP管理事务
**事务:**面试题
隔离级别
4.1 使用入门
方法/类添加注解@Transactional
@Override
@Transactional
public int insertDept(Dept dept) {
dept.setCreateTime(LocalDateTime.now());
dept.setUpdateTime(LocalDateTime.now());
int cnt = deptMapper.insertDept(dept);
return cnt;
}
基于spring做的声明式事务管理;还有编程式事务管理!
指定日志输出级别
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
##spring事务失效场景:
1. 事务方法未被Spring管理
2. 非public修饰的方法
3. 异常被内部catch,程序生吞异常
4. 多线程调用(事务管理基于一个线程,另一个线程无法获取事务管理器)
5. 抛出不是RuntimeException异常
6. 同一个类中的方法相互调用
7. 错误的使用传播行为
4.2 @Transactional属性
属性 | 说明 | 取值 |
---|---|---|
isolation | 隔离规则 | public enum Isolation { // 默认数据库的 DEFAULT(-1), READ_UNCOMMITTED(1), READ_COMMITTED(2), REPEATABLE_READ(4), SERIALIZABLE(8); } |
propagation | 传播行为 | public enum Propagation { // 默认REQUIRED REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED), SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS), REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW), NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED), ... } |
readOnly | 是否只读 | // 默认false false/true |
rollbackFor | 回滚规则 | // 默认RuntimeException Class<? extends Throwable>[] rollbackFor(); |
#事务传播行为参考:
https://www.jianshu.com/p/34bc1c5be703
面试:
事务传播行为有哪些?
1)REQUIRED 必需的
2)SUPPORTS 支持
3)REQUIRES_NEW
事务传播行为失效的情况:
调用自己的方法会失效;解决方案:自己注入自己,同注入的对象调用自己的方法
@Transactional(isolation = Isolation.DEFAULT)
public int delDeptById(Integer id){
// insert into table values(),(),()
int cnt = deptMapper.delById(id);
// int delCnt = empService.delByDeptId(id);
deptService.delByDeptId(id);
int i = 1/0;
// int delCnt = empMapper.delEmpByDeptid(id);
return cnt;
}
// 提交事务
@Transactional
@Override
public void delByDeptId(Integer id){
empMapper.delEmpByDeptid(id);
}
其他
1、自学技术:动态代理技术
springboot项目中,默认使用动态代理是CGLIB。基于被代理类的字节码生成代码对象!类似于继承的效果。
要求:被代理类不能是final的!!!
这是面试题! final的特点。
https://zhuanlan.zhihu.com/p/346173865
2、常见的分包
每个公司的分包包名不同,但大同小异!