AOP的理论及实践

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("终于走完了.....");
        }

    }

}

image.png

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切面位置:
image.png

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事务失效场景

##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、常见的分包

在这里插入图片描述

每个公司的分包包名不同,但大同小异!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

岛森年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值