@Before
定义与作用
@Before 声明前置通知(Before Advice),在目标方法执行之前触发。它是最简单、最常用的 Advice 类型,适用于参数校验、权限检查、日志记录等需要在业务逻辑开始前完成的操作。
关键特性:
@Before无法阻止目标方法的执行,除非在 Advice 方法中抛出异常@Before无法修改目标方法的参数值(若需修改参数,应使用@Around)@Before可以访问JoinPoint对象,获取目标方法名、参数、目标对象等信息
适用位置与常用属性
适用位置
@Before 标注在切面类的方法上,表示该方法是一个前置通知。该方法必须位于 @Aspect 切面类内部。
@Aspect
@Component
public class ValidationAspect {
@Before("execution(* com.feixiang.service.*.*(..))")
public void validateBefore(JoinPoint joinPoint) {
// 前置通知逻辑
}
}
常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
value | String | 切入点表达式,或引用 @Pointcut 方法名。这是唯一必填属性 |
argNames | String | 参数名称,用于绑定目标方法参数到 Advice 方法(通常可省略,编译器需开启 -parameters) |
核心原理
@Before 在代理链中的执行位置
当客户端通过代理对象调用目标方法时,@Before 在目标方法真正执行前被触发:
@Before 与代理链的关系
完整示例:飞翔科技学生管理系统参数校验
场景简述
广州飞翔科技公司的后端开发小崔负责学生成绩管理系统的参数校验模块。产品经理孔蓝要求:所有 Service 层方法的参数必须在进入业务逻辑前完成校验,例如学号不能为 null 或负数,成绩必须在 0 到 100 之间。小崔决定使用 @Before 实现统一的参数校验,避免在每个业务方法开头写 if (studentId == null)。
操作前代码/配置
在未使用 @Before 之前,小崔在每个 Service 方法中手动校验参数:
@Service
public class StudentScoreService {
public Score queryScore(Long studentId) {
// 重复!每个方法都要校验参数
if (studentId == null || studentId <= 0) {
throw new IllegalArgumentException("学号必须为正整数");
}
// 业务逻辑
return scoreDao.findByStudentId(studentId);
}
public void updateScore(Long studentId, Double newScore) {
// 重复!参数校验再次出现在业务方法中
if (studentId == null || studentId <= 0) {
throw new IllegalArgumentException("学号必须为正整数");
}
if (newScore == null || newScore < 0 || newScore > 100) {
throw new IllegalArgumentException("成绩必须在 0-100 之间");
}
// 业务逻辑
scoreDao.updateScore(studentId, newScore);
}
public void deleteScore(Long studentId) {
// 重复!第三次出现学号校验
if (studentId == null || studentId <= 0) {
throw new IllegalArgumentException("学号必须为正整数");
}
// 业务逻辑
scoreDao.deleteByStudentId(studentId);
}
}
问题:参数校验代码高度重复,且业务逻辑被淹没在校验代码中。如果孔蓝要求增加"学号必须以 2024 开头"的规则,所有方法都要修改。
使用该注解的完整代码
第一步:定义切点
@Aspect
@Component
public class ValidationAspect {
// 切点:所有 Service 层方法
@Pointcut("execution(* com.feixiang.student.service.*.*(..))")
public void serviceLayer() {}
// 切点:第一个参数为 Long 类型的方法
@Pointcut("args(studentId, ..)")
public void firstArgIsStudentId(Long studentId) {}
}
第二步:使用 @Before 实现参数校验
@Aspect
@Component
public class ValidationAspect {
@Pointcut("execution(* com.feixiang.student.service.*.*(..))")
public void serviceLayer() {}
// 校验学号:所有第一个参数为 Long studentId 的方法
@Before("serviceLayer() && args(studentId, ..)")
public void validateStudentId(Long studentId) {
if (studentId == null || studentId <= 0) {
throw new IllegalArgumentException(
"[飞翔科技-校验] 学号必须为正整数,当前值:" + studentId);
}
// 校验学号格式:必须以 2024 开头
String idStr = String.valueOf(studentId);
if (!idStr.startsWith("2024")) {
throw new IllegalArgumentException(
"[飞翔科技-校验] 学号必须以 2024 开头,当前值:" + studentId);
}
System.out.println("[飞翔科技-校验] 学号 " + studentId + " 校验通过");
}
// 校验成绩:所有包含 Double score 参数的方法
@Before("serviceLayer() && args(.., newScore)")
public void validateScore(Double newScore) {
if (newScore == null || newScore < 0 || newScore > 100) {
throw new IllegalArgumentException(
"[飞翔科技-校验] 成绩必须在 0-100 之间,当前值:" + newScore);
}
System.out.println("[飞翔科技-校验] 成绩 " + newScore + " 校验通过");
}
// 通用日志记录:所有 Service 方法执行前打印方法名和参数
@Before("serviceLayer()")
public void logMethodEntry(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.printf("[飞翔科技-日志] 进入方法 %s,参数:%s%n",
methodName, Arrays.toString(args));
}
}
第三步:纯净的业务 Service
@Service
public class StudentScoreService {
private final ScoreDao scoreDao;
public StudentScoreService(ScoreDao scoreDao) {
this.scoreDao = scoreDao;
}
public Score queryScore(Long studentId) {
// 纯粹的业务逻辑,无任何参数校验代码
System.out.println("[业务] 查询学生 " + studentId + " 的成绩");
return scoreDao.findByStudentId(studentId);
}
public void updateScore(Long studentId, Double newScore) {
// 纯粹的业务逻辑
System.out.println("[业务] 更新学生 " + studentId + " 的成绩为 " + newScore);
scoreDao.updateScore(studentId, newScore);
}
public void deleteScore(Long studentId) {
// 纯粹的业务逻辑
System.out.println("[业务] 删除学生 " + studentId + " 的成绩");
scoreDao.deleteByStudentId(studentId);
}
}
第四步:配置类
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
操作后运行结果及分析
测试代码:
public class BeforeDemo {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
StudentScoreService service = ctx.getBean(StudentScoreService.class);
// 测试正常参数
System.out.println("=== 测试正常参数 ===");
service.queryScore(2024001L);
System.out.println("\n=== 测试成绩更新 ===");
service.updateScore(2024001L, 92.5);
System.out.println("\n=== 测试非法学号 ===");
try {
service.queryScore(-1L);
} catch (IllegalArgumentException e) {
System.err.println("捕获异常:" + e.getMessage());
}
System.out.println("\n=== 测试非法成绩 ===");
try {
service.updateScore(2024001L, 150.0);
} catch (IllegalArgumentException e) {
System.err.println("捕获异常:" + e.getMessage());
}
ctx.close();
}
}
控制台输出:
=== 测试正常参数 ===
[飞翔科技-日志] 进入方法 queryScore,参数:[2024001]
[飞翔科技-校验] 学号 2024001 校验通过
[业务] 查询学生 2024001 的成绩
=== 测试成绩更新 ===
[飞翔科技-日志] 进入方法 updateScore,参数:[2024001, 92.5]
[飞翔科技-校验] 学号 2024001 校验通过
[飞翔科技-校验] 成绩 92.5 校验通过
[业务] 更新学生 2024001 的成绩为 92.5
=== 测试非法学号 ===
[飞翔科技-日志] 进入方法 queryScore,参数:[-1]
捕获异常:[飞翔科技-校验] 学号必须为正整数,当前值:-1
=== 测试非法成绩 ===
[飞翔科技-日志] 进入方法 updateScore,参数:[2024001, 150.0]
[飞翔科技-校验] 学号 2024001 校验通过
捕获异常:[飞翔科技-校验] 成绩必须在 0-100 之间,当前值:150.0
分析:
@Before在目标方法执行前完成参数校验,校验失败时抛出异常,目标方法不会执行args(studentId, ..)将目标方法的第一个参数绑定到 Advice 方法的studentId参数JoinPoint提供了getArgs()方法,可以获取所有参数值用于日志记录- 业务代码完全纯净,参数校验逻辑统一由切面维护
易错场景与面试考点
易错场景一:@Before 中试图修改参数值
反例:
@Before("serviceLayer() && args(studentId)")
public void modifyParam(Long studentId) {
// 错误!这不会修改目标方法的参数值
studentId = 2024999L;
}
现象:目标方法接收到的 studentId 仍然是原始值,没有被修改。
原因:Java 是值传递,studentId 是 Long 对象的引用副本,修改局部变量不会影响目标方法的参数。
正确做法:若需修改参数,使用 @Around:
@Around("serviceLayer() && args(studentId)")
public Object modifyParam(ProceedingJoinPoint joinPoint, Long studentId) throws Throwable {
Object[] args = joinPoint.getArgs();
args[0] = 2024999L; // 修改参数数组中的值
return joinPoint.proceed(args); // 使用修改后的参数执行
}
易错场景二:@Before 中未正确处理异常导致信息泄露
反例:
@Before("serviceLayer()")
public void checkPermission(JoinPoint joinPoint) {
try {
// 某些可能失败的操作
riskyOperation();
} catch (Exception e) {
// 错误!直接抛出原始异常可能泄露敏感信息
throw e;
}
}
正确做法:在 @Before 中抛出的异常会被直接返回给客户端,应转换为业务异常:
@Before("serviceLayer()")
public void checkPermission(JoinPoint joinPoint) {
if (!hasPermission()) {
throw new AccessDeniedException("无权访问该方法");
}
}
面试高频考点
考点一:@Before 能否阻止目标方法执行?如何阻止?
@Before本身无法主动阻止目标方法执行,但可以通过抛出异常间接阻止。一旦@Before抛出异常,代理链中断,目标方法不会被执行,异常直接返回给调用方。这是@Before与@Around的核心区别:@Around可以主动决定是否调用proceed(),而@Before只能被动地通过异常中断流程。
考点二:@Before 和 @Around 在日志场景下如何选择?
如果只需要在方法执行前记录日志或做简单校验,使用
@Before更简洁。如果需要同时记录方法执行前后的状态(如耗时),或需要修改参数/返回值,或需要控制是否执行目标方法,则必须使用@Around。@Before的优势是语义明确、代码简单;@Around的优势是功能强大、控制完整。
考点三:@Before 方法中的 JoinPoint 参数有什么作用?
JoinPoint提供了目标方法的上下文信息,包括:getSignature()获取方法签名(方法名、参数类型等);getArgs()获取实际参数值;getTarget()获取目标对象;getThis()获取代理对象。在@Before中,JoinPoint是可选参数(Spring 会自动注入),但通常建议声明以获取方法上下文。