Spring核心之AOP
一、前置基础-代理模式
在学习Spring的AOP之前我们需要补充下设计模式中的代理模式。这块是理解AOP的必备基础内容。
1. 静态代理
若代理类在程序运行前就已经存在,那么这种代理方式被成为 静态代理 ,这种情况下的代理类通常都是我们在Java代码中定义的。 通常情况下, 静态代理中的代理类和目标类会实现同一接口或是派生自相同的父类。
先定义公共接口
/**
* 代理模式
* 定义的公共接口
*/
public interface SomeService {
String doSome();
}
然后定义目标对象:
/**
* 代理模式
* 目标对象:Target Object
*/
public class SomeServiceImpl implements SomeService{
@Override
public String doSome() {
System.out.println("目标对象执行了。。。");
return "Hello";
}
}
然后定义我们的代理对象:
/**
* 代理模式
* 代理类:需要和目标对象实现相同的接口
*/
public class SomeProxy implements SomeService{
// 代理对象持有的目标对象
private SomeService target;
public SomeProxy(SomeService target){
this.target = target;
}
/**
* 代理对象需要增强的方法
* @return
*/
@Override
public String doSome() {
System.out.println("目标对象执行之前");
// 应该需要让目标对象来完成核心的业务
String msg = target.doSome();
System.out.println("目标对象执行之后");
return msg.toUpperCase();
}
}
最后做测试:
/**
* 静态代理的测试
*/
@Test
public void test1(){
// 获取目标对象
SomeService target = new SomeServiceImpl();
// 获取代理对象
SomeService proxy = new SomeProxy(target);
// 通过代理对象执行方法
System.out.println(proxy.doSome());
}
测试结果:
目标对象执行之前
目标对象执行了。。。
目标对象执行之后
HELLO
2. 动态代理
代理类在程序运行时创建的代理方式被成为 动态代理。 也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。
代理类型 使用场景:
- JDK动态代理:如果目标对象实现了接口,采用JDK的动态代理
- CGLIB动态代理:如果目标对象没有实现了接口,必须采用CGLIB动态代理
2.1 JDK动态代理
如何目标对象实现了相关的接口。那么我们就可以通过JDK动态代理来完成代理类的动态生成。
// 调用目标对象的方法
//String msg = target.doSome();
Object res = method.invoke(target, args); /**
* 实现JDK动态代理:目标对象必须实现相关的接口
* 我们就不需要显示的定义代理类
*/
@Test
public void test2(){
// 1.获取目标对象
SomeService target = new SomeServiceImpl();
// 2.获取代理对象
SomeService proxy = (SomeService) Proxy.newProxyInstance(
Test02.class.getClassLoader(), // 获取类加载器
target.getClass().getInterfaces(), // 获取目标对象实现的所有的接口
new InvocationHandler() { // 提供一个 InvocationHandler的对象
/**
* 该方法是代理对象执行目标对象方法的回调方法
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
System.out.println(args);
System.out.println("----start------");
// 调用目标对象的方法
String msg = target.doSome();
System.out.println("----end------");
return msg.toUpperCase();
}
}
);
// 3.通过代理对象来执行
System.out.println("proxy.doSome() = " + proxy.doSome());
}
}
执行结果:
2.2 CGLIB代理
如果目标对象没有实现任何的接口。那么我们只能通过CGLIB代理的方式来实现了。同时我们需要单独的添加CGLIB的依赖。
<!-- https://mvnrepository.com/artifact/cglib/cglib-nodep -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.3.0</version>
</dependency>
定义目标对象:注意不实现任何接口
/**
* 目标对象的定义
* 改目标对象没有实现任何的接口
*/
public class SomeService {
public String doSome(){
System.out.println("目标对象执行了....");
return "Hello Cglib";
}
}
然后定义cglib的代理对象
/**
* Cglib的代理类
*/
public class CglibProxy implements MethodInterceptor {
// 目标对象
private SomeService target;
public CglibProxy(SomeService target){
this.target = target;
}
/**
* 对外提供代理对象的方法
* @return
*/
public SomeService createTarget(){
// 创建cglib的增强器
Enhancer enhancer = new Enhancer();
// 需要指定父类
enhancer.setSuperclass(SomeService.class);
// 代理后的回调对象
enhancer.setCallback(this);
return (SomeService) enhancer.create();
}
/**
* 这个就是对应的增强的方法
* @param o
* @param method
* @param objects
* @param methodProxy
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("------start-----");
//String msg = target.doSome();
Object res = method.invoke(target, objects);
System.out.println("------end-----");
return msg.toUpperCase();
}
}
然后测试:
二、AOP-面向切面编程
1. AOP 概述及相关概念
1.1 AOP概述
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程OOP的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 日志、事务、安全检查等
1.2 AOP 术语
在学习AOP中我们会涉及到如下的相关概念
术语 | 说明 |
---|---|
切面 | 切面泛指交叉业务逻辑。比如事务处理、日志处理就可以理解为切面。常用的切面有通知与顾问。实际就是对主业务逻辑的一种增强 |
织入 | 织入是指将切面代码插入到目标对象的过程。 |
连接点 | 连接点指切面可以织入的位置。 |
切入点 | 切入点指切面具体织入的位置。 |
通知(Advice) | 通知是切面的一种实现,可以完成简单织入功能(织入功能就是在这里完成的)。通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。 |
顾问(Advisor) | 顾问是切面的另一种实现,能够将通知以更为复杂的方式织入到目标对象中,是将通知包装为更复杂切面的装配器。 不仅指定了切入时间点,还可以指定具体的切入点 |
下面这个图会更加的形象些:
通知的类型:
通知类型 | 说明 |
---|---|
前置通知(MethodBeforeAdvice) | 目标方法执行之前调用 |
后置通知(AfterReturningAdvice) | 目标方法执行完成之后调用 |
环绕通知(MethodInterceptor) | 目标方法执行前后都会调用方法,且能增强结果 |
异常处理通知(ThrowsAdvice) | 目标方法出现异常调用 |
最终通知(final Advice) | 无论程序执行是否正常,该通知都会执行。类似于try…catch中finally代码块 |
2. 基于注解实现
2.1 基本介绍
对于AOP这种编程思想,很多框架都进行了实现。Spring就是其中之一,可以完成面向切面编程。然而,AspectJ也实现了AOP的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring又将AspectJ的对于AOP的实现也引入到了自己的框架中。在Spring中使用AOP开发时,一般使用AspectJ的实现方式.
相关说明:
- 动态代理分为JDK动态代理和cglib动态代理
- 当目标类有接口的情况使用JDK动态代理和cglib动态代理,没有接口时只能使用cglib动态代理
- JDK动态代理动态生成的代理类会在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口
- cglib动态代理动态生成的代理类会和目标在在相同的包下,会继承目标类
- 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
- cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
- AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
2.2 基本案例
首先定义对应的接口
public interface Calculator {
int add(int i, int j);
int sub(int i ,int j);
int mul(int i , int j);
int div(int i , int j);
}
然后创建该接口的实现
package com.boge.service.impl;
import com.boge.service.Calculator;
import org.springframework.stereotype.Component;
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
return i + j;
}
@Override
public int sub(int i, int j) {
return i - j;
}
@Override
public int mul(int i, int j) {
return i * j;
}
@Override
public int div(int i, int j) {
return i / j;
}
}
创建对应的切面类
/**
* 切面类
*/
@Aspect // 被该注解所修饰的Java类就是一个切面类
@Component
public class LogAspect {
/**
* 前置通知:@Before()
*/
@Before("execution(public int com.boge.service.impl.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
System.out.println("前置通知执行了。。。。");
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("执行方法的相关信息:" + name + " 参数:" + args);
}
}
然后做对应的测试
@Test
public void test1(){
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
Calculator bean = ac.getBean(Calculator.class);
System.out.println(bean.add(4, 7));
System.out.println(bean.sub(4, 7));
System.out.println(bean.mul(4, 7));
}
结果打印:
2.3 其他通知
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
相关的通知的案例:
@Aspect // 被该注解所修饰的Java类就是一个切面类
@Component
public class LogAspect {
/**
* 前置通知:@Before()
*/
@Before("execution(public int com.boge.service.impl.CalculatorImpl.*(..))")
public void beforeMethod(JoinPoint joinPoint){
System.out.println("前置通知执行了。。。。");
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("执行方法的相关信息:" + name + " 参数:" + args);
}
/**
* 后置通知:可以获取目标方法的返回结果
*/
@AfterReturning(value = "execution(* com.boge.service.impl.*.*(..))",returning = "res")
public void afterReturningMethod(JoinPoint joinPoint,Object res){
String methodName = joinPoint.getSignature().getName();
System.out.println("后置通知:" + methodName + " 返回结果:" + res);
}
/**
* 环绕通知
*/
@Around("execution(* com.boge.service.impl.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
Object obj = null;
try {
System.out.println("环绕通知执行之前....");
obj =joinPoint.proceed(); // 执行目标对象的方法
System.out.println("环绕通知执行之后....");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知执行异常....");
}finally {
System.out.println("环绕通知执行....最终完成");
}
return obj;
}
/**
* 异常通知
*/
@AfterThrowing(value = "execution(* com.boge.service.impl.*.*(..))",throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("异常通知:" + methodName + " " + ex);
}
/**
* 最终通知
*/
@After(value = "execution(* com.boge.service.impl.*.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("最终通知执行了..." + methodName);
}
}
2.4 切入点表达式
切入点表达式要匹配的对象就是目标方法的方法名。所以,execution表达式中明显就是方法的签名。注意,表达式中加[ ]的部分表示可省略部分,各部分间用空格分开。在其中可以使用以下符号
语法要求:
作用:
细节介绍:
- 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
- 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
- 例如:*.Hello匹配com.Hello,不匹配com.boge.Hello
- 在包名的部分,使用“*…”表示包名任意、包的层次深度任意
- 在类名的部分,类名部分整体用*号代替,表示类名任意
- 在类名的部分,可以使用*号代替类名的一部分
- *例如:Service匹配所有名称以Service结尾的类或接口
- 在方法名部分,可以使用*号表示方法名任意
- 在方法名部分,可以使用*号代替方法名的一部分
- *例如:Operation匹配所有方法名以Operation结尾的方法
- 在方法参数列表部分,使用(…)表示参数列表任意
- 在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头
- 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
- 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
- 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
- 例如:execution(public int *…Service.(…, int)) 正确
例如:execution( int …Service.(…, int)) 错误
- 例如:execution(public int *…Service.(…, int)) 正确
如果一个切入点表达式需要被重复的复用。那么我们可以通过@Pointcut注解来定义表达式。然后我们在通知调用即可:
package com.boge.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 切面类
*/
@Aspect // 被该注解所修饰的Java类就是一个切面类
@Component
public class LogAspect2 {
/**
* 定义一个切入点表达式
*/
@Pointcut("execution(public int com.boge.service.impl.CalculatorImpl.*(..))")
public void ponitCut(){
}
/**
* 前置通知:@Before()
*/
@Before("ponitCut()")
public void beforeMethod(JoinPoint joinPoint){
System.out.println("前置通知执行了。。。。");
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("执行方法的相关信息:" + name + " 参数:" + args);
}
/**
* 后置通知:可以获取目标方法的返回结果
*/
@AfterReturning(value = "ponitCut()",returning = "res")
public void afterReturningMethod(JoinPoint joinPoint,Object res){
String methodName = joinPoint.getSignature().getName();
System.out.println("后置通知:" + methodName + " 返回结果:" + res);
}
/**
* 环绕通知
*/
@Around("ponitCut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
Object obj = null;
try {
System.out.println("环绕通知执行之前....");
obj =joinPoint.proceed(); // 执行目标对象的方法
System.out.println("环绕通知执行之后....");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知执行异常....");
}finally {
System.out.println("环绕通知执行....最终完成");
}
return obj;
}
/**
* 异常通知
*/
@AfterThrowing(value = "ponitCut()",throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("异常通知:" + methodName + " " + ex);
}
/**
* 最终通知
*/
@After(value = "ponitCut()")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("最终通知执行了22..." + methodName);
}
}
3. 基于XML实现
在Spring中AOP还有基于XML的实现方式。当然这种不是我们常用的方案。但是我们还是需要了解下
先定义对应的切面类:
/**
* 切面类
*/
@Component
public class LogAspect3 {
/**
* 前置通知:@Before()
*/
public void beforeMethod(JoinPoint joinPoint){
System.out.println("前置通知执行了。。。。");
String name = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("执行方法的相关信息:" + name + " 参数:" + args);
}
/**
* 后置通知:可以获取目标方法的返回结果
*/
public void afterReturningMethod(JoinPoint joinPoint,Object res){
String methodName = joinPoint.getSignature().getName();
System.out.println("后置通知:" + methodName + " 返回结果:" + res);
}
/**
* 环绕通知
*/
public Object aroundMethod(ProceedingJoinPoint joinPoint){
Object obj = null;
try {
System.out.println("环绕通知执行之前....");
obj =joinPoint.proceed(); // 执行目标对象的方法
System.out.println("环绕通知执行之后....");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知执行异常....");
}finally {
System.out.println("环绕通知执行....最终完成");
}
return obj;
}
/**
* 异常通知
*/
public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("异常通知:" + methodName + " " + ex);
}
/**
* 最终通知
*/
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("最终通知执行了..." + methodName);
}
}
然后定义对应的配置文件
<?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">
<!-- 添加扫描路径 -->
<context:component-scan base-package="com.boge.*"></context:component-scan>
<!-- 基于XML的AOP实现 -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="logAspect3">
<!-- 定义切入点表达式 -->
<aop:pointcut id="pointCut" expression="execution(* com.boge.service.impl.*.*(..))"/>
<!-- 配置相关的通知 -->
<aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before>
<aop:after-returning method="afterReturningMethod" pointcut-ref="pointCut" returning="res"></aop:after-returning>
<aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around>
<aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointCut" throwing="ex"></aop:after-throwing>
<aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after>
</aop:aspect>
</aop:config>
</beans>
然后测试即可