持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情
1 AOP简介
- AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming, 面向对象编程) 的补充。
- AOP 的主要编程对象是切面(aspect), 而切面模块化横切关注点。
- 在应用 AOP 编程时, 仍然需要定义公共功能, 但可以明确的定义这个功能在哪里, 以什么方式应用, 并且不必修改受影响的类. 这样一来横切关注点就被模块化到特殊的对象(切面)里。
- AOP 的好处:每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级业务模块更简洁, 只包含核心业务代码。
2 动态代理
示例:实现一个简单的加减乘除计算,添加日志,在程序执行期间追踪正在发生的活动。
接口类IArithmeticCalculator:
package site.exciter.spring.aop.helloworld;
public interface IArithmeticCalculator {
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 site.exciter.spring.aop.helloworld;
/**
* 普通方式添加log
* 缺点:
* 1、每个方法中非核心业务代码越来越多不好维护
* 2、冗余代码多
* 3、如果需求变更,需要修改多处代码
*/
public class ArithmeticCalculatorLoggingImpl implements IArithmeticCalculator {
@Override
public int add(int i, int j) {
System.out.println("The method add begin with [" + i + "+" + j + "]");
int result = i + j;
System.out.println("The method add end with [" + result + "]");
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("The method sub begin with [" + i + "-" + j + "]");
int result = i - j;
System.out.println("The method sub end with [" + result + "]");
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("The method mul begin with [" + i + "*" + j + "]");
int result = i * j;
System.out.println("The method mul end with [" + result + "]");
return result;
}
@Override
public int div(int i, int j) {
System.out.println("The method div begin with [" + i + "/" + j + "]");
int result = i / j;
System.out.println("The method div end with [" + result + "]");
return result;
}
}
测试:
@org.junit.Test
public void testLoggingImpl() {
ArithmeticCalculatorLoggingImpl calculator = new ArithmeticCalculatorLoggingImpl();
int resultAdd = calculator.add(1, 3);
System.out.println("--->" + resultAdd);
int resultMul = calculator.mul(2, 3);
System.out.println("--->" + resultMul);
}
出现的问题:
- 代码混乱:越来越多的非业务需求(日志和验证等)加入后, 原有的业务方法急剧膨胀. 每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。
- 代码分散: 以日志需求为例, 只是为了满足这个单一需求, 就不得不在多个模块(方法)里多次重复相同的日志代码. 如果日志需求发生变化, 必须修改所有模块。
使用动态代理解决:
新建一个纯净的计算方法实现类ArithmeticCalculatorLoggingImplSimple:
package site.exciter.spring.aop.helloworld;
public class ArithmeticCalculatorLoggingImplSimple implements IArithmeticCalculator {
@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;
}
}
使用代理对象:
package site.exciter.spring.aop.helloworld;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* 代理方式实现前后置log
*/
public class ArithmeticCalculatorLoggingProxy {
//要代理的对象
IArithmeticCalculator mTarget;
public ArithmeticCalculatorLoggingProxy(IArithmeticCalculator target) {
this.mTarget = target;
}
//返回一个日志代理
public IArithmeticCalculator getLoggingProxy() {
IArithmeticCalculator proxy;
//代理对象由哪一个类加载器负责加载
ClassLoader classLoader = mTarget.getClass().getClassLoader();
//代理对象的类型,即其中有哪些方法
Class[] interfaces = new Class[]{IArithmeticCalculator.class};
//当调用代理对象其中的方式时,该执行的代码
InvocationHandler invocationHandler = new 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("The method " + method.getName() + " begin with " + Arrays.asList(args));
//执行方法
Object result = method.invoke(mTarget, args);
//后置日志
System.out.println("The method " + method.getName() + " end with " + result);
return result;
}
};
proxy = (IArithmeticCalculator) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
return proxy;
}
}
测试:
@org.junit.Test
public void testLoggingProxy() {
IArithmeticCalculator target = new ArithmeticCalculatorLoggingImplSimple();
IArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy();
int resultAdd = proxy.add(1, 3);
System.out.println("--->" + resultAdd);
int resultMul = proxy.mul(2, 3);
System.out.println("--->" + resultMul);
}
3AspectJ
- 要在 Spring 中声明 AspectJ 切面, 只需要在 IOC 容器中将切面声明为 Bean 实例。当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理。
- 在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类。
- 通知是标注有某种注解的简单的 Java 方法。
- AspectJ 支持 5 种类型的通知注解:
@Before: 前置通知, 在方法执行之前执行
@After: 后置通知, 在方法执行之后执行
@AfterRunning: 返回通知, 在方法返回结果之后执行
@AfterThrowing: 异常通知, 在方法抛出异常之后
@Around: 环绕通知, 围绕着方法执行
Spring使用AOP框架Aspectj
1 引入依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.8.RC1</version>
</dependency>
2 在配置文件中加入aop和context命名空间
注意schemaLocation下引入的aop、context配置不能少,不然会报错。
<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/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
3 在Bean配置文件中加以下配置
<!--配置要自动扫描的包-->
<context:component-scan base-package="site.exciter.spring.aop.impl"/>
<!--使aspectj注解起作用:自动为匹配的类生成对象-->
<aop:aspectj-autoproxy/>
4 接口类
package site.exciter.spring.aop.impl;
public interface IArithmeticCalculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
5 实现类
package site.exciter.spring.aop.impl;
import org.springframework.stereotype.Component;
@Component("arithmeticCalculator")
public class ArithmeticCalculatorImpl implements IArithmeticCalculator {
@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;
}
}
6 创建切面
package site.exciter.spring.aop.impl;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
/**
* @Aspect当前类声明成一个切面
*/
@Component
@Aspect
public class LoggingAspect {
/**
* @param point 链接信息
* @Before声明该方法是一个前置方法:在目标方法开始执行之前执行
*
* execution(* site.exciter.spring.aop.impl. * . * ( int, int))
* 第一个*表示方法修饰符和返回值可以为任意类型,第二个*代表该包下所有的类,第三个*表示所有的方法
* JoinPoint可以访问链接细节:方法和参数等
*/
@Before("execution(* site.exciter.spring.aop.impl.*.*(int,int))")
public void beforeMethod(JoinPoint point) {
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("The method " + methodName + " begin with " + args);
}
}
7 测试
package site.exciter.spring.aop.impl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
@org.junit.Test
public void testImpl() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
IArithmeticCalculator calculator = (IArithmeticCalculator) context.getBean("arithmeticCalculator");
int result = calculator.add(3, 6);
System.out.println("result-->" + result);
}
}
8 Log
The method add begin with [3, 6]
result-->9
相关术语:
切面(Aspect): 横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象。
通知(Advice): 切面必须要完成的工作。
目标(Target): 被通知的对象。
代理(Proxy): 向目标对象应用通知之后创建的对象。
连接点(Joinpoint): 程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为 ArithmethicCalculator#add(); 方位为该方法执行前的位置。
切点(pointcut): 每个类都拥有多个连接点:例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
关注木水小站 (zhangmushui.cn)和微信公众号【木水Code】,及时获取更多最新技术干货。