代理模式
代理模式是一种设计模式,它允许一个对象控制对另一个对象的访问。在Spring中,代理模式主要用于实现AOP。代理对象可以在方法调用前后添加额外的逻辑,而不需要修改原始对象的代码。
1.1 静态代理
静态代理是通过手动编写代理类实现的,代理类实现了与目标对象相同的接口,并在调用目标方法时添加额外的逻辑。
接下来对比静态代理和动态代理来感受一下代理
如想在目标类的方法实现前后加上日志等操作
静态代理示例
// 接口
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);
}
// 目标类
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;
}
}
// 静态代理类
public class CalculatorStaticProxy implements Calculator {
private Calculator target;
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = target.add(i, j);
System.out.println("方法内部 result = " + result);
return result;
}
// 其他方法类似
// ...
}
虽然静态代理实现了功能的解耦,但它缺乏灵活性。如果在多个地方需要附加日志功能,就需要声明多个静态代理类,这会导致大量重复代码,且日志功能分散,不便于统一管理。
- 优点:
- 代码结构清晰,易于理解。
- 在编译期就能确定代理类和目标类之间的关系。
- 性能略好于动态代理,因为不需要反射机制。
- 缺点:
- 每个目标类都需要一个代理类,代码冗余。
- 扩展性差,如果需要添加新的功能,需要修改代理类代码。
- 无法在运行时动态地添加代理逻辑。
1.2 动态代理
动态代理通过反射机制在运行时生成代理类,避免了手动编写代理类的繁琐。在Spring中,最常用的动态代理技术有JDK动态代理和CGLIB代理。
JDK动态代理
JDK动态代理需要目标类实现接口。代理对象和目标对象有相同的接口。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 代理工厂类
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy() {
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
System.out.println("[动态代理][日志] 方法:" + method.getName() + ",参数:" + Arrays.toString(args));
result = method.invoke(target, args);
System.out.println("[动态代理][日志] 方法:" + method.getName() + ",结果:" + result);
} catch (Exception e) {
System.out.println("[动态代理][日志] 方法:" + method.getName() + ",异常:" + e.getMessage());
throw e;
} finally {
System.out.println("[动态代理][日志] 方法:" + method.getName() + ",执行完毕");
}
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, handler);
}
}
测试代码
public class DynamicProxyTest {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator) factory.getProxy();
proxy.add(1, 2);
proxy.div(4, 2);
}
}
- 优点:
- 减少代码冗余,不需要为每个目标类编写代理类。
- 扩展性强,可以通过动态代理添加新的功能,无需修改目标类代码。
- 可以动态地添加代理逻辑,方便灵活。
- 缺点:
- 性能略低于静态代理,因为使用了反射机制。
- 代码复杂,理解难度较高
共同缺点:
- 侵入性: 静态代理和动态代理都需要修改目标类代码,或者引入额外的代理类,这会增加代码复杂度,降低代码可维护性。
为何要使用AOP
为了解决静态代理和动态代理的缺点,引入了面向切面编程 (AOP)。AOP 是一种编程思想,它允许将横切关注点 (例如日志记录、事务管理、安全控制等) 从业务逻辑中分离出来,并以一种非侵入的方式进行管理。
AOP 的优势:
- 非侵入性: AOP 不会修改目标类代码,而是通过配置的方式实现代理逻辑。
- 模块化: AOP 将横切关注点模块化,提高代码可维护性和可扩展性。
- 集中管理: AOP 可以集中管理横切关注点,例如日志记录、事务管理等,方便维护和修改。
AOP
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它通过“切面”将横切关注点(如日志记录、事务管理)与业务逻辑分离。在Spring中,AOP是通过代理模式实现的。
2.1 AOP的核心概念
- 切面(Aspect):一个模块,包含横切关注点的逻辑。
- 连接点(Join Point):程序执行的某个点,如方法调用或异常抛出。
- 切入点(Pointcut):定义在哪些连接点上应用切面。
- 通知(Advice):切面在连接点上执行的动作。
- 目标对象(Target Object):被代理的对象。
- 织入(Weaving):将切面应用到目标对象的过程。
连接点与切入点
连接点(Join Point)
连接点是程序执行的某个点,例如方法调用或异常抛出。在Spring AOP中,连接点总是表示方法的执行。
切入点(Pointcut)
切入点是一个表达式,定义在哪些连接点上应用通知。切入点表达式可以通过注解或XML配置定义。
通知分类
前置通知:使用@Before注解标识,在被代理的目标方法前执行
返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行---目标返回值后执行
异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行---相当于在catch中
后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行---- 相当于在finally中
环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包 括上面四种通知对应的所有位置
关注你要的目标对象(方法),然后在其方法上加入通知
切入点表达式
AOP的核心注解
@Aspect
@Aspect
注解用于定义一个切面类。切面类包含了切入点和通知的定义。
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// 切面类内容
}
@Pointcut
@Pointcut
注解用于定义切入点表达式。切入点表达式指定了哪些连接点需要应用通知。
import org.aspectj.lang.annotation.Pointcut;
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {
// 切入点方法签名
}
@Before
@Before
注解表示前置通知,它在目标方法执行之前执行。
import org.aspectj.lang.annotation.Before;
@Before("serviceMethods()")
public void logBefore() {
System.out.println("Before method execution...");
}
@After
@After
注解表示后置通知,它在目标方法执行之后执行,无论方法是否抛出异常。
import org.aspectj.lang.annotation.After;
@After("serviceMethods()")
public void logAfter() {
System.out.println("After method execution...");
}
@AfterReturning
@AfterReturning
注解表示返回通知,它在目标方法成功执行并返回结果后执行。
import org.aspectj.lang.annotation.AfterReturning;
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfterReturning(Object result) {
System.out.println("Method returned with result: " + result);
}
@AfterThrowing
@AfterThrowing
注解表示异常通知,它在目标方法抛出异常后执行。
import org.aspectj.lang.annotation.AfterThrowing;
@AfterThrowing(pointcut = "serviceMethods()", throwing = "error")
public void logAfterThrowing(Throwable error) {
System.out.println("Method threw an error: " + error);
}
@Around
@Around
注解表示环绕通知,它围绕目标方法执行,可以在方法执行前后添加逻辑,并且可以决定是否执行目标方法。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
@Around("serviceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method execution...");
Object result = joinPoint.proceed(); // 执行目标方法
System.out.println("After method execution...");
return result;
}
切入点重用
自定义一个空方法, 加上注解里面写上切入点表达式,就可以在后续切入点表达式上直接写自定义的方法名
1.3 通知注解
通知是在连接点上执行的动作。Spring AOP提供了以下几种通知注解:
2.2 使用Spring AOP
Spring AOP主要有两种实现方式:基于注解和基于XML配置。
2.2.1 基于注解的AOP
Spring提供了几个常用的注解来定义切面、切入点和通知。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
// 定义切面
@Aspect
@Component
public class LoggingAspect {
// 定义切入点和通知
@Before("execution(* com.example.service.*.*(..))")
public void logBefore() {
System.out.println("Before method execution...");
}
}
// 目标类
@Component
public class MyService {
public void perform() {
System.out.println("Performing service...");
}
}
// 配置类
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// 配置类内容
}
2.2.2 基于XML的AOP
除了注解,Spring还支持通过XML配置AOP。
<!-- 定义切面 -->
<aop:config>
<aop:aspect ref="loggingAspect">
<!-- 定义切入点和通知 -->
<aop:pointcut id="serviceMethods" expression="execution(* com.example.service.*.*(..))"/>
<aop:before pointcut-ref="serviceMethods" method="logBefore"/>
</aop:aspect>
</aop:config>
<!-- 切面类 -->
<bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>