spring项目中经常有统一处理日志,对某些参数统一加解密、统一异常处理、统一监控接口请求等需求,此时,就可以使用AOP切面的功能增强。承接上一篇文章,自定义注解分析,今天继续进行。
1、切面的核心类伪代码:
@Aspect //核心AOP注解
@Configuration //扫描生成对象的注解,二选一
//@Component
@Slf4j
public class TestAspect {
@Autowired
private RedisUtil redisUtil;
private List<String> urlList = new ArrayList<>();//本地一级缓存
@Pointcut(value = "@annotation(com.nandao.demo.config.ReplaceUrl)")
public void pointcutTest(){
}
@Around(value = "pointcutTest()")//核心注解,执行过程中触发
public Object doAround(ProceedingJoinPoint pjp)throws Throwable{
Long start = System.currentTimeMillis();
Object proceed = pjp.proceed();
String s = proceed.toString();
// boolean flag = s.contains(ConstantsUtils.REPLACE_IMAGE_FLAG);
boolean flag = s.contains("aa");
log.info("是否包含需要替换的参数:{}",flag);
String replaceUrl = null;//有效的某个新域名
if(flag){
// replaceUrl = getUsableUrl();
replaceUrl = getUsableUrl();
if(StringUtil.isEmpty(replaceUrl)){
return proceed;
}
if(proceed instanceof R){
R r = (R)proceed;
Object object = r.getData();
/**
* R中直接包含JSONObject
*/
if(object instanceof JSONObject){//控制层用的是阿里的JSONObject
JSONObject jsonObject = (JSONObject)object;
Set<String> keySet = jsonObject.keySet();
for(String key : keySet){
object = jsonObject.get(key);
//抽取为公共方法
replaceObjet(object,replaceUrl);
}
}else {
/**
* R中可能直接包含对象、list、str、map
*/
String returnClassName = object.getClass().getName();
if(!object.getClass().isAnnotationPresent(ReplaceObject.class) && !(object instanceof List) && !(object instanceof Map) && !(object instanceof PageUtils)){
log.info("不需要替换的类名",returnClassName);
return proceed;
}
log.info("需要替换的类名",returnClassName);
replaceObjet(object,replaceUrl);
}
}else {
//如果返回值最外层不是R对象,此种情况暂时不存在
}
}
Long end = System.currentTimeMillis();
log.info("替换此接口的名耗时为[{}]毫秒",end-start);
return proceed;
}
}
判断返回的对象类型:
private <T> T replaceObjet(T object,String replaceUrl) throws Exception{
int count = 0;
if(object instanceof List){
log.info("开始扫描[{}]集合",List.class.getName());
/** 处理List类型 */
List list = (List) object;
for(Object item : list){
log.info("[{}]中对象的第[{}]个元素",List.class.getSimpleName(),++count);
if(item.getClass().isAnnotationPresent(ReplaceObject.class)){
processSingle(item,replaceUrl);
}
}
}else if (object instanceof Map){
Map<Object,Object> objectMap = (Map)object;
for(Map.Entry<Object,Object> entry : objectMap.entrySet()){
Object key = entry.getKey();
Object v = entry.getValue();
if(Objects.isNull(v)){
continue;
}
if(v instanceof String){
String s = (String)v;
s = replaceUrl(s,replaceUrl);
objectMap.put(key,s);
}
if(!(v.getClass().isAnnotationPresent(ReplaceObject.class)) && !(v instanceof List) && !(v instanceof Map)){
continue;
}
/** Map的Value可能是VO/LIST/MAP/String */
replaceObjet(v,replaceUrl);
}
}else if (object instanceof PageUtils){
PageUtils pageUtils = (PageUtils)object;
List list = pageUtils.getList();
for(Object item : list){
log.info("[{}]中对象的第[{}]个元素",List.class.getSimpleName(),++count);
if(item.getClass().isAnnotationPresent(ReplaceObject.class)){
processSingle(item,replaceUrl);
}
}
}else if (object.getClass().isAnnotationPresent(ReplaceObject.class)){
processSingle(object,replaceUrl);
}
return null;
}
遍历对象中的参数及类型:
private <T> T processSingle(T single,String replaceUrl) throws Exception{
/** 单个VO */
Class<?> clazz = single.getClass();
String className = clazz.getName();
Field[] fields = clazz.getDeclaredFields();
Class<?> classType = null;
for(Field field : fields){
field.setAccessible(true);
classType = field.getType();
/** 嵌套LIST、嵌套Map */
if(Objects.equals(List.class,classType)){
Class<?> actualClass = (Class<?>) getActualTypes(field)[0];
if(Objects.isNull(field.get(single))){
log.info("扫描到[{}]对象中存在嵌套集合[{}<{}> {}]但值为空,不予处理",className,classType.getSimpleName(),actualClass.getSimpleName(),field.getName());
continue;
}
log.info("扫描到[{}]对象中存在嵌套集合[{}<{}> {}]",className,classType.getSimpleName(),actualClass.getSimpleName(),field.getName());
replaceObjet(field.get(single),replaceUrl);
}else if(Objects.equals(Map.class,classType)){
Map<Object,Object> objectMap = (Map) single;
Type[] actualTypes = getActualTypes(field);
Class<?> keyActualClass = (Class<?>) actualTypes[0];
Class<?> valActualClass = (Class<?>) actualTypes[1];
if(Objects.isNull(field.get(single))){
log.info("扫描到[{}]对象中存在嵌套K-V集合[{}<{},{}> {}]但值为空,不予处理",className,classType.getSimpleName(),keyActualClass.getSimpleName(),valActualClass.getSimpleName(),field.getName());
continue;
}
log.info("扫描到[{}]对象中存在嵌套集合[{}<{},{}> {}]",className,classType.getSimpleName(),keyActualClass.getSimpleName(),valActualClass.getSimpleName(),field.getName());
for(Map.Entry<Object,Object> entry : objectMap.entrySet()){
Object v = entry.getValue();
Object key = entry.getKey();
if(Objects.isNull(v)){
continue;
}
if(v instanceof String){
String s = (String)v;
s = replaceUrl(s,replaceUrl);
objectMap.put(key,s);
}
if(!(v.getClass().isAnnotationPresent(ReplaceObject.class)) && !(v instanceof List) && !(v instanceof Map)){
continue;
}
/** Map的Value可能是PO/VO/LIST/MAP */
replaceObjet(v,replaceUrl);
}
}else if(classType.isAnnotationPresent(ReplaceObject.class)){
/** 嵌套VO/PO */
if(Objects.isNull(field.get(single))){
log.info("扫描到[{}]对象中存在嵌套属性[{} {}]但值为空,不予处理",className,classType.getSimpleName(),field.getName());
continue;
}
log.info("扫描到[{}]对象中存在嵌套属性[{} {}]",className,classType.getSimpleName(),field.getName());
processSingle(field.get(single),replaceUrl);
}
/** 处理被替换的字段 */
if(!field.isAnnotationPresent(ReplaceValue.class)){
continue;
}
Object original = field.get(single);
String s = String.valueOf(original);
if(s.contains(ConstantsUtils.REPLACE_IMAGE_FLAG)){//https://hu.com/fileservice/file/oss/?aaaaa
if(s.contains(ConstantsUtils.HTTP_REQUEST_FLAG)){
//获得第一个点的位置
int index=s.indexOf("/");
log.info(index+"");
//根据第一个点的位置 获得第二个点的位置
index=s.indexOf("/", index+1);
//根据第二个点的位置,获得第三个点的位置
index = s.indexOf("/",index+1);
//根据第三个点的位置,截取 字符串。得到结果 result
String r=s.substring(index);
s = replaceUrl+r;
}else {
s = replaceUrl+s;
}
field.set(single,s);
}else {
//asas
}
log.info("扫描到[{}]对象中需要被[{}]的属性[{}],[{}] => [{}]",className,field.getName(),original, s);
}
return single;
}
获取有效的参数:
private String getUsableUrl(){
String usableUrl = null;//有效的域名
boolean status = false;//域名是否有效的标识
usableUrl= (String)redisUtil.get(ConstantsUtils.REPLACE_LINK_KEY);
if(StringUtil.isNotEmpty(usableUrl)){
return usableUrl;
}
/**
* 本地缓存中如果没有有效的域名,就去数据库查询
*/
long s = System.currentTimeMillis();
usableUrl = isysConfigService.getConfigByKey(ConstantsUtils.REPLACE_LINK_KEY);
log.info("一次RPC调用耗时[{}]",(System.currentTimeMillis()-s));
if(StringUtil.isEmpty(usableUrl)){
log.info("后台没有配置新域名,传的参数是[{}]",ConstantsUtils.REPLACE_LINK_KEY);
/**
* redis缓存中如果没有有效的域名,就去本地缓存中取
*/
if(!CollectionUtils.isEmpty(urlList)){
usableUrl = urlList.get(0);
if(StringUtil.isNotEmpty(usableUrl)){
log.info("本地缓存的域名可能无法使用");
return usableUrl;
}
}
return null;
}
redisUtil.setEx(ConstantsUtils.REPLACE_LINK_KEY,usableUrl,ConstantsUtils.REPLACE_URL_TIME_OUT);//加超时时间,并发量很高时使用
urlList.add(usableUrl);
return usableUrl;
}
2、执行前触发:
/**
* 执行方法前
* @param pjp
* @return
*/
@Before(value = "pointcutImage()")
public Object doBefore(JoinPoint pjp){
return new Object();
}
3、接口执行后触发:
/**
* 方法执行后
* @return
*/
@AfterReturning(value = "pointcutImage()", returning = "result")
public Object afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("执行了afterReturning方法: result=");
return null;
}
/**
* 后置通知
* @param joinPoint 包含了目标方法的所有关键信息
* @After 表示这是一个后置通知,即在目标方法执行之后执行
*/
@After("@annotation(Action)")
public void after(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法执行结束了...");
}
4、接口执行后抛出异常:
/**
* 方法执行后 并抛出异常
* @param joinPoint
* @return
*/
@AfterThrowing(value = "pointcutImage()" , throwing = "ex")
public Object afterThrowing(JoinPoint joinPoint, Exception ex) {
System.out.println("执行了afterReturning方法: result=");
log.info("替换图片地址报错的接口名[{}],类名[{}]",joinPoint.getSignature().getName(),joinPoint.getSignature().getDeclaringTypeName());
log.info("报错日志{}",exception.getStackTrace());
ex.printStackTrace();
return null;
}
5、切面的接口经常返回object ,需要判断类型、范型之类的、比如:
/**
* 获取字段泛型
* @param field 字段
*/
private static Type[] getActualTypes(Field field){
Type[] actualTypes = null;
Type genericType = field.getGenericType();
/** 无泛型 */
if(Objects.isNull(genericType)) return actualTypes;
/** 有泛型 */
if(genericType instanceof ParameterizedType){
ParameterizedType pt = (ParameterizedType) genericType;
/** 泛型Class */
actualTypes = pt.getActualTypeArguments();
}
return actualTypes;
}
6、在切面里获取请求的域名、uri等信息:
/**
* 获取请求的方法名称
* @param joinPoint
* @return
*/
private String getClassMethodName(JoinPoint joinPoint) {
/**
* 后台管理admin服务中所有的控制层接口名被代理,只能取uri
*/
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 请求路径
// log.info("请求的uri"+request.getRequestURI());
return request.getRequestURI();
/**
* 控制层的接口没有被代理可以走这里
*/
//类方法
/* log.info("classMethod={}", joinPoint.getSignature().getDeclaringTypeName());
String declaringTypeName = joinPoint.getSignature().getDeclaringTypeName();
String className = declaringTypeName.substring(declaringTypeName.lastIndexOf(".")+1);
log.info("className={}", className);
log.info("Method={}", joinPoint.getSignature().getName());
return className +"."+ joinPoint.getSignature().getName();*/
}
7、切面里获取请求的参数:
/**
* 获取请求的参数
* @param joinPoint
* @return
*/
private String getMethodArgs(JoinPoint joinPoint){
// 构造参数组集合
List<Object> argList = new ArrayList<>();
for (Object arg : joinPoint.getArgs()) {
// request/response无法使用toJSON
if (arg instanceof HttpServletRequest) {
argList.add("request");
} else if (arg instanceof HttpServletResponse) {
argList.add("response");
} else {
argList.add(JSON.toJSON(arg));
}
}
return JSON.toJSON(argList).toString();
}
到此AOP切面的分享告一段落,切面中涉及到的注意点,也都给大家呈现了出来,不明白的地方可以留言。