libilibi项目总结(14)AOP校验登录和记录消息
这部分代码的核心是通过反射获取方法上的注解,并根据注解的内容决定是否执行后续的操作(如登录校验)。这种方法使得我们可以灵活地对需要登录校验的接口方法进行拦截,而不需要在每个方法中重复编写登录验证逻辑,只需在方法上添加注解即可。: 获取当前连接点(即被拦截的方法)的签名信息。Signature的子接口,提供了关于方法签名的更多细节。: 使用反射检查目标方法是否标记了注解。如果没有该注解,直接返回,不
登录校验
- 定义
@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;
}
解释步骤:
-
point.getSignature():point是JoinPoint类型的参数,代表当前切面所拦截的连接点(即当前被调用的方法)。JoinPoint提供了多个方法来访问当前方法的信息。point.getSignature()返回一个Signature类型的对象。Signature是 AOP 中的一个基础类,它代表了被拦截方法的签名信息(例如方法名、参数类型等)。
-
(MethodSignature) point.getSignature():Signature是一个接口,MethodSignature是它的一个子接口,表示的是方法的签名(即方法的信息)。MethodSignature比Signature提供了更多与方法相关的细节信息。- 使用
(MethodSignature)对point.getSignature()进行类型转换,将其转换为MethodSignature,这样我们就能访问更多特定于方法的信息,例如方法的参数类型、返回值类型等。
-
method.getAnnotation(GlobalInterceptor.class):method是当前目标方法的Method对象,它是通过((MethodSignature) point.getSignature()).getMethod()获得的。getAnnotation(GlobalInterceptor.class)方法通过反射获取当前方法上的@GlobalInterceptor注解。如果该方法上没有这个注解,返回null。GlobalInterceptor是一个自定义的注解,用于标记需要进行某些特定操作(例如校验登录)的目标方法。
-
if (null == interceptor) { return; }:- 如果当前方法上没有
@GlobalInterceptor注解,则interceptor会为null。 - 在这种情况下,
if语句会判断interceptor是否为null,如果是null,则直接返回,不做任何处理。这样就跳过了当前方法的拦截,意味着不需要执行后续的登录校验逻辑。
- 如果当前方法上没有
目的
这一部分的目的是:
-
检查目标方法是否有
@GlobalInterceptor注解:- 如果方法上有
@GlobalInterceptor注解,AOP 切面会对该方法进行处理,执行interceptorDo()方法中的逻辑。 - 如果方法上没有这个注解,说明该方法不需要经过此切面的拦截,所以直接返回,跳过校验。
- 如果方法上有
-
通过反射获取注解实例:
method.getAnnotation(GlobalInterceptor.class)用来通过反射机制获取@GlobalInterceptor注解实例。@GlobalInterceptor注解可以包含一些配置参数,比如checkLogin属性,指示是否需要进行登录校验。
-
决定是否继续执行拦截操作:
if (null == interceptor)判断是否存在该注解,如果没有找到@GlobalInterceptor注解,则当前方法不需要进行进一步的拦截处理。否则,继续执行后续的逻辑(如校验登录)。
举例
假设我们有以下一个方法:
@GlobalInterceptor(checkLogin = true)
public void someProtectedMethod() {
// 业务逻辑
}
当 someProtectedMethod() 被调用时,GlobalOperationAspect 切面会被触发。AOP 会执行以下流程:
- 在
interceptorDo()方法中,point.getSignature()获取到someProtectedMethod方法的签名信息。 ((MethodSignature) point.getSignature()).getMethod()获取到该方法的Method对象。method.getAnnotation(GlobalInterceptor.class)会返回@GlobalInterceptor注解实例,因为someProtectedMethod()上确实标记了该注解。checkLogin = true表示需要进行登录校验,于是调用checkLogin()方法进行校验。
如果 someProtectedMethod() 没有标记 @GlobalInterceptor 注解,那么 method.getAnnotation(GlobalInterceptor.class) 会返回 null,然后通过 if (null == interceptor) 判断,直接跳过校验,不会执行 checkLogin() 方法。
总结
这部分代码的核心是通过反射获取方法上的注解,并根据注解的内容决定是否执行后续的操作(如登录校验)。这种方法使得我们可以灵活地对需要登录校验的接口方法进行拦截,而不需要在每个方法中重复编写登录验证逻辑,只需在方法上添加 @GlobalInterceptor(checkLogin = true) 注解即可。
point.getSignature(): 获取当前连接点(即被拦截的方法)的签名信息。MethodSignature:Signature的子接口,提供了关于方法签名的更多细节。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是一个业务错误码,通常表示登录失效或用户未登录。
代码总结
该段代码的核心功能是:
- 拦截标记了
@GlobalInterceptor注解的方法。 - 如果注解中的
checkLogin属性为true,则进行登录校验。 - 校验逻辑包括从请求头获取 token、检查 token 是否有效(即是否存在于 Redis 中),如果无效则抛出异常,提示用户未登录。
应用场景
这个全局拦截器通常用于需要登录认证的 API 接口。通过这种方式,开发者可以在方法上使用 @GlobalInterceptor 注解,简化登录校验逻辑,而不需要在每个方法内部重复编写认证逻辑。
例如,某些方法可能是需要登录才能访问的,开发者只需要在方法上添加 @GlobalInterceptor(checkLogin = true) 注解,AOP 就会自动执行登录校验,确保请求中携带有效的 token。
扩展
当我们说“它代表了被拦截方法的签名信息”时,实际上是在描述 Signature 和它的子类 MethodSignature 在 AOP 中所代表的对象的含义。
1. 签名(Signature)是什么?
在 Java 中,签名(Signature)指的是方法的唯一标识符,它包含了方法的基本信息。对于 AOP 来说,方法签名通常指一个方法的名称、参数类型以及其他可能的修饰符(如返回类型等)。可以简单地理解为方法的 “签名” 就是这个方法的 “标识符”。
对于 AOP 切面编程中的拦截,它会涉及到“被拦截的方法”的签名信息。Spring AOP 通过 JoinPoint 和 Signature 来描述目标方法的元数据。
-
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. 方法签名包含的信息
通过 MethodSignature 和 Method 对象,我们可以获取被拦截方法的多种信息,以下是一些常见的方法:
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 类的实例,并且每个常量都有自己的构造函数参数 type 和 desc。
2. 构造函数
MessageTypeEnum(Integer type, String desc) {
this.type = type;
this.desc = desc;
}
- 枚举的构造函数
MessageTypeEnum(Integer type, String desc)用于初始化每个枚举常量的type和desc字段。构造函数是 私有的,因此它只能在枚举内部被调用。 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,自动生成并保存用户消息。具体来说,方法从目标方法的参数中提取相关数据(如 videoId、actionType 等),并使用这些数据构建用户消息。下面是对代码的逐行详细解释:
方法签名
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_REASON和PARAMETERS_CONTENT:如果参数名称是content或reason,则将对应的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块捕获各种异常(BusinessException、Exception、Throwable),记录错误日志并抛出相应的异常。
11. 总结
- 这个类实现了一个 AOP 切面,主要作用是记录用户消息。通过
@RecordUserMessage注解标注的方法,在执行时会自动记录用户消息。 - 切面在执行目标方法前后拦截,获取方法的参数,并根据注解配置保存消息。
- 该功能通过
userMessageService.saveUserMessage()方法保存消息,并使用 Redis 从中获取用户信息(通过 Token)。
为什么用 ProceedingJoinPoint 而不是 JoinPoint?
ProceedingJoinPoint 是环绕通知 (@Around) 中使用的特定类型的连接点(JoinPoint)。它与常规的 JoinPoint 不同,主要用于环绕通知,因为它提供了更多的控制能力,尤其是在执行目标方法时。
为什么用 ProceedingJoinPoint 而不是 JoinPoint?
在 Spring AOP 中,@Before, @After, @AfterReturning, 和 @AfterThrowing 通常使用 JoinPoint,它提供了关于目标方法的一些信息(比如方法签名、参数等)。然而,@Around 通知需要更强的控制能力,因为它允许你在执行目标方法之前或之后进行操作,甚至决定是否执行目标方法。
ProceedingJoinPoint 是 JoinPoint 的子接口,扩展了更多功能,最重要的就是它有一个 proceed() 方法。proceed() 方法让你可以在目标方法调用前后控制方法的执行,甚至可以选择不执行目标方法,或者修改目标方法的返回值。
ProceedingJoinPoint 的作用
-
调用目标方法:
- 通过
proceed()方法,你可以显式地调用目标方法并获取返回值。 proceed()方法会继续执行目标方法,并返回方法的返回值。
- 通过
-
控制目标方法的执行:
- 你可以在调用
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 提供的关键方法
proceed():这是最关键的方法,用来继续执行目标方法。如果你没有调用proceed(),目标方法将不会执行。getArgs():获取目标方法的参数列表,你可以通过它访问目标方法传入的参数。getSignature():获取目标方法的签名(包括方法名、返回类型、参数类型等)。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;
}
- 如果发送消息的用户是接收消息的用户(即
userId和sendUserId相同),则直接返回,不再保存消息。 - 这样避免了用户自己发的评论、点赞等操作被重复记录为消息。
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)将这条用户消息保存到数据库。
总结
该方法主要负责以下操作:
- 获取视频信息:通过视频 ID 查询视频信息,判断该视频是否有效。
- 创建用户消息:根据不同的消息类型(如评论、点赞、收藏等),创建用户消息对象,并设置相关的消息内容、接收用户等信息。
- 处理特殊逻辑:
- 对于 点赞和收藏,避免重复记录消息。
- 对于 评论,处理回复评论的特殊逻辑。
- 对于 系统消息,加入视频的审核状态等信息。
- 存储消息:将最终生成的消息对象存储到数据库中。
该方法是一个典型的异步处理方案,适用于需要延迟处理的操作(如记录消息),并避免在用户操作过程中增加响应时间。
@Async
@Async 注解是 Spring 提供的异步处理功能,允许方法在独立的线程中执行,避免阻塞调用者。这样,方法的执行不会影响主流程,特别适用于一些耗时的操作,如 I/O 操作、数据库操作等。在你的代码中,@Async 被用来异步保存用户消息。下面是对 @Async 的详细解释:
1. @Async 的作用
@Async 注解告诉 Spring,这个方法应该异步执行,也就是说方法会在另一个线程中执行,调用者无需等待方法执行完成,可以继续执行其他任务。
2. 如何工作
- 线程管理:当方法被标记为
@Async时,Spring 会在内部使用一个线程池来执行这个方法。如果没有自定义线程池,Spring 会使用默认的线程池。 - 返回值:
@Async注解的方法通常需要返回Future、CompletableFuture或ListenableFuture,这些返回值可以用于后续获取方法的执行结果。 - 非阻塞:调用
@Async注解的方法时,调用者不会等待方法执行完成,而是可以立即执行后续代码,方法会在后台线程中执行。
3. @Async 的关键点
- 线程池:当方法标记为
@Async后,Spring 会从线程池中获取一个线程来执行该方法。 - 返回类型:异步方法的返回值通常是
Future、ListenableFuture或CompletableFuture,这些类型用于跟踪异步操作的结果。 - 配置:默认情况下,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的区别
@Async 和 ExecutorService 都是 Java 中用来实现异步执行的机制,但它们有不同的使用方式、设计理念和适用场景。下面是对这两者的详细对比:
1. 概述
-
@Async:是 Spring 框架提供的一种异步执行机制,允许方法在另一个线程中异步执行。通过使用@Async,Spring 会自动管理线程池和异步执行的调度,简化了并发编程的实现。它通常与 Spring 的 AOP(面向切面编程)结合使用,方法的调用会被自动代理到异步执行。 -
ExecutorService:是 Java 提供的一个并发工具类,用于管理线程池和任务的异步执行。它提供了更灵活的线程管理和执行控制,适合需要高度自定义线程池配置和管理的场景。
2. 使用方式
-
@Async使用方式:- 声明异步方法:在方法上使用
@Async注解,将该方法标记为异步方法。 - 返回类型:通常返回
Future、CompletableFuture或ListenableFuture,这些类型可以用于获取异步执行的结果或异常。 - 线程池:Spring 自动为异步方法提供一个线程池,你可以通过配置自定义线程池,也可以使用默认线程池。
- 自动代理:
@Async基于 Spring AOP 机制,通过代理的方式将方法异步执行,调用者不需要显式创建线程池或管理线程。
@Async public Future<String> someAsyncMethod() { // 异步执行的任务 return new AsyncResult<>("Task Completed"); } - 声明异步方法:在方法上使用
-
ExecutorService使用方式:- 创建线程池:需要显式创建线程池(通过
Executors或ThreadPoolExecutor等),并向线程池提交任务。 - 提交任务:通过调用
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。
- 自动代理:Spring 会自动为标记为
-
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。
更多推荐


所有评论(0)