登录校验

  • 定义@GlobalInterceptor注解
@Target({ElementType.METHOD, ElementType.TYPE})
//@Retention注解指定了GlobalInterceptor注解在运行时(RetentionPolicy.RUNTIME)仍然有效,这意味着可以通过反射获取注解的信息。
@Retention(RetentionPolicy.RUNTIME)
public @interface GlobalInterceptor {
    //用于指定是否需要校验登录,默认值为false。
    boolean checkLogin() default false;
}

全局拦截器

@Component("operationAspect")
@Aspect
@Slf4j
public class GlobalOperationAspect {

    @Resource
    private RedisUtils redisUtils;

    //定义切入点
    //@Before注解指定了在目标方法执行之前执行interceptorDo方法。
    // 这里的表达式"@annotation(com.easylive.web.annotation.GlobalInterceptor)"
    // 表示如果方法上有GlobalInterceptor注解,则执行interceptorDo方法。
    @Before("@annotation(com.easylive.web.annotation.GlobalInterceptor)")
    public void interceptorDo(JoinPoint point) {
//        point.getSignature():这个方法返回当前连接点(JoinPoint)的签名(Signature)。
//        在Spring AOP中,连接点代表被拦截的点,比如一个方法的执行。
//        (MethodSignature):这是一个类型转换,将point.getSignature()的结果转换为MethodSignature类型。
//        MethodSignature是Signature的一个子接口,它提供了获取方法相关信息的能力。
//        getMethod():这个方法从MethodSignature对象中获取实际的Method对象,这个对象代表了被拦截的方法。
        Method method = ((MethodSignature) point.getSignature()).getMethod();
//        method.getAnnotation(GlobalInterceptor.class):这个方法使用反射机制来检查method对象是否有GlobalInterceptor注解。
//        如果有,它将返回该注解的实例;如果没有,它将返回null。
//        GlobalInterceptor interceptor:这是一个变量声明,用于存储获取到的GlobalInterceptor注解实例。
//        如果method没有GlobalInterceptor注解,interceptor将被赋值为null。
        GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class);
        //方法上没有该注解就直接返回
        if (null == interceptor) {
            return;
        }
        /**
         * 校验登录
         */
        //该方法上的注解中checkLogin的值为true就要进行登录校验
        if (interceptor.checkLogin()) {
            checkLogin();
        }
    }

    //校验登录
    private void checkLogin() {
//        RequestContextHolder.getRequestAttributes():RequestContextHolder是Spring框架提供的一个工具类,
//        用于在不同的组件之间传递当前请求的上下文信息。getRequestAttributes()方法返回当前请求的RequestAttributes对象,
//        该对象包含了请求相关的信息。
//        (ServletRequestAttributes):这是一个类型转换,将RequestAttributes对象转换为ServletRequestAttributes类型。
//        ServletRequestAttributes是RequestAttributes的一个子类,专门用于封装Servlet请求的属性。
//        getRequest():这个方法从ServletRequestAttributes对象中获取实际的HttpServletRequest对象,
//        这个对象代表了当前的HTTP请求。
        HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

//        request.getHeader(Constants.TOKEN_WEB):getHeader方法是HttpServletRequest的一个方法,
//        用于获取指定名称的请求头的值。在这个例子中,它被用来获取名为Constants.TOKEN_WEB的请求头的值,
//        这个常量通常定义了令牌(token)的名称。
//        String token:这是一个变量声明,用于存储从请求头中获取到的令牌值
        String token = request.getHeader(Constants.TOKEN_WEB);
        if (StringTools.isEmpty(token)) {
            throw new BusinessException(ResponseCodeEnum.CODE_901);
        }
        TokenUserInfoDto tokenUserInfoDto =
                (TokenUserInfoDto) redisUtils.get(Constants.REDIS_KEY_TOKEN_WEB + token);
        if (tokenUserInfoDto == null) {
            throw new BusinessException(ResponseCodeEnum.CODE_901);
        }
    }
}

这段代码是一个 Spring AOP(面向切面编程)实现的全局操作拦截器,专门用于校验请求是否携带有效的登录凭证(token)。通过 @GlobalInterceptor 注解来标记需要校验登录的接口方法,AOP 在方法执行前会进行登录校验,确保用户已登录。

详细解析:

1. 类定义和注解
@Component("operationAspect")
@Aspect
@Slf4j
public class GlobalOperationAspect {
  • @Component("operationAspect"): 将该类声明为一个 Spring Bean,名为 operationAspect,使其能被 Spring 管理。
  • @Aspect: 标记该类为一个切面,表示它包含横切关注点(cross-cutting concerns),这里是进行方法执行前的拦截。
  • @Slf4j: 提供日志功能,用于记录日志信息。
2. 字段注入
@Resource
private RedisUtils redisUtils;
  • 使用 @Resource 注解自动注入 RedisUtils,用于后续从 Redis 中获取用户的 token 信息。
3. 切入点定义
@Before("@annotation(com.easylive.web.annotation.GlobalInterceptor)")
public void interceptorDo(JoinPoint point) {
  • @Before 注解:指定在目标方法执行之前执行 interceptorDo() 方法。
  • @annotation(com.easylive.web.annotation.GlobalInterceptor): 这是一个切点表达式,表示只会拦截那些方法上有 @GlobalInterceptor 注解的接口。当 @GlobalInterceptor 注解标记的方法被调用时,interceptorDo() 方法会被执行。

好的,第四点主要是关于获取方法上的注解(@GlobalInterceptor)并进行相应处理的部分,下面将详细解释这一部分。

4. 获取方法和注解

Method method = ((MethodSignature) point.getSignature()).getMethod();
GlobalInterceptor interceptor = method.getAnnotation(GlobalInterceptor.class);
if (null == interceptor) {
    return;
}
解释步骤:
  1. point.getSignature():

    • pointJoinPoint 类型的参数,代表当前切面所拦截的连接点(即当前被调用的方法)。JoinPoint 提供了多个方法来访问当前方法的信息。
    • point.getSignature() 返回一个 Signature 类型的对象。Signature 是 AOP 中的一个基础类,它代表了被拦截方法的签名信息(例如方法名、参数类型等)。
  2. (MethodSignature) point.getSignature():

    • Signature 是一个接口,MethodSignature 是它的一个子接口,表示的是方法的签名(即方法的信息)。MethodSignatureSignature 提供了更多与方法相关的细节信息。
    • 使用 (MethodSignature)point.getSignature() 进行类型转换,将其转换为 MethodSignature,这样我们就能访问更多特定于方法的信息,例如方法的参数类型、返回值类型等。
  3. method.getAnnotation(GlobalInterceptor.class):

    • method 是当前目标方法的 Method 对象,它是通过 ((MethodSignature) point.getSignature()).getMethod() 获得的。
    • getAnnotation(GlobalInterceptor.class) 方法通过反射获取当前方法上的 @GlobalInterceptor 注解。如果该方法上没有这个注解,返回 null
    • GlobalInterceptor 是一个自定义的注解,用于标记需要进行某些特定操作(例如校验登录)的目标方法。
  4. if (null == interceptor) { return; }:

    • 如果当前方法上没有 @GlobalInterceptor 注解,则 interceptor 会为 null
    • 在这种情况下,if 语句会判断 interceptor 是否为 null,如果是 null,则直接返回,不做任何处理。这样就跳过了当前方法的拦截,意味着不需要执行后续的登录校验逻辑。

目的

这一部分的目的是:

  1. 检查目标方法是否有 @GlobalInterceptor 注解

    • 如果方法上有 @GlobalInterceptor 注解,AOP 切面会对该方法进行处理,执行 interceptorDo() 方法中的逻辑。
    • 如果方法上没有这个注解,说明该方法不需要经过此切面的拦截,所以直接返回,跳过校验。
  2. 通过反射获取注解实例

    • method.getAnnotation(GlobalInterceptor.class) 用来通过反射机制获取 @GlobalInterceptor 注解实例。@GlobalInterceptor 注解可以包含一些配置参数,比如 checkLogin 属性,指示是否需要进行登录校验。
  3. 决定是否继续执行拦截操作

    • if (null == interceptor) 判断是否存在该注解,如果没有找到 @GlobalInterceptor 注解,则当前方法不需要进行进一步的拦截处理。否则,继续执行后续的逻辑(如校验登录)。

举例

假设我们有以下一个方法:

@GlobalInterceptor(checkLogin = true)
public void someProtectedMethod() {
    // 业务逻辑
}

someProtectedMethod() 被调用时,GlobalOperationAspect 切面会被触发。AOP 会执行以下流程:

  1. interceptorDo() 方法中,point.getSignature() 获取到 someProtectedMethod 方法的签名信息。
  2. ((MethodSignature) point.getSignature()).getMethod() 获取到该方法的 Method 对象。
  3. method.getAnnotation(GlobalInterceptor.class) 会返回 @GlobalInterceptor 注解实例,因为 someProtectedMethod() 上确实标记了该注解。
  4. checkLogin = true 表示需要进行登录校验,于是调用 checkLogin() 方法进行校验。

如果 someProtectedMethod() 没有标记 @GlobalInterceptor 注解,那么 method.getAnnotation(GlobalInterceptor.class) 会返回 null,然后通过 if (null == interceptor) 判断,直接跳过校验,不会执行 checkLogin() 方法。

总结

这部分代码的核心是通过反射获取方法上的注解,并根据注解的内容决定是否执行后续的操作(如登录校验)。这种方法使得我们可以灵活地对需要登录校验的接口方法进行拦截,而不需要在每个方法中重复编写登录验证逻辑,只需在方法上添加 @GlobalInterceptor(checkLogin = true) 注解即可。

  • point.getSignature(): 获取当前连接点(即被拦截的方法)的签名信息。
  • MethodSignatureSignature 的子接口,提供了关于方法签名的更多细节。
  • method.getAnnotation(GlobalInterceptor.class): 使用反射检查目标方法是否标记了 @GlobalInterceptor 注解。如果没有该注解,直接返回,不做处理。
5. 校验登录
if (interceptor.checkLogin()) {
    checkLogin();
}
  • 如果 @GlobalInterceptor 注解的 checkLogin 属性为 true,则执行 checkLogin() 方法,进行登录校验。
6. 校验登录逻辑(checkLogin()
private void checkLogin() {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    String token = request.getHeader(Constants.TOKEN_WEB);
    if (StringTools.isEmpty(token)) {
        throw new BusinessException(ResponseCodeEnum.CODE_901);
    }
    TokenUserInfoDto tokenUserInfoDto = (TokenUserInfoDto) redisUtils.get(Constants.REDIS_KEY_TOKEN_WEB + token);
    if (tokenUserInfoDto == null) {
        throw new BusinessException(ResponseCodeEnum.CODE_901);
    }
}
  • 获取 HTTP 请求信息

    • RequestContextHolder.getRequestAttributes(): 用于获取当前请求的上下文对象(RequestAttributes)。这里使用的是 ServletRequestAttributes,它封装了 HTTP 请求的相关信息。
    • getRequest(): 从 RequestAttributes 中获取当前的 HttpServletRequest 对象,代表了当前的 HTTP 请求。
  • 从请求头中获取 token

    • request.getHeader(Constants.TOKEN_WEB): 通过 request.getHeader() 获取请求头中的 token 值。Constants.TOKEN_WEB 是存储 token 的常量。
  • 判断 token 是否为空

    • StringTools.isEmpty(token): 判断 token 是否为空或空字符串,如果为空,表示用户没有提供登录凭证,抛出 BusinessException 异常,错误码为 CODE_901(表示未登录)。
  • 从 Redis 中获取用户信息

    • redisUtils.get(Constants.REDIS_KEY_TOKEN_WEB + token): 根据 token 在 Redis 中查找对应的用户信息。如果 token 不存在,表示用户未登录或者 token 无效,也抛出 BusinessException 异常,返回 CODE_901 错误码。
7. 异常处理
throw new BusinessException(ResponseCodeEnum.CODE_901);
  • 如果 token 无效或 Redis 中没有对应的用户信息,则抛出 BusinessException 异常。CODE_901 是一个业务错误码,通常表示登录失效或用户未登录。

代码总结

该段代码的核心功能是:

  1. 拦截标记了 @GlobalInterceptor 注解的方法。
  2. 如果注解中的 checkLogin 属性为 true,则进行登录校验。
  3. 校验逻辑包括从请求头获取 token、检查 token 是否有效(即是否存在于 Redis 中),如果无效则抛出异常,提示用户未登录。

应用场景

这个全局拦截器通常用于需要登录认证的 API 接口。通过这种方式,开发者可以在方法上使用 @GlobalInterceptor 注解,简化登录校验逻辑,而不需要在每个方法内部重复编写认证逻辑。

例如,某些方法可能是需要登录才能访问的,开发者只需要在方法上添加 @GlobalInterceptor(checkLogin = true) 注解,AOP 就会自动执行登录校验,确保请求中携带有效的 token。

扩展

当我们说“它代表了被拦截方法的签名信息”时,实际上是在描述 Signature 和它的子类 MethodSignature 在 AOP 中所代表的对象的含义。

1. 签名(Signature)是什么?

在 Java 中,签名(Signature)指的是方法的唯一标识符,它包含了方法的基本信息。对于 AOP 来说,方法签名通常指一个方法的名称、参数类型以及其他可能的修饰符(如返回类型等)。可以简单地理解为方法的 “签名” 就是这个方法的 “标识符”。

对于 AOP 切面编程中的拦截,它会涉及到“被拦截的方法”的签名信息。Spring AOP 通过 JoinPointSignature 来描述目标方法的元数据。

  • Signature:是一个接口,它表示了一个方法(或者构造函数、字段等)的“签名”信息。Signature 只有一些常见的方法,比如获取方法名、获取方法参数类型等。

  • MethodSignature:是 Signature 接口的一个子接口,专门用于方法签名的表示。MethodSignature 继承了 Signature 并提供了更多的与方法相关的元数据,比如获取方法的返回类型、参数类型、方法参数、方法对象等。

2. point.getSignature() 解释

Method method = ((MethodSignature) point.getSignature()).getMethod();
  • point.getSignature()point 是 AOP 的 JoinPoint 对象,它代表当前切点的信息。通过 getSignature() 方法,可以获取该连接点的签名信息。

    • 对于方法切点(即拦截的是方法),point.getSignature() 返回的是 Signature 类型的对象。如果拦截的是一个方法,getSignature() 返回的是 MethodSignature 类型的对象,这样我们就可以进一步获取关于该方法的详细信息。
  • (MethodSignature) point.getSignature():由于 point.getSignature() 返回的是一个 Signature 类型的对象,而我们关心的是方法签名,因此需要将其强制转换为 MethodSignature。这样,我们可以使用 MethodSignature 提供的专门用于方法的信息获取功能。

  • .getMethod()MethodSignature 提供了 getMethod() 方法,用于获取被拦截的方法的 Method 对象。这个 Method 对象代表了 Java 中的反射 Method 类,允许我们获取方法的相关信息,例如方法名、参数类型、返回类型、注解等。

3. 方法签名包含的信息

通过 MethodSignatureMethod 对象,我们可以获取被拦截方法的多种信息,以下是一些常见的方法:

  • getMethodName():获取方法的名称。
  • getParameterTypes():获取方法的参数类型数组。
  • getReturnType():获取方法的返回类型。
  • getAnnotations():获取方法上的所有注解。
  • getParameterNames():获取方法参数的名称(对于 JDK 1.8 及以上版本,需要开启编译时的 -parameters 参数)。

这些信息可以帮助我们在切面中做更精细的处理,比如动态调整方法调用,进行日志记录、权限控制等。

4. 签名在 AOP 中的作用

在 AOP 中,签名(Signature)信息至关重要,因为它允许我们知道当前被拦截的具体方法是什么,从而在切面中做出相应的处理。例如:

  • 如果我们想要在日志中记录方法名、参数,或者方法的执行时间,我们可以通过签名信息获取到这些数据。
  • 如果我们需要对某些方法应用不同的逻辑(如不同的权限校验、不同的缓存策略等),通过签名可以区分不同的方法。

5. 总结

“签名信息”是指方法本身的标识信息,它包括了方法名、参数类型、返回值类型、注解等内容。在 AOP 中,我们通过 MethodSignature 来获取被拦截的目标方法的签名信息。这个签名信息让我们能够在切面中做进一步的操作,比如获取方法的参数、返回类型、注解等信息,从而进行精准的处理和控制。

简而言之,签名信息就是方法的“身份标识”,它使得 AOP 能够知道目标方法的相关细节,以便执行适当的操作。

消息记录

这是一个自定义注解 @RecordUserMessage 的定义。下面我将详细解释它的各个部分及其作用:

1. 注解的定义

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordUserMessage {
    MessageTypeEnum messageType();
}
1.1 @Target 注解
@Target({ElementType.METHOD, ElementType.TYPE})
  • @Target 注解定义了注解可以被应用的 Java 元素。ElementType 是一个枚举,表示注解的目标可以是不同的 Java 元素类型。
  • ElementType.METHOD 表示这个注解可以用于方法。
  • ElementType.TYPE 表示这个注解可以用于类、接口或枚举类型。

因此,@RecordUserMessage 注解可以应用于方法或类型(类、接口、枚举等)。

1.2 @Retention 注解
@Retention(RetentionPolicy.RUNTIME)
  • @Retention 注解定义了注解的保留策略,即注解在哪个阶段可用。
  • RetentionPolicy.RUNTIME 表示注解会在 运行时 保留,并且可以通过反射读取。这是最常见的策略,因为它允许程序在运行时动态访问注解的信息。

简而言之,@RecordUserMessage 注解会在程序运行时存在,因此可以通过反射读取它的信息。

1.3 public @interface RecordUserMessage
public @interface RecordUserMessage {
  • @interface 是用于声明注解的关键字,它标识了 RecordUserMessage 是一个注解类型。与类和接口不同,注解类型通常不包含具体的业务逻辑。
  • public 表示这个注解是公共的,可以被其他类或包访问。
1.4 messageType() 方法
MessageTypeEnum messageType();
  • messageType() 是该注解的一个成员方法,返回一个 MessageTypeEnum 枚举值。这个成员方法是 RecordUserMessage 注解的一个参数,意味着每次使用该注解时,都需要提供一个 messageType 值。
  • MessageTypeEnum 应该是一个枚举类型,表示不同的消息类型。枚举类型通常用于定义一组常量,例如可能的消息类型。

2. 注解的作用

@RecordUserMessage 注解用于记录用户消息,注解中的 messageType 属性用来指定消息的类型。通过将该注解应用到方法或类上,可以将用户消息的记录功能与方法或类绑定,便于后续处理(如日志记录、消息发送等)。

3. 示例用法

假设我们有以下的 MessageTypeEnum 枚举:

public enum MessageTypeEnum {
    LOGIN, LOGOUT, PURCHASE, SIGNUP;
}

我们可以在方法上使用 @RecordUserMessage 注解来标记该方法执行时需要记录的消息类型:

//发布评论
    @RequestMapping("/postComment")
    @RecordUserMessage(messageType = MessageTypeEnum.COMMENT)
    @GlobalInterceptor(checkLogin = true)
    public ResponseVO postComment(@NotEmpty String videoId,
                                  Integer replyCommentId,
                                  @NotEmpty @Size(max = 500) String content,
                                  @Size(max = 50) String imgPath) {

        TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
        VideoComment comment = new VideoComment();
        comment.setUserId(tokenUserInfoDto.getUserId());
        comment.setAvatar(tokenUserInfoDto.getAvatar());
        comment.setNickName(tokenUserInfoDto.getNickName());
        comment.setVideoId(videoId);
        comment.setContent(content);
        comment.setImgPath(imgPath);
        videoCommentService.postComment(comment, replyCommentId);
        return getSuccessResponseVO(comment);
    }

这里,userLogin() 方法会被标记为需要记录一个 LOGIN 类型的用户消息。

也可以用于类上:
@RecordUserMessage(messageType = MessageTypeEnum.PURCHASE)
public class PurchaseService {
    public void processPurchase() {
        // 处理购买逻辑
    }
}

在这个例子中,PurchaseService 类的所有方法都被标记为需要记录 PURCHASE 类型的用户消息。

4. 如何使用

通常,在业务逻辑中,我们会通过 AOP(面向切面编程)来拦截带有 @RecordUserMessage 注解的方法或者类,从而在方法执行前后记录相应的消息。以下是如何通过 AOP 实现此功能的一个简化示例:

@Aspect
@Component
public class RecordUserMessageAspect {

    @Before("@annotation(recordUserMessage)") // 针对所有被 @RecordUserMessage 注解标注的方法
    public void recordMessage(JoinPoint joinPoint, RecordUserMessage recordUserMessage) {
        MessageTypeEnum messageType = recordUserMessage.messageType();
        // 记录用户消息逻辑,根据 messageType 执行相应的操作
        System.out.println("Recording message of type: " + messageType);
    }
}

在这个 AOP 切面中,@Before("@annotation(recordUserMessage)") 会在目标方法执行之前拦截被 @RecordUserMessage 注解标注的方法,并从注解中获取 messageType 参数。然后可以在切面中记录或处理这个信息。

5. 总结

  • @RecordUserMessage 是一个自定义注解,用于标记方法,以便记录与用户相关的消息。
  • 它通过 messageType 属性来指定消息类型,MessageTypeEnum 枚举类型可以表示不同的消息类型(例如:登录、登出、购买、注册等)。
  • @Target 表示该注解可以应用于方法或类。
  • @Retention 表示该注解在运行时保留,允许通过反射读取。
  • 它通常配合 AOP 使用,在方法执行前后执行相应的逻辑,如记录日志、发送消息等。

枚举类说明

public enum MessageTypeEnum {
    SYS(1, "系统消息"),
    LIKE(2, "点赞"),
    COLLECTION(3, "收藏"),
    COMMENT(4, "评论");
    private Integer type;
    private String desc;

    MessageTypeEnum(Integer type, String desc) {
        this.type = type;
        this.desc = desc;
    }

    public Integer getType() {
        return type;
    }

    public String getDesc() {
        return desc;
    }

    public static MessageTypeEnum getByType(Integer type) {
        for (MessageTypeEnum statusEnum : MessageTypeEnum.values()) {
            if (statusEnum.getType().equals(type)) {
                return statusEnum;
            }
        }
        return null;
    }
}

这是一个枚举类 MessageTypeEnum,它定义了几种类型的消息,每种消息有一个 type(类型标识符)和 desc(描述信息)。下面我将详细解释这个枚举类的每一部分。

1. 枚举类的声明

public enum MessageTypeEnum {
    SYS(1, "系统消息"),
    LIKE(2, "点赞"),
    COLLECTION(3, "收藏"),
    COMMENT(4, "评论");
  • public enum MessageTypeEnum:定义了一个公共的枚举类型 MessageTypeEnum,它表示消息的不同类型。
  • 枚举值:
    • SYS(1, "系统消息"):表示系统消息,1 是它的类型标识,"系统消息" 是描述。
    • LIKE(2, "点赞"):表示点赞消息,2 是它的类型标识,"点赞" 是描述。
    • COLLECTION(3, "收藏"):表示收藏消息,3 是它的类型标识,"收藏" 是描述。
    • COMMENT(4, "评论"):表示评论消息,4 是它的类型标识,"评论" 是描述。

枚举常量是 MessageTypeEnum 类的实例,并且每个常量都有自己的构造函数参数 typedesc

2. 构造函数

MessageTypeEnum(Integer type, String desc) {
    this.type = type;
    this.desc = desc;
}
  • 枚举的构造函数 MessageTypeEnum(Integer type, String desc) 用于初始化每个枚举常量的 typedesc 字段。构造函数是 私有的,因此它只能在枚举内部被调用。
  • type 表示消息的类型标识符,desc 是该类型的描述信息。

3. 字段和方法

private Integer type;
private String desc;
  • type:用于存储消息的类型标识符,例如 1 表示系统消息,2 表示点赞消息等。
  • desc:用于存储消息类型的描述,例如 "系统消息""点赞" 等。
public Integer getType() {
    return type;
}

public String getDesc() {
    return desc;
}
  • getType() 方法返回消息的类型标识符(Integer 类型)。
  • getDesc() 方法返回消息的描述(String 类型)。

4. 静态方法:根据类型获取枚举值

public static MessageTypeEnum getByType(Integer type) {
    for (MessageTypeEnum statusEnum : MessageTypeEnum.values()) {
        if (statusEnum.getType().equals(type)) {
            return statusEnum;
        }
    }
    return null;
}
  • getByType(Integer type) 方法用于根据消息的 type 获取对应的 MessageTypeEnum 枚举实例。
    • MessageTypeEnum.values() 返回所有枚举常量的数组。
    • for (MessageTypeEnum statusEnum : MessageTypeEnum.values()) 遍历所有的枚举常量。
    • if (statusEnum.getType().equals(type)) 判断当前枚举常量的 type 是否等于传入的 type
    • 如果匹配成功,则返回对应的枚举常量。
    • 如果遍历所有枚举常量后没有找到匹配的,返回 null

5. 示例:如何使用 MessageTypeEnum

你可以通过枚举类型的 type 值来获取对应的枚举实例。例如:

获取枚举实例
MessageTypeEnum typeEnum = MessageTypeEnum.getByType(2);
System.out.println(typeEnum.getDesc());  // 输出:点赞
输出所有枚举类型及其描述
for (MessageTypeEnum typeEnum : MessageTypeEnum.values()) {
    System.out.println(typeEnum.getType() + ": " + typeEnum.getDesc());
}

这将打印:

1: 系统消息
2: 点赞
3: 收藏
4: 评论

6. 总结

  • MessageTypeEnum 是一个枚举类,用于定义不同类型的消息,如系统消息、点赞、收藏和评论。
  • 每个枚举常量都关联有一个 type(类型标识符)和 desc(描述)。
  • 通过 getType()getDesc() 方法,可以获取枚举常量的具体信息。
  • getByType(Integer type) 是一个静态方法,通过 type 获取相应的枚举常量。这在需要根据某个 type 查找对应消息类型的场景中非常有用。

该枚举类的设计使得消息类型的管理更加结构化和类型安全,避免了使用原始整数值来表示消息类型,提供了更直观和易于理解的代码。

消息记录全全局拦截器

@Component("userMessageOperationAspect")
@Aspect
@Slf4j
public class UserMessageOperationAspect {

    private static final String PARAMETERS_VIDEO_ID = "videoId";

    private static final String PARAMETERS_ACTION_TYPE = "actionType";

    private static final String PARAMETERS_REPLY_COMMENTID = "replyCommentId";

    private static final String PARAMETERS_AUDIT_REJECT_REASON = "reason";

    private static final String PARAMETERS_CONTENT = "content";

    @Resource
    private RedisUtils redisUtils;

    @Resource
    private UserMessageService userMessageService;


    //环绕通知,作用于方法调用前后
    @Around("@annotation(com.easylive.annotation.RecordUserMessage)")
    //环绕通知使用的是ProceedingJoinPoint point
    public ResponseVO interceptorDo(ProceedingJoinPoint point) throws Exception {
        try {
            //调用该原始方法并获取返回值
            ResponseVO result = (ResponseVO) point.proceed();
            Method method = ((MethodSignature) point.getSignature()).getMethod();
            RecordUserMessage recordUserMessage = method.getAnnotation(RecordUserMessage.class);
            if (recordUserMessage != null) {
                //传入注解中的值(消息类型),方法中的实际参数,方法中每个参数的名称(是个对象,不是string类型)
                saveUserMessage(recordUserMessage, point.getArgs(), method.getParameters());
            }
            return result;
        } catch (BusinessException e) {
            log.error("全局拦截器异常", e);
            throw e;
        } catch (Exception e) {
            log.error("全局拦截器异常", e);
            throw e;
        } catch (Throwable e) {
            log.error("全局拦截器异常", e);
            throw new BusinessException(ResponseCodeEnum.CODE_500);
        }
    }


    private void saveUserMessage(RecordUserMessage recordUserMessage, Object[] arguments, Parameter[] parameters) {
        String videoId = null;
        Integer actionType = null;
        Integer replyCommentId = null;
        String content = null;
        for (int i = 0; i < parameters.length; i++) {
            if (PARAMETERS_VIDEO_ID.equals(parameters[i].getName())) {
                videoId = (String) arguments[i];
            } else if (PARAMETERS_ACTION_TYPE.equals(parameters[i].getName())) {
                actionType = (Integer) arguments[i];
            } else if (PARAMETERS_REPLY_COMMENTID.equals(parameters[i].getName())) {
                replyCommentId = (Integer) arguments[i];
            } else if (PARAMETERS_AUDIT_REJECT_REASON.equals(parameters[i].getName())) {
                content = (String) arguments[i];
            } else if (PARAMETERS_CONTENT.equals(parameters[i].getName())) {
                content = (String) arguments[i];
            }
        }
        //获取消息类型
        //这行代码从recordUserMessage注解实例中获取messageType属性的值,并将其赋给messageTypeEnum变量。
        // messageType是一个方法,返回MessageTypeEnum枚举类型,代表消息的类型。
        MessageTypeEnum messageTypeEnum = recordUserMessage.messageType();
        //点赞和收藏使用的是同一个方法(注解中的值是value),所以要再次判断行为类型
        if (UserActionTypeEnum.VIDEO_COLLECT.getType().equals(actionType)) {
            messageTypeEnum = MessageTypeEnum.COLLECTION;
        }

        TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
        //管理端获取不到用户信息,系统发送,不需要用户id
        userMessageService.saveUserMessage(videoId, tokenUserInfoDto == null ? null : tokenUserInfoDto.getUserId(), messageTypeEnum, content, replyCommentId);
    }

    private TokenUserInfoDto getTokenUserInfoDto() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String token = request.getHeader(Constants.TOKEN_WEB);
        return (TokenUserInfoDto) redisUtils.get(Constants.REDIS_KEY_TOKEN_WEB + token);
    }
}

这段代码实现了一个名为 UserMessageOperationAspect 的 AOP 切面类,用于在方法执行过程中自动记录用户消息。通过该切面类,系统可以在方法执行前后拦截特定的业务方法,根据方法的参数自动生成并保存用户消息。下面详细解释每个部分的作用:

1. 类注解与切面定义

@Component("userMessageOperationAspect")
@Aspect
@Slf4j
public class UserMessageOperationAspect {
  • @Component("userMessageOperationAspect"):将这个类注册为 Spring 容器中的一个 Bean,便于依赖注入。
  • @Aspect:标记这个类为 AOP 切面类,表示它用于处理切面逻辑。
  • @Slf4j:这是 Lombok 提供的日志注解,自动生成一个 log 对象,用于日志记录。

2. 常量字段

private static final String PARAMETERS_VIDEO_ID = "videoId";
private static final String PARAMETERS_ACTION_TYPE = "actionType";
private static final String PARAMETERS_REPLY_COMMENTID = "replyCommentId";
private static final String PARAMETERS_AUDIT_REJECT_REASON = "reason";
private static final String PARAMETERS_CONTENT = "content";

这些常量是方法参数的名称,用于在后续代码中识别传递给方法的参数。通过这些名称,可以在方法执行时动态地获取参数值。

3. 依赖注入

@Resource
private RedisUtils redisUtils;

@Resource
private UserMessageService userMessageService;
  • redisUtils 用于与 Redis 交互,主要用于从 Redis 中获取用户的 Token 信息。
  • userMessageService 用于保存用户消息。

4. 环绕通知

@Around("@annotation(com.easylive.annotation.RecordUserMessage)")
public ResponseVO interceptorDo(ProceedingJoinPoint point) throws Exception {
  • @Around:环绕通知,表示在目标方法执行之前和之后都会执行这个通知方法。ProceedingJoinPoint 是环绕通知特有的对象,允许你控制目标方法的执行。
  • @annotation(com.easylive.annotation.RecordUserMessage):这个切入点表达式指定了该切面只会拦截标注了 @RecordUserMessage 注解的方法。

5. 执行原始方法并捕获返回值

ResponseVO result = (ResponseVO) point.proceed();
  • point.proceed():调用被拦截的方法并获取其返回值。proceed() 方法会继续执行原方法的逻辑,并返回执行结果。
  • result 保存了原始方法的返回值,类型为 ResponseVO

6. 获取方法信息与注解

Method method = ((MethodSignature) point.getSignature()).getMethod();
RecordUserMessage recordUserMessage = method.getAnnotation(RecordUserMessage.class);
  • Method method = ((MethodSignature) point.getSignature()).getMethod();:获取当前执行方法的 Method 对象。
  • recordUserMessage = method.getAnnotation(RecordUserMessage.class);:获取方法上的 @RecordUserMessage 注解实例,用于读取注解中的配置。

7. 保存用户消息

if (recordUserMessage != null) {
    saveUserMessage(recordUserMessage, point.getArgs(), method.getParameters());
}

如果方法上有 @RecordUserMessage 注解,则调用 saveUserMessage 方法保存用户消息。point.getArgs() 获取方法的参数,method.getParameters() 获取方法参数的元数据(即参数名称和类型)。

8. 保存用户消息的逻辑

这段代码是 UserMessageOperationAspect 类中的一个方法 saveUserMessage。它的作用是在业务方法执行时,基于传入的参数和注解 @RecordUserMessage,自动生成并保存用户消息。具体来说,方法从目标方法的参数中提取相关数据(如 videoIdactionType 等),并使用这些数据构建用户消息。下面是对代码的逐行详细解释:

方法签名

private void saveUserMessage(RecordUserMessage recordUserMessage, Object[] arguments, Parameter[] parameters) {
  • RecordUserMessage recordUserMessage:这是传入的注解对象,包含了 @RecordUserMessage 注解的所有信息。在这个方法中,我们主要通过它来获取消息类型(messageType)。
  • Object[] arguments:这是目标方法的参数值数组,保存了调用目标方法时传入的实际参数。
  • Parameter[] parameters:这是目标方法的参数元数据,包含了方法参数的名称和类型。通过它可以动态获取参数的名称,用于从 arguments 中提取对应的值。

初始化变量

String videoId = null;
Integer actionType = null;
Integer replyCommentId = null;
String content = null;
  • 这些变量是用来存储方法参数中提取的信息。它们分别对应视频 ID、动作类型、回复的评论 ID 和内容。

遍历参数并提取值

for (int i = 0; i < parameters.length; i++) {
    if (PARAMETERS_VIDEO_ID.equals(parameters[i].getName())) {
        videoId = (String) arguments[i];
    } else if (PARAMETERS_ACTION_TYPE.equals(parameters[i].getName())) {
        actionType = (Integer) arguments[i];
    } else if (PARAMETERS_REPLY_COMMENTID.equals(parameters[i].getName())) {
        replyCommentId = (Integer) arguments[i];
    } else if (PARAMETERS_AUDIT_REJECT_REASON.equals(parameters[i].getName())) {
        content = (String) arguments[i];
    } else if (PARAMETERS_CONTENT.equals(parameters[i].getName())) {
        content = (String) arguments[i];
    }
}
  • 这段代码遍历了目标方法的所有参数。对于每一个参数,首先通过 parameters[i].getName() 获取该参数的名称,然后与预先定义的常量进行比较。

    • PARAMETERS_VIDEO_ID:如果参数名称是 videoId,则将对应的 arguments[i] 值赋给 videoId 变量。
    • PARAMETERS_ACTION_TYPE:如果参数名称是 actionType,则将对应的 arguments[i] 值赋给 actionType 变量。
    • PARAMETERS_REPLY_COMMENTID:如果参数名称是 replyCommentId,则将对应的 arguments[i] 值赋给 replyCommentId 变量。
    • PARAMETERS_AUDIT_REJECT_REASONPARAMETERS_CONTENT:如果参数名称是 contentreason,则将对应的 arguments[i] 值赋给 content 变量。
  • 这种方法可以根据方法的实际参数动态提取相应的值。

获取消息类型

MessageTypeEnum messageTypeEnum = recordUserMessage.messageType();
  • recordUserMessage.messageType():从注解中获取消息类型。这是 @RecordUserMessage 注解中的一个属性,返回 MessageTypeEnum 枚举值,表示消息的类型。
  • messageTypeEnum 存储了从注解中获取到的消息类型,例如:系统消息、点赞消息、收藏消息等。

特殊处理:点赞和收藏

if (UserActionTypeEnum.VIDEO_COLLECT.getType().equals(actionType)) {
    messageTypeEnum = MessageTypeEnum.COLLECTION;
}
  • 这段代码是为了处理特殊情况:如果动作类型(actionType)是视频收藏(VIDEO_COLLECT),则将消息类型(messageTypeEnum)强制设置为 收藏消息MessageTypeEnum.COLLECTION)。
  • UserActionTypeEnum.VIDEO_COLLECT.getType() 获取的是视频收藏的类型值,actionType 是目标方法的参数,表示当前用户执行的操作类型。
  • 如果 actionType 是视频收藏操作类型,那么即使注解中指定的消息类型是其他类型(如点赞),也将消息类型修改为 收藏

获取用户信息

TokenUserInfoDto tokenUserInfoDto = getTokenUserInfoDto();
  • 调用 getTokenUserInfoDto() 方法从 Redis 中获取当前用户的信息。这个方法通过读取请求中的 Token,从 Redis 缓存中查找并返回 TokenUserInfoDto 对象,包含当前用户的详细信息(如用户 ID)。

保存用户消息

userMessageService.saveUserMessage(videoId, tokenUserInfoDto == null ? null : tokenUserInfoDto.getUserId(), messageTypeEnum, content, replyCommentId);
  • 调用 userMessageService.saveUserMessage() 方法保存用户消息。
    • videoId:视频的 ID,表示这条消息关联的视频。
    • tokenUserInfoDto == null ? null : tokenUserInfoDto.getUserId():当前用户的 ID,如果没有找到用户信息(例如管理员或无效 Token),则传递 null
    • messageTypeEnum:消息的类型(通过注解获取或根据 actionType 修改)。
    • content:消息的内容(可能是评论内容或审核拒绝理由等)。
    • replyCommentId:如果是回复评论,则传递被回复评论的 ID。

额外说明

1. 方法参数提取

这段代码根据目标方法的参数名称动态提取参数值。由于 Java 中的反射通常只能获取方法参数的类型和位置,而不能直接获取参数名称,因此 parameters[i].getName() 结合注解中的参数名称提供了方便的方式来进行映射。

2. Token 校验与用户信息获取

在实际应用中,用户信息通常存储在服务器端的缓存或数据库中,而通过 Token 来进行用户身份验证。getTokenUserInfoDto() 方法通过 HTTP 请求中的 Token 从 Redis 获取用户信息,这样可以确保消息记录与实际用户相关。

3. 消息类型和业务逻辑

MessageTypeEnum 枚举类和 UserActionTypeEnum 枚举类用于定义不同的操作和消息类型。业务逻辑中根据 actionType 对消息类型进行调整,例如将点赞操作映射为 “点赞” 消息,将视频收藏映射为 “收藏” 消息。

总结

  • 该方法 saveUserMessage 主要是从目标方法的参数中提取出必要的信息,如视频 ID、动作类型、内容等,然后根据注解中的配置自动生成并保存用户消息。
  • 它使用 AOP 自动化处理消息记录逻辑,无需在每个业务方法中手动添加消息保存的代码,提升了代码的可维护性和扩展性。

9. 获取用户信息

private TokenUserInfoDto getTokenUserInfoDto() {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    String token = request.getHeader(Constants.TOKEN_WEB);
    return (TokenUserInfoDto) redisUtils.get(Constants.REDIS_KEY_TOKEN_WEB + token);
}
  • 该方法从 HTTP 请求头中获取 TOKEN_WEB,然后从 Redis 中查找对应的用户信息,返回 TokenUserInfoDto 对象。

10. 异常处理

catch (BusinessException e) {
    log.error("全局拦截器异常", e);
    throw e;
} catch (Exception e) {
    log.error("全局拦截器异常", e);
    throw e;
} catch (Throwable e) {
    log.error("全局拦截器异常", e);
    throw new BusinessException(ResponseCodeEnum.CODE_500);
}
  • 通过 try-catch 块捕获各种异常(BusinessExceptionExceptionThrowable),记录错误日志并抛出相应的异常。

11. 总结

  • 这个类实现了一个 AOP 切面,主要作用是记录用户消息。通过 @RecordUserMessage 注解标注的方法,在执行时会自动记录用户消息。
  • 切面在执行目标方法前后拦截,获取方法的参数,并根据注解配置保存消息。
  • 该功能通过 userMessageService.saveUserMessage() 方法保存消息,并使用 Redis 从中获取用户信息(通过 Token)。

为什么用 ProceedingJoinPoint 而不是 JoinPoint?

ProceedingJoinPoint 是环绕通知 (@Around) 中使用的特定类型的连接点(JoinPoint)。它与常规的 JoinPoint 不同,主要用于环绕通知,因为它提供了更多的控制能力,尤其是在执行目标方法时。

为什么用 ProceedingJoinPoint 而不是 JoinPoint

在 Spring AOP 中,@Before, @After, @AfterReturning, 和 @AfterThrowing 通常使用 JoinPoint,它提供了关于目标方法的一些信息(比如方法签名、参数等)。然而,@Around 通知需要更强的控制能力,因为它允许你在执行目标方法之前或之后进行操作,甚至决定是否执行目标方法。

ProceedingJoinPointJoinPoint 的子接口,扩展了更多功能,最重要的就是它有一个 proceed() 方法。proceed() 方法让你可以在目标方法调用前后控制方法的执行,甚至可以选择不执行目标方法,或者修改目标方法的返回值。

ProceedingJoinPoint 的作用

  1. 调用目标方法

    • 通过 proceed() 方法,你可以显式地调用目标方法并获取返回值。
    • proceed() 方法会继续执行目标方法,并返回方法的返回值。
  2. 控制目标方法的执行

    • 你可以在调用 proceed() 之前执行一些逻辑,比如日志记录、权限检查等。
    • 你还可以根据条件决定是否调用目标方法,或者修改返回值。

示例:

@Around("@annotation(com.easylive.annotation.RecordUserMessage)")
public ResponseVO interceptorDo(ProceedingJoinPoint point) throws Exception {
    try {
        // 调用原始方法并获取返回值
        ResponseVO result = (ResponseVO) point.proceed();
        
        // 获取方法的注解信息
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        RecordUserMessage recordUserMessage = method.getAnnotation(RecordUserMessage.class);
        
        // 如果有注解,记录用户消息
        if (recordUserMessage != null) {
            saveUserMessage(recordUserMessage, point.getArgs(), method.getParameters());
        }

        return result; // 返回原方法的执行结果
    } catch (Exception e) {
        log.error("全局拦截器异常", e);
        throw e; // 如果发生异常,继续抛出
    }
}

ProceedingJoinPoint 提供的关键方法

  1. proceed():这是最关键的方法,用来继续执行目标方法。如果你没有调用 proceed(),目标方法将不会执行。
  2. getArgs():获取目标方法的参数列表,你可以通过它访问目标方法传入的参数。
  3. getSignature():获取目标方法的签名(包括方法名、返回类型、参数类型等)。
  4. getTarget():获取目标对象(即被代理的对象)。

为什么在这段代码中使用 ProceedingJoinPoint

在你的 UserMessageOperationAspect 类中,使用 ProceedingJoinPoint 是因为:

  • 需要在目标方法执行前后进行处理:例如,在执行原始方法之前,可能需要做一些日志记录、参数处理、用户消息保存等操作。
  • 获取目标方法的返回值:使用 proceed() 可以获取目标方法的返回值,并且你可以在此基础上进行修改或扩展操作。
  • 控制方法执行:你可以通过 proceed() 控制目标方法的执行时机,甚至可以根据条件决定是否调用目标方法。

总结:ProceedingJoinPoint@Around 通知专用的接口,它为开发者提供了更多的灵活性,能够控制目标方法的执行逻辑、修改方法参数或返回值等,因此它是环绕通知的理想选择。

userMessageService.saveUserMessage(videoId, tokenUserInfoDto == null ? null : tokenUserInfoDto.getUserId(), messageTypeEnum, content, replyCommentId);
	@Override
    @Async
    public void saveUserMessage(String videoId, String sendUserId, MessageTypeEnum messageTypeEnum, String content, Integer replyCommentId) {
        VideoInfo videoInfo = this.videoInfoPostMapper.selectByVideoId(videoId);
        if (videoInfo == null) {
            return;
        }

        //扩展信息
        UserMessageExtendDto extendDto = new UserMessageExtendDto();
        extendDto.setMessageContent(content);

        //接受消息的用户id
        String userId = videoInfo.getUserId();

        //收藏,点赞 已经记录过消息,不在记录
        if (ArrayUtils.contains(new Integer[]{MessageTypeEnum.LIKE.getType(), MessageTypeEnum.COLLECTION.getType()}, messageTypeEnum.getType())) {
            UserMessageQuery userMessageQuery = new UserMessageQuery();
            //userMessageQuery.setUserId(userId);
            userMessageQuery.setSendUserId(sendUserId);
            userMessageQuery.setVideoId(videoId);
            userMessageQuery.setMessageType(messageTypeEnum.getType());
            Integer count = userMessageMapper.selectCount(userMessageQuery);
            if (count > 0) {
                return;
            }
        }

        UserMessage userMessage = new UserMessage();
        userMessage.setUserId(userId);
        userMessage.setVideoId(videoId);
        userMessage.setReadType(MessageReadTypeEnum.NO_READ.getType());
        userMessage.setCreateTime(new Date());
        userMessage.setMessageType(messageTypeEnum.getType());
        userMessage.setSendUserId(sendUserId);

        //评论特殊处理
        if (replyCommentId != null) {
            VideoComment commentInfo = videoCommentMapper.selectByCommentId(replyCommentId);
            if (null != commentInfo) {
                //发布评论的人收到该信息
                userId = commentInfo.getUserId();
                extendDto.setMessageContentReply(commentInfo.getContent());
            }
        }
        if (userId.equals(sendUserId)) {
            return;
        }

        //系统消息特殊处理
        if (MessageTypeEnum.SYS == messageTypeEnum) {
            VideoInfoPost videoInfoPost = videoInfoPostMapper.selectByVideoId(videoId);
            extendDto.setAuditStatus(videoInfoPost.getStatus());
        }

        userMessage.setUserId(userId);
        userMessage.setExtendJson(JsonUtils.convertObj2Json(extendDto));
        this.userMessageMapper.insert(userMessage);
    }

这段代码是一个异步方法 saveUserMessage,用于保存用户消息的业务逻辑,主要处理用户评论、点赞、收藏等操作时生成的消息。它涉及到一些数据库查询、条件判断、消息处理和存储等操作。下面是详细的解释:

方法签名

@Override
@Async
public void saveUserMessage(String videoId, String sendUserId, MessageTypeEnum messageTypeEnum, String content, Integer replyCommentId)
  • @Override:表示该方法是重写父类或接口中的方法。
  • @Async:表示该方法是异步执行的。Spring 会在单独的线程中执行该方法,而不阻塞调用者,适用于不需要立即返回结果的场景,例如日志记录、消息推送等。
  • String videoId:视频的 ID,指明这条消息是与哪一个视频相关。
  • String sendUserId:发送消息的用户 ID,表示是谁发出了这个操作(如评论、点赞等)。
  • MessageTypeEnum messageTypeEnum:消息类型的枚举值,表示这条消息是属于哪种类型(评论、点赞、收藏等)。
  • String content:消息内容,通常是评论的文本或相关信息。
  • Integer replyCommentId:回复评论的 ID,如果是回复某条评论,则传入该评论 ID。

1. 获取视频信息

VideoInfo videoInfo = this.videoInfoPostMapper.selectByVideoId(videoId);
if (videoInfo == null) {
    return;
}
  • 使用 videoId 从数据库中查询该视频的详细信息(VideoInfo)。
  • 如果该视频信息不存在(即 videoInfo == null),则方法直接返回,表示不进行后续的消息保存操作。

2. 创建扩展信息

UserMessageExtendDto extendDto = new UserMessageExtendDto();
extendDto.setMessageContent(content);
  • 创建一个 UserMessageExtendDto 对象,用于存储消息的扩展信息。这个对象包含了消息的内容(如评论文本)。
  • 将传入的 content 设置为扩展信息中的消息内容。

3. 获取接收消息的用户 ID

String userId = videoInfo.getUserId();
  • videoInfo 中获取视频发布者的用户 ID(即接收消息的用户)。这意味着该视频的发布者将是消息的接收者。

4. 处理已记录的点赞和收藏消息

if (ArrayUtils.contains(new Integer[]{MessageTypeEnum.LIKE.getType(), MessageTypeEnum.COLLECTION.getType()}, messageTypeEnum.getType())) {
    UserMessageQuery userMessageQuery = new UserMessageQuery();
    userMessageQuery.setSendUserId(sendUserId);
    userMessageQuery.setVideoId(videoId);
    userMessageQuery.setMessageType(messageTypeEnum.getType());
    Integer count = userMessageMapper.selectCount(userMessageQuery);
    if (count > 0) {
        return;
    }
}
  • 检查是否已经记录过点赞或收藏消息
    • 如果消息类型是 点赞(LIKE)收藏(COLLECTION),则通过查询数据库检查是否已经记录了相同的消息。
    • 如果 该消息已经记录count > 0),则直接返回,不再保存相同的消息。

5. 创建新的用户消息对象

UserMessage userMessage = new UserMessage();
userMessage.setUserId(userId);
userMessage.setVideoId(videoId);
userMessage.setReadType(MessageReadTypeEnum.NO_READ.getType());
userMessage.setCreateTime(new Date());
userMessage.setMessageType(messageTypeEnum.getType());
userMessage.setSendUserId(sendUserId);
  • 创建一个新的 UserMessage 对象,准备将其保存到数据库中。
  • 设置以下属性:
    • userId:接收消息的用户 ID(视频的发布者)。
    • videoId:视频 ID,表示消息与哪个视频相关。
    • readType:消息的阅读状态,设置为未读。
    • createTime:消息的创建时间。
    • messageType:消息类型(如评论、点赞等)。
    • sendUserId:发送消息的用户 ID(即操作视频的用户)。

6. 处理评论的特殊逻辑

if (replyCommentId != null) {
    VideoComment commentInfo = videoCommentMapper.selectByCommentId(replyCommentId);
    if (null != commentInfo) {
        userId = commentInfo.getUserId();
        extendDto.setMessageContentReply(commentInfo.getContent());
    }
}
  • 如果 replyCommentId 不为 null,表示这条消息是回复某条评论。
  • 根据 replyCommentId 从数据库中查询对应的评论(VideoComment)。
  • 如果找到了该评论,更新:
    • userId:接收消息的用户 ID 更新为评论的发布者(即回复评论的人)。
    • extendDto.setMessageContentReply(commentInfo.getContent()):设置扩展信息中的回复内容(即原评论的内容)。

7. 防止发送者收到自己的消息

if (userId.equals(sendUserId)) {
    return;
}
  • 如果发送消息的用户是接收消息的用户(即 userIdsendUserId 相同),则直接返回,不再保存消息。
  • 这样避免了用户自己发的评论、点赞等操作被重复记录为消息。

8. 系统消息的特殊处理

if (MessageTypeEnum.SYS == messageTypeEnum) {
    VideoInfoPost videoInfoPost = videoInfoPostMapper.selectByVideoId(videoId);
    extendDto.setAuditStatus(videoInfoPost.getStatus());
}
  • 如果消息类型是 系统消息(SYS),则获取该视频的信息(VideoInfoPost),并将视频的审核状态(status)加入扩展信息 extendDto 中。
  • 系统消息可能包含视频审核状态等特定信息。

9. 保存用户消息到数据库

userMessage.setUserId(userId);
userMessage.setExtendJson(JsonUtils.convertObj2Json(extendDto));
this.userMessageMapper.insert(userMessage);
  • 设置用户消息对象的 extendJson 属性,将 extendDto 对象转为 JSON 格式并存储到数据库中。
  • 最后,调用 userMessageMapper.insert(userMessage) 将这条用户消息保存到数据库。

总结

该方法主要负责以下操作:

  1. 获取视频信息:通过视频 ID 查询视频信息,判断该视频是否有效。
  2. 创建用户消息:根据不同的消息类型(如评论、点赞、收藏等),创建用户消息对象,并设置相关的消息内容、接收用户等信息。
  3. 处理特殊逻辑
    • 对于 点赞和收藏,避免重复记录消息。
    • 对于 评论,处理回复评论的特殊逻辑。
    • 对于 系统消息,加入视频的审核状态等信息。
  4. 存储消息:将最终生成的消息对象存储到数据库中。

该方法是一个典型的异步处理方案,适用于需要延迟处理的操作(如记录消息),并避免在用户操作过程中增加响应时间。

@Async

@Async 注解是 Spring 提供的异步处理功能,允许方法在独立的线程中执行,避免阻塞调用者。这样,方法的执行不会影响主流程,特别适用于一些耗时的操作,如 I/O 操作、数据库操作等。在你的代码中,@Async 被用来异步保存用户消息。下面是对 @Async 的详细解释:

1. @Async 的作用

@Async 注解告诉 Spring,这个方法应该异步执行,也就是说方法会在另一个线程中执行,调用者无需等待方法执行完成,可以继续执行其他任务。

2. 如何工作

  • 线程管理:当方法被标记为 @Async 时,Spring 会在内部使用一个线程池来执行这个方法。如果没有自定义线程池,Spring 会使用默认的线程池。
  • 返回值@Async 注解的方法通常需要返回 FutureCompletableFutureListenableFuture,这些返回值可以用于后续获取方法的执行结果。
  • 非阻塞:调用 @Async 注解的方法时,调用者不会等待方法执行完成,而是可以立即执行后续代码,方法会在后台线程中执行。

3. @Async 的关键点

  • 线程池:当方法标记为 @Async 后,Spring 会从线程池中获取一个线程来执行该方法。
  • 返回类型:异步方法的返回值通常是 FutureListenableFutureCompletableFuture,这些类型用于跟踪异步操作的结果。
  • 配置:默认情况下,Spring 使用一个简单的 ThreadPoolTaskExecutor 来管理线程池。但是,你也可以自定义线程池,控制线程数、队列容量等。
  • 异常处理:如果异步方法抛出异常,它不会直接传递给调用者。你需要在异步方法内部处理异常(例如通过回调或 Future 中处理)。

4. 你代码中的 @Async 解释

在你的代码中,@Async 被应用于 saveUserMessage 方法:

@Async
public void saveUserMessage(String videoId, String sendUserId, MessageTypeEnum messageTypeEnum, String content, Integer replyCommentId) {
    // 处理保存用户消息的逻辑
}

这意味着每次调用 saveUserMessage 方法时,它将异步执行,即方法会在后台的独立线程中执行,调用者无需等待其完成。

调用 saveUserMessage 方法时:
  • 调用者行为:方法被调用时,调用者不会阻塞等待方法执行完成,可以继续执行后续任务。
  • 线程处理:Spring 会从线程池中选择一个线程来执行 saveUserMessage 方法。
  • 没有返回值:这个方法没有返回值,调用者无法获得异步执行的结果,也不需要等待它完成,适合像保存消息这样的操作。

5. @Async 使用的配置要求

  • 启用异步支持:要使 @Async 生效,需要在 Spring 配置类中启用异步处理。这可以通过添加 @EnableAsync 注解来实现:

    @Configuration
    @EnableAsync
    public class AsyncConfig {
    }
    
  • 自定义线程池(可选):你可以定义一个自定义的线程池,如果希望控制线程池的大小、队列容量等参数,可以这样配置:

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);  // 核心池大小
        executor.setMaxPoolSize(10);  // 最大池大小
        executor.setQueueCapacity(25);  // 队列容量
        executor.setThreadNamePrefix("Async-");  // 线程名称前缀
        executor.initialize();
        return executor;
    }
    

    这样可以控制线程池的行为,比如最大线程数、队列容量等。

6. @Async 在你的代码中的作用

在你的场景中,saveUserMessage 方法被标记为 @Async,目的是在用户进行某些操作(比如发表评论、点赞或收藏)后,异步保存相关的用户消息。保存消息是一个与用户界面交互无关的后台任务,用户不需要等待消息保存完成即可得到反馈(比如立即返回操作结果)。

  • 避免阻塞:如果没有使用 @Async,每次用户进行评论等操作时,系统必须等到消息保存到数据库后才能返回响应,这样会导致用户体验不佳,特别是在高并发场景下。而使用 @Async 后,消息保存操作会在后台异步执行,不会阻塞主流程。
  • 异步执行的优势:通过异步执行,用户可以在评论、点赞或收藏后立即获得响应,不需要等待数据库操作完成。这样可以提升用户体验和系统响应速度。

7. 使用 @Async 的注意事项

  • 并发处理:因为异步方法是在独立的线程中执行的,所以需要确保方法是线程安全的,避免并发问题。
  • 异常处理:异步方法抛出的异常不会直接传递给调用者。你需要在异步方法中显式处理异常,或者通过 Future 等机制处理。
  • 性能考虑:如果在程序中滥用 @Async 或进行过多的长时间运行的异步操作,可能会导致线程池耗尽、资源紧张等问题。你需要合理配置线程池,并对异步操作进行优化。

8. 总结

@Async 注解是 Spring 提供的一个强大功能,可以帮助你将一些耗时的操作(如数据库操作、文件处理等)放到后台线程中执行,避免阻塞主线程,从而提升应用的响应性能和用户体验。在使用时,需要合理配置线程池、处理异常,并确保方法是线程安全的。

@Async和ExecutorService的区别

@AsyncExecutorService 都是 Java 中用来实现异步执行的机制,但它们有不同的使用方式、设计理念和适用场景。下面是对这两者的详细对比:

1. 概述

  • @Async:是 Spring 框架提供的一种异步执行机制,允许方法在另一个线程中异步执行。通过使用 @Async,Spring 会自动管理线程池和异步执行的调度,简化了并发编程的实现。它通常与 Spring 的 AOP(面向切面编程)结合使用,方法的调用会被自动代理到异步执行。

  • ExecutorService:是 Java 提供的一个并发工具类,用于管理线程池和任务的异步执行。它提供了更灵活的线程管理和执行控制,适合需要高度自定义线程池配置和管理的场景。

2. 使用方式

  • @Async 使用方式

    • 声明异步方法:在方法上使用 @Async 注解,将该方法标记为异步方法。
    • 返回类型:通常返回 FutureCompletableFutureListenableFuture,这些类型可以用于获取异步执行的结果或异常。
    • 线程池:Spring 自动为异步方法提供一个线程池,你可以通过配置自定义线程池,也可以使用默认线程池。
    • 自动代理@Async 基于 Spring AOP 机制,通过代理的方式将方法异步执行,调用者不需要显式创建线程池或管理线程。
    @Async
    public Future<String> someAsyncMethod() {
        // 异步执行的任务
        return new AsyncResult<>("Task Completed");
    }
    
  • ExecutorService 使用方式

    • 创建线程池:需要显式创建线程池(通过 ExecutorsThreadPoolExecutor 等),并向线程池提交任务。
    • 提交任务:通过调用 submit()execute() 方法提交任务,返回 Future 对象可以用来获取任务结果或异常。
    • 管理线程池:你需要管理线程池的创建、销毁、线程数和任务队列等配置。可以根据需求灵活配置线程池的大小和其他参数。
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    
    Callable<String> task = () -> {
        // 执行任务
        return "Task Completed";
    };
    
    Future<String> future = executorService.submit(task);
    String result = future.get();  // 阻塞直到任务完成
    

3. 异步执行的控制

  • @Async

    • 自动代理:Spring 会自动为标记为 @Async 的方法生成代理,并在后台线程池中异步执行。无需手动管理线程池。
    • 简化编程:通过注解简化了异步编程,不需要手动创建和配置线程池,适用于多数简单的异步任务。
    • 线程池配置:可以通过 Spring 配置文件或 Java 配置类自定义线程池,例如 ThreadPoolTaskExecutor
  • ExecutorService

    • 灵活性更高:你可以根据任务的需求灵活创建和管理线程池,例如使用 ThreadPoolExecutor 配置核心线程数、最大线程数、队列容量等。
    • 更复杂的控制:你可以直接控制任务的提交、执行、调度等,适合需要更精细控制的场景。

4. 异常处理

  • @Async

    • 异常不直接抛出@Async 方法中的异常不会直接抛出给调用者,而是包装在 Future 对象中,调用者需要通过 Future.get() 方法获取并处理异常。
    • 全局异常处理:可以通过配置 Spring 的 @Async 异常处理器来捕获全局异常。
  • ExecutorService

    • 异常捕获:如果通过 submit() 提交的任务抛出异常,异常会被封装在 Future 中,可以通过 Future.get() 方法获取并抛出。
    • 任务执行结果的控制ExecutorService 需要手动处理任务执行的异常,通常可以通过 try-catch 来处理。

5. 线程池管理

  • @Async

    • 线程池管理自动化:Spring 会自动管理线程池,虽然你可以自定义线程池,但大部分情况下,Spring 会根据配置自动分配资源。适用于一般情况下的异步任务,不需要手动管理线程池的细节。
    • 简化线程池管理:Spring 的 @Async 使用了 AOP 自动管理线程池和异步执行的调度,开发者不需要直接操作线程池。
  • ExecutorService

    • 手动管理线程池:需要显式创建和配置线程池。你可以根据需求创建不同类型的线程池,如固定大小线程池、单线程池、缓存线程池等。
    • 灵活性高ExecutorService 提供了更多的线程池管理功能,允许开发者自由定制线程池的行为。

6. 性能与适用场景

  • @Async

    • 适用简单场景:适用于方法调用不需要精细控制线程池或任务调度的场景。它的简单性使其适合大多数中小型项目中异步操作的需求。
    • 性能影响:如果使用默认线程池或没有合理配置线程池,可能会造成线程池资源紧张,从而影响性能。在高并发场景下,可能需要自定义线程池。
  • ExecutorService

    • 适用复杂场景:适合需要细粒度控制线程池、任务调度、任务优先级等的场景。你可以使用 ThreadPoolExecutor 来完全控制线程池的配置。
    • 高并发和大规模任务:适用于需要高并发、复杂线程池管理、或多个异步任务协调的场景。

7. 优缺点总结

特性 @Async ExecutorService
易用性 使用简单,只需在方法上加注解 需要手动创建线程池和管理任务提交
灵活性 灵活性较差,适合简单场景 非常灵活,适用于复杂任务调度
线程池管理 自动管理,少量配置即可 需要手动管理线程池,灵活配置线程池行为
性能 默认线程池适用于一般场景,性能不一定最优 可根据需求配置线程池,适应高并发和复杂场景
异常处理 异常通过 Future 处理,支持全局异常处理 异常通过 Future.get() 获取和处理
适用场景 简单的异步任务,方法级别的异步执行 高度自定义线程池,复杂的任务调度和管理

8. 总结

  • 使用 @Async 适合简单的异步任务,尤其是在 Spring 项目中,能够简化异步编程,并自动管理线程池,适用于大多数常见的异步任务。
  • ExecutorService 提供更灵活和细致的控制,适合复杂的异步任务和高并发场景,尤其是当你需要精细调整线程池配置、任务调度等时。

根据具体的需求来选择使用 @Async 还是 ExecutorService,通常如果是 Spring 项目,且需求较简单,可以优先选择 @Async;而如果需要更复杂的线程管理和调度,则建议使用 ExecutorService

Logo

永洪科技,致力于打造全球领先的数据技术厂商,具备从数据应用方案咨询、BI、AIGC智能分析、数字孪生、数据资产、数据治理、数据实施的端到端大数据价值服务能力。

更多推荐