5.1 Aop
Aop(Aspect Oriented Programming),面向切面编程,这是对面向对象思想的一种补充。
面向切面编程,就是在程序运行时,不改变程序源码的情况下,动态的增强方法的功能,常见的使用场景非常多:
- 日志
- 事务
- 数据库操作
- ….
这些操作中,无一例外,都有很多模板化的代码,而解决模板化代码,消除臃肿就是 Aop 的强项。
在 Aop 中,有几个常见的概念:
概念 | 说明 |
---|---|
切点 | 要添加代码的地方,称作切点 |
通知(增强) | 通知就是向切点动态添加的代码 |
切面 | 切点+通知 |
连接点 | 切点的定义 |
5.1.1 Aop 的实现
在 Aop 实际上集基于 Java 动态代理来实现的。
Java 中的动态代理有两种实现方式:
- cglib
- jdk
5.2 动态代理
基于 JDK 的动态代理。
1.定义一个计算器接口:
public interface MyCalculator {
int add(int a, int b);
}
2.定义计算机接口的实现:
public class MyCalculatorImpl implements MyCalculator {
public int add(int a, int b) {
return a+b;
}
}
3.定义代理类
public class CalculatorProxy {
public static Object getInstance(final MyCalculatorImpl myCalculator) {
return Proxy.newProxyInstance(CalculatorProxy.class.getClassLoader(), myCalculator.getClass().getInterfaces(), new InvocationHandler() {
/**
* @param proxy 代理对象
* @param method 代理的方法
* @param args 方法的参数
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+"方法开始执行啦...");
Object invoke = method.invoke(myCalculator, args);
System.out.println(method.getName()+"方法执行结束啦...");
return invoke;
}
});
}
}
Proxy.newProxyInstance 方法接收三个参数,第一个是一个 classloader,第二个是代理多项实现的接口,第三个是代理对象方法的处理器,所有要额外添加的行为都在 invoke 方法中实现。
5.3 五种通知
Spring 中的 Aop 的通知类型有 5 种:
- 前置通知
- 后置通知
- 异常通知
- 返回通知
- 环绕通知
具体实现,这里的案例和 5.2 中的一样,依然是给计算器的方法增强功能。
首先,在项目中,引入 Spring 依赖(这次需要引入 Aop 相关的依赖):
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
接下来,定义切点,这里介绍两种切点的定义方式:
- 使用自定义注解
- 使用规则
其中,使用自定义注解标记切点,是侵入式的,所以这种方式在实际开发中不推荐,仅作为了解,另一种使用规则来定义切点的方式,无侵入,一般推荐使用这种方式。
自定义注解
首先自定义一个注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
}
然后在需要拦截的方法上,添加该注解,在 add 方法上添加了 @Action 注解,表示该方法将会被 Aop 拦截,而其他未添加该注解的方法则不受影响。
@Component
public class MyCalculatorImpl {
@Action
public int add(int a, int b) {
return a + b;
}
public void min(int a, int b) {
System.out.println(a + "-" + b + "=" + (a - b));
}
}
接下来,定义增强(通知、Advice):
@Component
@Aspect//表示这是一个切面
public class LogAspect {
/**
* @param joinPoint 包含了目标方法的关键信息
* @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
*/
@Before(value = "@annotation(Action)")
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法开始执行了...");
}
/**
* 后置通知
* @param joinPoint 包含了目标方法的所有关键信息
* @After 表示这是一个后置通知,即在目标方法执行之后执行
*/
@After("@annotation(Action)")
public void after(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法执行结束了...");
}
/**
* @param joinPoint
* @@AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object
*/
@AfterReturning(value = "@annotation(Action)",returning = "r")
public void returing(JoinPoint joinPoint,Integer r) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法返回:"+r);
}
/**
* 异常通知
* @param joinPoint
* @param e 目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者所抛出的异常的父类,只有这样,才会捕获。如果想拦截所有,参数类型声明为 Exception
*/
@AfterThrowing(value = "@annotation(Action)",throwing = "e")
public void afterThrowing(JoinPoint joinPoint,Exception e) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法抛异常了:"+e.getMessage());
}
/**
* 环绕通知
*
* 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似于在这里通过反射执行方法
* @param pjp
* @return 注意这里的返回值类型最好是 Object ,和拦截到的方法相匹配
*/
@Around("@annotation(Action)")
public Object around(ProceedingJoinPoint pjp) {
Object proceed = null;
try {
//这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
}
通知定义完成后,接下来在配置类中,开启包扫描和自动代理:
@Configuration
@ComponentScan
@EnableAspectJAutoProxy//开启自动代理
public class JavaConfig {
}
然后,在 Main 方法中,开启调用:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
MyCalculatorImpl myCalculator = ctx.getBean(MyCalculatorImpl.class);
myCalculator.add(3, 4);
myCalculator.min(3, 4);
}
}
再来回顾 LogAspect 切面,我们发现,切点的定义不够灵活,之前的切点是直接写在注解里边的,这样,如果要修改切点,每个方法上都要修改,因此,我们可以将切点统一定义,然后统一调用。
@Component
@Aspect//表示这是一个切面
public class LogAspect {
/**
* 可以统一定义切点
*/
@Pointcut("@annotation(Action)")
public void pointcut() {
}
/**
* @param joinPoint 包含了目标方法的关键信息
* @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
*/
@Before(value = "pointcut()")
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法开始执行了...");
}
/**
* 后置通知
* @param joinPoint 包含了目标方法的所有关键信息
* @After 表示这是一个后置通知,即在目标方法执行之后执行
*/
@After("pointcut()")
public void after(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法执行结束了...");
}
/**
* @param joinPoint
* @@AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object
*/
@AfterReturning(value = "pointcut()",returning = "r")
public void returing(JoinPoint joinPoint,Integer r) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法返回:"+r);
}
/**
* 异常通知
* @param joinPoint
* @param e 目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者所抛出的异常的父类,只有这样,才会捕获。如果想拦截所有,参数类型声明为 Exception
*/
@AfterThrowing(value = "pointcut()",throwing = "e")
public void afterThrowing(JoinPoint joinPoint,Exception e) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法抛异常了:"+e.getMessage());
}
/**
* 环绕通知
*
* 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似于在这里通过反射执行方法
* @param pjp
* @return 注意这里的返回值类型最好是 Object ,和拦截到的方法相匹配
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) {
Object proceed = null;
try {
//这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
}
但是,大家也注意到,使用注解是侵入式的,我们还可以继续优化,改为非侵入式的。重新定义切点,新切点的定义就不在需要 @Action 注解了,要拦截的目标方法上也不用添加 @Action 注解。下面这种方式是更为通用的拦截方式:
@Component
@Aspect//表示这是一个切面
public class LogAspect {
/**
* 可以统一定义切点
*/
@Pointcut("@annotation(Action)")
public void pointcut2() {
}
/**
* 可以统一定义切点
* 第一个 * 表示要拦截的目标方法返回值任意(也可以明确指定返回值类型
* 第二个 * 表示包中的任意类(也可以明确指定类
* 第三个 * 表示类中的任意方法
* 最后面的两个点表示方法参数任意,个数任意,类型任意
*/
@Pointcut("execution(* org.javaboy.aop.commons.*.*(..))")
public void pointcut() {
}
/**
* @param joinPoint 包含了目标方法的关键信息
* @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
*/
@Before(value = "pointcut()")
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法开始执行了...");
}
/**
* 后置通知
* @param joinPoint 包含了目标方法的所有关键信息
* @After 表示这是一个后置通知,即在目标方法执行之后执行
*/
@After("pointcut()")
public void after(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法执行结束了...");
}
/**
* @param joinPoint
* @@AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object
*/
@AfterReturning(value = "pointcut()",returning = "r")
public void returing(JoinPoint joinPoint,Integer r) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法返回:"+r);
}
/**
* 异常通知
* @param joinPoint
* @param e 目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者所抛出的异常的父类,只有这样,才会捕获。如果想拦截所有,参数类型声明为 Exception
*/
@AfterThrowing(value = "pointcut()",throwing = "e")
public void afterThrowing(JoinPoint joinPoint,Exception e) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法抛异常了:"+e.getMessage());
}
/**
* 环绕通知
*
* 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似于在这里通过反射执行方法
* @param pjp
* @return 注意这里的返回值类型最好是 Object ,和拦截到的方法相匹配
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) {
Object proceed = null;
try {
//这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
}
5.4 XML 配置 Aop
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
接下来,定义通知/增强,但是单纯定义自己的行为即可,不再需要注解:
public class LogAspect {
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法开始执行了...");
}
public void after(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法执行结束了...");
}
public void returing(JoinPoint joinPoint,Integer r) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法返回:"+r);
}
public void afterThrowing(JoinPoint joinPoint,Exception e) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法抛异常了:"+e.getMessage());
}
public Object around(ProceedingJoinPoint pjp) {
Object proceed = null;
try {
//这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
}
接下来在 spring 中配置 Aop:
<bean class="org.javaboy.aop.LogAspect" id="logAspect"/>
<aop:config>
<aop:pointcut id="pc1" expression="execution(* org.javaboy.aop.commons.*.*(..))"/>
<aop:aspect ref="logAspect">
<aop:before method="before" pointcut-ref="pc1"/>
<aop:after method="after" pointcut-ref="pc1"/>
<aop:after-returning method="returing" pointcut-ref="pc1" returning="r"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pc1" throwing="e"/>
<aop:around method="around" pointcut-ref="pc1"/>
</aop:aspect>
</aop:config>
最后,在 Main 方法中加载配置文件:
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
MyCalculatorImpl myCalculator = ctx.getBean(MyCalculatorImpl.class);
myCalculator.add(3, 4);
myCalculator.min(5, 6);
}
}