全部笔记All notes

Web程序设计笔记07——第三章:基于AspectJ实现AOP

阅读 8m 15s8m 15s read

基于 AspectJ 实现 AOP 完整指南

概述

AspectJ 是一个功能强大的 AOP 框架,它扩展了 Java 语言,提供了更灵活的切入点表达式和更丰富的通知类型。Spring AOP 集成了 AspectJ 的注解和切入点表达式语言,使得 AOP 编程更加便捷。

💡 核心优势:

  • 精确控制:通过切入点表达式精确控制切面的织入位置
  • 多样化通知:支持前置、后置、环绕、异常、最终通知
  • 注解驱动:支持使用注解定义切面,简化配置
  • 性能优异:编译时织入,运行时性能更好

知识回顾

代理模式回顾

JDK 动态代理 vs CGLIB 代理
特性JDK 动态代理CGLIB 代理
被代理对象必须实现接口可以是普通类
代理生成方式基于接口,使用反射基于继承,使用字节码生成
性能JDK 8+ 性能优化后较好创建代理对象较快
使用场景Spring 默认(有接口时)Spring 默认(无接口时)
Spring AOP 实现步骤
  1. 依赖引入:

    • Spring 核心包:spring-context, spring-beans, spring-core, spring-expression
    • AOP 支持包:spring-aop, aopalliance
  2. 核心组件:

    • 被代理对象(目标对象)
    • 切面类(包含通知方法)
    • 代理对象(由 Spring 容器生成)

AOP 核心概念

术语说明示例
切面(Aspect)横切关注点的模块化日志切面、事务切面
连接点(Join Point)程序执行的特定点方法调用、异常抛出
切入点(Pointcut)匹配连接点的表达式execution(* com.service..(..))
通知(Advice)切面在特定连接点执行的动作前置通知、后置通知
目标对象(Target)被代理的对象UserService 实例
代理(Proxy)创建的代理对象包含增强逻辑的对象
织入(Weaving)将切面应用到目标对象的过程编译时、加载时、运行时

传统 AOP 的局限性

❗ 问题:传统 Spring AOP 会对目标对象的所有方法进行拦截,缺乏精细化控制。

✅ 解决方案:使用 AspectJ 框架,通过切入点表达式精确控制切面的应用范围。

——————————————————————————————————————————————————

基于AspectJ实现AOP(实现代理)

一、基于xml

(1)导包:5个核心包:commons-logging spring-context spring-core spring-beans spring-expression 2个包:spring-aspects aspectjweaver (2) 创建被代理对象 (3) 创建切面类 (4) 创建代理对象:xml文件使用aop编程 (5) 测试

代码实现如下:

package com.aspectj.xml;

public interface UserDao {
    void addUser();
    void deleteUser();
}
package com.aspectj.xml;

public class UserDaoImpl implements UserDao {

    @Override
    public void addUser() {
        System.out.println("添加用户");
//        模拟异常
//        System.out.println(1/0);
    }

    @Override
    public void deleteUser() {
        System.out.println("删除用户");
    }
}
package com.aspectj.xml;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class MyAspect {
    //前置通知
    public void myBefore(JoinPoint joinPoint){
        System.out.print("前置通知:模拟执行权限检查...,");
        System.out.print("目标类是:"+joinPoint.getTarget());
        System.out.println(",被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
    }
    //后置通知
    public void myAfterRturning(JoinPoint joinPoint) {
        System.out.print("后置通知:模拟记录日志...");
        System.out.println("被植入增强处理的目标方法为:" + joinPoint.getSignature().getName());
    }

//    环绕通知
//    ProceedingJoinPoint 是 JoinPoint 子接口,表示可以执行目标方法
//    1.必须是 Object 类型的返回值
//    2.必须接收一个参数,类型为 ProceeddingJoinPoint
//    3.必须 throws Throwable

    public Object myAround(ProceedingJoinPoint proceedingJoinPoint)throws Throwable{
        //开始
        System.out.println("环绕开始:执行目标方法之前,模拟开始事务...");
//        执行当前目标方法
        Object obj = proceedingJoinPoint.proceed();
        //结束
        System.out.println("环绕结束:执行目标方法之后,模拟关闭事物...");
        return obj;
    }

    //异常通知
    public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
        System.out.println("异常通知:"+"出错了"+e.getMessage());
    }

    //最终通知
    public void myAfter(){
        System.out.println("最终通知:模拟方法结束后的释放资源...");
    }
}
<?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"
       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">
<!--    1.目标对象:被代理对象-->
    <bean id="userDao" class="com.aspectj.xml.UserDaoImpl"/>
<!--    2.切面类对象-->
    <bean id="myAspect" class="com.aspectj.xml.MyAspect"/>
<!--    3.产生代理对象的aop编程-->
    <aop:config>
<!--    配置切面    -->
        <aop:aspect ref="myAspect">
<!--            1. *目标方法返回值-->
<!--            2. com.aspectj.xml 包-->
<!--            3. * 包下所有类-->
<!--            4. * 类中所有方法-->
<!--            5. (..)类中所有方法-->
<!--            配置切入点-->
            <aop:pointcut id="myPointCut" expression="execution(* com.aspectj.xml.*.*(..))"/>
<!--            配置前置通知-->
            <aop:before method="myBefore" pointcut-ref="myPointCut"/>
<!--            配置后置通知:目标方法被成功执行后,才调用的方法-->
            <aop:after-returning method="myAfterRturning" pointcut-ref="myPointCut"/>
<!--            配置环绕通知:目标方法调用前会被调用,目标方法被成功执行后,还会被调用-->
            <aop:around method="myAround" pointcut-ref="myPointCut"/>
<!--            配置异常通知:调用目标方法时发生异常才会被调用-->
            <aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e"/>
<!--            配置最终通知:目标方法被调用后,不管是否被成功调用,此方法都会被调用-->
        </aop:aspect>
    </aop:config>
</beans>
package com.aspectj.xml;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class XmlTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("a.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        userDao.addUser();
        System.out.println("_______________");
        userDao.deleteUser();
    }
}

正常运行结果:

image-20220328214914512

运行发生异常截图:出现结果为零异常

image-20220328214954611

二、基于注解(Annotation)-------减少xml中的代码量

(1) 导包 5个核心包:commons-logging spring-context spring-core spring-beans spring-expression 2个包:spring-aspects aspectjweaver (2) 创建被代理对象 (3) 创建切面类 注意:切面类中的注解相当于a.xml文件中的哪一句代码 (4) 创建xml文件:扫描所有的注解和开启AspectJ实现AOP

package com.aspectj.annotation;

public interface UserDao {
    void addUser();
    void deleteUser();
}
package com.aspectj.annotation;

import org.springframework.stereotype.Repository;

//注解实现
//@Repository("userDao")
public class UserDaoImpl implements UserDao {

    @Override
    public void addUser() {
        System.out.println("添加用户");
//        模拟异常
//        System.out.println(1/0);
    }

    @Override
    public void deleteUser() {
        System.out.println("删除用户");
    }
}
package com.aspectj.annotation;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

//切面类,在此类中编写通知
@Aspect
@Component
public class MyAspect {
//    定义切入点表达式
    @Pointcut("execution(* com.aspectj.annotation.*.*(..))")
//    使用一个返回值为 void 、方法体为空的方法来命名切入点
    private void myPointCut(){}
//    前置通知
    @Before("myPointCut()")
    public void myBefore(JoinPoint joinPoint){
        System.out.println("前置通知:模拟执行权限检查...,");
        System.out.print("目标类是:"+joinPoint.getTarget());
        System.out.println(",被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
    }
    //后置通知
    @AfterReturning("myPointCut()")
    public void myAfterReturning(JoinPoint joinPoint){
        System.out.println("后置通知:模拟记录日志...");
        System.out.println("被植入增强处理的目标方法为:" + joinPoint.getSignature().getName());
    }
    //环绕通知
    @Around("myPointCut()")
    public Object myAround(ProceedingJoinPoint proceedingJoinPoint)throws Throwable{
        //开始
        System.out.println("环绕开始:执行目标方法之前,模拟开始事务...");
        //执行当前目标方法
        Object obj = proceedingJoinPoint.proceed();
        //结束
        System.out.println("环绕结束:执行目标方法之后,模拟关闭事物...");
        return obj ;
    }
    //异常通知
    @AfterThrowing(value = "myPointCut()",throwing = "e")
    public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
        System.out.println("异常通知:"+"出错了"+e.getMessage());
    }

    //最终通知
    @After("myPointCut()")
    public void myAfter(){
        System.out.println("最终通知:模拟方法结束后的释放资源...");
    }
}
<?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:context="http://www.springframework.org/schema/context"

       xmlns:aop="http://www.springframework.org/schema/aop"

       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd

       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd

       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--    通过Bean实现-->
    <bean id="userDao" class="com.aspectj.annotation.UserDaoImpl"/>
<!--扫描所有注解-->
    <context:component-scan base-package="com.aspectj.annotation"/>
<!--    开始Aspectj实现AOP-->
    <aop:aspectj-autoproxy/>
</beans>
package com.aspectj.annotation;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class AnnotationTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("b.xml");
        UserDao userDao = (UserDao) applicationContext.getBean("userDao");
        userDao.deleteUser();
        userDao.addUser();
    }
}

运行截图:

image-20220328215304530

AspectJ 框架介绍

AspectJ 是一个面向切面的编程框架,它扩展了 Java 语言,提供了强大的 AOP 功能。

AspectJ 的优势

  1. 精细化控制:通过切入点表达式精确指定切面应用位置
  2. 丰富的通知类型:支持前置、后置、环绕、异常、最终通知
  3. 注解驱动:简化配置,提高开发效率
  4. 编译时织入:更高的执行效率

Spring 集成 AspectJ

Spring AOP 使用了 AspectJ 的部分功能:

  • 使用 AspectJ 的切入点表达式语言
  • 使用 AspectJ 的注解来定义切面
  • 但仍然使用 Spring 的代理机制(运行时动态代理)

AspectJ 切入点表达式

表达式格式

execution([modifiers-pattern] return-type-pattern [declaring-type-pattern] method-name-pattern(param-pattern) [throws-pattern])

常用表达式示例

表达式说明
execution(* com.service.*.*(..))匹配 com.service 包下所有类的所有方法
execution(public * *(..))匹配所有 public 方法
execution(* save*(..))匹配所有以 save 开头的方法
execution(* com.service.UserService.*(..))匹配 UserService 类的所有方法
execution(* com.service..*.*(..))匹配 com.service 包及其子包下的所有方法

其他切入点指示符

  • within():指定类型内的方法
  • @annotation():匹配带有特定注解的方法
  • args():匹配参数类型
  • @within():匹配带有特定注解的类

AspectJ 通知类型

通知类型注解执行时机应用场景
前置通知@Before目标方法执行前权限验证、参数校验
后置通知@After目标方法执行后(无论成功或异常)资源清理
返回通知@AfterReturning目标方法正常返回后结果处理、日志记录
异常通知@AfterThrowing目标方法抛出异常后异常处理、错误日志
环绕通知@Around目标方法执行前后性能监控、事务管理

实战示例

示例1:日志记录切面

@Aspect
@Component
public class LoggingAspect {
    
    @Before("execution(* com.service.*.*(..))") 
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("执行方法:" + joinPoint.getSignature().getName());
    }
    
    @AfterReturning(value = "execution(* com.service.*.*(..))",
                    returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("方法返回:" + result);
    }
}

示例2:性能监控切面

@Aspect
@Component  
public class PerformanceAspect {
    
    @Around("@annotation(com.annotation.Monitor)")
    public Object measurePerformance(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        
        try {
            return pjp.proceed();
        } finally {
            long executionTime = System.currentTimeMillis() - start;
            System.out.println(pjp.getSignature() + " 执行时间:" + executionTime + "ms");
        }
    }
}

高级特性

1. 切入点复用

@Aspect
public class CommonPointcuts {
    
    @Pointcut("execution(* com.service.*.*(..))")
    public void serviceLayer() {}
    
    @Pointcut("execution(* com.dao.*.*(..))")
    public void daoLayer() {}
    
    @Pointcut("serviceLayer() || daoLayer()")
    public void applicationLayer() {}
}

2. 参数传递

@Aspect
public class ValidationAspect {
    
    @Before("execution(* com.service.*.save(..)) && args(entity,..)")
    public void validateEntity(Object entity) {
        // 验证实体参数
        if (entity == null) {
            throw new IllegalArgumentException("实体不能为空");
        }
    }
}

3. 注解驱动的 AOP

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
    String value() default "";
    int timeout() default 3600;
}

// 切面实现
@Aspect
public class CacheAspect {
    
    @Around("@annotation(cacheable)")
    public Object cache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
        String key = generateKey(pjp, cacheable);
        Object cached = getFromCache(key);
        
        if (cached != null) {
            return cached;
        }
        
        Object result = pjp.proceed();
        putToCache(key, result, cacheable.timeout());
        return result;
    }
}

性能优化

1. 切入点优化

  • 使用更精确的切入点表达式减少不必要的拦截
  • 避免在频繁调用的方法上使用复杂的切面逻辑
  • 使用 @Pointcut 复用切入点定义

2. 通知优化

  • 优先使用特定通知类型(如 @Before)而非 @Around
  • 在通知方法中避免耗时操作
  • 合理使用异步处理

3. 代理选择

# application.yml
spring:
  aop:
    proxy-target-class: true  # 强制使用 CGLIB 代理
    auto: true                # 自动创建代理

常见问题

问题 1:AOP 不生效

原因:

  • 切入点表达式错误
  • 目标类没有被 Spring 管理
  • 内部方法调用绕过代理

解决方案:

// 错误示例:内部调用
public class UserService {
    public void method1() {
        method2(); // AOP 不生效
    }
    
    public void method2() {
        // ...
    }
}

// 正确做法:通过代理对象调用
@Service
public class UserService {
    @Autowired
    private UserService self;
    
    public void method1() {
        self.method2(); // AOP 生效
    }
}

问题 2:切面执行顺序

解决方案:使用 @Order 注解

@Aspect
@Order(1)  // 数值越小,优先级越高
public class SecurityAspect { }

@Aspect
@Order(2)
public class LoggingAspect { }

问题 3:获取目标对象信息

@Before("execution(* com.service.*.*(..))") 
public void beforeAdvice(JoinPoint joinPoint) {
    // 获取目标对象
    Object target = joinPoint.getTarget();
    
    // 获取方法名
    String methodName = joinPoint.getSignature().getName();
    
    // 获取参数
    Object[] args = joinPoint.getArgs();
}

问题 4:异常处理

@Aspect
public class ExceptionHandlingAspect {
    
    @AfterThrowing(pointcut = "execution(* com.service.*.*(..))",
                   throwing = "ex")
    public void handleException(JoinPoint joinPoint, Exception ex) {
        // 记录错误日志
        logger.error("方法 {} 执行失败:{}", 
                    joinPoint.getSignature().getName(), 
                    ex.getMessage());
    }
}

总结

AspectJ 为 Spring AOP 提供了强大的功能支持:

🎯 核心优势

  1. 灵活的切入点表达式:精确控制切面作用范围
  2. 丰富的通知类型:满足各种 AOP 需求
  3. 注解支持:简化配置,提高开发效率
  4. 与 Spring 无缝集成:充分利用 Spring 容器功能

📊 适用场景

  • 日志记录:统一记录系统操作日志
  • 权限控制:方法级的安全控制
  • 事务管理:声明式事务处理
  • 性能监控:方法执行时间统计
  • 缓存管理:方法级缓存实现

🚀 最佳实践

  1. 合理使用切入点表达式,避免过度拦截
  2. 选择合适的通知类型,避免滥用 @Around
  3. 注意 AOP 的性能影响,在关键路径慎用
  4. 利用注解驱动,提高代码可维护性

相关文章

前后章节导航

相关主题