Aop

作者: lclandld | 来源:发表于2021-03-26 16:27 被阅读0次

    前面根据实际项目做了一个日志切面的Demo出来SpringAop。发现自己会使用了,可以理论知识理解得还不是很到位,所有继续完善一下自己的知识体系。

    1.Aop简介

    AOP是Aspect Oriented Programming,即面向切面编程。

    那什么是AOP?

    我们先回顾一下OOP:Object Oriented Programming,OOP作为面向对象编程的模式,获得了巨大的成功,OOP的主要功能是数据封装、继承和多态。

    而AOP是一种新的编程方式,它和OOP不同,OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)。

    要理解AOP的概念,我们先用OOP举例,比如一个业务组件UserService,它有几个业务方法:

    • createUser:添加新的用户;
    • updateUser:修改用户信息;
    • deleteUser:删除此用户。

    对于每个业务方法来说,除了业务逻辑,还需要安全检查、日志记录、参数校验、拦截xss攻击和事务处理,它的伪代码像这样:

    public class UserService {
        public void createUser(User user) {
            securityCheck();
            Transaction tx = startTransaction();
            try {
                // 核心业务逻辑
                tx.commit();
            } catch (RuntimeException e) {
                tx.rollback();
                throw e;
            }
            log("created user: " + user);
        }
    }
    
    

    继续编写updateUser(),伪代码如下:

    public class UserService {
        public void updateUser(User user) {
            securityCheck();
            Transaction tx = startTransaction();
            try {
                // 核心业务逻辑
                tx.commit();
            } catch (RuntimeException e) {
                tx.rollback();
                throw e;
            }
            log("updated userInfo: " + user);
        }
    }
    
    

    对于安全检查、日志、事务等代码,它们会重复出现在每个业务方法中。使用OOP,我们很难将这些四处分散的代码模块化。

    考察业务模型可以发现,UserService关心的是自身的核心业务逻辑,但是整个完整的系统还必须要求关注安全检查、日志、事务、性能等功能,这些功能实际上“横跨”多个业务方法,为了实现这些功能,不得不在每个业务方法上重复编写代码。

    一种可行的方式是使用Proxy模式,将某个功能,例如,权限检查,放入到Proxy中:

    public class SecurityCheckUserService implements UserService {
        private final UserService target;
    
        public SecurityCheckUserService (UserService target) {
            this.target = target;
        }
    
        public void createUser(User user) {
            securityCheck();
            target.createUser(user);
        }
    
        public void updateUser(User user) {
            securityCheck();
            target.updateUser(user);
        }
    
        public void deleteUser(User user){
            securityCheck();
            target.deleteUser(user);
        }
    
        private void securityCheck() {
            ...
        }
    }
    
    

    这种方式的缺点是比较麻烦,必须先抽取接口,然后,针对每个方法实现Proxy,要以后业务逻辑越来越庞大,写代理中的方法都得写吐血。

    另一种方法是,既然SecurityCheckUserService的代码都是标准的代理模板代码,不如把权限检查视作一种切面(Aspect),把日志、事务、性能检测、验证参数、限流、拦截xss攻击等都视为切面,然后,以某种自动化的方式,把切面织入到核心逻辑中,实现Proxy模式。

    如果我们以AOP的视角来编写上述业务,可以依次实现:

    1. 核心逻辑,即UserService;
    2. 切面逻辑,即:
      权限检查的Aspect;
      日志的Aspect;
      事务的Aspect;
      性能检测的Aspect...

    然后,以某种方式,让框架来把上述对应的Aspect以Proxy的方式“织入”到UserService中,这样子就不必编写复杂而冗长的Proxy模式。

    image.png

    2.AOP原理

    如何把切面织入到核心逻辑中?这正是AOP需要解决的问题。
    在Java平台上,对于AOP的织入,有3种方式:

    • 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
    • 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
    • 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。

    最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理。由于JVM的动态代理要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB这些第三方库实现。

    AOP技术看上去比较神秘,但实际上,它本质就是一个动态代理,让我们把一些常用功能如权限检查、日志、事务等,从每个业务方法中剥离出来。

    3 AOP术语及使用

    切面(aspect)其实就是一个“何时(advice)” 在 ”何处(pointcut)” 干 “什么(advice)” 的一个理论。

    3.1连接点(Join point)

    连接点表示具体要拦截的方法。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。
    切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

    3.2切点(Poincut)
    3.2.1 定义:切点定义了从何处切入。

    切点的定义会匹配通知所要织入的一个或多个连接点。
    通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
    eg:以下代码是在删除课程的时候切入

       /**
       * 删除课程是确认
       */
       @Pointcut("execution(* com.bimstudy.service.ICourseService.deleteCourse(..))")
        public void deletedCourseUpdateLineStep() {
        }
    
    3.2.2表达式:

    可以看看官方文档

    只有部分图,详情点官方文档
    • 基本语法格式为:
      execution(修饰符模式 ? 返回类型模式 方法名模式(参数模式)异常模式?) 除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。

    • 表达式的含义


      此表达式的解释
    符号 含义
    execution() 表达式的主体
    第一个”*“符号 表示返回值的类型任意
    com.xyz.service 要切入的服务的包名
    包名后面的" .. " 表示当前包及子包
    第二个”*“ 表示类名,*即所有类
    .*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型
    • 关于参数模式的一个说明
    符号 含义
    (*) 表示任务类型的参数
    (..) 表示任务类型的参数
    (*,String) 表示两个参数,第一个参数是任何类型的,第二个参数必须是String
    () 表示没有参数
    3.3通知(Advice)

    切面必须要完成的工作即称为通知。通知定义了切面是什么以及什么时候使用
    有以下5种类型通知:

    • 前置通知(Before):在目标方法被调用之前调用通知功能,如果这里面抛出异常,那么目标方法就不执行了;
    • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么,无论目标方法是否抛异常,此段代码都会执行;
    • 返回通知(AfterReturning):在目标方法正常完成之后调用通知;
        @AfterReturning(value = "deletedCourseUpdateLineStep()", returning = "rvt")
        public void deletedCourse(JoinPoint joinPoint, Object rvt) {
            Long courseId = (Long) joinPoint.getArgs()[0];
            log.info("用户{{}}删除了课程{{}}。,更新相应的路线的完成情况", UserUtil.getCurrentUser().getId(), courseId);
            updateStudyLineByCourse(courseId);
        }
    
    • 异常通知(AfterThrowing):在目标方法抛出异常后调用通知;
         /**
         * 修改用户的课程进度切面
         */
        @Pointcut("execution(* com.bimstudy.service.IUserVideoProgressService.updateProgress(..))")
        public void updateUserProgressAspect() {
        }
        @AfterThrowing(value = "updateUserProgressAspect()", throwing = "ex")
        public void doAfterThrowingException(JoinPoint joinPoint, Exception ex) {
            UserVideoProgressVO courseVo = (UserVideoProgressVO) joinPoint.getArgs()[0];
            log.info("捕获用户已看过视频异常,更新课程{{}}相关的路线进度", courseVo.getCourseId());
            if (ex instanceof BusinessException
                    && BusinessExceptionCode.VIDEO_USER_COMPLETE_LIMIT.getInnerCode().equals(((BusinessException) ex).getCode())
                    && ComUtil.isNotEmpty(courseVo)) {
                update(courseVo.getCourseId());
            }
        }
    
    
    • 环绕通知(Around)
      既可以在目标方法之前织入增强动作,也可以在执行目标方法之后织入增强动作;
      可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标目标方法的执行;
      可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值;
      当需要改变目标方法的返回值时,只能使用Around方法;
      虽然Around功能强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturing增强方法就可以解决的事情,就没有必要使用Around增强处理了。

    EG:一个用Around修改目标方法参数的切面

    注解

    /**
     * 标注需要织入部门id的方法
     * @author caob
     */
    @Target( { ElementType.METHOD } )
    @Retention( RetentionPolicy.RUNTIME )
    @Documented
    public @interface WeaveDeptId {
        Constant.RolesRelation value() default Constant.RolesRelation.Dept;
        String name() default "deptId";
    
    }
    

    切面

    这个切面的作用是当登录的账户是部门管理员的时候,需要去找到部分管理员的depetId并设置到要使用的接口中

    /**
     * 织入部门id
     */
    @Slf4j
    @Aspect
    @Component
    public class WeaveDeptIdAop {
    
        @Autowired
        private IUserService userService;
    
        @Around("@annotation(weaveDeptId)")
        public Object before(ProceedingJoinPoint jp, WeaveDeptId weaveDeptId) throws Throwable {
            String name = weaveDeptId.name();
            //首先获取方法名称列表
            MethodSignature msg = (MethodSignature)jp.getSignature();
            String[] paramName = msg.getParameterNames();
            List<String> paramNameList = Arrays.asList(paramName);
    
            //获取传入的参数
            Object[] args = jp.getArgs();
    
            //如果有deptId这个参数
            if (paramNameList.contains(name)) {
                //返回参数位置
                Integer pos = paramNameList.indexOf(name);
                Long paramValue = (Long)args[pos];
    
                if (paramValue== null){
                    Subject subject = SecurityUtils.getSubject();
                    Object principals = subject.getPrincipals();
                    if (!ComUtil.isEmpty(principals)){
                        String token = principals.toString();
                        Long userNo = Long.valueOf(JWTUtil.getUserNo(token));
                        Long loginFromAppId = Long.valueOf(JWTUtil.getLoginFromAppId(token));
                        Long tenantId = Long.valueOf(JWTUtil.getTenantId(token));
                        boolean onlyTargetRole = userService.isOnlyTargetRole(userNo, tenantId, loginFromAppId,weaveDeptId.value());
                        log.info("是否仅为部门管理员:{}",onlyTargetRole);
                        if (onlyTargetRole){
                            //织入部门id
                            args[pos]=userService.getById(userNo).getDeptId();
                        }
                    }
                }
            }
    
            Object result = jp.proceed(args);
            return result;
        }
    
    }
    
     
    

    Controller

     /**
         * 获取积分明细/获取积分排名
         */
        @WeaveDeptId(name="departmentId")
        @ApiOperation(value = "获取积分明细/获取积分排名"+LICHUNLAN)
        @ApiImplicitParams({
            @ApiImplicitParam(name = "pageIndex", value = "第几页" , dataType = "int",paramType="query"),
            @ApiImplicitParam(name = "pageSize", value = "每页多少条" , dataType = "int",paramType="query"),
                @ApiImplicitParam(name = "departmentId", value = "部门名Id", dataType = "Long", paramType = "query"),
                @ApiImplicitParam(name = "userName", value = "姓名", dataType = "String", paramType = "query"),
                @ApiImplicitParam(name = "userCode", value = "员工编码", dataType = "String", paramType = "query")
        })
        @Log(action="findByPage",modelName= "UserPointController",description="获取积分明细/获取积分排名")
        @RequiresPermissions("userPoint:find")
        @GetMapping("/findByPage")
        public ResponseModel<IPage<UserPointListVO>> findByPage(
            @RequestParam(name = "pageIndex", defaultValue = "1", required = false) Integer pageIndex,
            @RequestParam(name = "pageSize", defaultValue = "20", required = false) Integer pageSize,
            @RequestParam(name = "departmentId", required = false) Long departmentId,
            @RequestParam(name = "userName", required = false) String userName,
            @RequestParam(name = "userCode", required = false) String userCode){
            UserPointListVO userPointListVO = new UserPointListVO();
            Page<UserPointListVO> page = new Page<>(pageIndex, pageSize);
            userPointListVO.setTenantId(UserUtil.getCurrentTenantId());
            userPointListVO.setUserName(userName);
            userPointListVO.setDeptId(departmentId);
            userPointListVO.setUserCode(userCode);
            return ResponseHelper.succeed(userPointService.findUserPoint(page,userPointListVO));
        }
    
    
    3.4切面(Aspect)

    切面是通知和切点的结合。通知和切点共同定义了切面的全部内容----它是什么,在何时和何处完成其功能。

    • 正常的执行顺序


      image.png
    • 异常的执行顺序


      image.png

    参考:
    https://www.liaoxuefeng.com/wiki/1252599548343744/1310052317134882

    相关文章

      网友评论

          本文标题:Aop

          本文链接:https://www.haomeiwen.com/subject/ssjpjhtx.html