写点什么

Spring AOP 基础、快速入门

  • 2024-12-10
    福建
  • 本文字数:9900 字

    阅读完需:约 32 分钟

介绍


AOP,面向切面编程,作为面向对象的一种补充,将公共逻辑(事务管理、日志、缓存、权限控制、限流等)封装成切面,跟业务代码进行分离,可以减少系统的重复代码降低模块之间的耦合度。切面就是那些与业务无关,但所有业务模块都会调用的公共逻辑。


先看一个例子:如何给如下 UserServiceImpl 中所有方法添加进入方法的日志,


public class UserServiceImpl implements IUserService {
@Override public List<User> findUserList() { System.out.println("execute method: findUserList"); return Collections.singletonList(new User("seven", 18)); }
@Override public void addUser() { System.out.println("execute method: addUser"); // do something }
}
复制代码


将记录日志功能解耦为日志切面,它的目标是解耦。进而引出 AOP 的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中!



OOP 面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。AOP 则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异



AOP 相关术语


首先要知道,aop 不是 spring 所特有的,同样的,这些术语也不是 spring 所特有的。是由 AOP 联盟定义的


切面(Aspect):切面是增强切点的结合,增强和切点共同定义了切面的全部内容。多个切面之间的执行顺序如何控制?首先要明确,在“进入”连接点的情况下,最高优先级的增强会先执行;在“退出”连接点的情况下,最高优先级的增强会最后执行。


通常使用 @Order 注解直接定义切面顺序


实现 Ordered 接口重写 getOrder 方法。Ordered.getValue()方法返回值(或者注解值)较低的那个有更高的优先级。


连接点(Join point):一般指方法,在 Spring AOP 中,一个连接点总是代表一个方法的执行。连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。当然,连接点也可能是类初始化、方法执行、方法调用、字段调用或处理异常等


增强(或称为通知)(Advice):在 AOP 术语中,切面的工作被称为增强。知实际上是程序运行时要通过 Spring AOP 框架来触发的代码段。


前置增强(Before):在目标方法被调用之前调用增强功能;


后置增强(After):在目标方法完成之后调用增强,此时不会关心方法的输出是什么;


返回增强(After-returning ):在目标方法成功执行之后调用增强;


异常增强(After-throwing):在目标方法抛出异常后调用增强;


环绕增强(Around):增强包裹了被增强的方法,在被增强的方法调用之前和调用之后执行自定义的逻辑

切点(Pointcut):切点的定义会匹配增强所要织入的一个或多个连接点。通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。以 AspectJ 举例,说白了就可以理解为是 execution 表达式


引入(Introduction):引入允许我们向现有类添加新方法或属性。 在 AOP 中表示为干什么(引入什么)


目标对象(Target Object): 被一个或者多个切面(aspect)所增强(advise)的对象。它通常是一个代理对象。


织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。在 AOP 中表示为怎么实现的;织入分为编译期织入、类加载期织入、运行期织入;SpringAOP 是在运行期织入



execution 表达式格式:


execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
复制代码


  • ret-type-pattern 返回类型模式, name-pattern 名字模式和 param-pattern 参数模式是必选的, 其它部分都是可选的。返回类型模式决定了方法的返回类型必须依次匹配一个连接点。 使用的最频繁的返回类型模式是*它代表了匹配任意的返回类型

  • declaring-type-pattern, 一个全限定的类型名将只会匹配返回给定类型的方法。

  • name-pattern 名字模式匹配的是方法名。 可以使用*通配符作为所有或者部分命名模式。

  • param-pattern 参数模式稍微有点复杂:()匹配了一个不接受任何参数的方法, 而(..)匹配了一个接受任意数量参数的方法(零或者更多)。 模式(*)匹配了一个接受一个任何类型的参数的方法。 模式(*,String)匹配了一个接受两个参数的方法,第一个可以是任意类型, 第二个则必须是 String 类型。


例如:


execution(* com.seven.springframeworkaopannojdk.service.*.*(..))
复制代码


Spring AOP 和 AspectJ 的关系


AspectJ 是一个 java 实现的 AOP 框架,它能够对 java 代码进行 AOP 编译(一般在编译期进行),让 java 代码具有 AspectJ 的 AOP 功能(当然需要特殊的编译器)。可以这样说 AspectJ 是目前实现 AOP 框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ 与 java 程序完全兼容,几乎是无缝关联,因此对于有 java 编程基础的工程师,上手和使用都非常容易。


AspectJ 是更强的 AOP 框架,是实际意义的 AOP 标准


Spring 为何不写类似 AspectJ 的框架? Spring AOP 使用纯 Java 实现, 它不需要专门的编译过程, 它一个重要的原则就是无侵入性(non-invasiveness); Spring 小组完全有能力写类似的框架,只是 Spring AOP 从来没有打算通过提供一种全面的 AOP 解决方案来与 AspectJ 竞争。Spring 的开发小组相信无论是基于代理(proxy-based)的框架如 Spring AOP 或者是成熟的框架如 AspectJ 都是很有价值的,他们之间应该是互补的而不是竞争的关系


Spring 小组喜欢 @AspectJ 注解风格更胜于 Spring XML 配置; 所以在 Spring 2.0 使用了和 AspectJ 5 一样的注解,并使用 AspectJ 来做切入点解析和匹配但是,AOP 在运行时仍旧是纯的 Spring AOP,并不依赖于 AspectJ 的编译器或者织入器(weaver)


Spring 2.5 对 AspectJ 的支持:在一些环境下,增加了对 AspectJ 的装载时编织支持,同时提供了一个新的 bean 切入点。


下表总结了 Spring AOP 和 AspectJ 之间的关键区别:



AOP 的实现原理


AOP 有两种实现方式:静态代理和动态代理。


静态代理


静态代理分为:编译时织入(特殊编译器实现)、类加载时织入(特殊的类加载器实现)。


代理类在编译阶段生成,在编译阶段将增强织入 Java 字节码中,也称编译时增强。AspectJ 使用的是静态代理


缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。


动态代理


动态代理:代理类在程序运行时创建,AOP 框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类


Spring 的 AOP 实现原理


而 Spring 的 AOP 的实现就是通过动态代理实现的。


如果为 Spring 的某个 bean 配置了切面,那么 Spring 在创建这个 bean 的时候,实际上创建的是这个 bean 的一个代理对象,后续对 bean 中方法的调用,实际上调用的是代理类重写的代理方法。而 Spring 的 AOP 使用了两种动态代理,分别是 JDK 的动态代理,以及 CGLib 的动态代理。


  • 如果目标类实现了接口,Spring AOP 会选择使用 JDK 动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK 动态代理的核心是InvocationHandler接口和Proxy类。


  • 如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。CGLIB 是通过继承的方式做的动态代理,因此 CGLIB 存在的束:类是 final 的,或是方法是 final 的,或是方法是 private,或是静态方法,也就是无法被子类实现的方法都无法使用 CGLIB 实现代理。


那么什么时候采用哪种动态代理呢?


  1. 如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理实现 AOP

  2. 如果目标对象实现了接口,可以强制使用 CGLIB 实现 AOP

  3. 如果目标对象没有实现了接口,必须采用 CGLIB 库


AOP 的配置方式


基于 XML


Spring 提供了使用"aop"命名空间来定义一个切面,我们来看个例子


  • 定义目标类

public class AopDemoServiceImpl {
public void doMethod1() { System.out.println("AopDemoServiceImpl.doMethod1()"); }
public String doMethod2() { System.out.println("AopDemoServiceImpl.doMethod2()"); return "hello world"; }
public String doMethod3() throws Exception { System.out.println("AopDemoServiceImpl.doMethod3()"); throw new Exception("some exception"); }}
复制代码


  • 定义切面类

public class LogAspect {
public Object doAround(ProceedingJoinPoint pjp) throws Throwable { System.out.println("-----------------------"); System.out.println("环绕通知: 进入方法"); Object o = pjp.proceed(); System.out.println("环绕通知: 退出方法"); return o; }
public void doBefore() { System.out.println("前置通知"); }
public void doAfterReturning(String result) { System.out.println("后置通知, 返回值: " + result); }
public void doAfterThrowing(Exception e) { System.out.println("异常通知, 异常: " + e.getMessage()); }
public void doAfter() { System.out.println("最终通知"); }
}
复制代码


  • XML 配置 AOP

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:aop="http://www.springframework.org/schema/aop"       xmlns:context="http://www.springframework.org/schema/context"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.seven.springframeworkaopxml" />
<aop:aspectj-autoproxy/>
<!-- 目标类 --> <bean id="demoService" class="com.seven.springframeworkaopxml.service.AopDemoServiceImpl"> <!-- configure properties of bean here as normal --> </bean>
<!-- 切面 --> <bean id="logAspect" class="com.seven.springframeworkaopxml.aspect.LogAspect"> <!-- configure properties of aspect here as normal --> </bean>
<aop:config> <!-- 配置切面 --> <aop:aspect ref="logAspect"> <!-- 配置切入点 --> <aop:pointcut id="pointCutMethod" expression="execution(* com.seven.springframeworkaopxml.service.*.*(..))"/> <!-- 环绕通知 --> <aop:around method="doAround" pointcut-ref="pointCutMethod"/> <!-- 前置通知 --> <aop:before method="doBefore" pointcut-ref="pointCutMethod"/> <!-- 后置通知;returning属性:用于设置后置通知的第二个参数的名称,类型是Object --> <aop:after-returning method="doAfterReturning" pointcut-ref="pointCutMethod" returning="result"/> <!-- 异常通知:如果没有异常,将不会执行增强;throwing属性:用于设置通知第二个参数的的名称、类型--> <aop:after-throwing method="doAfterThrowing" pointcut-ref="pointCutMethod" throwing="e"/> <!-- 最终通知 --> <aop:after method="doAfter" pointcut-ref="pointCutMethod"/> </aop:aspect> </aop:config>
</beans>
复制代码


  • 测试类

public static void main(String[] args) {    // create and configure beans    ApplicationContext context = new ClassPathXmlApplicationContext("aspects.xml");
// retrieve configured instance AopDemoServiceImpl service = context.getBean("demoService", AopDemoServiceImpl.class);
// use configured instance service.doMethod1(); service.doMethod2(); try { service.doMethod3(); } catch (Exception e) { // e.printStackTrace(); }}
复制代码


基于 AspectJ 注解(直接写表达式)


基于 XML 的声明式 AspectJ 存在一些不足,需要在 Spring 配置文件配置大量的代码信息,为了解决这个问题,Spring 使用了 @AspectJ 框架为 AOP 的实现提供了一套注解。



基于 JDK 动态代理


基于JDK动态代理例子源码点这里


  • 定义接口

public interface IJdkProxyService {
void doMethod1();
String doMethod2();
String doMethod3() throws Exception;}
复制代码


  • 实现类

@Servicepublic class JdkProxyDemoServiceImpl implements IJdkProxyService {
@Override public void doMethod1() { System.out.println("JdkProxyServiceImpl.doMethod1()"); }
@Override public String doMethod2() { System.out.println("JdkProxyServiceImpl.doMethod2()"); return "hello world"; }
@Override public String doMethod3() throws Exception { System.out.println("JdkProxyServiceImpl.doMethod3()"); throw new Exception("some exception"); }}
复制代码


  • 定义切面

@EnableAspectJAutoProxy@Component@Aspectpublic class LogAspect {
/** * define point cut. */ @Pointcut("execution(* com.seven.springframeworkaopannojdk.service.*.*(..))") private void pointCutMethod() { }

/** * 环绕通知. * * @param pjp pjp * @return obj * @throws Throwable exception */ @Around("pointCutMethod()") public Object doAround(ProceedingJoinPoint pjp) throws Throwable { System.out.println("-----------------------"); System.out.println("环绕通知: 进入方法"); Object o = pjp.proceed(); System.out.println("环绕通知: 退出方法"); return o; }
/** * 前置通知. */ @Before("pointCutMethod()") public void doBefore() { System.out.println("前置通知"); }

/** * 后置通知. * * @param result return val */ @AfterReturning(pointcut = "pointCutMethod()", returning = "result") public void doAfterReturning(String result) { System.out.println("后置通知, 返回值: " + result); }
/** * 异常通知. * * @param e exception */ @AfterThrowing(pointcut = "pointCutMethod()", throwing = "e") public void doAfterThrowing(Exception e) { System.out.println("异常通知, 异常: " + e.getMessage()); }
/** * 最终通知. */ @After("pointCutMethod()") public void doAfter() { System.out.println("最终通知"); }
}
复制代码


  • APP 启动

public class App {    public static void main(String[] args) {        // create and configure beans        ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannojdk");
// retrieve configured instance IJdkProxyService service = context.getBean(IJdkProxyService.class);
// use configured instance service.doMethod1(); service.doMethod2(); try { service.doMethod3(); } catch (Exception e) { // e.printStackTrace(); } }}
复制代码


非接口使用 Cglib 代理


基于Cglib代理例子源码点这里


  • 类定义

@Servicepublic class CglibProxyDemoServiceImpl {
public void doMethod1() { System.out.println("CglibProxyDemoServiceImpl.doMethod1()"); }
public String doMethod2() { System.out.println("CglibProxyDemoServiceImpl.doMethod2()"); return "hello world"; }
public String doMethod3() throws Exception { System.out.println("CglibProxyDemoServiceImpl.doMethod3()"); throw new Exception("some exception"); }}
复制代码


  • 切面定义


和上面相同


  • APP 启动

public class App {    public static void main(String[] args) {        // create and configure beans        ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannocglib");
// cglib proxy demo CglibProxyDemoServiceImpl service = context.getBean(CglibProxyDemoServiceImpl.class); service.doMethod1(); service.doMethod2(); try { service.doMethod3(); } catch (Exception e) { // e.printStackTrace(); } }}
复制代码


使用注解装配 AOP


上面使用 AspectJ 的注解,并配合一个复杂的execution(* com.seven.springframeworkaopannojdk.service.*.*(..)) 语法来定义应该如何装配 AOP。还有另一种方式,则是使用注解来装配 AOP,这两者一般存在与不同的应用场景中:


  • 对于业务开发来说,一般使用 注解的方式来装配 AOP,因为如果要使用 AOP 进行增强,业务开发就需要配置注解,业务能够很好的感知到这个方法(这个类)进行了增强。如果使用 表达式来装配 AOP,当后续新增 Bean,如果不清楚现有的 AOP 装配规则,容易被强迫装配,而在开发时未感知到,导致出现线上故障。例如,Spring 提供的@Transactional就是一个非常好的例子。如果自己写的 Bean 希望在一个数据库事务中被调用,就标注上@Transactional

  • 对于基础架构开发来说,无需业务感知到增强了什么方法,则可以使用表达式的方式来装配 AOP。需要记录所有接口的耗时时长,直接写表达式,对业务无侵入

  • 定义注解

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface LogAspectAnno {
}
复制代码


  • 修改切面类,使用注解的方式定义

@EnableAspectJAutoProxy@Component@Aspectpublic class LogAspect {
@Around("@annotation(logaspectanno)") //注意,括号里为logaspectanno,而不是LogAspectAnno public Object doAround(ProceedingJoinPoint pjp, LogAspectAnno logaspectanno) throws Throwable { System.out.println("-----------------------"); System.out.println("环绕通知: 进入方法"); Object o = pjp.proceed(); System.out.println("环绕通知: 退出方法"); return o; } }
复制代码


  • 修改实现类,这里只对 doMethod1 方法装配 AOP

@Servicepublic class CglibProxyDemoServiceImpl {
@LogAspectAnno() public void doMethod1() { System.out.println("CglibProxyDemoServiceImpl.doMethod1()"); }
public String doMethod2() { System.out.println("CglibProxyDemoServiceImpl.doMethod2()"); return "hello world"; }}
@Servicepublic class JdkProxyDemoServiceImpl implements IJdkProxyService {
@LogAspectAnno @Override public void doMethod1() { System.out.println("JdkProxyServiceImpl.doMethod1()"); }
@Override public String doMethod2() { System.out.println("JdkProxyServiceImpl.doMethod2()"); return "hello world"; }}
复制代码


  • APP 类

// create and configure beansApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannotation");
// cglib proxy demoCglibProxyDemoServiceImpl service1 = context.getBean(CglibProxyDemoServiceImpl.class);service1.doMethod1();service1.doMethod2();
IJdkProxyService service2 = context.getBean(IJdkProxyService.class);service2.doMethod1();service2.doMethod2();
复制代码


  • 输出:

-----------------------环绕通知: 进入方法CglibProxyDemoServiceImpl.doMethod1()环绕通知: 退出方法CglibProxyDemoServiceImpl.doMethod2()-----------------------环绕通知: 进入方法JdkProxyServiceImpl.doMethod1()环绕通知: 退出方法JdkProxyServiceImpl.doMethod2()
复制代码


可以看到,只有 doMethod1 方法被增强了,doMethod2 没有被增强,就是因为 @LogAspectAnno 只注解了 doMethod1() 方法,从而实现更精细化的控制,是业务感知到这个方法是被增强了。


应用场景


我们知道 AO 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度提高系统可拓展性和可维护性


  1. 基于 AOP 实现统一的日志管理。

  2. 基于 Redisson + AOP 实现了接口防刷,一个注解即可限制接口指定时间内单个用户可以请求的次数。

  3. 基于 Spring Security 提供的 @PreAuthorize 实现权限控制,其底层也是基于 AOP。


日志记录


利用 AOP 方式记录日志,只需要在 Controller 的方法上使用自定义 @Log 日志注解,就可以将用户操作记录到数据库。


@Log(description = "新增用户")@PostMapping(value = "/users")public ResponseEntity create(@Validated @RequestBody User resources){    checkLevel(resources);    return new ResponseEntity(userService.create(resources),HttpStatus.CREATED);}
复制代码


AOP 切面类 LogAspect用来拦截带有 @Log 注解的方法并处理:


@Aspect@Componentpublic class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
// 定义切点,拦截带有 @Log 注解的方法 @Pointcut("@annotation(com.example.annotation.Log)") // 这里需要根据你的实际包名修改 public void logPointcut() { }
// 环绕通知,用于记录日志 @Around("logPointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //... }}
复制代码


限流


利用 AOP 方式对接口进行限流,只需要在 Controller 的方法上使用自定义的 @RateLimit 限流注解即可。


/** * 该接口 60 秒内最多只能访问 10 次,保存到 redis 的键名为 limit_test, */@RateLimit(key = "test", period = 60, count = 10, name = "testLimit", prefix = "limit")public int test() {     return ATOMIC_INTEGER.incrementAndGet();}
复制代码


AOP 切面类 RateLimitAspect用来拦截带有 @RateLimit 注解的方法并处理:


@Slf4j@Aspectpublic class RateLimitAspect {      // 拦截所有带有 @RateLimit 注解的方法      @Around("@annotation(rateLimit)")    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {      //...    }}
复制代码


关于限流实现这里多说一句,这里并没有自己写 Redis Lua 限流脚本,而是利用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。


权限控制


Spring Security 使用 AOP 进行方法拦截。在实际调用 update 方法之前,Spring 会检查当前用户的权限,只有用户权限满足对应的条件才能执行。


@Log(description = "修改菜单")@PutMapping(value = "/menus")// 用户拥有 `admin`、`menu:edit` 权限中的任意一个就能能访问`update`方法@PreAuthorize("hasAnyRole('admin','menu:edit')")public ResponseEntity update(@Validated @RequestBody Menu resources){    //...}
复制代码


文章转载自:Seven

原文链接:https://www.cnblogs.com/seven97-top/p/18596202

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Spring AOP基础、快速入门_Java_不在线第一只蜗牛_InfoQ写作社区