欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring boot 自定义注解 记录service层日志 AOP

程序员文章站 2022-05-02 11:33:31
...

现在项目中要实现一个小功能,就是在当前系统中调用其他服务的接口,如果只是用日志记录的话查找问题就会很麻烦,所以现在要实现的就是用AOP来处理调用某一个方法时记录调用的详细信息,并保存到数据库中。

1.引入AOP依赖

<!--spring切面aop依赖-->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

在application.properties文件里加这样一条配置
spring.aop.auto=true //这个配置我的例子中没有加 也正常运行

application.yml文件这样配置
spring:
  aop:
    auto: true

2.创建记录的日志表与数据库对应


import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;

public class TInvokCoreLog implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 记录日志ID
     */
    private Long invokCoreLogId;

    /**
     * 调用参数,以;分隔
     */
    private String invokParam;

    /**
     * 调用方法
     */
    private String invokMethod;

    /**
     * 调用返回值或异常信息
     */
    private String invokReturn;

    /**
     * 调用路径
     */
    private String invokProturl;

    /**
     * 开始时间
     */
    private Date startTime;

    /**
     * 结束时间
     */
    private Date endTime;

    /**
     * 调用耗时 单位秒
     */
    private BigDecimal invokCost;

    /**
     * 调用状态 Y:成功 N:失败
     */
    private String invokStatus;
    }

3.创建自定义注解


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(
{ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokCoreLog
{
	String value() default "";
}

4.上述注解的实现类,需要AOP处理的逻辑


import java.math.BigDecimal;
import java.util.Date;

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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSON;
import com.micro.core.enums.CoreConstant;
import com.micro.core.exceptions.ValidateException;
import com.micro.serv.training.dao.mybatis.TInvokCoreLog;
import com.micro.serv.training.enums.YesOrNoType;
import com.micro.serv.training.service.CommonService;

@Aspect
@Component
public class InvokCoreLogAspect
{
	private static final Logger log = LoggerFactory.getLogger(this.getClass());
	@Autowired
	private CommonService commonService;

    //定义切点 @Pointcut
    //在注解的位置切入代码  括号中为上述自定义注解的包路经
	@Pointcut("@annotation(com.service.aop.InvokCoreLog)")
	public void serviceAspect()
	{
	}

	@Around("serviceAspect()")
	public Object doServiceAround(ProceedingJoinPoint joinPoint) throws Throwable
	{
		Object returnObj = null;
		Date startDate = new Date();
		try
		{
			returnObj = joinPoint.proceed();
			this.saveTInvokCoreLog(joinPoint, returnObj, startDate, YesOrNoType.Y.getCode());
			return returnObj;
		}
		catch (Throwable e)
		{
			returnObj = e.getMessage();
			this.saveTInvokCoreLog(joinPoint, returnObj, startDate, YesOrNoType.N.getCode());
			return e;
		}
	}

	private void saveTInvokCoreLog(ProceedingJoinPoint joinPoint, Object returnObj, Date startDate, String invokStatus)
	{
		Date endDate = new Date();
		TInvokCoreLog tInvokCoreLog = new TInvokCoreLog();
		tInvokCoreLog.setInvokCoreLogId(commonService.generatePK());
		tInvokCoreLog.setInvokParam(this.getParam(joinPoint));
		tInvokCoreLog.setInvokMethod(joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName());
		tInvokCoreLog.setInvokReturn(returnObj.getClass().getName() + ":" + JSON.toJSONString(returnObj));
		tInvokCoreLog.setInvokProturl(String.valueOf(joinPoint.getArgs()[1]));
		tInvokCoreLog.setStartTime(startDate);
		tInvokCoreLog.setEndTime(endDate);
		tInvokCoreLog.setInvokCost(new BigDecimal((startDate.getTime() - endDate.getTime()) / 1000 % 60));
		tInvokCoreLog.setInvokStatus(invokStatus);
		//保存数据库,这里打印出来查看
		log.info(JSON.toJSONString(tInvokCoreLog));
	}
	

    /**
     * 获取方法参数
     *
     * @param joinPoint
     * @return
     */
    private String getParam(ProceedingJoinPoint joinPoint)
    {
        StringBuilder params = new StringBuilder();
        if (joinPoint.getArgs() != null && joinPoint.getArgs().length > 0)
        {
            for (int i = 0; i < joinPoint.getArgs().length; i++)
            {
                params.append(joinPoint.getArgs()[i].getClass()).append(":").append(JSON.toJSONString(joinPoint.getArgs()[i])).append(";");
            }
        }
        return params.toString();
    }
	
}

5.在方法上添加注解

注意点: 方法前缀一定是 public 才会生效


    @Override
    @InvokCoreLog
    public UserReq grantDivide(String orderId, UserReq userReq) throws Exception
    {
        logger.info("====================进入UserServiceImpl,执行业务逻辑====================");
        this.getCode(orderId, userReq);
        logger.info("====================结束UserServiceImpl====================");
        return userReq;
    }

至此,当Controller层调用grantDivide方法时会先通过InvokCoreLog的实现类InvokCoreLogAspect,当执行InvokCoreLogAspect中的joinPoint.proceed();时,才会进入到service层,我们就记录了一次地宫用grantDivide方法的详细信息:包括调用的时间,调用耗时,传入参数,返回值等等信息

但是在实际业务中我们需要调用的往往不是直接从Controller调用,而是经过了很多方法才最终调用了我们需要记录日志的方法;我的当前的场景就是,Controller调Service,Service内部经过了一个方法的处理才最终才会到记录的方法(同样是在Service中),这个时候直接加注解是不生效的,

实际业务代码(模拟)


    @Override
    public UserReq grantDivide(String orderId, UserReq userReq) throws Exception
    {
        this.getCode(orderId, userReq);
        return userReq;
    }

    @InvokCoreLog
    public UserReq getCode(String orderId, UserReq userReq)
    {
        logger.info("====================进入UserServiceImpl,执行业务逻辑====================");
        logger.info("====================结束UserServiceImpl====================");
        return userReq;
    }

调用UserService中的grantDivide()方法时,Spring的动态代理帮我们动态生成了一个代理的对象,暂且叫他$UserService。所以调用hello()方法实际上是代理对象$UserService调用的。但是在grantDivide()方法内调用同一个类的另外一个注解方法getCode()时,实际上是通过this.getCode()执行的, this 指的是UserService 对象,并不是$UserService代理对象调用的,没有走代理。所以注解失效

转自Spring之AOP注解失效原因和解决方法

解决方法 通过实现ApplicationContext获取代理对象。新建获取代理对象的工具类SpringUtil


import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringUtil implements ApplicationContextAware
{

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
    {
        if (SpringUtil.applicationContext == null)
        {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    //获取applicationContext
    public static ApplicationContext getApplicationContext()
    {
        return applicationContext;
    }

    //通过name获取 Bean.
    public static Object getBean(String name)
    {
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz)
    {
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz)
    {
        return getApplicationContext().getBean(name, clazz);
    }

}

将实际业务代码(模拟)修改为:将this改为SpringUtil.getBean(this.getClass())即可


    @Override
    public UserReq grantDivide(String orderId, UserReq userReq) throws Exception
    {
        SpringUtil.getBean(this.getClass()).getCode(orderId, userReq);
        return userReq;
    }

    @InvokCoreLog
    public UserReq getCode(String orderId, UserReq userReq)
    {
        logger.info("====================进入UserServiceImpl,执行业务逻辑====================");
        logger.info("====================结束UserServiceImpl====================");
        return userReq;
    }

重新启动项目,会发现调用getCode方法时进入了AOP的InvokCoreLogAspect中

参考文件

java/springboot自定义注解实现AOP
Spring Aop实例@Aspect、@Before、@[email protected] 注解方式配置
springboot—spring aop 实现系统操作日志记录存储到数据库
[email protected] 失效原因及解决办法
Spring之AOP注解失效原因和解决方法