目录
在软件开发中,我们经常会遇到需要在不修改源代码的前提下增强方法功能的需求(如日志、事务、权限校验等)。传统面向对象编程(OOP)通过继承或组合实现功能扩展,但面对横切关注点时显得力不从心。面向切面编程(AOP)正是解决这类问题的优雅方案,本文将通过代码示例详解 Spring AOP 的核心概念与实现方式。
一、AOP 核心概念解析
1.1 什么是 AOP?
AOP(Aspect-Oriented Programming)即面向切面编程,是一种通过预编译或动态代理技术,将横切关注点(如日志、安全校验)与业务逻辑分离的编程范式。
核心思想:
- 将分散在多个类中的相同逻辑(如事务管理)抽取到切面(Aspect)中
- 通过切入点(Pointcut)定义需要增强的方法
- 使用通知(Advice)描述切面逻辑的执行时机
1.2 AOP 的优势
优势项 | 传统 OOP 实现方式 | AOP 实现方式 |
---|---|---|
代码复用 | 继承/组合(冗余代码) | 横切关注点统一管理 |
维护成本 | 修改多处代码 | 修改单一切面 |
系统扩展性 | 纵向继承体系臃肿 | 横向切面灵活扩展 |
1.3 底层实现原理
Spring AOP 主要通过两种技术实现动态代理:
JDK 动态代理:
- 基于接口创建代理对象
- 要求被代理类必须实现接口
CGLIB 代理:
- 通过生成子类实现代理
- 支持无接口类的代理
二、Spring AOP 配置文件方式实现
2.1 核心术语
- Joinpoint(连接点):可被增强的方法(如
User.add()
) - Pointcut(切入点):定义需要增强的方法集合(通过表达式)
- Advice(通知):切面执行的具体逻辑(如前置通知)
- Aspect(切面):= 切入点 + 通知
2.2 开发步骤
步骤 1:添加 Maven 依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--AOP联盟-->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<!--Spring Aspects-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--aspectj-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.3</version>
</dependency>
</dependencies>
步骤 2:定义目标类
public class Login {
public void login(String name,String password){
//第一种方法:直接在login方法里写
System.out.println("登陆成功!!!");
}
//在登录之前进行权限验证
}
步骤 3:配置切面类
public class Authorization {
//正常通知
public void authorization(){
System.out.println("进行了权限验证!!!");
}
//环绕通知
public void authorization(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("在方法执行之前,进行了权限验证!!!");
proceedingJoinPoint.proceed();
System.out.println("在方法执行之后,进行了权限验证!!!");
}
}
步骤 4: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">
<bean id="login" class="com.qcby.Login"/>
<bean id="auth" class="com.qcby.Authorization"/>
<!--配置切面-->
<aop:config>
<!--配置切面 = 切入点 + 通知组成-->
<aop:aspect ref="auth">
<!--前置通知:UserServiceImpl的save方法执行前,会增强-->
<!--pointcut:后边是切入点表达式,作用是知道对对面的那个方法进行增强-->
<!-- 前置通知 -->
<!-- <aop:before method="authorization" pointcut="execution(public void com.qcby.Login.login(..))"/>-->
<!-- 最终通知:无论我们的切入点是好是坏(成功执行与否)都会进行通知 -->
<!-- <aop:after method="authorization" pointcut="execution(public void com.qcby.Login.login(..))"/>-->
<!-- 后置通知:切入点只有执行成功后才会执行 -->
<!-- <aop:after-returning method="authorization" pointcut="execution(public void com.qcby.Login.login(..))"/>-->
<!-- 异常通知:只有在切入点出现问题的时候才会通知 -->
<!-- <aop:after-throwing method="authorization" pointcut="execution(public void com.qcby.Login.login(..))"/>-->
<!-- 环绕通知:目标方法执行前后,都可以进行增强。目标对象的方法需要手动执行 -->
<aop:around method="authorization" pointcut="execution(public void com.qcby.Login.login(..))"/>
</aop:aspect>
</aop:config>
</beans>
步骤 5:测试验证
public class LoginTest {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("Spring.xml");
Login login=(Login) ac.getBean("login");
login.login("张三","11");
}
}
2.3 切入点表达式详解
表达式格式:
execution([修饰符] 返回值类型 包名.类名.方法名(参数列表))
常用通配符:
*
:匹配任意字符(不包括包分隔符)- ..:匹配任意参数或子包
示例:
表达式 | 含义 |
---|---|
execution(* com.service.*.*(..)) | 匹配 com.service 包下所有类的所有方法 |
execution(* com.service.User.add(..)) | 精确匹配 User 类的 add 方法 |
三、Spring AOP 注解方式实现
3.1 注解开发步骤
步骤 1:启用注解扫描
<?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">
<!--1.配置xml扫描注解-->
<!--开启注解扫描-->
<context:component-scan base-package="com.aopImpl"></context:component-scan>
<!--开启Aspect生成代理对象-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
步骤 2:定义切面类,使用注解增强方法
//2.配置注解
@Component
@Aspect //增强类
public class Authorization {
//环绕通知
@Around(value = "execution(public void com.qcby.Login.login(..))")
public void authorization(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("在方法执行之前,进行了权限验证!!!");
proceedingJoinPoint.proceed();
System.out.println("在方法执行之后,进行了权限验证!!!");
}
//正常通知
// @Before(value = "execution(public void com.qcby.Login.login(..))") //前置通知
// @After(value = "execution(public void com.qcby.Login.login(..))") //最终通知
// @AfterReturning(value = "execution(public void com.qcby.Login.login(..))") //后置通知
// @AfterThrowing(value = "execution(public void com.qcby.Login.login(..))") //异常通知
public void authorization(){
System.out.println("进行了权限验证!!!");
}
}
步骤 3:测试验证
public class LoginTest {
@Test
public void test(){
ApplicationContext ac=new ClassPathXmlApplicationContext("Spring.xml");
Login login=(Login) ac.getBean("login");
login.login("张三","11");
}
}
3.2 通知类型注解
注解类型 | 执行时机 | 方法签名要求 |
---|---|---|
@Before | 方法执行前 | 无返回值,参数匹配切入点参数 |
@AfterReturning | 方法正常返回后 | 可获取返回值 |
@AfterThrowing | 方法抛出异常后 | 必须包含异常参数 |
@After | 方法最终执行后(无论成功与否) | 无特殊要求 |
@Around | 包裹方法执行 | 必须包含 ProceedingJoinPoint |
四、最佳实践建议
- 切入点复用:使用
@Pointcut
定义可复用的切入点表达式 - 代理方式选择:优先使用 JDK 动态代理(接口代理);无接口类时使用 CGLIB 代理
- 性能考虑:避免在通知中执行耗时操作
- 异常处理:在
@AfterThrowing
中处理特定异常类型
五、总结
Spring AOP 通过声明式编程简化了横切关注点的管理,其核心优势在于:
- 非侵入式增强:无需修改原有业务代码
- 关注点分离:将系统级功能(日志、安全)与业务逻辑解耦
- 灵活配置:支持 XML 和注解两种配置方式