1. 介绍
Spring提供了一种面向切面的编程,内部的实现是一种基于Cglib的代理模式,使用切面,可以在不更改源码的情况下,简化大量的无用重复代码,使代码更关注实现业务逻辑,而不需要考虑是否需要配置日志、安全验证等。
2. 环境配置
本文采用Maven进行搭建环境,如果之前没使用过Maven,请下载相应的jar包放入项目的lib库即可。
以下是所用到的pom文件:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
3. 切面介绍
3.1 通知(Advice)
通知是定义了切面是什么以及何时使用,除了描述切面要完成的工作,通知还解决了需要什么时候执行这个工作的问题
Spring切面有5种类型的通知:
- 前置通知
Before
:在方法调用之前触发 - 后置通知
After
:在方法调用之后触发 - 返回通知
AfterReturning
:在方法正常返回之后触发 - 异常通知
AfterThrowing
:在方法抛出异常之后触发
- 环绕通知
Around
:在方法调用前和调用后触发,由于和上述四种不同,Spring对此专门做了一层包装,因此也可以理解为,Around
可以替代上述四种。
3.2 连接点(JoinPoint)
连接点是应用在执行过程中能够插入切面的一个点。这个点可以是在调用方法时、抛出异常时、甚至修改一个字段时。
3.3 切点(PointCut)
切点定义了何处执行,切点会匹配通知所需要织入的一个或多个连接点。
3.4 切面(Aspect)
切面由通知+切点构成,可定义为:它是什么,需要在什么时候执行,以及何处完成这个功能。
3.5 织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。
4. 切面语法介绍
4.1 语法介绍
语法:execution([修饰符] 返回值 包.类.方法名(参数) throws异常)
- 修饰符可选择,一般为public,也可以设置为*,表示任意
- 返回值:不能省略
- void:不返回内容
- int/double/Object:返回对应的内容
- *:不指定需要返回的内容类型,匹配任意返回
- 包:
- com.test.web:固定包名
- com.test.web.*.entity:web包下,所有的entity包
- com.test.web…:web下的所有包
- 类:
- UserEntity:直接给定固定类
- *Entity:所有以Entity的类
- User*:所有以User开头的类
- *:任意类
- 方法名:
- addUser:直接给定方法名
- add*:以add开头的方法
- *User:以User结尾的方法
- *:任意方法
- 参数:
- ():无参
- (int):需要传递一个int类型的参数
- (int,int):需要传递两个int类型的参数
- (…):任意参数
- throws:可省略,一般不写
4.2 演示小例子
execution(public * com.test.aspect.Divided.*(..))
说明:方法修饰符必须为public,返回值任意,扫描com.test.aspect包下的Divided类的任意方法,参数不限制
5. 基础用法
新建一个测试类:
public class Divided {
public int divide(int a, int b) {
return a / b;
}
}
新建切面:本例子用于计算测试类执行方法消耗的时间
@Aspect
public class DividedAspect {
private long startTime;
@Pointcut(value = "execution(public * aspect.Divided.*(..))")
public void pointCut() {}
@Before("pointCut()")
public void logStart() {
System.out.println("日志开始");
startTime = System.currentTimeMillis();
}
@After("pointCut()")
public void logEnd() {
System.out.println("日志结束");
System.out.println("运行时间:" + (System.currentTimeMillis() - startTime) + "ms!");
}
@AfterReturning(value = "pointCut()")
public void logReturn() {
System.out.println("日志正常返回,返回结果!");
}
@AfterThrowing(value = "pointCut()")
public void logException() {
System.out.println("异常信息:");
}
}
新建JavaConfig文件
@Configuration
@EnableAspectJAutoProxy//需要使用该注解,启动Spring对AspectJ的支持
public class BeanAspectAnnotation {
@Bean
public Divided createDivided() {
return new Divided();
}
@Bean//将切面类加入Spring容器进行管理
public DividedAspect createAspect() {
return new DividedAspect();
}
}
新建测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanAspectAnnotation.class)
public class BeanAspectAnnotationTest extends TestCase {
@Autowired
private Divided divided;
public void testCreateDivided() {
divided.divide(10, 1);
}
}
测试结果
日志开始
日志结束
运行时间:10ms!
日志正常返回,返回结果!
6. 高级用法
6.1 获得运行环境时的参数
Spring提供了获得运行时参数的对象JoinPoint
,我们可以将我们之前的例子进行改编一下:
@Aspect
public class DividedAspect {
private long startTime;
@Pointcut(value = "execution(public * aspect.Divided.*(..))")
public void pointCut() {}
@Before("pointCut()")
public void logStart(JoinPoint joinPoint) {
System.out.println("日志开始");
startTime = System.currentTimeMillis();
Object[] args = joinPoint.getArgs();
System.out.println("参数列表:" + Arrays.asList(args));
String methodName = joinPoint.getSignature().getName();
System.out.println("方法名称:" + methodName);
String kind = joinPoint.getKind();
System.out.println("类型:" + kind);
String longString = joinPoint.toLongString();
System.out.println("连接点表达式:" + longString);
}
@After("pointCut()")
public void logEnd() {
System.out.println("日志结束");
System.out.println("运行时间:" + (System.currentTimeMillis() - startTime) + "ms!");
}
@AfterReturning(value = "pointCut()", returning = "result")
public void logReturn(Object result) {
System.out.println("日志正常返回,返回结果:" + result);
}
@AfterThrowing(value = "pointCut()", throwing = "exception")
public void logException(Exception exception) {
System.out.println("异常信息:" + exception);
}
}
我们在被@Before
、@AfterReturning
、@AfterThrowing
三个注解修饰的方法中,添加了参数,其中@Before
中可添加的参数与@After
是一致的,因此在此使用@Before
进行讲解。
-
@Before
:joinPoint.getArgs()
:获得参数列表joinPoint.getSignature().getName()
:获得方法名称joinPoint.getKind()
:获得方法类型joinPoint.toLongString()
:获得连接点表达式
-
@AfterReturning
:returning = "result"
:必须指定需要返回的参数名称,否则无法得到对应的参数
-
@AfterThrowing
throwing = "exception"
:必须指定需要抛出的异常信息
日志开始
参数列表:[10, 1]
方法名称:divide
类型:method-execution
连接点表达式:execution(public int aspect.Divided.divide(int,int))
日志结束
运行时间:10ms!
日志正常返回,返回结果:10
6.2 使用环绕通知代替上述四大方法
修改切面类
@Aspect
public class DividedAspect {
@Pointcut(value = "execution(public * aspect.Divided.*(..))")
@Around(value = "pointCut()")
public Object logAround(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("环绕通知开始!");
try {
return proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("环绕通知结束!");
return null;
}
}
此处实现的原理与上面是一致的,但是此处使用了ProceedingJoinPoint
,这是Spring进行包装的一层代理,并且需要通过调用proceedingJoinPoint.proceed()
,最终结果才能正确被得到。
6.3 不修改原有代码前提下,扩展方法
假设:我们现在需要对Divided
类进行扩展,但是又不能修改原有代码,那么我们可以通过扩展接口来实现功能的扩展。
新建MathFunction
接口:
public interface MathFunction {
int multiply(int a, int b);
}
新建接口实现类:
public class MultiImpl implements MathFunction {
@Override
public int multiply(int a, int b) {
return a * b;
}
}
扩展Aspect:
@Aspect
public class DividedAspect {
// 省略其他代码
@DeclareParents(value = "aspect.Divided+",defaultImpl = MultiImpl.class)
public MathFunction mathFunction;
}
解释:
value
:表示将要修饰的类,后面的+表示修饰不仅仅是类本身,还包括子类
defaultImpl
:表示具体的实现类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = BeanAspectAnnotation.class)
public class BeanAspectAnnotationTest extends TestCase {
@Autowired
private Divided divided;
public void testCreateDivided() {
divided.divide(10, 1);
MathFunction mathFunction = (MathFunction) divided;
int result = mathFunction.multiply(10, 2);
System.out.println(result);
}
}
测试结果:
日志开始
参数列表:[10, 1]
方法名称:divide
类型:method-execution
连接点表达式:execution(public int aspect.Divided.divide(int,int))
日志结束
运行时间:10ms!
日志正常返回,返回结果:10
20
注意:被新方法修饰的方法,如果没有添加切面,那么一样无法使用切面。