系列文章目录
【Spring框架一】——Spring框架简介
【Spring框架二】——什么是Spring IOC、 什么是Spring 依赖注入
【Spring框架三】——Spirng IOC和DI的实现
Spring AOP 注解实现和xml方式实现
- 系列文章目录
- 前言
- 一、什么是Spring AOP(Aspect-Oriented Programming)
- 什么是横切关注点
- 二、Spring AOP中的基本概念
- Cross Cutting Concern
- Aspect
- Advice
- Pointcut
- Joinpoint
- Weave
- TargetObject
- Proxy
- Introduction
- 三、使用xml文件的方式实现Spring AOP
- 原spring项目的代码结构
- pom.xml
- UserManager类
- UserManagerImpl
- **Client类**
- applicationContext.xml
- 当前运行的结果
- xml方式实现Spring AOP
- 补充关于Spring AOP 切入点的表达式
- 常用表达式示例
- 四、通过注解的方式实现Spring AOP
- 五、Spring对AOP的支持
- 如何强制使用CGLIB
- xml文件方式
- 注解的方式
- 通过注解方式强制使用CGLIB的示例:
- JDK动态代理的特点
- CGLIB动态代理的特点
- 区别
- 六、总结
前言
Spring AOP是Spring框架中的核心内容之一,本篇博客主要讲解的是如何使用Spring Aop通过本篇博客能够对什么是Spring AOP以及如何将Spring AOP运行到具体业务有一个清晰的步骤。本篇博客主要从两个维度实现Spring AOP 一个是使用纯xml文件方式,另一个是使用纯注解的方式实现。
一、什么是Spring AOP(Aspect-Oriented Programming)
Spring AOP是面向切面编程的技术,能够对程序进行横切关注点的处理,例如日志记录、安全控制、性能统计、事物处理等。
通过AOP一方面可以将一些与业务无关、但是对业务产生重要影响的公共行为(例如事物管理、日志记录等)从业务逻辑中分离出来,这样可以使程序的模块化程度更高、同时也便于维护和扩展。
Spring AOP通过运行时的动态代理的方式,在不修改原有代码的情况下,将切面逻辑织入到目标对象中,从而实现对目标对象的增强。主要依赖与代理模式和反射机制。
备注:如果想要了解什么是代理,可以访问博主的另外两篇博客
设计模式——代理模式
Java JDK动态代理
什么是横切关注点
corss-cutting concern:指的是一个系统中存在的、多个模块都会用到的,与核心业务逻辑无关的公共关注点。例如:日志记录、性能统计、安全控制。
在传统的面向对象编程中,这些横切关注点都会分散到各个模块中,导致代码复杂、难以维护。
而AOP的目的是将这些横切关注点从核心业务逻辑中分离出来,从而使系统更易于维护和扩展。
二、Spring AOP中的基本概念
结合具体业务的示意图:addUser为原有的具体业务的实现,现在需要在其之前之后等使用Spring AOP切入一个与原有业务无关的校验安全性。
我们通过这个业务来理解Spring AOP的概念
Cross Cutting Concern
横切性关注点:它是一种独立服务,在我们这个例子中它是检查安全性。它会遍布在系统的处理流程之中。
Aspect
切面: 对横切性关注点的模块化,图中为SecurityHandler
Advice
通知:对横切性关注点的具体实现,常见的通知类型有(Before、After、AfterReturning、AfterThrowing、Around)
Pointcut
切点:它定义了Advice应用到那些JoinPoint上,对Spring来说是方法调用
Spring只支持方法的JoinPoint,但是对于AspectJ这个点也可以是属性修改,如。
Joinpoint
连接点:在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。 在Spring AOP中,一个连接点 总是 代表一个方法的执行。 通过声明一个org.aspectj.lang.JoinPoint类型的参数可以使通知(Advice)的主体部分获得连接点信息。
Weave
织入:将Advice应用到TargetObject上的过程叫织入,Spring支持的是动态织入
TargetObject
目标对象:目标对象是被一个或多个切面所通知的对象。
Proxy
代理:AOP代理是在目标对应和切面之间进行通信的对象;Spring AOP默认使用JDK的动态代理,它的代理是运行时创建,也可以使用CGLIB代理
Introduction
引入:引入允许将新的接口和实现引入到现有的类中,从而为类添加额外的行为。
这些核心概念共同构成了Spring AOP的基础,通过它们可以实现对横切关注点的管理和控制,从而提供横切关注点的复用和模块化。
三、使用xml文件的方式实现Spring AOP
原spring项目的代码结构
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.8</version>
</dependency>
<!--spring aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
</dependencies>
UserManager类
package com.wangwei.springaop.service;
public interface UserManager {
public void addUser(String username, String password);
public void delUser(int userId);
public int findUserById(int userId);
public void modifyUser(int userId, String username, String password);
}
UserManagerImpl
package com.wangwei.springaop.service.impl;
import com.wangwei.springaop.service.UserManager;
public class UserManagerImpl implements UserManager {
public void addUser(String username, String password) {
System.out.println("---------UserManagerImpl.add()--------");
}
public void delUser(int userId) {
System.out.println("---------UserManagerImpl.delUser()--------");
}
public int findUserById(int userId) {
System.out.println("---------UserManagerImpl.findUserById()--------");
return userId;
}
public void modifyUser(int userId, String username, String password) {
System.out.println("---------UserManagerImpl.modifyUser()--------");
}
}
Client类
package com.wangwei.springaop.client;
import com.wangwei.springaop.service.UserManager;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Client {
public static void main(String[] args) {
//读取xml配置文件,申明工厂
BeanFactory factory = new ClassPathXmlApplicationContext("applicationContext.xml");
//获取beanid为userManager的bean
UserManager userManager = (UserManager)factory.getBean("userManager");
userManager.addUser("wangwei", "123");
}
}
applicationContext.xml
<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 https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userManager" class="com.wangwei.springaop.service.impl.UserManagerImpl"/>
</beans>
当前运行的结果
xml方式实现Spring AOP
目前业务是需要在程序执行addUser()这个方法之前,通过Spring AOP技术切入一个检查安全性的服务。
我们的横切性关注点(Cross Cutting Concern)就是这个检查安全性的服务。
现在我们将这个横切性关注点进行模块化
创建SecurityHandler类,它就代表的是Aspect,是对横切性关注点的模块化。
package com.wangwei.springaop.service.impl;
public class SecurityHandler {
private void checkSecurity() {
System.out.println("-------checkSecurity-------");
}
}
在XML配置文件中进行配置
<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 https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userManager" class="com.wangwei.springaop.service.impl.UserManagerImpl"/>
<bean id ="securityHandlerAspect" class="com.wangwei.springaop.service.impl.SecurityHandler"/>
<!-- 配置切面通知和切点-->
<aop:config>
<aop:aspect ref="securityHandlerAspect">
<aop:before method="checkSecurity" pointcut="execution(* com.wangwei.springaop.service.impl.UserManagerImpl.addUser(..))"/>
</aop:aspect>
</aop:config>
</beans>
上面的通知(advice)配置的为Before,
切点它定义了Advice应用到那些JoinPoint上,这里是通过表达式execution定位到连接点(joinPoint)
joinPoin代表一个方法的执行,这里指的是addUser()这个方法的执行。
实现效果:
补充关于Spring AOP 切入点的表达式
切入点表达式有两部分组成:签名和指示器
签名部分指定了要匹配的连接点的方法签名。
例如:"execution(public * com.example.service…(…))"表示匹配com.example.service包中的所有公共方法。
指示器部分用于进一步细化匹配的连接点。常见的指示器包括:
execution:匹配方法执行连接点。
within:匹配指定类型内的方法执行连接点。
args:匹配参数类型符合指定类型的方法执行连接点。
annotation:匹配带有指定注解的方法执行连接点。
以下是一些切入点表达式的示例:
- execution(public * com.example.service…(…)):匹配com.example.service包中的所有公共方法。
- within(com.example.service.*):匹配com.example.service包中所有方法。
- execution(* com.example.service.UserService.addUser(…)):匹配com.example.service.UserService类的addUser方法。
- args(String, *):匹配第一个参数为String类型的方法。
annotation(org.springframework.transaction.annotation.Transactional):匹配带有@Transactional注解的方法。
切入点表达式的语法非常灵活,可以根据需要进行组合和定制,以精确地选择要应用切面的连接点。
常用表达式示例
一般用的是execution执行表达式的格式如下:
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
其中,各个部分的含义如下:
- modifiers-pattern(可选):用于匹配方法的修饰符,如
public、protected等。可以使用通配符*匹配任意修饰符。 - return-type-pattern:用于匹配方法的返回类型,可以是具体的类型或通配符*表示任意类型。
- declaring-type-pattern(可选):用于匹配方法所在的类的类型,可以是具体的类型或通配符*表示任意类型。
- method-name-pattern:用于匹配方法的名称,可以是具体的方法名或使用通配符
*匹配任意方法名。 - param-pattern:用于匹配方法的参数类型,可以是具体的类型、通配符表示任意类型、…表示任意个参数、()表示一个参数,以及(*, *)表示两个参数等。
- throws-pattern(可选):用于匹配方法可能抛出的异常类型,可以是具体的异常类型或通配符*表示任意异常。
下面是一些示例:
execution(public * com.example.service..(…)):匹配com.example.service包中的所有公共方法。
execution(* com.example.service.UserService.addUser(String, )):匹配com.example.service.UserService类的addUser方法,第一个参数为String类型,第二个参数为任意类型。
execution( com.example.service..(…) throws java.io.IOException):匹配com.example.service包中所有方法,且可能抛出java.io.IOException异常。
四、通过注解的方式实现Spring AOP
下面我们使用纯注解的方式实现上面的业务需求,程序执行addUser()这个方法之前,通过Spring AOP技术切入一个检查安全性的服务。
- 还是和之前一样,首先明确横切性关注点是谁,根据上面的需求我们知道了检查安全性的服务为横切性关注点。
- 将横切性关注点进行模块化,定义一个SecurityHandler这个也就是切面(Aspect)
代码实例: - 添加@Aspect注解,表名这个类是一个切面,并且添加@Component注解表名将其交给spring ioc进行管理
package com.wangwei.springaop.service.impl;
import org.aspectj.lang.annotation.Aspect;
@Aspect
@Component
public class SecurityHandler {
private void checkSecurity() {
System.out.println("-------checkSecurity-------");
}
}
- 将横切性关注点具体实现,也就是通知(advice)我们需要将其设置设置为前置通知,添加@Before注解
package com.wangwei.springaop.service.impl;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
@Component
public class SecurityHandler {
@Before("")
private void checkSecurity() {
System.out.println("-------checkSecurity-------");
}
}
- 定义切点,定义通知(Advice)应用到那些JoinPoint上,我们这儿的连接点是addUser()这个方法。
package com.wangwei.springaop.service.impl;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class SecurityHandler {
// @Pointcut("execution(* com.wangwei.springaop.service.impl.UserManagerImpl.addUser(..))")
// public void myServiceMethods() {}
//
// @Before("myServiceMethods()")
// private void checkSecurity(JoinPoint joinPoint) {
// System.out.println("-------checkSecurity-------");
// }
@Before("execution(public * com.wangwei.springaop.service.impl.UserManagerImpl.addUser(..))")
private void checkSecurity(JoinPoint joinPoint) {
//可以通过在advice中添加JoinPoint类型的参与来获取客户端调用的方法名或者参数等等...
for (int i = 0; i < joinPoint.getArgs().length; i++) {
//打印参数
System.out.println(joinPoint.getArgs()[i]);
}
//获取方法名
System.out.println(joinPoint.getSignature().getName());
System.out.println("-------checkSecurity-------");
}
}
- 另外将其UserManagerImpl类通过@Service也纳入Spring 容器管理
package com.wangwei.springaop.service.impl;
import com.wangwei.springaop.service.UserManager;
import org.springframework.stereotype.Service;
@Service
public class UserManagerImpl implements UserManager {
public void addUser(String username, String password) {
System.out.println("---------UserManagerImpl.add()--------");
}
public void delUser(int userId) {
System.out.println("---------UserManagerImpl.delUser()--------");
}
public int findUserById(int userId) {
System.out.println("---------UserManagerImpl.findUserById()--------");
return userId;
}
public void modifyUser(int userId, String username, String password) {
System.out.println("---------UserManagerImpl.modifyUser()--------");
}
}
- 客户端进行调用
package com.wangwei.springaop.client;
import com.wangwei.springaop.service.UserManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.wangwei.springaop")
//启动Aspect的自动代理,Spring默认不会自动启用AspectJ的自动代理功能
@EnableAspectJAutoProxy
public class Client {
public static void main(String[] args) {
// BeanFactory factory = new ClassPathXmlApplicationContext("applicationContext.xml");
// UserManager userManager = (UserManager)factory.getBean("userManager");
// userManager.addUser("wangwei", "123");
//我们使用AnnotationConfigApplicationContext来加载配置类,使用MyAppApplication.class作为配置类,即告诉Spring使用MyAppApplication这个类中的注解来构建应用程序上下文
ApplicationContext context = new AnnotationConfigApplicationContext(Client.class);
/*
使用context对象来获取我们的bean并进行依赖注入和使用。注意由于使用Spring Aop时将其目标类进行了代理,所以在Spring IOC容器中
UserManagerImpl的类型变为了代理类
下面是打印容器中的对象和对象的类型,可以看到UserManagerImpl的类型为Proxy类型
*/
String[] beanNames = context.getBeanDefinitionNames();
for (String beanName : beanNames) {
Class<?> beanType = context.getType(beanName);
System.out.println("Bean Name: " + beanName);
System.out.println("Bean Type: " + beanType);
System.out.println("-----------------------");
}
//由于UserManagerImpl的类型变为了代理类,所以这里通过默认的bean的名称进行获取
UserManager userManager = (UserManager) context.getBean("userManagerImpl");
userManager.addUser("wangwei","123");
}
}
- 运行效果
五、Spring对AOP的支持
Spring通过JDK的动态代理实现AOP,可以通过使用CGLIB代理实现AOP。
- 如果目标对象实现了接口,在默认情况下会采用JDK的动态代理实现AOP
- 如果目标对象实现了接口,也可以强制使用CGLIB生成代理实现AOP
- 如果目标对象没有实现接口,那么必须引入CGLIB,spring会在JDK的动态代理和CGLIB代理之间切换
如何强制使用CGLIB
在Spring中可以通过配置文件或注解来强制使用CGLIB代理。
xml文件方式
使用proxy-target-class="true"属性来强制使用CGLIB代理。
<aop:config>
<aop:proxy proxy-target-class="true"/>
<!-- 其他的切面和通知配置 -->
</aop:config>
注解的方式
在@EnableAspectJAutoProxy注解中设置proxyTargetClass = true,表示强制使用CGLIB代理。
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
// 配置其他的Bean和切面
}
通过注解方式强制使用CGLIB的示例:
客户端代码
package com.wangwei.springaop.client;
import com.wangwei.springaop.service.UserManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("com.wangwei.springaop")
//启动Aspect的自动代理,Spring默认不会自动启用AspectJ的自动代理功能
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Client {
public static void main(String[] args) {
// BeanFactory factory = new ClassPathXmlApplicationContext("applicationContext.xml");
// UserManager userManager = (UserManager)factory.getBean("userManager");
// userManager.addUser("wangwei", "123");
//我们使用AnnotationConfigApplicationContext来加载配置类,使用MyAppApplication.class作为配置类,即告诉Spring使用MyAppApplication这个类中的注解来构建应用程序上下文
ApplicationContext context = new AnnotationConfigApplicationContext(Client.class);
/*
使用context对象来获取我们的bean并进行依赖注入和使用。注意由于使用Spring Aop时将其目标类进行了代理,所以在Spring IOC容器中
UserManagerImpl的类型变为了代理类
下面是打印容器中的对象和对象的类型,可以看到UserManagerImpl的类型为Proxy类型
*/
String[] beanNames = context.getBeanDefinitionNames();
for (String beanName : beanNames) {
Class<?> beanType = context.getType(beanName);
System.out.println("Bean Name: " + beanName);
System.out.println("Bean Type: " + beanType);
System.out.println("-----------------------");
}
//由于UserManagerImpl的类型变为了代理类,所以这里通过默认的bean的名称进行获取
UserManager userManager = (UserManager) context.getBean("userManagerImpl");
userManager.addUser("wangwei","123");
}
}
运行截图:
可以看到目标类的类型由之前的proxy变为了CGLIB
JDK动态代理的特点
优势:
- 基于接口的代理,适用于接口实现类的代理。
- 使用Java自带的反射机制实现代理,无需额外的依赖库。
劣势: - 代理的目标对象必须实现至少一个接口。
- 动态代理的性能相对较低,因为每次调用代理方法都会涉及到反射调用。
- 对于无接口的类,无法直接使用JDK动态代理。
CGLIB动态代理的特点
优势:
- 基于继承的代理,适用于无接口或者无法通过接口进行代理的类。
- 无需目标对象实现接口,可以直接代理普通类。
- CGLIB通过生成子类的方式实现代理,因此性能相对较高。
利弊: - 生成的代理类是目标类的子类,如果目标类被声明为final,将无法使用CGLIB代理。
- CGLIB需要额外的依赖库。
区别
- JDK动态代理基于接口,通过实现接口的方式进行代理,适用于接口实现类的代理;而CGLIB基于继承,通过生成子类的方式进行代理,适用于无接口或者无法通过接口进行代理的类。
- JDK动态代理基于接口,通过实现接口的方式进行代理,适用于接口实现类的代理;而CGLIB基于继承,通过生成子类的方式进行代理,适用于无接口或者无法通过接口进行代理的类。
- DK动态代理的性能相对较低,因为每次调用代理方法都会涉及到反射调用;而CGLIB通过生成子类的方式实现代理,性能相对较高。
- JDK动态代理对于无接口的类无法直接使用,而CGLIB可以直接代理普通类。
六、总结
将Spring AOP运行到具体业务的步骤:
- 抽象出横切性关注点
- 将横切性关注点进行模块化,模块化的结果就是切面(Aspect)
- 将横切性关注点具体实现,实现的结果为通知(advice)并定义通知的类型
- 定义切点,也就是将通知advice应用到那些连接点上(joinpoint)
这个过程中涉及到代理、织入(weave)、引入。
如果选择JDK动态代理还是CGLIB代理:
选择使用JDK动态代理还是CGLIB取决于具体的需求。
- 如果目标类实现了接口且性能要求较低,可以选择JDK动态代理;
- .如果目标类无接口或者无法通过接口进行代理,或者性能要求较高,可以选择CGLIB代理。
- spring框架会自动在动态代理和CGLIB代理之间切换