• Spring_AOP切面编程


    AOP切面编程

    简介

    AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

    应用场景

    实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核业务。在编程实现时我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般就是借助AOP进行实现。

    AOP就是要基于OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能并可以"控制"对象的执行。例如AOP应用于项目中的日志处理,事务处理,权限处理,缓存处理等等。如图所示:

    原理分析

    Spring AOP底层基于代理机制实现功能扩展:

    1. 假如目标对象(被代理对象)实现接口,则底层可以采用JDK动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
    2. 假如目标对象(被代理对象)没有实现接口,则底层可以采用CGLIB代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)。

    注意, 如果是springboot, 默认使用的CGLIB代理机制, 如果想使用JDK代理机制, 可使用如下配置

    # 使用jdk代理机制
    spring.aop.proxy-target-class=false
    

    相关术语

    • 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 ....

    • 切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类。

    • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。

    • 目标(Target):被通知对象。

    • 代理(Proxy):向目标对象应用通知之后创建的对象。

    • 切入点(PointCut):切面通知 执行的 “地点”的定义。

    • 连接点(JointPoint):与切入点匹配的执行点。

    快速入门(注解方式)

    导入pom依赖

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.5</version>
    </dependency>
    <!--spring等依赖略-->
    

    业务类

    假设这是我们的业务层 (service接口略)

    @Service
    public class UserServiceImpl implements UserService {
        @Override
        public void add() {
            System.out.println("新增用户");
        }
    }
    

    定义切面

    注意, @Aspect 和 @Component 这两个注解必须有

    @Aspect
    @Component
    public class UserServiceAspect {
    	/**
    	 * @PointCut 注解用户定义注解, 具体方式可以基于特定表达式进行实现
    	 * 1. bean为一种切入点表达式类型
    	 * 2. sysUserServiceImpl 为Spring容器中的一个bean的名字
    	 * 这里的含义是当sysUserServiceImpl对象中的任意方法执行时, 都有本切面
    	 * 对象的通知方法做功能增强
    	 */
        @Pointcut("bean(userServiceImpl)")
        public void doUserService() {}
        
    	/**
    	 * 由@Around 注解描述的方法为一个环绕通知方法, 我们可以在此方法内部
    	 * 手动调用目标方法(通过连接点对象ProceedingJoinPoint的proceed方法进行调用)
    	 * 环绕通知 此环绕通知使用的切入点为"bean(sysUserServiceImpl)"
    	 * 环绕通知的特点: 
    	 * 1. 编写(格式): 
    	 * 		a. 方法的返回值为Object, 
    	 * 		b. 方法参数为ProceedingJoinPoint类型, 
    	 * 		c. 方法抛出异常为Throwable
    	 * 2. 应用:
    	 * 		a. 目标方法执行之前和之后都可以进行功能拓展
    	 * 		b. 相对于其他通知优先级最高
    	 * @param jp 为一个连接对象(封装了正在要执行的目标方法信息)
    	 * @return 目标方法的执行结果
    	 */
        @Around("doUserService()") // 环绕消息
        public Object AroundUserService(ProceedingJoinPoint pj) throws Throwable {
            System.out.println("执行前");
            Object result = pj.proceed(); // 执行业务中的方法
            System.out.println("执行后");
            return null;
        }
    
    }
    

    配置文件

    <context:component-scan base-package="com.aaron"/>
    <context:annotation-config/>
    <!-- 此功能为开启aop的注解功能, 否则aop注解无效 -->
    <aop:aspectj-autoproxy/>
    

    进行测试

    public class AopTest {
    
        AbstractApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    
        @Test
        public void test1() { // 测试环境没有问题
            UserService userService = (UserService) ac.getBean("userServiceImpl");
            userService.add();
        }
    
    }
    

    结果

    执行前
    新增用户
    执行后
    

    至此 OJBK 了

    切面通知

    通知类型

    在基于Spring AOP编程的过程中,基于AspectJ框架标准,spring中定义了五种类型的通知(通知描述的是一种扩展业务),它们分别是:

    • 前置通知 (@Before) 。
    • 返回通知 (@AfterReturning) 。
    • 异常通知 (@AfterThrowing) 。
    • 后置通知 (@After)。
    • 环绕通知 (@Around) :重点掌握(优先级最高)

    通知执行顺序

    假如这些通知全部写到一个切面对象中,其执行顺序及过程,如图所示:

    我们可以对其进行测试, 这里依然使用上面的service

    @Service
    public class UserServiceImpl implements UserService {
        @Override
        public void add() {
            System.out.println("新增用户");
        }
    }
    

    切面

    @Aspect
    @Component
    public class UserServiceAspect {
    
        @Pointcut("bean(userServiceImpl)")
        public void doUserService() {}
    
        @Before("doUserService()")
        public void doBefore() {
            System.out.println("@Before");
        }
    
        @Around("doUserService()") // 环绕消息
        public Object AroundUserService(ProceedingJoinPoint pj) throws Throwable {
            System.out.println("@Around.Before");
            Object result = pj.proceed();
            System.out.println("@Around.After");
            return null;
        }
    
        @After("doUserService()")
        public void doAfter() {
            System.out.println("@After");
        }
    
        @AfterReturning("doUserService()")
        public void doAfterReturnin() {
            System.out.println("@AfterReturning");
        }
    
        @AfterThrowing("doUserService()")
        public void doAfterThrowing() {
            System.out.println("@AfterThrowing");
        }
    
    }
    

    运行结果

    @Around.Before
    @Before
    新增用户
    @Around.After
    @After
    @AfterReturning
    

    切入点表达式

    表达式用于@Pointcut() 注解中的参数

    Spring中通过切入点表达式定义具体切入点,其常用AOP切入点表达式定义及说明:

    指示符 作用
    bean 用于匹配指定bean对象的所有方法
    within 用于匹配指定包下所有类内的所有方法
    execution 用于按指定语法规则匹配到具体方法
    @annotation 用于匹配指定注解修饰的方法

    bean表达式(重点)

    bean表达式一般应用于类级别,实现粗粒度的切入点定义,案例分析:

    • bean("userServiceImpl")指定一个userServiceImpl类中所有方法
    • bean("*ServiceImpl")指定所有后缀为ServiceImpl的类中所有方法

    说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的名字应该是spring容器中某个bean的name。

    within表达式(了解)

    within表达式应用于类级别,实现粗粒度的切入点表达式定义,案例分析:

    • within("aop.service.UserServiceImpl")指定当前包中这个类内部的所有方法
    • within("aop.service.*") 指定当前目录下的所有类的所有方法
    • within("aop.service..*") 指定当前目录以及子目录中类的所有方法

    execution表达式(了解)

    语法:execution(返回值类型 包名.类名.方法名(参数列表))。

    • execution(void aop.service.UserServiceImpl.addUser())匹配addUser方法。
    • execution(void aop.service.PersonServiceImpl.addUser(String)) 方法参数必须为String的addUser方法。
    • execution(* aop.service..*.*(..)) 万能配置。

    @annotation表达式(重点)

    @annotaion表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析

    一般我们可以使用自定义注解放在指定的方法上

    • @annotation(@Pointcut("@annotation(com.aaron.annotation.RequiredCache)")) 匹配有此@RequiredCache注解描述的方法。

    自定义注解如下:

    /**
     * 自定义注解: (使用@interface定义注解, 默认所有注解都继承Annotation)
     * @Target 注解用于告诉JDK我们自己写的注解可以描述的对象
     * @Rectention 注解用于告诉JDK我们自己写的注解何时有效
     * 说明: 所有的注解都是一种元数据(Meta Data) 一种描述数据的数据
     * 	(例如使用的注解描述类, 描述方法, 描述属性, 木奥数方法参数等等)
     * @author zpk
     *
     */
    @Target(ElementType.METHOD) // 只能用于方法上
    @Retention(RetentionPolicy.RUNTIME) // 在运行时有效
    public @interface RequiredCache {
    
    }
    

    其中@RequiredCache为我们自己定义的注解,当我们使用@RequiredCache注解修饰业务层方法时,系统底层会在执行此方法时进行日扩展操作。

    注解表达式案例

    自定义注解

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
    
    }
    

    service实现类(接口略)

    这里我们只对删除用户使用自定义的注解

    @Service
    public class UserServiceImpl implements UserService {
        @Override
        public void add() {
            System.out.println("新增用户");
        }
    
        @MyAnnotation
        @Override
        public void delete() {
            System.out.println("删除用户");
        }
    }
    

    切面

    @Aspect
    @Component
    public class UserServiceAspect {
    
        @Pointcut("@annotation(com.aaron.annotation.MyAnnotation)")
        public void doUserService() {}
    
        @Around("doUserService()")
        public void doAround(ProceedingJoinPoint pj) throws Throwable {
            System.out.println("before"); // 执行前输出
            pj.proceed(); // 执行业务方法
        }
    
    }
    

    测试

    对add方法进行测测试

    public class AopTest {
        AbstractApplicationContext ac = 
            	new ClassPathXmlApplicationContext("applicationContext.xml");
        @Test
        public void test1() { // 测试环境没有问题
            UserService userService = (UserService) ac.getBean("userServiceImpl");
            userService.add();
        }
    }
    

    结果

    新增用户
    

    对delete方法进行测试

    public class AopTest {
        AbstractApplicationContext ac = 
            	new ClassPathXmlApplicationContext("applicationContext.xml");
        public void test1() { // 测试环境没有问题
            UserService userService = (UserService) ac.getBean("userServiceImpl");
            userService.delete();
        }
    }
    

    结果

    before
    删除用户
    

    可见, 只能对有自定义注解的方法横切

    多切面优先级

    可以在@Aspect注解的位置加上一个@Order(num)注解, num参数即为切面的优先级顺序, 数值越小, 优先级越高

    参数默认值为: Integer.MAX_VALUE

    如下案例:

    业务层

    @Service
    public class UserServiceImpl implements UserService {
        @Override
        public void add() {
            System.out.println("新增用户");
        }
    
        @Override
        public void delete() {
            System.out.println("删除用户");
        }
    }
    

    切面一: 优先级为1

    @Aspect
    @Component
    @Order(1)
    public class UserServiceAspect {
    
        @Pointcut("bean(userServiceImpl)")
        public void doUserService() {}
    
        @Around("doUserService()") // 环绕消息
        public Object AroundUserService(ProceedingJoinPoint pj) throws Throwable {
            System.out.println("@Around.Before1");
            Object result = pj.proceed();
            System.out.println("@Around.After1");
            return null;
        }
    
    }
    

    切面二: 优先级为2

    @Aspect
    @Component
    @Order(2)
    public class UserServiceAspect2 {
    
        @Pointcut("bean(userServiceImpl)")
        public void doUserService() {}
    
        @Around("doUserService()") // 环绕消息
        public Object AroundUserService(ProceedingJoinPoint pj) throws Throwable {
            System.out.println("@Around.Before2");
            Object result = pj.proceed();
            System.out.println("@Around.After2");
            return null;
        }
    
    }
    

    测试

    public class AopTest {
    
        AbstractApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    
        @Test
        public void test1() { // 测试环境没有问题
            UserService userService = (UserService) ac.getBean("userServiceImpl");
            userService.delete();
        }
    
    }
    

    结果:

    @Around.Before1
    @Around.Before2
    删除用户
    @Around.After2
    @Around.After1
    

    基于XML的实现(了解)

    以上面切面通知顺序测试的案例为基础, 配置文件如下

    <!-- 切面, 注册bean -->
    <bean class="com.aaron.aspect.UserServiceAspect">
    </bean>
    
    <!-- 配置 -->
    <aop:config>
        <aop:aspect ref="userServiceAspect">
            <!-- 配置切点: 相当于注解声明中的空方法
    		id属性是切点的命名, 
       		expression属性是切入点表达式
    		-->
            <aop:pointcut id="doUserService" expression="bean(userServiceImpl)"/>
            
            <!-- 
    		method属性指向的是对应的方法, 
    		pointcut-ref属性是切点-->
            <aop:before method="doBefore" pointcut-ref="doUserService"/>
            <aop:around method="AroundUserService" pointcut-ref="doUserService"/>
            <aop:after method="doAfter" pointcut-ref="doUserService"/>
            <aop:after-returning method="doAfterReturnin" pointcut-ref="doUserService" />
            <aop:after-throwing method="doAfterThrowing" pointcut-ref="doUserService"/>
            ...
        </aop:aspect>
        ...
    </aop:config>
    

    扩展: 注解表达式高级操作

    假设自定义注解中有参数

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
        String value() default "";
    }
    

    我们还使用指点的service实现类(接口略)

    @Service
    public class UserServiceImpl implements UserService {
        @Override
        public void add() {
            System.out.println("新增用户");
        }
    
        @MyAnnotation("注解提示: 执行删除")
        @Override
        public void delete() {
            System.out.println("删除用户");
        }
    }
    

    切面

    我们可以获取方法以及对应的注解, 和注解中的内容

    @Aspect
    @Component
    public class UserServiceAspect {
    
        @Pointcut("@annotation(com.aaron.annotation.MyAnnotation)")
        public void doUserService() {}
    
        @Around("doUserService()")
        public void doAround(ProceedingJoinPoint pj) throws Throwable {
            // 获取方法签名
            MethodSignature ms = (MethodSignature) pj.getSignature();
            // 获取参数类型字节码对象的数组
            Class<?>[] typeParameters = ms.getMethod().getParameterTypes();
            // 获取正在执行的方法参数数组
            Object[] args = pj.getArgs();
            // 获取此注解所在的类字节码对象
            Class<?> clazz = pj.getTarget().getClass();
            // 获取包名+类名
            String className = clazz.getName();
            // 获取此注解下正在执行的方法名字
            String methodName = ms.getMethod().getName();
            // 获取参类型字节码对象数组
            Class<?>[] parameterTypes = ms.getMethod().getParameterTypes();
            // 获取类中对应的方法
            Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
            // 获取方法上的注解
            MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
            // 获取注解的参数
            String value = annotation.value();
    
            pj.proceed();
        }
    
    }
    

    扩展: 自定义缓存

    自定义一个简单的缓存

    简单Cache对象的实现, SimpleCache

    产品级别Cache要考虑:

    • 存储结构, 存储内容(存储对象字节还是存储对象引用)
    • 缓存淘汰策略(缓存满的时候是否要淘汰数据)
    • GC策略(JVM内存不足时, 是否允许清除Cache中数据)
    • 任务调度策略(是否需要每隔一段时间刷新一下缓存)
    • 日志记录方式(记录命中次数)
    • 线程安全
    • ......
    @Component
    public class SimpleCache {
    	
    	private Map<Object, Object> cache = new ConcurrentHashMap<>();
    	
        // 存入缓存
    	public boolean putObject(Object key, Object value) {
    		cache.put(key, value);
    		return true;
    	}
    	
        // 从缓存中获取
    	public Object getObject(Object key) {
    		return cache.get(key);
    	}
    	
        // 清空缓存
    	public boolean clearObject() {
    		cache.clear();
    		return true;
    	}
    }
    
  • 相关阅读:
    笔记04_正确使用Heterogeneous元件
    java网络通信:伪异步I/O编程(PIO)
    java网络通信:异步非阻塞I/O (NIO)
    lua源码学习篇二:语法分析
    lua源码学习篇三:赋值表达式解析的流程
    java网络通信:netty
    lua源码学习篇一:环境部署
    lua源码学习篇四:字节码指令
    java网络通信:同步阻塞式I/O模型(BIO)
    前端项目开发流程
  • 原文地址:https://www.cnblogs.com/zpKang/p/13187470.html
Copyright © 2020-2023  润新知