SpringAop
背景概念
介绍
- “横切”的技术,剖解开封装对象的内部,将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为“Aspect”,即切面;简单来说就是将那些与业务无关,却为业务模块所公共调用的逻辑或者责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性性和可维护性;
- AOP把软件系统分为两部分:核心关注点和横切关注点;比如:日志系统、
编程范式概览
- 面向过程编程
- 面向对象编程
- 函数式编程
- 事件驱动编程
- 面向切面编程
解耦分离(关注点分离、Separation of Concerns)
- 项目角度
水平分离:展示层 ==> 服务层 ==> 持久层
- 业务角度
- 功能角度
使用AOP的好处?
- 集中处理某一关注点/横切逻辑
- 可以很方便地添加/删除关注点
- 侵入性少,增强代码可读性及可维护性
场景
- 权限控制(@PreAuthorize)
- 缓存控制(@Cacheable)
- 事务控制(@Transtacional)
-
异常处理(@AfterThrowing)
- 审计日志、性能监控、分布式追踪
补充概念
连接点(joinPoint):被拦截到的点,因为spring只支持方法类型的连接点,所以在spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器;
使用方法
注解的方式:@Aspect(切面配置类)
- @PointCut:用expression表达式,描述在哪些类/方法注入代码
- @Advice:执行方法的什么时机注入
@PointCut的expression表达式
designators(指示器):
- 匹配方法:execution()
- 匹配注解:@Target()、@Within()、@args()、@annotation()
- 匹配包/类型:within()
- 匹配对象:this()、bean()、target()
- 匹配参数:args()
wildcards(通配符)
- * :匹配任意数量的字符
- .. :匹配任意数量的子包、参数
- + :指定类及其子类
operators(运算符)
- &&:与
- ||:或
- !:非
举例:
修饰符为public、返回值任意、包名为com.demo.service、任意类、任意方法、里面的任意参数
- “execution(public * com.demo.service.*.*(..))”
修饰符为public、返回值任意、包名为com.demo.service及其子包、任意类、任意方法、里面的任意参数
- “execution(public * com.demo.service..*.*(..))”
@Advice注解
- @Before:前置通知
- @After(finally):后置通知,方法执行完之后
- @AfterReturning:返回通知,成功执行之后
- @AfterThrowing:异常通知,抛出异常之后
- @Around:环绕通知
代码演示
思路:在service包及其子包的以Impl结束的类,类里面有insert开头的方法、delete开头的方法。利用AOP在前面判断权限、而不直接调用实现方法
举例:在service.impl.ProductServiceImpl里面的insert()、delete()进行切入。
目录结构
代码实现:
Pojo的实体类、使用@Accessors(chain = true)可以进行链式编程
package com.example.springaop.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; /** * @author SHshuo * @data 2021/10/25--9:33 */ @Data @Accessors(chain = true) @AllArgsConstructor @NoArgsConstructor public class Product { private Long id; private String name; }接口
package com.example.springaop.service; import com.example.springaop.pojo.Product; /** * @author SHshuo * @data 2021/10/25--9:35 */ public interface ProductService { void insert(Product product); void delete(Long id); }实现接口的具体方法
package com.example.springaop.service.Impl; import com.example.springaop.pojo.Product; import org.springframework.stereotype.Service; /** * @author SHshuo * @data 2021/10/25--9:36 */ @Service public class ProductServiceImpl implements com.example.springaop.service.ProductService { // @Autowired // private CheckAccess checkAccess; @Override public void insert(Product product) { // checkAccess.checkAccess(); System.out.println("insert Product"); } @Override public void delete(Long id) { // checkAccess.checkAccess(); System.out.println("delete Product"); } }进行判断的方法类
package com.example.springaop.service.Impl; import com.example.springaop.security.CurrentUserHolder; import org.springframework.stereotype.Component; /** * @author SHshuo * @data 2021/10/25--9:51 */ @Component public class CheckAccess { public void checkAccess(){ String user = CurrentUserHolder.get(); if(!"admin".equals(user)){ System.out.println("operation not allow"); }else{ System.out.println("operation allow"); } } }使用ThreadLocal将对象共有资源
我感觉其实也可以注入@Autowired private Product product,但是这样实体类就需要添加@Component注解添加到IOC中,就违背了封装性的原则、增加了耦合。
package com.example.springaop.security; /** * @author SHshuo * @data 2021/10/25--9:47 * 将对象暴露出来、共有资源 */ public class CurrentUserHolder { private static final ThreadLocal<String> holder = new ThreadLocal<>(); public static String get(){ return holder.get() == null ? "unknown" : holder.get(); } public static void set(String user){ holder.set(user); } }
使用AOP进行切入:先将类声明为@Aspect
package com.example.springaop.security; import com.example.springaop.service.Impl.CheckAccess; import org.aspectj.lang.annotation.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @author SHshuo * @data 2021/10/25--10:08 * 声明Aop */ @Component @Aspect public class SecurityAspect { @Autowired private CheckAccess checkAccess; // service目录下、包含子目录。。后缀为Impl的类里面已insert开头的方法的任意参数 @Pointcut("execution(* com.example.springaop.service..*Impl.insert*(..))") public void insert(){ } // service目录下、包含子目录。。后缀为Impl的类里面已delete开头的方法的任意参数 @Pointcut("execution(* com.example.springaop.service..*Impl.delete*(..))") public void delete(){ } @Before("insert() || delete()") public void check(){ checkAccess.checkAccess(); } }使用测试单元测试
package com.example.springaop; import com.example.springaop.pojo.Product; import com.example.springaop.security.CurrentUserHolder; import com.example.springaop.service.ProductService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class SpringAopApplicationTests { @Autowired private ProductService productService; @Test() public void deleteTest() { try { // 创建共有对象 CurrentUserHolder.set("admin"); // 创建对象 Product product = new Product(); product.setId(2L).setName("admin"); // 调用方法 productService.insert(product); productService.delete(1L); }catch (Exception e){ e.printStackTrace(); } } }
原理
Spring AOP 代理的生成过程:
- 通过ProxyFactoryBean(FactoryBean接口的实现)来去配置相应的代理对象的信息
- 在获取ProxyFactoryBean实例时,本质上并未获取到ProxyFactoryBean的对象,而是获取到由ProxyFactoryBean所返回的那个对象实例(getObject方法)
- 在整个ProxyFactoryBean实例的构建与缓存的过程中,其流程与普通bean的对象完全一致
- 差别在于,当创建完成ProxyFactoryBean对象后,Spring会判断当前所创建的对象是否是一个FactoryBean实例;如果不是,那么Spring就直接将其返回
- 如果是,Spring会根据我们在配置信息中所指定的各种元素,如目标对象是否实现了接口以及Advisor等信息,使用动态代理或是CGLIB等方式来为目标对象创建相应的代理对象
- 当相应的代理对象创建完毕后,Spring就会通过ProxyFactoryBean的getObject方法将所创建的代理对象返回
- 对象返回到调用端,它本质上是一个代理对象,可以代理对目标对象的访问与调用,这个代理对象对用户来说,就好像是一个目标对象一样
- 客户在使用代理对象时,可以正常调用目标对象的方法,同时在执行过程中,会根据我们在配置文件中所配置的信息来在调用前后执行额外的附加逻辑
织入时机
- 编译期(AspectJ)
- 类加载时(AspectJ5 + )
- 运行时(SpringAOP)
运行时织入实现
通过代理对象实现、两种实现方式:静态代理、动态代理
静态代理
- 静态代理类似于代理模式的实现(具体看设计模式里面的代理模式)
- 参考:https://blog.nowcoder.net/n/dffcbae4c4dd4a389872a0ff34fc4e37
- 缺点:
100个target需要委托(represents)100个proxy。但是proxy执行方法前后基本一致,就会产生大量的冗余。
同时接口每多增加一个方法,对应的proxy代理类就需要重写一次,建立与对应的方法的委托。
动态代理:
- proxy代理类不需要一个一个手动的增加新的委托,基本不需要更改。利用反射实现,给我的感觉就是动态的与真实的类里面所有方法进行委托
- JDK代理:基于接口的代理实现
- Cglib代理:基于继承的代理实现
JDK动态代理:
要点:
- 通过proxy类动态生成代理类(Proxy.newProxyInstance)
- 实现接口,实现织入的逻辑(invocationHandler)
目录结构:
代码实现:
接口:
package com.example.jdkproxy.pojo; /** * @author SHshuo * @data 2021/10/31--14:56 */ public interface Subject { void request(); void newRequest(); }
真实的类:
package com.example.jdkproxy.pojo; /** * @author SHshuo * @data 2021/10/31--14:44 */ public class RealSubject implements Subject{ @Override public void request(){ System.out.println("hshuo"); } @Override public void newRequest() { System.out.println("new Hshuo"); } }代理类:method.invoke(realSubject, args)
package com.example.jdkproxy.proxy; import com.example.jdkproxy.pojo.RealSubject; import org.springframework.beans.factory.annotation.Autowired; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * @author SHshuo * @data 2021/10/31--14:43 * 相当于Aspect、jdk动态代理。不需要实现subject接口、再委托对应的方法了 * method.invoke()感觉是直接与对应的真实类联系起来、而不需要每次都委托具体的方法 */ public class JdkProxy implements InvocationHandler { @Autowired private RealSubject realSubject; // 传入参数 public JdkProxy (RealSubject realSubject){ this.realSubject = realSubject; } // 动态反射方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try { result = method.invoke(realSubject, args); }catch (Exception e){ System.out.println("e:" + e.getMessage()); throw e; }finally { System.out.println("反射结束"); } return result; } }Client调用:Proxy.newProxyInstance
package com.example.jdkproxy.controller; import com.example.jdkproxy.pojo.RealSubject; import com.example.jdkproxy.pojo.Subject; import com.example.jdkproxy.proxy.JdkProxy; import java.lang.reflect.Proxy; /** * @author SHshuo * @data 2021/10/31--14:44 * 使用Proxy.newProxyInstance反射创建对象 */ public class ClentController { public static void main(String[] args) { Subject subject = (Subject) Proxy.newProxyInstance(ClentController.class.getClassLoader(), new Class[]{Subject.class}, new JdkProxy(new RealSubject())); subject.request(); subject.newRequest(); } }
Cglib动态代理:
要点:
- 调用enhancer.create()
- 织入实现MethodInterceptor里面的intercept
- 给我感觉的区别:将代理类实现的代码部分移动到client设置
目录结构:
代码实现:
真实的类与接口与JDK代理代码一致
接口:
package com.example.cglibproxy.service; /** * @author SHshuo * @data 2021/10/31--16:01 */ public interface Subject { void request(); void newRequest(); }真实的类
package com.example.cglibproxy.service.Impl; import com.example.cglibproxy.service.Subject; /** * @author SHshuo * @data 2021/10/31--16:02 * 真实对象 */ public class RealSubject implements Subject { @Override public void request() { System.out.println("hshuo"); } @Override public void newRequest() { System.out.println("new Hshuo"); } }代理类:methodProxy.invokeSuper(o, objects)
package com.example.cglibproxy.proxy; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * @author SHshuo * @data 2021/10/31--16:03 * 基于MethodInterceptor实现 */ public class CglibProxy implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { Object result = null; try { result = methodProxy.invokeSuper(o, objects); }catch (Exception e){ System.out.println("e:" + e.getMessage()); throw e; }finally { System.out.println("反射结束"); } return result; } }Client调用:enhancer.create()
package com.example.cglibproxy.controller; import com.example.cglibproxy.proxy.CglibProxy; import com.example.cglibproxy.service.Impl.RealSubject; import com.example.cglibproxy.service.Subject; import org.springframework.cglib.proxy.Enhancer; /** * @author SHshuo * @data 2021/10/31--16:01 * Enhancer调用 */ public class ClientController { public static void main(String[] args) { // 相当于proxy.newProxyInstance() Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(RealSubject.class); enhancer.setCallback(new CglibProxy()); Subject subject = (Subject) enhancer.create(); // 调用 subject.request(); subject.newRequest(); } }
Cglib与JDK代理的区别:
- JDK代理只能针对有接口(InvocationHandler)的类的接口方法进行动态代理
- Cglib基于继承来实现代理,无法对static(方法+类)、final类进行代理
- JDK、Cglib无法对private方法进行代理
Spring如何创建AOP代理类
- 由AopProxyFactory根据AdvisedSupport对象的配置类决定;
- 默认的策略是如果目标类是接口,则使用JDK代理;否则使用Cglib代理。
hshuo的面试之路 文章被收录于专栏
作者目标是找到一份Java后端方向的工作 此专栏用来记录从Bilibili、书本、其他优质博客上面学习的内容 用于巩固、总结内容 主要包含Docker、Dubbo、Java基础、JUC、Maven、MySQL、Redis、SpringBoot、SpringCloud、数据结构、杂文、算法、计算机网络、操作系统、设计模式等相关内容