tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 探索SpringBoot

  • 常用功能实现

    • SpringBoot常用功能实现_1
    • SpringBoot中拦截器与日志
    • 多环境配置与数据绑定
    • 异步方法(线程池)的实现
    • controller参数接收
    • SpringBoot中关于日志的更好使用
    • 异常捕获的一些细节
    • 时间跟其他数据的序列化
    • 面向切面跟自定义注解的结合
      • 面向切面的使用
        • 添加依赖
        • 目标类和定义切面
        • 参数的意思
        • 测试
      • 有参数的使用
        • JoinPoint的使用
        • 一个例子
        • 参数介绍
        • 更广泛的定位
      • 结合注解使用切面
        • 注解实例
        • 多个属性的自定义注解
        • 多属性例子
        • 使用 JoinPoint 和直接参数注入
        • 注意-注解-全类名路径
  • Security认证授权

  • 扩展

  • 实战与注意事项

  • 其它

  • 《SpringBoot》笔记
  • 常用功能实现
EffectTang
2025-05-22
目录

面向切面跟自定义注解的结合

# 面向切面跟自定义注解的结合

# 面向切面的使用

关于面向切面是什么,这里不过多赘述了,直接上操作。不过,演示之前,还是想问一个问题,面相切面的原理是什么——动态代理,那动态代理的基础是什么——反射。

在 Spring Boot 应用中使用面向切面编程(AOP,Aspect-Oriented Programming)可以帮助你分离横切关注点(如日志记录、事务管理等),从而使业务逻辑代码更加简洁和清晰。下面通过一个简单的例子来演示如何在 Spring Boot 中配置和使用 AOP。

# 添加依赖

<dependencies>
    <!-- 其他依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>
1
2
3
4
5
6
7

# 目标类和定义切面

  • 目标类
package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
    
    public void findUser() {
        System.out.println("Finding user...");
        // 模拟业务逻辑
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 定义切面

记得加入IOC容器,并标明这是一个切面类,就是加上@Aspect和@Component。

package com.example.demo.aspect;

import org.aspectj.lang.annotation.After;
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.demo.service.UserService.findUser(..))")
    public void beforeFindUser() {
        System.out.println("Before finding user.");
    }

    @After("execution(* com.example.demo.service.UserService.findUser(..))")
    public void afterFindUser() {
        System.out.println("After finding user.");
    }
  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 参数的意思

上述切面类中,@Before 注解是 Spring AOP 中用于定义前置通知(Advice)的注解,它允许你在目标方法执行之前插入额外的处理逻辑。至于括号中的内容, @Before("execution(* com.example.demo.service.UserService.findUser(..))") 则是一个切入点表达式(Pointcut Expression),这个表达式定义了哪些方法会被该前置通知拦截。

  • 切入点表达式

在你的例子中,切入点表达式为 "execution(* com.example.demo.service.UserService.findUser(..))",下面是对这个表达式的详细解释:

  • execution:这是最常用的切入点指示符之一,用于匹配方法执行连接点。换句话说,它帮助我们选择特定的方法调用作为切入点。
  • (*):表示返回类型。星号(*)意味着任何返回类型都将被匹配。如果你只想针对特定返回类型的方法进行拦截,则可以在这里指定具体的类型,例如 java.lang.String。
  • com.example.demo.service.UserService:指定了包含目标方法的类路径。在这个例子中,表示 UserService 类位于 com.example.demo.service 包下。
  • findUser:这是要匹配的方法名。如果你想对多个方法应用相同的切面逻辑,你可以使用通配符(如 find* 来匹配所有以 find 开头的方法)。
  • (..):圆括号内的双点号(..)表示任意数量和类型的参数。如果你想要精确匹配参数的数量和类型,可以直接在这里列出参数类型,比如 (int, String) 表示只匹配有两个参数且第一个参数为 int,第二个为 String 的方法。

# 测试

这个简单的例子配置好后,下面就开始简单测试啦

private final UserService userService;

@Test
public void run(String... args) throws Exception {
    userService.findUser();
}
1
2
3
4
5
6

最后,你会发现它的输出有三行

Before finding user.
Finding user...
After finding user.
1
2
3

# 有参数的使用

# JoinPoint的使用

上述的例子很简单,面向切面一般用作参数补充或者业务逻辑补充,不过实际开发中,目标类中的某个方法基本都有参数,遇到这种情况,该怎么处理呢?如果想使用其中的参数该怎么办?或者说切面方法,怎么获得目标类的参数?

  • 使用参数-JoinPoint joinPoint

在 Spring AOP 中,JoinPoint 是一个非常重要的接口,它提供了访问连接点(Join Point)信息的能力。连接点是指程序执行过程中明确的点,比如方法的调用或者异常的抛出等。当你使用 @Before、@After 等注解定义通知(Advice)时,可以通过参数传递 JoinPoint 对象来获取关于当前拦截的方法的信息。

它的作用:

  • 提供对当前拦截方法的元数据访问:包括方法签名、方法参数、目标对象等。
  • 允许你动态地获取或修改某些行为:例如,可以检查传入的参数值,甚至可以在某些情况下改变它们(尽管直接修改参数的情况较少见)。

# 一个例子

以下是一个简单的用户实体类 User 和一个服务类 UserService。

package com.example.demo.model;

public class User {
    private Long id;
    private String name;

    // Constructors, getters and setters
    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{id=" + id + ", name='" + name + "'}";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

以下是服务类

package com.example.demo.service;

import com.example.demo.model.User;

public class UserService {

    public User findUserById(Long id) {
        // 模拟从数据库查询用户
        return new User(id, "John Doe");
    }
}
1
2
3
4
5
6
7
8
9
10
11

最后是一个切面类 LoggingAspect,它会在 UserService.findUserById 方法被调用之前记录相关信息。

package com.example.demo.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Before("execution(* com.example.demo.service.UserService.findUserById(..))")
    public void logMethodCallDetails(JoinPoint joinPoint) {
        // 获取方法签名
        String methodName = joinPoint.getSignature().getName();
        
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        StringBuilder params = new StringBuilder();
        for (Object arg : args) {
            params.append(arg).append(", ");
        }
        if (params.length() > 0) {
            params.setLength(params.length() - 2); // 移除最后多余的逗号和空格
        }

        // 日志输出
        logger.info("Calling method: {} with arguments: {}", methodName, params.toString());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

在这个示例中,logMethodCallDetails 方法是一个前置通知,它会在 UserService.findUserById 方法执行前运行。通过 JoinPoint 对象,我们可以获取到当前被拦截的方法的名字 (methodName) 及其参数 (args)。然后,这些信息被格式化并记录下来。

# 参数介绍

下面对参数做进一步介绍:

Object[] args = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
1
2
3
4
  • joinPoint.getArgs():
    • 获取被拦截方法的所有参数(以数组形式返回)。
    • 比如某个方法是:public void saveUser(User user, String name),那么 args[0] 是 user,args[1] 是 "name"。
  • joinPoint.getSignature():
    • 获取当前连接点的方法签名信息(如方法名、参数类型等)。
  • MethodSignature:
    • 这是一个接口,扩展自 Signature,专门用于表示方法级别的签名信息。
    • 需要向下转型才能调用 .getMethod() 方法。
  • methodSignature.getMethod():
    • 获取当前被拦截的 Java 方法对象(java.lang.Method),可用于反射操作,比如获取注解、参数类型等。

# 更广泛的定位

@Before("execution(* com.example.demo.service.UserService.findUserById(..))"),上述的例子中,是拦截具体的某个方法,不管其中传入的参数,只要名字符合即可。

那如何想一次性拦截多个方法呢?

可以使用以下的写法

 @Pointcut("execution(* com.sky.mapper.*.*(..))")
 public void autoFillPointCut(){
    // *********
 }

@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
    /////////////////////重要////////////////////////////////////
    //可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解
    log.info("开始进行公共字段自动填充...");

}
1
2
3
4
5
6
7
8
9
10
11
12

这个切入点表达式的意思是:“匹配 com.sky.mapper 包下的所有类中的所有方法”。

那如果你想拦截不同包下的不同方法呢?上述的这种方法似乎不太行。

要想实现上述目的,则可以考虑结合注解来实现。

# 结合注解使用切面

配合注解,将某个方法,加上注解后,比如加上某个自定义的注解,最后再在切面类,定义Pointcut时,加上注解定位即可,达到精准定位的目的,这样就实现了不同包,不同类,不同方法的

@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}

/**
 * 前置通知,在通知中进行公共字段的赋值
 */
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
    /////////////////////重要////////////////////////////////////
    //可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解
    log.info("开始进行公共字段自动填充...");

}
1
2
3
4
5
6
7
8
9
10
11
12
13

上述例子表示,,@annotation(com.example.annotation.AutoFill) 表示这个前置通知只会应用到那些被 @AutoFill 注解标记的方法上。同时,它又加上了&&与——操作,则表示只有在,com.sky.mapper包下且方法上有 自定义注解@AutoFill的方法才行。

那方法中的属性怎么获取呢?

也很简单。

# 注解实例

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //数据库操作类型:UPDATE INSERT
    OperationType value();
}
1
2
3
4
5
6
  • 获取注解属性
 		@Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint){
        log.info("开始进行公共字段自动填充...");

        //获取到当前被拦截的方法上的数据库操作类型
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
      	// 获取 注解
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
        // 获取 注解的属性值
      	OperationType operationType = autoFill.value();//获得数据库操作类型

        //获取到当前被拦截的方法的参数--实体对象
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0){
            return;
        }
      	// *** 更多后续操作
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 多个属性的自定义注解

上述的方法是通过,JoinPoint,获得注解,但在 AOP 切面中更常用的方式是直接在通知(Advice)的方法签名中声明注解作为参数,这样 Spring AOP 会自动将匹配的方法上的注解实例传递给你的通知方法。

下面将展示如何做到这一点。

# 多属性例子

假设我们有一个带有多个属性的自定义注解 @OperationLog,以及相应的业务逻辑和切面代码。

  • 自定义多属性注解
package com.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 定义注解,仅允许在方法上使用
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OperationLog {
    String operationType(); // 操作类型
    String description() default "No description"; // 描述,默认值为"No description"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 业务逻辑及接口实现
package com.example.service;

public interface UserService {
    void createUser(String username);
}

package com.example.service.impl;

import com.example.annotation.OperationLog;
import com.example.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    @Override
    @OperationLog(operationType = "CREATE", description = "Creating a new user")
    public void createUser(String username) {
        System.out.println("Creating user: " + username);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 使用 JoinPoint 和直接参数注入

在这个例子中,我们将演示两种方式来获取注解及其属性:

  • 直接参数注入:这是推荐的方式。
  • 通过 JoinPoint 获取
package com.example.aspect;

import com.example.annotation.OperationLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
public class OperationLogAspect {

    // 直接参数注入的方式
    @AfterReturning("@annotation(operationLog)")
    public void logOperationWithDirectParameter(OperationLog operationLog) {
        System.out.println("操作类型(直接参数): " + operationLog.operationType());
        System.out.println("描述(直接参数): " + operationLog.description());
    }

    // 使用 JoinPoint 获取注解的方式
    @AfterReturning("execution(* com.example.service..*(..))") // 更通用的切入点表达式
    public void logOperationWithJoinPoint(JoinPoint joinPoint) throws NoSuchMethodException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 从方法中获取注解
        OperationLog operationLog = method.getAnnotation(OperationLog.class);
        if (operationLog != null) {
            System.out.println("操作类型(JoinPoint): " + operationLog.operationType());
            System.out.println("描述(JoinPoint): " + operationLog.description());
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

在上述代码片段中,@annotation(operationLog) 并没有显式地指定包名。这是因为我们在通知方法的参数列表中已经声明了 OperationLog operationLog 参数。Spring AOP 能够根据这个参数类型自动推断出对应的注解类型,因此不需要再在 @annotation 表达式中添加包名。

但是,如果你希望在切入点表达式中明确指定注解的全限定名,也可以这样做。例如:

@Before("@annotation(com.example.annotation.OperationLog)")
public void logOperation(OperationLog operationLog) {
    // ...
}
1
2
3
4

# 注意-注解-全类名路径

有时候你发现不使用注解的全类名路径时,可能会报错,而有时候不会。

报错与否,完全取决你——是否要将,注解绑定到方法参数上。

如果你不绑定注解到方法参数上,可以在切点表达式中使用全类名路径(如 @annotation(com.sky.annotations.TestAnnotation))。 但如果你希望将注解实例绑定到方法参数上供后续使用,则不能直接在表达式中写全类名,而必须使用一个变量名(如 @annotation(testAnnotation)),Spring AOP 才能将其绑定到方法参数。

  • 只做匹配,不绑定参数
@Before("execution(* com.sky.service.impl.*.*(..)) && @annotation(com.sky.annotations.TestAnnotation)")
public void autoFill(JoinPoint joinPoint) {
    ...
}

1
2
3
4
5
  • 要做参数绑定,就必须用变量名
@Before("execution(* com.sky.service.impl.*.*(..)) && @annotation(testAnnotation)")
public void autoFill(JoinPoint joinPoint, TestAnnotation testAnnotation) {
    ...
}

1
2
3
4
5

最后还要提醒你是,为了避免命名冲突,还是建议你使用全类名路径。

  • 如果项目中存在多个同名的自定义注解(例如 com.package1.TestAnnotation 和 com.package2.TestAnnotation),使用简写会导致 Spring 无法确定具体引用的是哪个注解,从而引发错误。
  • 全限定类名可以唯一标识注解类型,避免歧义。
上次更新: 2025/05/24, 15:01:47
时间跟其他数据的序列化
认证授权与SpringSecurity_概念篇

← 时间跟其他数据的序列化 认证授权与SpringSecurity_概念篇→

最近更新
01
时间跟其他数据的序列化
05-19
02
数据加密与安全
05-17
03
异常捕获的一些细节
05-14
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式