Spring AOP面向切面编程,可以用来配置事务、做日志、权限验证、在用户请求时做一些处理等等。用@Aspect做一个切面,就可以直接实现。
1.首先定义一个切面类,加上@Component @Aspect这两个注解
@Aspect
@Configuration
//或者@Component
public class InterfaceLogAspect {
private final Logger logger = LoggerFactory.getLogger(InterfaceLogAspect.class);
private final int cacheTime = 3600*30;
。。。
}
2.定义切点
private final String POINT_CUT = "execution(* com.nio.portal.controller.api.*.*(..))";
@Pointcut(POINT_CUT)
public void pointCut(){}
或者
// 定义切点Pointcut
@Pointcut("execution(* com.nio.portal.controller.api.*.*(..))")
public void excudeService() {
}
切点表达式中,..两个点表明多个,*代表一个, 上面表达式代表切入com.xhx.springboot.controller包下的所有类的所有方法,方法参数不限,返回类型不限。 其中访问修饰符可以不写,不能用*,,第一个*代表返回类型不限,第二个*表示所有类,第三个*表示所有方法,..两个点表示方法里的参数不限。
3.Advice,通知增强,主要包括五个注解Before,After,AfterReturning,AfterThrowing,Around,下面代码中关键地方都有注释,我都列出来了。
@Before 在切点方法之前执行
@After 在切点方法之后执行
@AfterReturning 切点方法返回后执行
@AfterThrowing 切点方法抛异常执行
@Around 属于环绕增强,能控制切点执行前,执行后,,用这个注解后,程序抛异常,会影响@AfterThrowing这个注解
package com.nio.portal.common.aspect;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import com.nio.portal.common.util.CacheMap;
import com.nio.portal.common.util.MessageUtil;
import com.nio.portal.model.output.AppInterface;
import com.nio.portal.model.output.ApplicationInfo;
import com.nio.portal.service.ApplicationService;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.nio.portal.common.outresult.Result;
import com.nio.portal.common.outresult.ResultType;
import com.nio.portal.common.util.JsonHelper;
import com.nio.portal.common.util.RequestUtil;
import com.nio.portal.persistence.entity.InterfaceLogEntity;
import com.nio.portal.service.MongoService;
@Aspect
@Configuration
public class InterfaceLogAspect {
private final Logger logger = LoggerFactory.getLogger(InterfaceLogAspect.class);
private final int cacheTime = 3600*30;
// private final int signCheckCacheTime = 3600*60;
@Autowired
MongoService mongoService;
@Autowired
ApplicationService applicationService;
@Value("${api.check.sign}")
private String needCheckSign;
// 定义切点Pointcut
@Pointcut("execution(* com.nio.portal.controller.api.*.*(..))")
public void excudeService() {
}
@Around("excudeService()")
public Object addInterfaceLog(ProceedingJoinPoint proceeding) throws Throwable {
Result result = null;
InterfaceLogEntity logEntity = new InterfaceLogEntity();
String requestId = UUID.randomUUID().toString();
logEntity.setStartTime(new Date());
//获取
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes)ra;
HttpServletRequest request = sra.getRequest();
//获取调用的url
String url = request.getRequestURL().toString();
//接口调用的时间记录
//包括业务参数bizData和系统级参数
Map<String, String> map = RequestUtil.getAllParameter(request);
String ip = RequestUtil.getRemoteAddr(request);
String method = request.getMethod();
String uri = request.getRequestURI().replaceAll("//", "/");;
//记录日志
logger.info("请求开始reqeustId:{}, ip: {}, url: {}, method: {}, uri: {}, params: {}",requestId,ip, url, method, uri, map);
Map<String, String> headerMap = RequestUtil.getHeaderParameter(request);
//设置接口输入
String input = map.get("bizData");
logEntity.setInput(input);
//获取调用的url
logEntity.setRequestUrl(url.substring(0,url.contains("?")?url.indexOf("?"):url.length()));
//记录request id
logEntity.setSysRequestId(requestId);
try{
//开始校验系统级别参数
boolean checkParam= true;
StringBuilder errorInfo = new StringBuilder();
String timeStamp = getSystemLevelParam(headerMap,map,"timestamp");
if(StringUtils.isEmpty(timeStamp)){
//没有传入调用时间
logEntity.setStatus(2);
checkParam = false;
errorInfo.append("没有传入调用时间timestamp").append(" ");
}else{
logEntity.setRequestTime(new Date(Long.parseLong(timeStamp)));
}
if("true".equalsIgnoreCase(needCheckSign)){
String appId = getSystemLevelParam(headerMap,map,"appId");
if(StringUtils.isEmpty(appId)){
//没有传入app id
logEntity.setStatus(2);
checkParam = false;
errorInfo.append("没有传入系统id appId").append(" ");
}else{
//从缓存中获取application信息
ApplicationInfo applicationInfo = CacheMap.appCache.get(appId);
logger.info("缓存中的applicationInfo:" + JsonHelper.parseToJson(applicationInfo));
if(applicationInfo == null || new Date().getTime() - applicationInfo.getTime().getTime() > cacheTime){
//缓存中没有或者已经超过1小时 从数据库中重新获取
applicationInfo = applicationService.getApplicationInfoByAppId(appId);
logger.info("查询出的applicationInfo:" + JsonHelper.parseToJson(applicationInfo));
if(applicationInfo != null){
applicationInfo.setTime(new Date());
CacheMap.appCache.put(appId,applicationInfo);
}
}
if(applicationInfo == null){
//app id 不存在
logEntity.setStatus(2);
checkParam = false;
errorInfo.append("app id 不存在").append(" ");
}else{
//进行接口校验 当前application 是否可以调用改接口
logger.info("校验applicationInfo:" + JsonHelper.parseToJson(applicationInfo));
logger.info("请求uri:" + uri);
AppInterface appInterface = getAppInterfaceByValue(uri,applicationInfo);
if(appInterface == null){
logEntity.setStatus(2);
checkParam = false;
errorInfo.append("当前接口不能被此appId调用").append(" ");
}else{
logEntity.setApiName(appInterface.getName());
//进行签名校验
Map<String,Object> signMap = new HashMap<>();
String sign = getSystemLevelParam(headerMap,map,"sign");
signMap.put("sign",sign);
signMap.put("timestamp",timeStamp);
signMap.put("appId",appId);
signMap.put("bizData",input);
//取出调用的系统名称
String system = getSystemLevelParam(headerMap,map,"system");
if(StringUtils.isNotEmpty(system)){
signMap.put("system",system);
}
logger.info("签名参数为:"+JsonHelper.parseToJson(signMap));
logger.info("签名秘钥为:"+applicationInfo.getSecurityKey());
if(!MessageUtil.verifySign(signMap,applicationInfo.getSecurityKey())){
//签名校验失败
logEntity.setStatus(2);
checkParam = false;
errorInfo.append("签名错误 ").append(" ");
}
}
}
}
}
if(checkParam ){
//调用接口
result = (Result) proceeding.proceed();//(动态代理重点)
//设置接口输出
logEntity.setOutput(JsonHelper.parseToJson(result));
//设置log状态
if(ResultType.SUCCESS.getCode().equalsIgnoreCase(result.getResultCode())){
//接口调用成功
logEntity.setStatus(1);
}else{
//接口调用失败
logEntity.setStatus(2);
}
}else{
logEntity.setOutput(errorInfo.toString().trim());
result = Result.result(ResultType.API_INVOTE_ERROR.getCode(),errorInfo.toString().trim());
}
}catch (Exception e){
logEntity.setOutput(e.toString());
logEntity.setStatus(2);
result = Result.result(ResultType.SYSTEM_ERROR);
logger.error("请求结束requestId:{}, 发生异常:" ,requestId);
logger.error(uri, e);
} finally{
//设置返回时间
logEntity.setEndTime(new Date());
logEntity.setType(1);
try {
//保存日志
mongoService.insert(logEntity);
logger.info("接口日志保存成功! parms={}",JsonHelper.parseToJson(logEntity));
} catch (Exception e2) {
logger.error("日志保存失败!",e2);
}
logger.info("请求结束requestId:{}, controller的返回值是:{}" ,requestId,JsonHelper.parseToJson(result));
}
return result;
}
private String getSystemLevelParam(Map<String,String> headerMap,Map<String,String> paramMap,String name){
String value = null;
if(headerMap.get(name) != null){
value = headerMap.get(name);
}else if(paramMap.get(name) != null){
value = paramMap.get(name);
}
return value;
}
private AppInterface getAppInterfaceByValue(String value,ApplicationInfo applicationInfo){
if(StringUtils.isEmpty(value) || applicationInfo == null){
return null;
}
List<AppInterface> list = applicationInfo.getInterfaces();
if(list == null || list.isEmpty()){
return null;
}
for(AppInterface appInterface : list){
if(appInterface.getUrl().equalsIgnoreCase(value)){
return appInterface;
}
}
return null;
}
}
动态代理部分:
上面代码中用到了ProceedingJoinPoint,下面来解释一下ProceedingJoinPoint与JoinPoint的区别和联系:
现在AOP的场景越来越多,所以我们有必要理解下和AOP相关的一些概念和机制。基础知识和原理类大家搜索spring aop/aspectj,有大量现成的可以参考,基本上只要理解了jdk动态代理、cglib字节码动态生成代理就足够了,而且必须知道这个代理类是spring托管的(如果是自己创建的代理类,是无法被拦截的,此时只能使用过滤器/拦截器机制,他们本身是链式的,跟代理无关),所以这里就不重复废话了。
import org.aspectj.lang.reflect.SourceLocation;
public interface JoinPoint {
String toString(); //连接点所在位置的相关信息
String toShortString(); //连接点所在位置的简短相关信息
String toLongString(); //连接点所在位置的全部相关信息
Object getThis(); //返回AOP代理对象,也就是com.sun.proxy.$Proxy18
Object getTarget(); //返回目标对象,一般我们都需要它或者(也就是定义方法的接口或类,为
什么会是接口呢?这主 要是在目标对象本身是动态代理的情况下,例如Mapper。所以返回的是定义方法的对象如aoptest.daoimpl.GoodDaoImpl或com.b.base.BaseMapper<T, E, PK>)
Object[] getArgs(); //返回被通知方法参数列表
Signature getSignature(); //返回当前连接点签名 其getName()方法返回方法的FQN,如void
aoptest.dao.GoodDao.delete()或 com.b.base.BaseMapper.insert(T)(需要注意的是,很多时候我们定义了子类继承父类的时候,我们希望拿到基于子类的FQN,这直接可拿不到,要依赖于
AopUtils.getTargetClass(point.getTarget())获取原始代理对象,下面会详细讲解)
SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置
String getKind(); //连接点类型
StaticPart getStaticPart(); //返回连接点静态部分
}
public interface ProceedingJoinPoint extends JoinPoint {
public Object proceed() throws Throwable;
public Object proceed(Object[] args) throws Throwable;
}
JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等:
public interface StaticPart {
Signature getSignature(); //返回当前连接点签名
String getKind(); //连接点类型
int getId(); //唯一标识
String toString(); //连接点所在位置的相关信息
String toShortString(); //连接点所在位置的简短相关信息
String toLongString(); //连接点所在位置的全部相关信息
}
环绕通知 ProceedingJoinPoint 执行proceed方法的作用是让目标方法执行,这也是环绕通知和前置、后置通知方法的一个最大区别。
Proceedingjoinpoint 继承了 JoinPoint 。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。
暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。建议看一下 JdkDynamicAopProxy的invoke方法,了解一下代理链的执行原理。
典型的用法如下:
public Object around(ProceedingJoinPoint point) throws Throwable {
Signature signature = point.getSignature();
// AopUtils.getTargetClass(point.getTarget())获取原始对象,例如对于Mapper而言,它获取的是具体代理的Mapper如com.b.mapper.DefaultDsMapper(如果前者继承了后者的话)而不是定义该方法的Mapper如com.b.base.BaseMapper<Info, InfoExample, InfoKey>,如下图
Type[] types = AopUtils.getTargetClass(point.getTarget()).getGenericInterfaces(); // getGenericInterfaces方法能够获取类/接口实现的所有接口
Annotation nologgingAnno = ((Class)types[0]).getAnnotation(Nologging.class); // type是所有类型的父接口
MethodSignature methodSignature = (MethodSignature)signature;
Method targetMethod = methodSignature.getMethod();
现在来补充下Java中Type接口与Class类的区别联系。
package java.lang.reflect;
/**
* Type is the common superinterface for all types in the Java
* programming language. These include raw types, parameterized types,
* array types, type variables and primitive types.
*
* @since 1.5
*/
public interface Type {
/**
* Returns a string describing this type, including information
* about any type parameters.
*
* @implSpec The default implementation calls {@code toString}.
*
* @return a string describing this type
* @since 1.8
*/
default String getTypeName() {
return toString();
}
}
总结来说:
- Type是一个接口。
- Type是Java中所有类型的父接口,有一些子类,如上所示。
- Type包括:raw type(原始类型,对应Class),parameterized types(参数化类型), array types(数组类型), type variables(类型变量) and primitive types(基本类型,对应Class).
- Type是JDK1.5引入的,主要是为了泛型。
Type接口与Class类的区别联系
- Type是Class的父接口。
- Class是Type的子类。
每天进步一点点,未来可期!加油!!!!
下文中还会对@advice方法中的几个类做介绍!!!
e.g: RequestContextHolder ServletRequestAttributes