spring的两大特性,AOP和DI。DI应该使用比较频繁,而AOP一般局限于拦截器的使用上。
但是今天遇到一个场景,考虑起来还是用AOP更合适一些。
场景介绍:
需要在现有服务基础上添加redis支持,before service验证是否已经在redis中有了缓存。 afterrunning 将servie返回的结果写入redis。
由于此拦截并不是针对url进行的,并且拦截器接口HandlerInterceptor限制了输入输出的数据类型,使用起来不是很方便。所以最终决定采用注解AOP的方式完成。
1、需要添加AOP依赖
我是用的springboot所以添加maven配置如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、添加一个aop配置类
package com.netease.lede.aop;
import org.apache.commons.collections.set.SynchronizedSortedSet;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import com.netease.lede.entity.request.BaseBusinessRequest;
import com.netease.lede.entity.request.GetOnlinePageRequest;
import com.netease.lede.entity.response.BaseResponse;
/**
* @author 张啸雷 E-mail:bjzhangxiaolei1@corp.netease.com
* @version 创建时间:2016年9月27日 下午3:30:52
* 类说明 由于redis的set和get与业务逻辑相关度很高,不能够直接采用interceptor方式完成,所以考虑在service的实现上添加aop
*/
@Aspect
@Configuration
public class RedisAop {
//任意定义在com.netease.lede.service.api包里的任意方法的执行
private BaseBusinessRequest query;
private BaseResponse result;
@Pointcut(
//定义切面范式,针对com.netease.lede.service.api包下面的所有接口的所有方法的实现。
//由于是多个接口,这就要求所有接口的参数和返回值必须一样,接口名称可以不同。
//com.netease.lede.entity.request.BaseBusinessRequest是接口参数类型
//args(query)是定义了参数名称
"execution(* com.netease.lede.service.api.*.*(com.netease.lede.entity.request.BaseBusinessRequest)) && args(query)")
//"execution(* com.netease.lede.service.api.*.*(T)) && args(query)")
//"execution(* com.netease.lede.service.api.*.*(..))")
public void bussinessService(BaseBusinessRequest query){ }
@Before("bussinessService(query)")
public void getResultFromRedis(BaseBusinessRequest query){
System.out.println(query.getRedisKey());
System.out.println("getResultFromRedis");
}
//value="bussinessService(query)"定义了切面名称
//returning="result"定义了返回值名称
@AfterReturning(value="bussinessService(query)",returning="result")
public void setResultToRedis(BaseBusinessRequest query,BaseResponse result){
System.out.println(result.getClass().getName());
System.out.println("setResultToRedis");
}
}
private BaseBusinessRequest query; //所有接口的入参类型
private BaseResponse result; //所有接口的返回值类型
如上配置后:如下接口类中的各个方法则都会被拦截。
package com.netease.lede.service.api;
import org.springframework.web.bind.annotation.RequestBody;
import com.netease.lede.entity.request.BaseBusinessRequest;
import com.netease.lede.entity.request.ChangeCompareDateRequest;
import com.netease.lede.entity.request.GetOnlineFigureRequest;
import com.netease.lede.entity.request.GetOnlinePageRequest;
import com.netease.lede.entity.response.BaseResponse;
import com.netease.lede.entity.response.ChangeCompareDateResponse;
import com.netease.lede.entity.response.GetFigureResponse;
import com.netease.lede.entity.response.GetOnlinePageResponse;
/**
* @author 张啸雷 E-mail:bjzhangxiaolei1@corp.netease.com
* @version 创建时间:2016年9月21日 上午9:35:44 类说明 实时数据相关的service
*/
public interface IOnlineService {
BaseResponse getFrame(BaseBusinessRequest requestEntity);
BaseResponse changeCompareDate(BaseBusinessRequest requestEntity);
BaseResponse getFigures( BaseBusinessRequest requestEntity);
}
遇到的几个坑:
1、Can not set com.netease.lede.service.impl.ProductServiceImpl field com.netease.lede.controller.ProductController.service to com.sun.proxy.$Proxy108
报这个错的原因是controller中DI时使用的是实现类,而不是接口类。在AOP之前没有什么问题,一旦配置了AOP,则由于AOP底层是基于动态代理,所以必须指明接口类。
@Controller
@RequestMapping("/product")
public class ProductController {
@Autowired
public IProductService service;
//ProductServiceImpl service;
修改成如上形式就可以了。
2、Caused by: java.lang.IllegalArgumentException: warning no match for this type name: BaseBusinessRequest [Xlint:invalidAbsoluteTypeName]
在AOP时,pointcut处配置参数的类型时,需要写带报名的全限定名,如果直接写类名则会报上述错误。
@Pointcut(
//定义切面范式,针对com.netease.lede.service.api包下面的所有接口的所有方法的实现。
//由于是多个接口,这就要求所有接口的参数和返回值必须一样,接口名称可以不同。
//com.netease.lede.entity.request.BaseBusinessRequest是接口参数类型
//args(query)是定义了参数名称
"execution(* com.netease.lede.service.api.*.*(com.netease.lede.entity.request.BaseBusinessRequest)) && args(query)")
//"execution(* com.netease.lede.service.api.*.*(BaseBusinessRequest)) && args(query)")
public void bussinessService(BaseBusinessRequest query){ }
关于增强通知方法
在AOP的过程中,我们往往需要目标方法的参数,怎么才能做到呢?就需要使用增强通知了。
增强通知非常简单,只需要在通知方法里面使用JoinPoint joinPoint就可以了。
JoinPoint类提供了若干方法获取目标方法的各种属性。
在使用@Around通知时,由于要调用目标方法,所以使用的是JoinPoint方法的子类ProceedingJoinPoint
在上面的例子中,想要达到的效果是,在通知方法运行之前判断redis中是否已经有相关记录,如果有则取redis中的值,如果没有则执行目标方法,并将目标方法的返回值写入redis。
最初使用@Before和@AfterReturning通知来进行上述功能的实现遇到两个问题。
1、@Before不能中断对目标方法的调用,也就是当判断redis中有记录时,取回reids中的记录,但目标方法依然会被执行。
2、@AfterReturning通知在方法返回后执行,这就导致,无论结果是不是新计算的,都会向redis中写入一次。这显然不是我们想看到的。
所以综合上面的两点,最终还是需要用@Around方法来实现通知。
package com.netease.lede.aop;
import org.apache.commons.collections.set.SynchronizedSortedSet;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.netease.lede.entity.request.BaseBusinessRequest;
import com.netease.lede.entity.request.GetOnlinePageRequest;
import com.netease.lede.entity.response.BaseResponse;
/**
* @author 张啸雷 E-mail:bjzhangxiaolei1@corp.netease.com
* @version 创建时间:2016年9月27日 下午3:30:52
* 类说明 由于redis的set和get与业务逻辑相关度很高,不能够直接采用interceptor方式完成,所以考虑在service的实现上添加aop
*/
@Aspect
@Configuration
public class RedisAop {
String redisKeyName="magiccubedata";
@Autowired
RedisTemplate<String,BaseResponse> redisTemplate;
//任意定义在com.netease.lede.service.api包里的任意方法的执行
//private BaseBusinessRequest query;
//private BaseResponse result;
@Pointcut(
"execution(* com.netease.lede.service.api.*.*(..))")
public void bussinessService(){ }
@Around(value = "bussinessService()")
public BaseResponse getResultFromRedis(ProceedingJoinPoint pjp) throws Throwable{
BaseBusinessRequest query=(BaseBusinessRequest)pjp.getArgs()[0];
System.out.println(query.getRedisKey());
HashOperations<String,String, BaseResponse> hop=redisTemplate.opsForHash();
BaseResponse reponse=hop.get(redisKeyName, query.getRedisKey());
if(null!=reponse){
System.out.println("getResultFromRedis");
return reponse;
}else{
System.out.println("getResultFromMysql");
reponse=(BaseResponse) pjp.proceed(new Object[]{query});
hop.put(redisKeyName, query.getRedisKey(), reponse);
return reponse;
}
}
}