Spring AOP 实现日志记录功能

Posted hanruikai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring AOP 实现日志记录功能相关的知识,希望对你有一定的参考价值。

最近项目中需要记录日志,跟大家想的一样 ,利用spring aop去实现,之前也参考了一些代码,自己实现了一套设计,供大家参考。

之前看到网上很多是基于切面类Aspect去实现了,在切面类中定义before after around等逻辑以及要拦截等方法。本文利用注解实现了一套可以扩展等日志记录模块。

1. 定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public abstract @interface RequiredLogInterceptor 
    boolean required() default true;

    String targetGenerator() default  "";

    OperateType  operateType() default  OperateType.GET;

requried:注解是否生效

targetGenerator: 每个模块记录等内容不同,入口参数不同,所以需要个性化定制日志等记录内容,每个模块的日志生成有自己定义的generator类,并且重写generateContent方法。

operateType:当前方法是增加,删除,还是修改

public abstract class ContentGerator 

    public   static String SPLIT="/";

    public  static String CONTENT_SPLIT=",";

    public  static String VALUE_SPLIT=":";

    abstract  List<UserOperateLog> generateContent(Object returnValue, Object[] args, OperateType operateType);


2. 定义拦截器

本模块主要是后置通知,主要逻辑如下:

1.拦截方法,判断是否有注解loginterceptor

2. 如果有判断是否执行成功,成功则记录log,失败不记录

3. 获取注解中配置的generator类,利用反射调用generateContent方法,生成个性化日志内容

5.在日志中添加其他公共属性,比如用户id,创建时间等等。所有个性化定制的日志信息都是在generator类中产生。


public class LogAfterInterceptor implements AfterReturningAdvice 

    @Autowired
    private LogService logService;


    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable 
        RequiredLogInterceptor requiredLogInterceptor = AnnotationUtils.findAnnotation(method, RequiredLogInterceptor.class);
        if (requiredLogInterceptor != null) 
            if(returnValue!=null&&returnValue instanceof  Response)
                Response response=(Response)returnValue;
                String code=response.getCode();
                String code200= MegCodeEnums.ResponseCodeEnum.C200.getCode();
                String code201= MegCodeEnums.ResponseCodeEnum.C201.getCode();
                if (!Strings.isNullOrEmpty(code)&&!code.equalsIgnoreCase(code200)&&!code.equalsIgnoreCase(code201))
                    return;
                

            
            String targetGeneratorName=requiredLogInterceptor.targetGenerator();
            OperateType operateType=requiredLogInterceptor.operateType();
            Class targetGeneratorclass=Class.forName("com.puhui.flowplatform.manage.log."+targetGeneratorName);
            Method executeMethod=targetGeneratorclass.getMethod("generateContent",Object.class,Object[].class,OperateType.class);
            ContentGerator targetGeneratorBean=(ContentGerator)SpringContextHolder.getBean(targetGeneratorclass);
            List<UserOperateLog> userOperateLogList=(List<UserOperateLog>)executeMethod.invoke(targetGeneratorBean,returnValue,args,operateType);
            if(CollectionUtils.isNotEmpty(userOperateLogList))
                userOperateLogList.forEach(userOperateLog -> 
                    userOperateLog.setCreateTime(new Date());
                    //token
                    long userId=0L;
                    if (args.length>0&&args[0] instanceof String)
                        userId = CommonUtils.getManageCurUserId(args[0].toString());
                    
                    userOperateLog.setUserId(userId);
                );
                logService.batchInsertLog(userOperateLogList);
            

        
    


3 Generator类

继承统一的ContentGenerator类,便于共享一些常量。根据当前操作类型,生成对应的日志内容就可以了。如果需要新增模块, 先定义自己的日志generator类,然后添加注解到对应模块就可以。

@Service
public class ContentGeneratorForRoleMgt extends   ContentGerator 


    @Autowired
    private MenuService menuService;

    private  String generateMenus(VoRole voRole)
        List<Menus> menusList=voRole.getMenusList();
        StringBuffer stringBuffer=new StringBuffer();
        if (CollectionUtils.isNotEmpty(menusList))
            menusList.forEach(menus -> 
                Long menuId=menus.getId();
                Menus menusTemp=menuService.queryMenuByMenuId(menuId);
                stringBuffer.append(menusTemp.getDisplayTitle()+CONTENT_SPLIT);
            );
            stringBuffer.deleteCharAt(stringBuffer.length() - 1);
        
        return  stringBuffer.toString();

    
    @Override
    public    List<UserOperateLog> generateContent(Object returnValue, Object[] args, OperateType operateType) 
        
            List<UserOperateLog> userOperateLogList=new ArrayList<>();
            UserOperateLog userOperateLog=new UserOperateLog();
            if (operateType==OperateType.ADD||operateType==OperateType.UPDATE)
                VoRole voRole=(VoRole)args[1];
                String menus=generateMenus(voRole);
                userOperateLog.setOperateContent("角色名称"+VALUE_SPLIT+voRole.getDisplayName()+SPLIT+"权限"+VALUE_SPLIT+menus);
                userOperateLog.setOperateType(operateType==OperateType.ADD?LogOperateTypeEnum.ADD_ROLE.getCode():LogOperateTypeEnum.UPDATE_ROLE.getCode());
            
            if (operateType==OperateType.DELETE)
                if(returnValue!=null)
                    Response response=(Response) returnValue;
                    String roleName=response.getData().toString();
                    userOperateLog.setOperateContent(roleName);
                    userOperateLog.setOperateType(LogOperateTypeEnum.DELETE_ROLE.getCode());
                

            
            userOperateLogList.add(userOperateLog);
            return  userOperateLogList;

        

    

4. 注解应用

 @PutMapping(value = "roles/roleId")
    @RequiredLogInterceptor(targetGenerator = "ContentGeneratorForRoleMgt",operateType= OperateType.UPDATE)
    @ApiOperation(value = "修改角色", httpMethod = "PUT", response = Response.class, notes = "修改角色")
    public Response<Object> updateRole(@RequestHeader String token,@RequestBody VoRole voRole, @PathVariable("roleId") String roleId) 
        LOGGER.info("updateRole入参:", JSONObject.toJSONString(voRole));

5. Configuration

public class SpringMvcConfig extends WebMvcConfigurerAdapter 

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) 
        super.configureMessageConverters(converters);
        // 初始化转换器
        FastJsonHttpMessageConverter fastConvert = new FastJsonHttpMessageConverter();
        // 初始化一个转换器配置
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
        // 将配置设置给转换器并添加到HttpMessageConverter转换器列表中
        fastConvert.setFastJsonConfig(fastJsonConfig);
        converters.add(fastConvert);
    

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) 
        registry.addResourceHandler("/swagger-ui.html").addResourceLocations(
                ResourceUtils.CLASSPATH_URL_PREFIX + "/META-INF/resources/");
        registry.addResourceHandler("/static/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/",
                ResourceUtils.CLASSPATH_URL_PREFIX + "/dist/static/");
        registry.addResourceHandler("/page/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/dist/");
        super.addResourceHandlers(registry);
    

    @Bean
    public ViewResolver viewResolver() 
        FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
        resolver.setCache(true);
        resolver.setPrefix(ResourceUtils.CLASSPATH_URL_PREFIX + "templates/");
        resolver.setSuffix(".ftl");
        resolver.setContentType("text/html; charset=UTF-8");
        return resolver;
    

    // 创建Advice或Advisor
    @Bean
    public BeforeAdvice beforeControllerInterceptor() 
        return new BeforeControllerInterceptor();
    
    @Bean
    public AfterAdvice logAfterInterceptor() 
        return new LogAfterInterceptor();
    
    // 创建Advice或Advisor
    @Bean
    public BeforeAdvice logBeforeInterceptor() 
        return new LogBeforeInterceptor();
    

    // 使用BeanNameAutoProxyCreator来创建代理

    @Bean
    public BeanNameAutoProxyCreator beanAutoProxyCreator() 
        BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
        beanNameAutoProxyCreator.setProxyTargetClass(true); // 设置要创建代理的那些Bean的名字
        beanNameAutoProxyCreator.setBeanNames("*Controller"); //
        // 设置拦截链名字(这些拦截器是有先后顺序的)
        beanNameAutoProxyCreator.setInterceptorNames("logAfterInterceptor");
        return beanNameAutoProxyCreator;
    

    @Bean
    public BeanNameAutoProxyCreator beanBeforeAutoProxyCreator() 
        BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
        beanNameAutoProxyCreator.setProxyTargetClass(true);
        // 设置要创建代理的那些Bean的名字
        beanNameAutoProxyCreator.setBeanNames("*Controller");
        // 设置拦截链名字(这些拦截器是有先后顺序的)
        beanNameAutoProxyCreator.setInterceptorNames("beforeControllerInterceptor");
        beanNameAutoProxyCreator.setInterceptorNames("logBeforeInterceptor");

6.写在结尾

本来实现都代码版本中,所有都日志生成代码都在后置拦截器中,并且根据当前执行都方法都classname和methodname去判断当前都方法,出现很多if 判断,且method name都不一样,有的是addXXX,有的是createXXX,显然设计不合理。后来重新进行了设计,有什么不足,希望大家可以指出。


7.非自定义注解实现方式

package com.puhui.flowplatform.manage.interceptor;

import com.puhui.flowplatform.common.model.manage.UserOperateLog;
import com.puhui.flowplatform.manage.service.LogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;

import java.lang.reflect.Method;
import java.util.Date;

@Aspect
public class LogAspect 

	public Long id=null;

	@Autowired
	LogService logService;

	/**
	 * 添加业务逻辑方法切入点
	 */
	@Pointcut("execution(*  com.puhui.flowplatform.manage.service.*.add*(..))")
	public void insertCell() 
	

	/**
	 * 修改业务逻辑方法切入点
	 */
	@Pointcut("execution(*  com.puhui.flowplatform.manage.service.*.update*(..))")
	public void updateCell() 
	

	/**
	 * 删除业务逻辑方法切入点
	 */
	@Pointcut("execution(*  com.puhui.flowplatform.manage.service.*.delete*(..))")
	public void deleteCell() 
	


	/**
	 * 添加操作日志(后置通知)
	 *
	 * @param joinPoint
	 * @param object
	 */
	@AfterReturning(value = "insertCell()", argNames = "object", returning = "object")
	public void insertLog(JoinPoint joinPoint, Object object) throws Throwable 
		// Admin admin=(Admin)
		// request.getSession().getAttribute("businessAdmin");
		// 判断参数
		if (joinPoint.getArgs() == null) // 没有参数
			return;
		
		// 获取方法名
		String methodName = joinPoint.getSignature().getName();
		// 获取操作内容
		String opContent = optionContent(joinPoint.getArgs(), methodName);

		UserOperateLog log = new UserOperateLog();
		log.setOperateContent(opContent);
		log.setUserId(id);;
		log.setOperateType(1);//enum 增加
		log.setCreateTime(new Date());
		logService.insertLog(log);
	

	/**
	 * 管理员修改操作日志(后置通知)
	 *
	 * @param joinPoint
	 * @param object
	 * @throws Throwable
	 */
	@AfterReturning(value = "updateCell()", argNames = "object", returning = "object")
	public void updateLog(JoinPoint joinPoint, Object object) throws Throwable 
		// Admin admin=(Admin)
		// request.getSession().getAttribute("businessAdmin");

		// 判断参数
		if (joinPoint.getArgs() == null) // 没有参数
			return;
		
		// 获取方法名
		String methodName = joinPoint.getSignature().getName();
		// 获取操作内容
		String opContent = optionContent(joinPoint.getArgs(), methodName);

		// 创建日志对象
		UserOperateLog log = new UserOperateLog();
		log.setOperateContent(opContent);
		log.setUserId(id);;
		log.setOperateType(2);//enum 修改
		log.setCreateTime(new Date());
		logService.insertLog(log);
	

	/**
	 * 删除操作
	 *
	 * @param joinPoint
	 * @param object
	 */
	@AfterReturning(value = "deleteCell()", argNames = "object", returning = "object")
	public void deleteLog(JoinPoint joinPoint, Object object) throws Throwable 
		// Admin admin=(Admin)
		// request.getSession().getAttribute("businessAdmin");
		// 判断参数
		if (joinPoint.getArgs() == null) // 没有参数
			return;
		
		// 获取方法名
		String methodName = joinPoint.getSignature().getName();

		StringBuffer rs = new StringBuffer();
		rs.append(methodName);
		String className = null;
		for (Object info : joinPoint.getArgs()) 
			// 获取对象类型
			className = info.getClass().getName();
			className = className.substring(className.lastIndexOf(".") + 1);
			rs.append("[参数,类型:" + className + ",值:(id:"
					+ joinPoint.getArgs()[0] + ")");
		

		// 创建日志对象
		UserOperateLog log = new UserOperateLog();
		log.setOperateContent(rs.toString());
		log.setUserId(id);;
		log.setOperateType(3);//删除
		log.setCreateTime(new Date());
		logService.insertLog(log);
	

	/**
	 * 使用Java反射来获取被拦截方法(insert、update)的参数值, 将参数值拼接为操作内容
	 *
	 * @param args
	 * @param mName
	 * @return
	 */
	public String optionContent(Object[] args, String mName) 
		if (args == null) 
			return null;
		
		StringBuffer rs = new StringBuffer();
		rs.append(mName);
		String className = null;
		int index = 1;
		// 遍历参数对象
		for (Object info : args) 
			// 获取对象类型
			className = info.getClass().getName();
			className = className.substring(className.lastIndexOf(".") + 1);
			rs.append("[参数" + index + ",类型:" + className + ",值:");
			// 获取对象的所有方法
			Method[] methods = info.getClass().getDeclaredMethods();
			// 遍历方法,判断get方法
			for (Method method : methods) 
				String methodName = method.getName();
				// 判断是不是get方法
				if (methodName.indexOf("get") == -1) // 不是get方法
					continue;// 不处理
				
				Object rsValue = null;
				try 
					// 调用get方法,获取返回值
					rsValue = method.invoke(info);
				 catch (Exception e) 
					continue;
				
				// 将值加入内容中
				rs.append("(" + methodName + ":" + rsValue + ")");
			
			rs.append("]");
			index++;
		
		return rs.toString();
	







以上是关于Spring AOP 实现日志记录功能的主要内容,如果未能解决你的问题,请参考以下文章

SSH 下做一个spring AOP的 操作日志记录功能

我使用Spring AOP实现了用户操作日志功能

我使用Spring AOP实现了用户操作日志功能

从头认识Spring-3.8 简单的AOP日志实现(注解版)-扩展增加检查订单功能,以便记录并检测输入的参数

从头认识Spring-3.4 简单的AOP日志实现-扩展添加检查订单功能,以便记录并检測输入的參数

从头认识Spring-3.8 简单的AOP日志实现(注解版)-扩展添加检查订单功能,以便记录并检測输入的參数