Java中Spring使用AOP获取值类型

一:AOP简介

1.AOP简介

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。这种在运行时,动态地将代码切入到类的指定方法或指定位置上的编程思想就是面向切面的编程。利用AOP可以将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来作为公共部分,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

2.AOP作用

日志记录,性能统计,安全控制,事务处理,异常处理等等。
在面向切面编程AOP的思想里面,核心业务和切面通用功能(例如事务处理、日志管理、权限控制等)分别独立进行开发,然后把切面功能和核心业务功能 “编织” 在一起,这就叫AOP。这种思想有利于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

3.AOP相关术语

通知(Advice)

通知描述了切面要完成的工作以及何时执行。比如我们的日志切面需要记录每个接口调用时长,就需要在接口调用前后分别记录当前时间,再取差值。

  • 前置通知(Before):在目标方法调用前调用通知功能;
  • 后置通知(After):在目标方法调用之后调用通知功能,不关心方法的返回结果;
  • 返回通知(AfterReturning):在目标方法成功执行之后调用通知功能;
  • 异常通知(AfterThrowing):在目标方法抛出异常后调用通知功能;
  • 环绕通知(Around):通知包裹了目标方法,在目标方法调用之前和之后执行自定义的行为。

切点(Pointcut)

切点定义了通知功能被应用的范围。比如日志切面的应用范围就是所有接口,即所有controller层的接口方法。

切面(Aspect)

切面是通知和切点的结合,定义了何时、何地应用通知功能。

引入(Introduction)

在无需修改现有类的情况下,向现有的类添加新方法或属性。

织入(Weaving)

把切面应用到目标对象并创建新的代理对象的过程。

连接点(JoinPoint)

通知功能被应用的时机。比如接口方法被调用的时候就是日志切面的连接点。

3.1 JointPoint和ProceedingJoinPoint

JointPoint是程序运行过程中可识别的连接点,这个点可以用来作为AOP切入点。JointPoint对象则包含了和切入相关的很多信息,比如切入点的方法,参数、注解、对象和属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。

(1)JointPoint
通过JpointPoint对象可以获取到下面信息

# 返回目标对象,即被代理的对象
Object getTarget();
 
# 返回切入点的参数
Object[] getArgs();
 
# 返回切入点的Signature
Signature getSignature();
 
# 返回切入的类型,比如method-call,field-get等等,感觉不重要 
 String getKind();

(2)ProceedingJoinPoint

Proceedingjoinpoint 继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的。

暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,,这也是环绕通知和前置、后置通知方法的一个最大区别。这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。

ProceedingJoinPoint可以获取切入点的信息:

  • 切入点的方法名字及其参数
  • 切入点方法标注的注解对象(通过该对象可以获取注解信息)
  • 切入点目标对象(可以通过反射获取对象的类名,属性和方法名)
//获取切入点方法的名字,getSignature());是获取到这样的信息 :修饰符+ 包名+组件名(类名) +方法名
String methodName = joinPoint.getSignature().getName()
 
//获取方法的参数,这里返回的是切入点方法的参数列表
Object[] args = joinPoint.getArgs();
 
//获取方法上的注解
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null)
{
   xxxxxx annoObj= method.getAnnotation(xxxxxx.class);
}
 
//获取切入点所在目标对象
Object targetObj =joinPoint.getTarget();
//可以发挥反射的功能获取关于类的任何信息,例如获取类名如下
String className = joinPoint.getTarget().getClass().getName();

3.2.ProceedingJoinPoint获取返回类型、参数名称/值等一些常用方法

3.2.1、参数值
Object[] args = joinPoint.getArgs();
3.2. 2、参数名称
Signature signature = joinPoint.getSignature();
if (signature instanceof MethodSignature) {
    MethodSignature methodSignature = (MethodSignature) signature;
    String[] properties = methodSignature.getParameterNames();
}
3.2.3、返回类型

Signature signature = joinPoint.getSignature();
if (signature instanceof MethodSignature) {
    MethodSignature methodSignature = (MethodSignature) signature;
    // 被切的方法
    Method method = methodSignature.getMethod();
    // 返回类型
    Class<?> methodReturnType = method.getReturnType();
    // 实例化
    Object o = methodReturnType.newInstance();
}
 
3.2.4、全限定类名
Signature signature = joinPoint.getSignature();
signature.getDeclaringTypeName()
3.2.5、方法名
Signature signature = joinPoint.getSignature();
signature.getName()

4.AOP相关注解

Spring中使用注解创建切面

  • @Aspect:用于定义切面
  • @Before:通知方法会在目标方法调用之前执行
  • @After:通知方法会在目标方法返回或抛出异常后执行
  • @AfterReturning:通知方法会在目标方法返回后执行
  • @AfterThrowing:通知方法会在目标方法抛出异常后执行
  • @Around:通知方法会将目标方法封装起来
  • @Pointcut:定义切点表达式
  • 切点表达式:指定了通知被应用的范围,表达式格式:
execution(方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数)
//com.hs.demo.controller包中所有类的public方法都应用切面里的通知
execution(public * com.hs.demo.controller.*.*(..))
//com.hs.demo.service包及其子包下所有类中的所有方法都应用切面里的通知
execution(* com.hs.demo.service..*.*(..))
//com.hs.demo.service.EmployeeService类中的所有方法都应用切面里的通知
execution(* com.hs.demo.service.EmployeeService.*(..))

(1)@POINTCUT定义切入点,有以下2种方式:

方式一:设置为注解@LogFilter标记的方法,有标记注解的方法触发该AOP,没有标记就没有。

@Aspect
@Component
public class LogFilter1Aspect {
	@Pointcut(value = "@annotation(com.hs.aop.annotation.LogFilter)")
	public void pointCut(){
	
	}
}

方式二:采用表达式批量添加切入点,如下方法,表示AopController下的所有public方法都添加LogFilter1切面。

@Pointcut(value = "execution(public * com.train.aop.controller.AopController.*(..))")
public void pointCut(){
 
}

@Around环绕通知

@Around集成了@Before、@AfterReturing、@AfterThrowing、@After四大通知。需要注意的是,他和其他四大通知注解最大的不同是需要手动进行接口内方法的反射后才能执行接口中的方法,换言之,@Around其实就是一个动态代理。
   /**
	 * 环绕通知是spring框架为我们提供的一种可以在代码中手动控制增强部分什么时候执行的方式。
	 *
	 */
	public void aroundPringLog(ProceedingJoinPoint pjp)
   {
      //拿到目标方法的方法签名
       Signature signature = pjp.getSignature();
      //获取方法名
      String name = signature.getName();
 
	  try {
			//@Before
			System.out.println("【环绕前置通知】【"+name+"方法开始】");
            //这句相当于method.invoke(obj,args),通过反射来执行接口中的方法
			proceed = pjp.proceed();
			//@AfterReturning
			System.out.println("【环绕返回通知】【"+name+"方法返回,返回值:"+proceed+"】");
		} catch (Exception e) {
			//@AfterThrowing
			System.out.println("【环绕异常通知】【"+name+"方法异常,异常信息:"+e+"】");
		}
        finally{
			//@After
			System.out.println("【环绕后置通知】【"+name+"方法结束】");
		}
	}
proceed = pjp.proceed(args)这条语句其实就是method.invoke,以前手写版的动态代理,也是method.invoke执行了,jdk才会利用反射进行动态代理的操作,在Spring的环绕通知里面,只有这条语句执行了,spring才会去切入到目标方法中。

为什么说环绕通知就是一个动态代理呢?

proceed = pjp.proceed(args)这条语句就是动态代理的开始,当我们把这条语句用try-catch包围起来的时候,在这条语句前面写的信息,就相当于前置通知,在它后面写的就相当于返回通知,在catch里面写的就相当于异常通知,在finally里写的就相当于后置通知。

二:自定义实现AOP注解

1.引入依赖

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-aop</artifactId>  
</dependency>

2.自定义日志注解

package com.wondertek.center.aspect.annotation;

import java.lang.annotation.*;

/**
 * 定义操作日志注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogInfo
{
}

package com.wondertek.center.aspect;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.allcammon.service.user.request.UserInfoRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlemon.collect.Maps;
import com.wondertek.cache.util.RedisUtil;
import com.wondertek.center.aspect.annotation.LogInfo;
import com.wondertek.center.client.CustomerUserService;
import com.wondertek.center.constants.UserConstants;
import com.wondertek.center.model.dto.AccountInfoRequest;
import com.wondertek.center.model.dto.AccountInfoResponse;
import com.wondertek.center.model.dto.ExcuteSendAIResultDTO;
import com.wondertek.center.model.entity.AlgorithmFactory;
import com.wondertek.center.model.vo.AccountRoleVo;
import com.wondertek.center.model.vo.ExcuteSendAIResultVO;
import com.wondertek.center.response.AbilityLog;
import com.wondertek.center.service.AbilityLogService;
import com.wondertek.center.service.AlgorithmFactoryService;
import com.wondertek.util.StringUtil;
import com.wondertek.web.exception.BizErrorException;
import com.wondertek.web.exception.enums.BizErrorCodeEnum;
import com.wondertek.web.response.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apachemons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

/**
 * @author xiaoxiangyuan
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LogInfoAspect {

    @Resource
    private CustomerUserService customerUserService;
    @Resource
    private RedisUtil redisUtil;

    @Resource
    private AbilityLogService abilityLogService;

    @Resource
    private AlgorithmFactoryService algorithmFactoryService;

    @PostConstruct
    public void init() {
        log.info("LogInfo()初始化完成");
    }
    // 定义一个切入点
    @Pointcut("@annotation(com.wondertek.center.aspect.annotation.LogInfo)")
    protected void logInfo() {
    }
    @Around("logInfo()")
    public Object recordOpLog(ProceedingJoinPoint joinPoint) throws Throwable {
        boolean success = false;
        Object result=null;
        try {
            result = joinPoint.proceed();
            success = true;
            return result;
        } finally {
            try {
                handlerSaveLog(joinPoint, success,result);
            } catch (Exception e) {
                log.error("record op log failed.", e);
            }
        }

    }


    private void handlerSaveLog(ProceedingJoinPoint joinPoint, boolean success,Object result) throws InstantiationException, IllegalAccessException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Object parameter = getParameter(signature.getMethod(), joinPoint.getArgs());
        String dto = JSONUtil.toJsonStr(parameter);
        ExcuteSendAIResultDTO excuteSendAIResultDTO = JSON.parseObject(JSON.toJSONString(parameter), ExcuteSendAIResultDTO.class);
        saveLog(result,excuteSendAIResultDTO,dto);


    }

    private void saveLog(Object result, ExcuteSendAIResultDTO excuteSendAIResultDTO,String dto) {

        //String token = request.getHeader("token");
        String token = excuteSendAIResultDTO.getToken();
        String roleType = "";
        String accountName = "";
        String realName = "";
        String userId = "";
        if (StringUtils.isNotBlank(token)) {
            //管理员显示全部,普通用户只显示普通用户
            AccountInfoResponse userInfo = getUserInfo(token);
            userId = String.valueOf(userInfo.getId());
            accountName = userInfo.getAccountName();
            realName = userInfo.getRealName();
            List<AccountRoleVo> roles = userInfo.getRoles();
            for (AccountRoleVo role : roles) {
                if (role.getRoleName().contains("管理员")) {
                    roleType = "管理员";
                } else {
                    roleType = "普通用户";
                }

            }
        }


        //获取响应结果
        String bo = JSONUtil.toJsonStr(result);
        Result<List<ExcuteSendAIResultVO>> response = JSONObject.parseObject(bo, new TypeReference<Result<List<ExcuteSendAIResultVO>>>() {
        });

        String responseStr = "";
        if (HttpStatus.OK.value() == (response.getCode())) {
            if (CollectionUtil.isNotEmpty(response.getData())) {
                List<ExcuteSendAIResultVO> data = response.getData();
                responseStr = JSONUtil.toJsonStr(data);

            }
        }
        String aiCode = excuteSendAIResultDTO.getAiCode();
        AlgorithmFactory algorithmByAiCode = algorithmFactoryService.getAlgorithmByAiCode(aiCode);
        //根据aiCode获取算法相关信息
        AbilityLog abilityLog = new AbilityLog();
        abilityLog.setUserid(userId);
        abilityLog.setAbilityUrl(algorithmByAiCode.getInterfaceAddress());
        abilityLog.setArtworkUrl(excuteSendAIResultDTO.getFileUrl());
        abilityLog.setResponseResult(responseStr);
        abilityLog.setCallType(excuteSendAIResultDTO.getType());
        abilityLog.setRoleName(roleType);
        abilityLog.setUserName(accountName);
        abilityLog.setAbilityName(algorithmByAiCode.getAlgorithmName());
        abilityLog.setCreateTime(new Date());
        abilityLog.setUpdateTime(new Date());
        abilityLog.setAiCode(aiCode);
        abilityLog.setDtoParam(dto);
        abilityLogService.insert(abilityLog);

        log.info("保存日志成功!对象信息为:{}", JSONUtil.toJsonStr(abilityLog));

    }

    private AccountInfoResponse getUserInfo(String token) {
        if (StringUtil.isEmpty(token)) {
            throw new BizErrorException(BizErrorCodeEnum.USER_TOKEN_EXPIRED);
        }
        String redisKey = UserConstants.GET_USER_ID_BY_TOKEN + ":" + token;
        Object o = redisUtil.get(redisKey);
        if (o == null) {
            throw new BizErrorException(BizErrorCodeEnum.USER_TOKEN_EXPIRED);
        }
        Long userId = Long.valueOf(String.valueOf(o));
        AccountInfoRequest infoRequest = new AccountInfoRequest();
        infoRequest.setId(userId);
        Result<AccountInfoResponse> result = customerUserService.queryAccountInfo(infoRequest);
        result.assertSuccess();
        AccountInfoResponse data = result.getData();
        return data;
    }



    /**
     * 根据方法和传入的参数获取请求参数
     */
    private Object getParameter(Method method, Object[] args)
    {
        List<Object> argList = new ArrayList<>();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            //将RequestBody注解修饰的参数作为请求参数
            RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
            if (requestBody != null) {
                argList.add(args[i]);
            }
            //将RequestParam注解修饰的参数作为请求参数
            RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
            if (requestParam != null) {
                Map<String, Object> map = new HashMap<>();
                String key = parameters[i].getName();
                if (!StringUtils.isEmpty(requestParam.value())) {
                    key = requestParam.value();
                }
                map.put(key, args[i]);
                argList.add(map);
            }
        }
        if (argList.size() == 0) {
            return null;
        } else if (argList.size() == 1) {
            return argList.get(0);
        } else {
            return argList;
        }
    }

}

3.在Controller层使用注解

  @ApiOperation(value = "测试", notes = "测试")
  @LogInfo()
  @PostMapping(value = CenterApi.EXCUTE_SEND_AI_RESULT)
  public Result excuteSendAIResult(
      @RequestBody @Validated ExcuteSendAIResultDTO excuteSendAIResultDTO) {
    return new Result(this.algorithmFactoryService.excuteSendAIResult(excuteSendAIResultDTO));
  }

更多推荐

Java中Spring使用AOP获取值类型