由AOP引发的几点思考

毕业面试那会儿,被问最多的问题便是:请你解释一下什么是AOP思想。当时最喜欢的回答方式是先将英文全称给呈现出来“威慑”下面试官,即——Aspect Oriented Programming。然后把网上搜集的各种解释,使用场景理直气壮地背一遍。这样的回答能唬住一些对应届生要求不高的面试官,但真遇上爱刨根问底的大佬就该GG了。最近在项目中用到了AOP,想把几个思考点总结一下。
在讲AOP之前,首先先回顾一下POP——Process Oriented Programming以及OOP——Object Oriented Programming

  • POP与OOP

    • 对于一个问题,给出解决问题的所需的步骤,POP是一种以功能实现为导向的编程思想,换句话说:功能性的目标实现了就行。然而OOP注重封装,强调实现过程中的模块化,对象化,将对象内部的属性和外部分开。用大家租房时可能遇到的户型来说,POP偏向于"开放式房型",布局中有床,灶台,浴缸,沙发等各类功能的事物。作为一个整体提供人们住房的实现。而OOP偏向于传统的户型,卫生间,厨房,卧室,客厅等具体事物之间有门隔开,作为整体起到“房子”该有的功能和效果,但彼此之间又相对独立,具有较低的耦合性。这样的房子也能正常居住。
    • POP的设计,节约了空间成本(无需“门”的设计),这种设计在早期计算机配置低,内存小的情况下,是一种以时间换空间的良好设计。但这种设计的房子使得各个事物时间相互暴露互相串味,整理布局来看,也显得有些杂乱无章。OOP的户型则提供了更加优雅的设计,使得各个事物都有自己“该待”的地方(类的概念)。不同的类之间无需知道对方的细节,是需提供彼此之间自身的功能和属性(方法调用等)即可,各类各司其职,达到整体上服务租客的效果。
    • 当然不能因为我是一名Java开发而肯定OOP思想的同时否认POP思想,两种思想都是不同的时期人类思考的产物,OOP和POP相对来说是整体和局部的概念,这也是从方法论的角度来看待两者。POP之间互相暴露的功能实现就像OOP实现中某个类,同类属性和方法是有相互了解的权利的。
    • 由OOP编程思想主导的项目里,充斥着众多相互依赖同时相互隔离的对象,将一些数据(属性)和算法(方法)封装在类里面,使得系统更加安全,也更便于后期修改维护。但随着系统的复杂性的增强,应用会进行相应的升级。OOP设计思想也逐渐开始暴露弊端。
  • 场景一

    • 背景A:ClassA,ClassB以及ClassC,有各自需实现的业务方法。
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:40
 */
public class ClassA {

    public void doSomethingInA(){
        // Biz code omitted here.
    }
}
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:41
 */
public class ClassB {

    public void doSomethingInB(){
         // Biz code omitted here.

    }

    private void checkIfLogIn() {

    }
}
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:41
 */
public class ClassC {

    public void doSomethingInC(){
        // Biz code omitted here.
    }

}
  • 背景B: 各个类的各个方法执行前需增加“用户是否登录”的权限校验功能。
    • A猿灵机一动。给每一个类的每一个方法原代码之前加入了如下的判断。
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:40
 */
public class ClassA {
    
    private  User user;

    public void doSomethingInA(){
        if (!user.isStatus()){
            return;
        }
         // Biz code omitted here.
    }
}
  • 背景B: 各个类的各个方法执行前需增加“用户是否登录”的权限校验功能。
    • B猿一阵嘲笑:呵呵,这不是侵入原代码了么?要是再来几个其他的功能入侵怎么办。下面是他的修改方案。将原“非法入侵”部分抽离成另一个私有方法进行调用。其他几个类的方法也进行了同样的处理。
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:40
 */
public class ClassA {

    private  User user;

    public void doSomethingInA(){
        
        checkIfLogIn();
         // Biz code omitted here.
    }

    private void checkIfLogIn() {
        if (user.isStatus()){
            return;
        }
    }
}
  • 背景B: 各个类的各个方法执行前需增加“用户是否登录”的权限校验功能。
    • C猿看了摇摇头,心想:从整体来看,这三个类中被抽离出来的方法实现的功能不是一样的么?这对于整个系统来说不还是冗余代码么?于是他有了以下的改方案。利用AOP切面,在需进行用户登录校验的方法上加相应的注解,在不侵入原业务代码的同时也能实现较大程度的代码复用。这样的实现,代码的扩展性,可维护性也更强。
 /**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:40
 */
public class ClassA {

    private  User user;

    @CheckLogInListener
    public void doSomethingInA(){
         // Biz code omitted here.
    }
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 23:04
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckLogInListener {
    String name() default "";
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 23:09
 */
@Aspect
@Component
@Slf4j
public class CheckLogInHandler {
    private static final String EXECUTION="@annotation(checkLogInListener)";

    @Before(EXECUTION)
    public void checkIfLogIn(CheckLogInListenercheckLogInListener) {
        // checkIfLogIn code omitted here.
    }
}
  • 背景C: 各个类的各个方法执行前去掉“用户是否登录”的权限校验功能,并加上方法前后的日志打印以及业务异常失败重试功能。并要求日志打印的功能执行顺序在也是失败重试功能之前。
    • 聪明的A猿听了背景B下C猿的解法后恍然大悟,刷刷刷给出了下面的解法。
 /**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 16:30
 */
@Service
public class AopTestService {

    @RetryListener(retryForException = Exception.class)
    @LogListener(logLevel = "info")
    public String testForAopOrder(MyInfo myInfo) {
    
        return myInfo.toString();
        
    }
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/29 22:11
 */
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LogListener {

    String logLevel() default "";

}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/29 22:14
 */

@Slf4j
@Aspect
@Component
public class LogHandler {

    private static final String EXECUTION = "@annotation(logListener)";

    @Around(EXECUTION)
    public void doAround(ProceedingJoinPoint point, LogListener logListener) {
        log.info("LOG-begins. Args={}, logLevel={}", point.getArgs(), logListener.logLevel());
        try {
            Object returnObj = point.proceed();
            log.info("LOG-ends. ReturnObj={}", returnObj);
        } catch (Throwable throwable) {
            log.error("Log-ends with error,error={}", throwable);
        }
    }
}
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 16:17
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RetryListener {

    Class<?>[] retryForException();

    Class<?>[] noRetryForException() default Exception.class;
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 16:28
 */
@Slf4j
@Aspect
@Component
public class RetryHandler {
    private static final String EXECUTION = "@annotation(retryListener)";

    @Around(EXECUTION)
    public void doAround(ProceedingJoinPoint point, RetryListener retryListener) {
        log.info("Retry-begins. Args={}, retryForExceptions={}", point.getArgs(), retryListener.retryForException());
        try {
            Object returnObj = point.proceed();
            log.info("Retry-ends. ReturnObj={}", returnObj);
        } catch (Throwable throwable) {
            log.error("Retry-ends with error,error={}", throwable);
        }
    }
}
  • 背景C: 各个类的各个方法执行前去掉“用户是否登录”的权限校验功能,并加上方法前后的日志打印以及业务失败重试功能。并要求日志打印的功能执行顺序在也是失败重试功能之前。
    • 请求后的结果是:
      在这里插入图片描述
  • 背景C: 各个类的各个方法执行前去掉“用户是否登录”的权限校验功能,并加上方法前后的日志打印以及业务失败重试功能。并要求日志打印的功能执行顺序在也是失败重试功能之前。
    • 看到希望的执行结果,A猿沾沾自喜,但B猿却提出了A猿的解法有投机取巧的嫌疑:虽然此解法得到了想要的结果,但并没有指定具体的Aspect之间的执行顺序。B猿接过代码,在两个Aspect上分别加上了@Order注解。并给出了解释:@Order中的数字代表优先级,数字越小,优先级越高(越先执行)。
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/29 22:14
 */
@Order(0)
@Slf4j
@Aspect
@Component
public class LogHandler {

    private static final String EXECUTION = "@annotation(logListener)";

    @Around(EXECUTION)
    public void doAround(ProceedingJoinPoint point, LogListener logListener) {
        log.info("LOG-begins. Args={}, logLevel={}", point.getArgs(), logListener.logLevel());
        try {
            Object returnObj = point.proceed();
            log.info("LOG-ends. ReturnObj={}", returnObj);
        } catch (Throwable throwable) {
            log.error("Log-ends with error,error={}", throwable);
        }
    }
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 16:28
 */
@Order(1)
@Slf4j
@Aspect
@Component
public class RetryHandler {
    private static final String EXECUTION = "@annotation(retryListener)";

    @Around(EXECUTION)
    public void doAround(ProceedingJoinPoint point, RetryListener retryListener) {
        log.info("Retry-begins. Args={}, retryForExceptions={}", point.getArgs(), retryListener.retryForException());
        try {
            Object returnObj = point.proceed();
            log.info("Retry-ends. ReturnObj={}", returnObj);
        } catch (Throwable throwable) {
            log.error("Retry-ends with error,error={}", throwable);
        }
    }
}
  • 背景C: 各个类的各个方法执行前去掉“用户是否登录”的权限校验功能,并加上方法前后的日志打印以及业务失败重试功能。并要求日志打印的功能执行顺序在也是失败重试功能之前。
    • 那么为什么在不指定Order的情况下,A猿的实验结果也是“正确”的呢?C猿通过阅读源码给出了下面的解释:
      • 首先,不指定Order的情况下,所有的Aspect的优先级都是最低的(lowest precedence)
      • 在不指定Order的情况下,Aspect的执行顺序遵从目标对象在容器中的注册顺序有关。
      • 这也就表明了面向切面编程的“不可控性”,当同一套代码部署PROD环境中,如果不指定Order,可能会出现与DEV环境不同的执行顺序,导致不可预料的效果。之前在Spring.doc文档中看过作者形容不指定Order时,不同的Aspect执行的顺序,印象很深的一个词是“Arbitrary”,即Aspect的执行顺序是随意的,不同的jvm对于执行顺序都有其随机算法。不知道这样理解对不对。但希望今后大家在使用AOP编程时能使用有效的方法控制切入代码和原有代码的执行顺序,否则后患无穷。
        在这里插入图片描述
        在这里插入图片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章