Aop

2021-03-26  本文已影响0人  lclandld

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

1.Aop简介

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

那什么是AOP?

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

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

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

对于每个业务方法来说,除了业务逻辑,还需要安全检查、日志记录、参数校验、拦截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种方式:

最简单的方式是第三种,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() 表达式的主体
第一个”*“符号 表示返回值的类型任意
com.xyz.service 要切入的服务的包名
包名后面的" .. " 表示当前包及子包
第二个”*“ 表示类名,*即所有类
.*(..) 表示任何方法名,括号表示参数,两个点表示任何参数类型
符号 含义
(*) 表示任务类型的参数
(..) 表示任务类型的参数
(*,String) 表示两个参数,第一个参数是任何类型的,第二个参数必须是String
() 表示没有参数
3.3通知(Advice)

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

    @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);
    }
     /**
     * 修改用户的课程进度切面
     */
    @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());
        }
    }

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)

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

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

上一篇下一篇

猜你喜欢

热点阅读