背景
开发项目的时候,对出入参可以通过idea 的debug模式实现。但是项目一旦发布到线上如果发现有数据存在问题,那么究竟是哪一个环节出现的问题呢。有些情况就会不好分析。或者在系统间互相调用的时候,自己作为被调用方,如何证明调用方的参数正确与否呢。当然是通过日志实现,可以通过如果log日志输出实现。但是更建议写成一个通用的,要么放到基础组件中,也可以放到各自微服务中选择是否启用。
分析
思考一番个人觉得可以采用三种方式实现
- 拦截器
- 过滤器
- aop切面(个人更推荐使用)
拦截器和过滤器实现方式差不多,因此我就只说其中一个和aop切面实现。
filter实现
首先在项目启动的时候注入过滤器bean
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new TimeFilter());
bean.addUrlPatterns("/remote/*");
return bean;
}
}
过滤实现bean
@Component
public class TimeFilter implements Filter {
private Logger log = LoggerFactory.getLogger(TimeFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
ServletRequest requestWrapper = null;
if(HttpMethod.GET.toString().equalsIgnoreCase(httpServletRequest.getMethod())){
log.info("入参参数--get-----" + JSONObject.toJSONString(httpServletRequest.getQueryString()));
}else if(HttpMethod.POST.toString().equalsIgnoreCase(httpServletRequest.getMethod())){
requestWrapper = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) request);
byte[] bodyBytes = StreamUtils.copyToByteArray(requestWrapper.getInputStream());
String body = new String(bodyBytes, httpServletRequest.getCharacterEncoding());
log.info("入参参数--post-----" + body);
}
long start = System.currentTimeMillis();
if (null == requestWrapper) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
log.info(request.getRemoteAddr() + ":" + request.getLocalPort() + httpServletRequest.getRequestURI() + " 耗时 : " + (System.currentTimeMillis() - start) + "ms");
}
@Override
public void destroy() {
}
}
如果用过滤器实现,目前我只想到的在请求中通过流的方式把请求方式拿取出来。但是使用流有一个重要的问题:
流只能读取一次, 流只能读取一次, 流只能读取一次
重要的事情说三遍
因此如果硬要使用, 那必须得处理一下request stream。
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
String bodyString = HttpHelper.getBodyString(request);
body = bodyString.getBytes(Charset.forName("UTF-8"));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
public class HttpHelper {
public static String getBodyString(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(
new InputStreamReader(inputStream, Charset.forName("UTF-8")));
char[] bodyCharBuffer = new char[1024];
int len = 0;
while ((len = reader.read(bodyCharBuffer)) != -1) {
sb.append(new String(bodyCharBuffer, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
}
总结filter实现
这样就可以通过流获取请求参数并且request中还有流。这种实现方法有一个弊端:先把流转成字符串,又把字符串转回流,这样的目的就为了输出请求参数。在我看来有点好钢没有用在刀刃上的感觉。毕竟我们做这个也只是一个辅助性的util,当线上并发大的时候这样来回转换流会拉低不少系统的并发。
切面实现(个人比较推荐)
aop是spring很重要的一个属性,因此在这里可以使用一下。直接上干货有两种实现方式二选一,两种方式大家按需选择
方式一
pom引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Aspect
@Component
@Order(1)
@Slf4j
public class AspectConfig {
@Pointcut("execution(* com.xxx.xxx.xxx.controller..*.*(..))")
private void webLog(){
}
@Before(value="webLog()")
public void before(JoinPoint joinPoint){
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
log.info("请求日志的打印");
log.info("请求地址:{}", Optional.ofNullable(request.getRequestURI().toString()).orElse(null));
log.info("请求方式:{}",request.getMethod());
log.info("请求类方法:{}",joinPoint.getSignature());
log.info("请求类方法参数:{}", JSONObject.toJSONString(JSONObject.toJSONString(filterArgs(joinPoint.getArgs()))));
}
private List<Object> filterArgs(Object[] objects) {
return Arrays.stream(objects).filter(obj -> !(obj instanceof MultipartFile)
&& !(obj instanceof HttpServletResponse)
&& !(obj instanceof HttpServletRequest)).collect(Collectors.toList());
}
}
此种方式在请求之前拦截,但是没法实现整个请求所消耗的时间。在拦截配置中配置需要拦截的包,这种方式适合只拦截参数并且不对请求时长数据做输出的。
方式二
@Aspect
@Component
@Order(1)
@Slf4j
public class AspectConfig {
@Around("@within(org.springframework.web.bind.annotation.RestController)" +
"||@within(org.springframework.stereotype.Controller)")
public Object after(ProceedingJoinPoint joinPoint) throws Throwable{
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
log.info("请求日志的打印");
log.info("请求地址:{}", Optional.ofNullable(request.getRequestURI().toString()).orElse(null));
log.info("请求方式:{}",request.getMethod());
log.info("请求类方法:{}",joinPoint.getSignature());
log.info("请求类方法参数:{}", JSONObject.toJSONString(filterArgs(joinPoint.getArgs())));
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(joinPoint.getArgs());
long end = System.currentTimeMillis();
log.info("执行耗时:{}", end - start);
return result;
}
private List<Object> filterArgs(Object[] objects) {
return Arrays.stream(objects).filter(obj -> !(obj instanceof MultipartFile)
&& !(obj instanceof HttpServletResponse)
&& !(obj instanceof HttpServletRequest)).collect(Collectors.toList());
}
}
这种方式通过对注解切面实现,通用性很强,适合做成项目级别,每个微服务引用基础包后自动启用。这里注意一个点public Object after方法必须要有返回值,不然请求的返回值是null。Object result = joinPoint.proceed(joinPoint.getArgs());这个就是调用请求result即为调用请求后的response。
以上就是我的三种实现在项目中出入参监控的实现方式,如有问题,希望大家评论区留言多多指教!