面向切面编程,指扩展功能不修改源代码,将功能代码从业务逻辑代码中分离出来。
一个Bean是Spring容器中的一个对象实例,这个对象实例通常是通过Java类(或接口)的实例化得到的。
Bean作为对象,具有对象的所有基本特性,包括状态(即对象的属性)和行为(即对象的方法)。但是,与普通对象不同的是,Bean的生命周期由Spring容器控制,包括创建、初始化、使用和销毁等阶段。此外,Bean还支持依赖注入(DI)和面向切面编程(AOP)等Spring框架的高级特性。
在Spring框架中,Bean的声明通常是通过在类上添加特定的注解(如@Component
、@Service
、@Repository
、@Controller
等)或在配置类中使用@Bean
注解的方法来完成的。这些声明告诉Spring容器哪些类需要被实例化并注册为Bean。然后,Spring容器会在启动时读取这些声明,并根据配置信息来创建Bean的实例,并将它们存储在容器中以便后续使用。
而AOP则是通过在不修改源代码的情况下,为Bean提供额外的行为,如日志记录、事务管理、安全控制等,从而实现了横切关注点(cross-cutting concerns)的模块化。
1.代理对象
AOP的实现通常依赖于动态代理技术。当Spring容器创建Bean时,如果检测到该Bean需要进行AOP处理(例如,通过注解或XML配置指定了切面),Spring会为该Bean生成一个代理对象。这个代理对象在方法调用时会执行额外的操作(即切面的逻辑),然后调用原始Bean的方法。因此,从外部看来,就像是直接对原始Bean进行了操作,但实际上是通过代理对象来完成的。
这里对代理对象进行说明
public class UserService {
private String username;
public UserService(String username) {
this.username = username;
}
public void addUser() {
System.out.println("User " + username + " added.");
}
}
// 代理类
public class UserServiceProxy {
private UserService userService;
public UserServiceProxy(UserService userService) {
this.userService = userService; // 持有原对象的引用
}
public void addUser() {
System.out.println("Before adding user."); // 增强逻辑
userService.addUser(); // 调用原对象的方法
}
}
// 主程序
public class Main {
public static void main(String[] args) {
UserService userService = new UserService("Alice");
UserServiceProxy proxy = new UserServiceProxy(userService);
proxy.addUser(); // 通过代理调用
}
}
- AOP 创建一个代理对象,代理对象实现了原对象的接口(对于 JDK 动态代理)或是原对象的子类(对于 CGLIB 代理)。
- 当外部调用代理对象的方法时,实际上是通过代理来执行目标方法。
所以AOP实际上是操作的代理对象(Proxy),而不是原对象(目标对象Target),代理对象是在原对象的基础上进行了扩展(增强),从另一个角度看,AOP是在对(bean对象)对象进行处理,就是 f(x)。
2.切入点
切入点指定了哪些Bean对象的方法调用应该被拦截。
在AOP(面向切面编程)的上下文中,通俗点理解方法调用可以被视为对象的行为,而AOP的主要功能之一就是检测这些对象的行为(即方法调用),并在特定条件下触发额外的逻辑(即切面的通知)。
具体来说,AOP框架会监视应用程序中的方法调用,以识别出与切面切入点表达式相匹配的连接点。一旦检测到这样的连接点(即特定的方法调用),AOP框架就会根据切面的定义在该连接点之前、之后或周围执行相应的通知。
切入点可以通过表达式定义,这些表达式描述了要拦截的方法或类。以下是定义切入点的常见方式。
1. 使用 AspectJ 表达式
在 Spring AOP 中,切入点通常使用 AspectJ 表达式来定义,常见的表达式包括:
方法名匹配:
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {
// 切入点定义
}
这个切入点匹配 com.example.service
包下的所有类的所有方法。
特定方法匹配:
@Pointcut("execution(* com.example.service.UserService.addUser(..))")
public void addUserMethod() {
// 切入点定义
}
这个切入点仅匹配 UserService
类中的 addUser
方法。
类匹配:
@Pointcut("within(com.example.service.*)")
public void allServiceMethods() {
// 切入点定义
}
这个切入点匹配 com.example.service
包下的所有类的所有方法。
注解匹配:
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMappingMethods() {
// 切入点定义
}
这个切入点匹配所有带有 @PostMapping
注解的方法。
2. 组合切入点
切入点可以通过逻辑运算符组合,以实现更复杂的匹配条件:
并且:
@Pointcut("execution(* com.example.service.*.*(..)) && within(com.example.service.UserService)")
public void specificServiceMethods() {
// 切入点定义
}
或:
@Pointcut("execution(* com.example.service.UserService.*(..)) || execution(* com.example.service.AdminService.*(..))")
public void userAndAdminServiceMethods() {
// 切入点定义
}
非:
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.UserService.addUser(..))")
public void allServiceMethodsExceptAddUser() {
// 切入点定义
}
切入点方法的主要目的是提供一个可重用的切入点定义,不需要在方法体中写任何逻辑。这样可以使得切入点的管理更加集中和清晰。
3.连接点
程序执行中的一个特定点,在这个点上可以插入额外的行为(即通知)。连接点可以是方法调用、异常处理、对象创建等,具体取决于 AOP 的实现和配置。在 Spring AOP 中,连接点主要是方法执行。在 AspectJ 中,连接点的范围更广,包括方法执行、构造函数调用、字段访问等。切入点定义了哪些连接点应该被通知。也就是说,连接点是实际的执行点,而切入点是匹配连接点的条件。常指的是目标对象中执行的方法。
package com.example.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// 定义切入点,匹配 UserService 中的所有方法
@Around("execution(* com.example.service.UserService.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 前置逻辑
System.out.println("Before executing method: " + joinPoint.getSignature().getName());
// 调用目标方法
Object result = joinPoint.proceed();
// 后置逻辑
System.out.println("After executing method: " + joinPoint.getSignature().getName());
// 可以对结果进行修改
return "Extended: " + result;
}
}
1. JoinPoint
- 定义:表示程序执行中的一个特定点,通常用于获取有关连接点的信息,如方法名、参数、目标对象等。
- 用途:一般用于
@Before
和@After
通知中,只能用于访问连接点的信息,但不能用于控制目标方法的执行。
2. ProceedingJoinPoint
- 定义:是
JoinPoint
的子接口,主要用于@Around
通知。 - 用途:不仅可以获取连接点的信息,还允许你控制目标方法的执行。通过调用
proceed()
方法,可以在切面中执行目标方法,并可选择修改其返回值。
4.通知/增强
指的是在特定连接点执行的额外行为或逻辑。通知可以在目标方法执行的不同阶段插入特定的操作,以实现对目标方法的增强和扩展。
直接在通知中使用切入点表达式是可以的,适合简单场景。也就是说,可以不定义切入点。
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 环绕通知逻辑
}
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {
// 切入点定义
}
@Around("serviceMethods()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 环绕通知逻辑
}
上述两个都是可以的。
前置通知(Before Advice):
- 在目标方法执行之前执行。
- 用于进行准备工作或记录日志。
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before executing method: " + joinPoint.getSignature().getName());
}
后置通知(After Advice):
- 在目标方法执行之后执行,不管目标方法是否正常完成。
- 用于清理资源或记录结束日志。
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After executing method: " + joinPoint.getSignature().getName());
}
返回通知(After Returning Advice):
- 在目标方法正常返回后执行。
- 可用于处理返回值。
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("Method returned: " + result);
}
异常通知(After Throwing Advice):
- 在目标方法抛出异常后执行。
- 可用于处理异常和记录错误信息。
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
System.out.println("Method threw: " + error);
}
环绕通知(Around Advice):
- 包裹了目标方法的执行,可以在执行前后插入逻辑。
- 允许控制目标方法的执行。
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed(); // 调用目标方法
System.out.println("After method: " + joinPoint.getSignature().getName());
return result;
}
5.引介
是 AOP(面向切面编程)中的一种特殊类型的通知,它允许向现有的类中添加新的方法或属性,而无需修改原有类的代码。引介通常用于增强类的功能,使其实现额外的接口。
-
增强现有类:引介可以给目标类添加新的功能,比如新的方法或属性。
-
实现接口:通过引介,可以让目标类实现一个或多个新的接口,从而增加其行为。
-
不修改原有代码:通过引介,可以在不修改原始类的情况下添加新功能,这有助于保持代码的可维护性。
//定义接口
package com.example;
public interface Monitorable {
void monitor();
}
//实现用户服务
package com.example.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void addUser(String username) {
System.out.println("User " + username + " added.");
}
}
//创建引介
package com.example.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareMixin;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MonitoringAspect {
@DeclareMixin("com.example.service.UserService")
public Monitorable createMonitorable(UserService userService) {
return new Monitorable() {
@Override
public void monitor() {
System.out.println("Monitoring UserService actions.");
}
};
}
}
//使用引介
package com.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.example.service.UserService;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
// 使用引介提供的新方法
if (userService instanceof Monitorable) {
((Monitorable) userService).monitor();
}
userService.addUser("Alice");
}
}
//结果
Monitoring UserService actions.
User Alice added.
一个HTTP请求在Web应用中的经过顺序通常是:首先通过过滤器,然后经过拦截器的预处理(preHandle
),接着可能触发AOP切面进行额外的处理,之后到达控制器执行具体的业务逻辑,控制器处理完毕后可能再次触发AOP切面进行后处理,最后经过拦截器的后处理(postHandle
和afterCompletion
)和过滤器的后处理,最终将响应返回给客户端。这个顺序可能因具体的配置和使用的技术而有所不同。