1、应用场景
需求是:在a系统每次字典数据变更时,都需要给b系统同步一次数据,以保持两个系统字典数据相同。
字典的增、删、改、合并接口,都需要执行数据推送操作,如果不用AOP、这些接口都需要增加推送操作的代码,这样会大大增加主业务与推送的耦合度,而且改起来很麻烦、代码也很不优雅。
下面的应用实例
- 我们使用AOP来降低耦合度;
- 同时为了不影响主业务的执行效率,采用多线程异步执行推送;
- 为了优雅,采用自定义注解标记连接点。
2、应用实例
1)约定
2)配置
3)自定义注解
@Target(ElementType.METHOD) /*注解作用范围*/
@Retention(RetentionPolicy.RUNTIME) /*注解生命周期*/
public @interface PushDataToB {
/*加两个变量,方便业务功能拓展*/
//数据类型:1=机构,2=字典
String dataType() default "1";
//方法类型:mm=合并
//合并时给b系统两次,一次传删除的、一次传留下的
String methodType() default "nmm";
}
4)写切面类
@Component
@Aspect
public class PushDataToBAspect {
@Resource(name = "threadPool")
private ThreadPoolTaskExecutor taskExecutor;
/*@Pointcut("@annotation(com.hhh.bbb.annotation.PushDataToB)")
public void annotationPointCut() {
}*/
@AfterReturning("@annotation(pushDataToB)")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
//获取参数
String dataType = pushDataToB.dataType();
String methodType = pushDataToB.methodType();
Object argObj = getParams(joinPoint);
//执行任务
PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
/*Thread t = new Thread(pushDataToBExecute);
t.start();*/
taskExecutor.execute(pushDataToBExecute);
}
//获取连接点参数
private Object getParams(JoinPoint joinPoint) {
/*Map<String, Object> param = new HashMap<>();
//获取参数值数组
Object[] paramValues = joinPoint.getArgs();
//获取参数名数组
String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
//组装,返回jsonobject
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return JSONObject.parseObject(JSON.toJSONString(param));*/
//获取参数值数组
Object[] paramValues = joinPoint.getArgs();
return paramValues[0];
}
}
代码基本可以分为以下几个部分:
@Pointcut
第一部分,是@Pointcut注解及其方法,用来定义切入点、即我们的通知要在什么地方执行;我这里用的注解标记,表示在用了@PushDataToB这个注解的地方,按照通知类型织入我们的通知。
其实,这个方法并不是必须的,因为通知注解也可以指明通知在什么地方执行。
那么@Pointcut在什么时候用呢?
当类中有多个方法定义的切面相同时,可以单独定义一个方法,不必写方法体,统一设置@Pointcut;当单独指定了一个方法设置好@Pointcut后,类中的其他方法如果要跟单独指定的方法设置相同的切面时,只需要引用设置@Pointcut方法的名字即可。所以上面的示例代码还可以是以下形式的
@Component
@Aspect
public class PushDataToBAspect {
...
@Pointcut("@annotation(com.hhh.bbb.annotation.PushDataToB)")
public void annotationPointCut() {
}
@Before("annotationPointCut()")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
xxx方法代码
}
@AfterReturning("annotationPointCut()")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
yyy方法代码
}
...
}
可以看到代码中我给注掉了,因为我的需求只要一个通知方法就能实现,不需要公用切入点(而且即使公用也不是必须的)。
通知方法
第二个部分是通知方法
@AfterReturning("@annotation(pushDataToB)")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
//获取参数
String dataType = pushDataToB.dataType();
String methodType = pushDataToB.methodType();
Object argObj = getParams(joinPoint);
//执行任务
PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
/*Thread t = new Thread(pushDataToBExecute);
t.start();*/
taskExecutor.execute(pushDataToBExecute);
}
五类通知对应五类通知注解
前置通知(@Before):在目标方法运行之前运行
后置通知(@After):在目标方法运行结束之后运行(无论方法正常结束还是异常结束)
返回通知(@AfterReturning):在目标方法正常返回之后运行
异常通知(@AfterThrowing):在目标方法出现异常以后运行
环绕通知(@Around):动态代理,手动推进目标方法运行(joinPoint.procced())
可以在通知实现方法中增加JoinPoint类型的参数,用以接收实际业务(连接点方法)的方法名称和参数列表等信息,举例说明一下:
下面代码一个字典新增方法的实现方法,按照需求、字典新增我们要把数据推送到B系统,于是我们在方法上加自定义注解
@PushDataToB(dataType = "2")
@Override
public void insertSelective(Dictionary dictionary) {
xxx新增字典的实际代码,校验、新增等
dictionaryDAO.insertSelective(dictionary);
}
在切面类通知方法中,我们需要获取入参dictionary,所以通知方法如下:
我们使用最终通知@AfterReturning,因为这是一个新增,在新增执行成功前,没有字典id等数据,所以选择最终通知、在字典插入方法执行成功后,再获取参数、执行推送任务。
@AfterReturning("@annotation(pushDataToB)")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
//获取参数
String dataType = pushDataToB.dataType();
String methodType = pushDataToB.methodType();
Object argObj = getParams(joinPoint);
//执行任务
PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
taskExecutor.execute(pushDataToBExecute);
}
dataType、methodType是注解自定义的参数,如果需要正常获取就行;
JoinPoint
argObj参数是通过JoinPoint参数、以及动态代理实现的,具体代码如下:
//获取连接点参数
private Object getParams(JoinPoint joinPoint) {
//获取参数值数组
Object[] paramValues = joinPoint.getArgs();
return paramValues[0];
}
因为示例中的主业务方法只有一个参数,所以我们直接获取第一个参数值就满足需求了;如果主业务方法有多个参数时,也可以获取,可以这样:
//获取连接点参数
private JSONObject getParams(JoinPoint joinPoint) {
Map<String, Object> param = new HashMap<>();
//获取参数值数组
Object[] paramValues = joinPoint.getArgs();
//获取参数名数组
String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
//组装,返回jsonobject
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return JSONObject.parseObject(JSON.toJSONString(param));
}
上面是得到JoinPoint以后,通过它获取主业务参数的代码,有一点需要注意
无论通知实现方法有多少个参数,JoinPoint类型的参数必须是第一个参数,否则Spring将无法识别。
ProceedingJoinPoint
还有一个ProceedingJoinPoint类,它继承了JoinPoint,而且添加了proceed()等方法,proceed()方法在环绕通知时会用到,通过它可以控制连接点方法的执行时机,即可以主动控制环绕通知方法的代码,哪些在连接点方法执行前要做,哪些在连接点方法执行后要做,应用示例如下:
@Around("@annotation(pushDataToB)")
public Object around(ProceedingJoinPoint joinPoint, PushDataToB pushDataToB) {
//获取参数
String event = pushDataToB.event();
String dataPlace = pushDataToB.value();
JSONObject argObj = getNameAndValue(joinPoint);
Object returnValue = joinPoint.proceed();
//
JSONObject returnJsonOb = (JSONObject) JSONObject.toJSON(returnValue);
excutePush(returnJsonOb,argObj,dataPlace,event);
return returnValue;
}
上面代码是一个环绕通知方法,前三行代码是在主业务方法(也就是连接点方法)执行前执行的,最后三行代码是在主业务方法执行后执行的,这个功能在有些应用场景下非常好用。
5)多线程异步调用
在applicationContext.xml中配置一个线程池
<!-- 线程池配置 -->
<bean id="threadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数 -->
<property name="corePoolSize" value="5" />
<!-- 最大线程数 -->
<property name="maxPoolSize" value="30" />
<!-- 队列最大长度 >=mainExecutor.maxSize -->
<property name="queueCapacity" value="500" />
<!-- 线程池维护线程所允许的空闲时间 -->
<property name="keepAliveSeconds" value="200" />
<!-- 线程池对拒绝任务(无线程可用)的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
</property>
</bean>
推动任务放在run()方法中
写一个通知执行类,实现Runnable,将通知任务(也就是推送业务)放在run()方法中
public class PushDataToBExecute implements Runnable{
private static Logger logger = LogManager.getLogger(PushDataToBExecute.class.getName());
//服务器地址
private String bServerUrl = PropertiesSource.getProperty("B_SERVER");
private String headPicHost = PropertiesSource.getProperty("HEAD_PIC_HOST");
private Object data = null;
private String dataType;
private String methodType;
@Override
public void run() {
try {
//判断是否是合并操作,合并推送两次:一次传删除的、一次传留下的
if ("mm".equals(methodType)){
xxx代码
}else {
String pwd = Md5Utils.hash("xxx");
Map<String, String> params = new HashMap<String, String>();//请求参数集合
params.put("type", dataType);
params.put("pwd", pwd);
params.put("json", JSONObject.toJSONString(data));
logger.info("Thread:{},推送数据到B系统Param :{}", Thread.currentThread(), JSONObject.toJSONString(params));
String res = HttpUtils.doPostWithJson("http://xxxx", params);
System.out.print(res);
}
}catch (Exception e){
logger.error("Thread:{},推送数据到B系统出现错误:{},", Thread.currentThread(), e);
}
}
public PushDataToBExecute(Object data, String dataType, String methodType){
this.data = data;
this.dataType = dataType;
this.methodType = methodType;
}
}
在切面类注入线程池
@Resource(name = "threadPool")
private ThreadPoolTaskExecutor taskExecutor;
异步执行推送任务
@AfterReturning("@annotation(pushDataToB)")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
xxx代码
//异步执行推送任务
PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
taskExecutor.execute(pushDataToBExecute);
}