目录
自定义注解
第一步:定义注解
代码
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotifyAnnotation {
/**
* 这里注解属性用的是枚举类型,也可以是其它类型,如String
* String value();
* value是默认属性名称,如果自定义注解中只有一个名为 value 的属性时,
* 可以直接用 @NotifyAnnotation("参数"),否则需要 @NotifyAnnotation(属性名="参数")
*
* 还可以使用 defalt 关键字设定默认值,
* String value() default "默认参数";
* 此时可以使用注解不传参 @NotifyAnnotation
*/
/**
* 行为类型
*/
OperateTypeEnum[] operationType();
/**
* 通知种类
*/
NoticeTypeEnum[] noticeType();
注解含义
@interface
:表示该类是注解类型。
@Documented
:属于标志注解,添加这个注解会被 javadoc工具记录,在生成javadoc的时候就会把@Documented注解给显示出来,正常情况下javadoc中不包含注解的。
@Target(ElementType.METHOD)
:用于设定注解使用范围
ElementType的用法
METHOD:可用于方法上
TYPE:可用于类或者接口上
ANNOTATION_TYPE:可用于注解类型上(被@interface修饰的类型)
CONSTRUCTOR:可用于构造方法上
FIELD:可用于域上
LOCAL_VARIABLE:可用于局部变量上
PACKAGE:用于记录java文件的package信息
PARAMETER:可用于参数上
@Retention(RetentionPolicy.RUNTIME)
:用于定义注解的生命周期
RetentionPolicy.SOURCE
:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
RetentionPolicy.CLASS
:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
RetentionPolicy.RUNTIME
:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
这3个生命周期分别对应于:Java源文件(.java文件) —> .class文件 —> 内存中的字节码。
那怎么来选择合适的注解生命周期呢?
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
注解中的枚举类属性
/**
* 行为类型
*
* @author MrTan
*/
public enum OperateTypeEnum {
/**
* 新增操作
*/
ADD("01", "新增"),
/**
* 删除操作
*/
DEL("02", "删除"),
/**
* 修改操作
*/
MOD("03", "修改");
/**
* 编号
*/
private final String code;
/**
* 名称
*/
private final String name;
OperateTypeEnum(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "OperateTypeEnum{" +
"code='" + code + '\'' +
", name='" + name + '\'' +
'}';
}
}
用法如下,在方法上添加注解
第二步:定义注解的AOP(切面逻辑)类
@Aspect //把当前类标识为一个切面,供容器读取。
@Component //官方的原话是:带此注解的类看为组件,当使用基于注解的配置和类路径扫描的时候,这些类就会被实例化。通俗的讲就是标志将该类的实例交给Spring容器管理,随着程序的运行而运行,不需要自己new
public class NotifyAspect {
//异步发送功能的Service
private final NoticeAsyncService noticeAsyncService;
//通过构造方法给本类中的Service赋值
public NotifyAspect(NoticeAsyncService noticeAsyncService) {
this.noticeAsyncService = noticeAsyncService;
}
/**
21 * @Around:环绕通知
22 * 属性:value 切入点表达式
23 * 位置 在方法的定义前后执行
24 *
25 * 特点:
26 * 1. 他是功能最强的通知
27 * 2. 在目标方法的前后都能增强功能
28 * 3. 控制目标方法是否被调用执行
29 * 4. 修改原来的目标方法的执行结果,影响最后的调用结果
30 * 环绕通知等同于JDK动态代理的InvocationHandler接口
31 *
32 * 参数:ProceedingJoinPoint 就等同于Method,作用:执行目标方法的
33 * 返回值:就是目标方法的执行结果,可以被修改。
34 *
35 * 环绕通知:经常做事务,在目标方法之前开启事务,执行目标方法,目标方法执行完成后结束事务
36 */
@Around("@annotation(notifyAnnotation)")
public Object doAround(ProceedingJoinPoint joinPoint, NotifyAnnotation notifyAnnotation) throws Throwable {
//获取注解的属性值
OperateTypeEnum[] operationType = notifyAnnotation.operationType();
NoticeTypeEnum[] noticeType = notifyAnnotation.noticeType();
//获取方法的参数
Object[] args = joinPoint.getArgs();
//ObjectMapper 的 convertValue方法可以将数组转换成指定对象
ObjectMapper objectMapper = new ObjectMapper();
//消息通知封装类
SysNotice notice = new SysNotice();
List<Map<String, String>> data = new ArrayList();
//编号以1结尾代表是角色变动,否则是用户变动
if (noticeType[0].getCode().endsWith("1")) {
RolegroupsRoleListDto rolegroupsRoles = objectMapper.convertValue(args[0], RolegroupsRoleListDto.class);
operation.setRelationId(rolegroupsRoles.getRoleGroupsId());
for (String roleId : rolegroupsRoles.getRoleIds()) {
Map<String, String> map = new HashMap<>();
map.put("roleId", roleId);
data.add(map);
}
} else {
UsergroupUserListDto usergroupsUserList = objectMapper.convertValue(args[0], UsergroupUserListDto.class);
operation.setRelationId(usergroupsUserList.getUserGroupsId());
for (String userId : usergroupsUserList.getUserIds()) {
Map<String, String> map = new HashMap<>();
map.put("userId", userId);
data.add(map);
}
}
notice.setOperationMessage(data);
notice.setNoticeType(noticeType[0]);
notice.setId(UUID.randomUUID().toString());
Object result;
try {
//执行切入点(被注解)的方法,得到执行结果
result = joinPoint.proceed();
} catch (Exception var6) {
notice.setResult(var6.getMessage());
this.noticeAsyncService.send(notice);
throw var6;
}
//被注解的方法执行成功时记录执行结果和时间,并将消息异步发送到MQ
//如果执行不成功则在try catch中捕捉异常并记录,然后继续发送消息到MQ
notice.setResult(result);
notice.setEndTime(LocalDateTime.now());
//调用异步发送消息方法
this.noticeAsyncService.send(notice);
return result;
}
}
关于 ProceedingJoinPoint 和 JointPoint 的区别和使用方法推荐阅读这篇博文SpringAOP中的JointPoint和ProceedingJoinPoint使用详解(附带详细示例)
第三步:定义异步发送消息类
需要在启动类上添加@EnableAsync
开启异步功能
/**
* 异步发送
* @author MrTan
*/
@Service
public class NoticeAsyncService {
private final StreamBridge streamBridge;
public NoticeAsyncService(StreamBridge streamBridge) {
this.streamBridge = streamBridge;
}
@Async //异步发送注解
//将消息发送到指定的交换机
public void send(SysNotice notice) {
this.streamBridge.send("source-out-1", notice);
}
}
添加RabbitMQ
SpringCloud 的 stream 包中包含对消息中间件的支持,只需要简单的配置就能在服务中使用 rabbit 详情可浏览:官方文档
第一步:添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
第二步:添加配置
spring:
#创建链接
rabbitmq:
#地址,默认本机5672端口
addresses: localhost:5672
#用户名和密码 默认 guest
username: guest
password: guest
#虚拟机节点 / 表示根节点,这里的 blockchain 表示在根节点下的 blockchain 分支,通常生产环境中会根据项目发布在各分支方便隔离区分和管理
virtual-host: blockchain
#定义信通和交换机,
#生产者发布的消息由交换机接收,交换机需要在信通中创建(交换机不具备存储的能力)
cloud:
stream:
bindings:
# 定义信通,命名格式 类型(direct/topic/fanout/headers)- 角色(生产者 out/消费者 in) - 序号(从0开始)
topic-out-0:
# 绑定交换机(已存在交换机会绑定,不存在会生成交换机并绑定)
destination: authmanager
topic-out-1:
destination: authmanager_group
RabbitMQ 的消息模式(交换机类型)
direct 路由模式:通过 routing-key 匹配指定队列进行消息的消费
topic 主题模式:模糊的 routing-key 匹配,可以用 # 匹配所有,用 * 匹配一个
如:student.zhangsan.27 匹配: 不匹配: # * student.# student.* #.27 *.27 student.*.27
fanout 发布订阅模式:是一种广播机制,它没有路由key,所有绑定交换机的队列都会收到消息。
heders 参数模式:通过携带的参数进行匹配
第三步:启动服务访问 rabbit 图形化界面
找到我们定义的交换机,单机可以查看交换机的信息。
单击我们的消息队列查看详细信息
至此成功完成了自定义注解在方法调用时,异步发送消息到 RabbitMQ 进行通知.