自己动手实现一个控制层出入参日志切面
导言:在Spring MVC的项目中,记录控制层的出入参数是很常见的需求,下面我将记录下这一实现过程.并实现日志存入redis.可开关接口记录日志.
-
利用Controller的增强注解-------@RestControllerAdvice,还可以利用该注解实现全局异常捕获.详细用法请百度.
-
首先实现两个接口,分别是RequestBodyAdvice和ResponseBodyAdvice.
-
记录入参日志:
-
package com.hdstcloud.hdst_admin.common.aspect; import com.alibaba.fastjson.JSON; import com.hdstcloud.hdst_admin.common.utils.RedisUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; /** * @author js */ @Slf4j @RestControllerAdvice public class ResponseAopLogComponent implements ResponseBodyAdvice { @Autowired private LogAspectConfig logAspectConfig; @Autowired private RedisUtil redisUtil; @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String requestUri = requestAttributes.getRequest().getRequestURI(); String sessionId = requestAttributes.getRequest().getSession().getId(); //如果在忽略的接口中,不要记录返回日志 if (!Arrays.stream(logAspectConfig.getIgnoreUri()).anyMatch(it -> it.equals(request.getURI().getPath()))) { log.debug("请求的方法路径是:{}\n返回的结果是:{}\n请求的ip是:{}\nsessionId是:{}", requestUri, JSON.toJSONString(body), request.getRemoteAddress(), sessionId); if (logAspectConfig.getRedisOff()) { //当前时间转换成固定格式,会和日志打印时间有一点点差距,可以忽略 String time = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS").format(LocalDateTime.now()); //接口路径加上时间做主键,存储对象 IoLog ioLog = new IoLog(); ioLog.setRequestUri(requestUri); ioLog.setIp(requestAttributes.getRequest().getRemoteAddr()); ioLog.setDataTime(time); ioLog.setSessionId(sessionId); ioLog.setResponseBody(JSON.toJSONString(body)); redisUtil.set(requestUri + time, ioLog); } } return body; } }
-
记录返回日志:
-
package com.hdstcloud.hdst_admin.common.aspect; import com.alibaba.fastjson.JSON; import com.hdstcloud.hdst_admin.common.utils.RedisUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; /** * @author js */ @Slf4j @RestControllerAdvice public class ResponseAopLogComponent implements ResponseBodyAdvice { @Autowired private LogAspectConfig logAspectConfig; @Autowired private RedisUtil redisUtil; @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String requestUri = requestAttributes.getRequest().getRequestURI(); String sessionId = requestAttributes.getRequest().getSession().getId(); //如果在忽略的接口中,不要记录返回日志 if (!Arrays.stream(logAspectConfig.getIgnoreUri()).anyMatch(it -> it.equals(request.getURI().getPath()))) { log.debug("请求的方法路径是:{}\n返回的结果是:{}\n请求的ip是:{}\nsessionId是:{}", requestUri, JSON.toJSONString(body), request.getRemoteAddress(), sessionId); if (logAspectConfig.getRedisOff()) { //当前时间转换成固定格式,会和日志打印时间有一点点差距,可以忽略 String time = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS").format(LocalDateTime.now()); //接口路径加上时间做主键,存储对象 IoLog ioLog = new IoLog(); ioLog.setRequestUri(requestUri); ioLog.setIp(requestAttributes.getRequest().getRemoteAddr()); ioLog.setDataTime(time); ioLog.setSessionId(sessionId); ioLog.setResponseBody(JSON.toJSONString(body)); redisUtil.set(requestUri + time, ioLog); } } return body; } }
-
日志参数的实体类如下:
-
package com.hdstcloud.hdst_admin.common.aspect; import lombok.Data; /** * @author :js * @date :Created in 2021-06-08 17:02 * @description: io日志对象 * @version: 1.0 */ @Data public class IoLog { /** * 请求路径 */ private String requestUri; private String sessionId; /** * 请求参数 */ private String param; /** * ip地址 */ private String ip; /** * 返回的数据 */ private String responseBody; /** * 数据时间 */ private String dataTime; }
-
读取配置文件的类,该类主要是读取 resource/config/logAspect.properties 下的参数的值,参数分为: 不要记录的接口以及是否开启redis存储:
-
LogAspectConfig.java如下:
package com.hdstcloud.hdst_admin.common.aspect; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; /** * @author :js * @date :Created in 2021-06-09 9:09 * @description: * @version: */ @Data @ConfigurationProperties(prefix = "log", ignoreUnknownFields = false) @PropertySource(value = "classpath:config/logAspect.properties") @Component public class LogAspectConfig { /** * 忽略记录的接口 */ private String[] ignoreUri; /** * 是否存入redis */ private Boolean redisOff; }
-
logAspect.properties如下:
#要被忽略的接口 log.ignoreUri[0]=/admin/position/create #开启redis存储 log.redisOff=true
-
当然我们也要把springboot整合redis的配置类和工具类准备好:
-
第一步先导入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>你自己的版本</version> </dependency>
-
redis配置
package com.hdstcloud.hdst_admin.config; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @Configuration @EnableCaching public class RedisConfig { @Bean public RedisSerializer fastJson2JsonRedisSerialize(){ return new FastJson2JsonRedisSerialize<>(Object.class); } @Bean public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory, RedisSerializer fastJson2JsonRedisSerialize){ RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); //设置Key的序列化采用StringRedisSerializer redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); //设置值的序列化采用FastJsonRedisSerializer redisTemplate.setValueSerializer(fastJson2JsonRedisSerialize); redisTemplate.setHashValueSerializer(fastJson2JsonRedisSerialize); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // 生成一个默认配置,通过config对象即可对缓存进行自定义配置 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // 设置缓存的默认过期时间,也是使用Duration设置 config = config.entryTtl(Duration.ofMinutes(5)) // 设置 key为string序列化 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 设置value为fastJson序列化 // .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJson2JsonRedisSerialize())) // 不缓存空值 .disableCachingNullValues(); // 使用自定义的缓存配置初始化一个cacheManager return RedisCacheManager .builder(redisConnectionFactory) .cacheDefaults(config) .transactionAware() .build(); } }
-
fastjson序列化配置
package com.hdstcloud.hdst_admin.config; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** * @author :js * @date :Created in 2020-10-29 11:15 * @description: * @version: 1.0 */ public class FastJson2JsonRedisSerialize<T> implements RedisSerializer<T> { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private final Class<T> clazz; static { // ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //如果遇到反序列化autoType is not support错误,请添加并修改一下包名到bean文件路径 // ParserConfig.getGlobalInstance().addAccept("com.example.redisdemo.domain"); } public FastJson2JsonRedisSerialize(Class clazz){ super(); this.clazz = clazz; } /** * 序列化 * @param t * @return * @throws SerializationException */ @Override public byte[] serialize(T t) throws SerializationException { if (null == t){ return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } /** * 反序列化 * @param bytes * @return * @throws SerializationException */ @Override public T deserialize(byte[] bytes) throws SerializationException { if (null == bytes || bytes.length <= 0){ return null; } String str = new String(bytes,DEFAULT_CHARSET); return (T) JSON.parseObject(str,clazz); } }
-
redis工具类
package com.hdstcloud.hdst_admin.common.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @Component @SuppressWarnings("all") public final class RedisUtil { @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public boolean del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { return redisTemplate.delete(key[0]); } else { return key.length == redisTemplate.delete(CollectionUtils.arrayToList(key)); } } return false; } /** * 普通缓存获取 * * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * * @param key 键 * @param delta 要增加几(大于0) */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @param key 键 * @param delta 要减少几(小于0) */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } // ================================Map================================= /** * HashGet * * @param key 键 不能为null * @param item 项 不能为null */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * * @param key 键 * @return 对应的多个键值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并设置时间 * * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) * @return true成功 false失败 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除hash表中的值 * * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * * @param key 键 * @param item 项 * @param by 要增加几(大于0) */ public double hincr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @param key 键 * @param item 项 * @param by 要减少记(小于0) */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } // ============================set============================= /** * 根据key获取Set中的所有值 * * @param key 键 */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据value从一个set中查询,是否存在 * * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将数据放入set缓存 * * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 将set数据放入缓存 * * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) { expire(key, time); } return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取set缓存的长度 * * @param key 键 */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值为value的 * * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } // ===============================list================================= /** * 获取list缓存的内容 * * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取list缓存的长度 * * @param key 键 */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通过索引 获取list中的值 * * @param key 键 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据索引修改list中的某条数据 * * @param key 键 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N个值为value * * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } }
-
在yml文件中配置你的redis
spring: application: name: admin redis: host: 127.0.0.1 port: 6379
-
-
当然如果你不要保存日志到redis中,可以不添加上一步的redis相关代码,代码的运行效果如下:
1. 请求如下:
- 请求日志:
-
返回日志:
-
redis存储的数据为:
-
上述的方法比较复杂,由于想把上面代码做成jar包,给其他人使用时,使用者可以自己编写 logAspect.properties 文件来适应自己的业务需求,这样的话在jar包中的代码想读到使用者工程的配置文件就做成这个样子了.下面提供一个简单的日志切面
-
package com.hdstcloud.device.common.aspect; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Arrays; /** * @author :js * @date :Created in 2020-11-26 13:18 * @description: Controller层日志切面 * @version: 1.0 */ @Aspect @Component @Slf4j public class LogAspect { /** * 切面的范围是com.hdstcloud.device.controller.impl包及其子包下所有类的所有方法 */ @Pointcut("execution(* com.hdstcloud.device.controller.impl..*.*(..))") public void log() { } @Before("log()") public void doBefore(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes != null; HttpServletRequest request = attributes.getRequest(); String url = request.getRequestURL().toString(); String ip = request.getRemoteAddr(); String classMethod = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); RequestLog requestLog = new RequestLog(url, ip, classMethod, args); log.info("Request : {}", requestLog); } @After("log()") public void doAfter() { // log.info("--------doAfter--------"); } @AfterReturning(returning = "result", pointcut = "log()") public void doAfterReturn(Object result) { log.info("Result : {}", result); } private static class RequestLog { private final String url; private final String ip; private final String classMethod; private final Object[] args; public RequestLog(String url, String ip, String classMethod, Object[] args) { this.url = url; this.ip = ip; this.classMethod = classMethod; this.args = args; } @Override public String toString() { return "{" + "url='" + url + '\'' + ", ip='" + ip + '\'' + ", classMethod='" + classMethod + '\'' + ", args=" + Arrays.toString(args) + '}'; } } }
-