需求说明:近期,公司大佬提出将用户操作系统的信息记录写入es,方便后续追踪操作,排查问题。接受任务,开始开发。
首先的思路,肯定是使用拦截器拦截请求,获取请求参数,返回结果等常规信息。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 仅拦截对方法的访问,对于其他静态资源不进行拦截
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
System.out.println("进入参数注解拦截");
Map<String, String[]> parameterMap = request.getParameterMap();
System.out.println(parameterMap);
}
return super.preHandle(request, response, handler);
}
查阅资料发现,拦截器不能拿到请求的返回值。
之后改变思路,尝试使用过滤器,看看能不能拿到返回结果。
此时,心里又有了疑惑,拦截器和过滤器,二者的功能似乎差别不大,区别是什么?
网上搜了下资料,这张图片说的比较清晰。
java请求过程:https://blog.csdn.net/Cxf007200/article/details/120933034
过滤器(Filter)
过滤器,是在java web中将你传入的request、response提前过滤掉一些信息,或者提前设置一些参数。然后再传入Servlet或Struts2的 action进行业务逻辑处理。比如过滤掉非法url(不是login.do的地址请求,如果用户没有登陆都过滤掉),或者在传入Servlet或Struts2的action前统一设置字符集,或者去除掉一些非法字符。
拦截器(Interceptor)
拦截器,是面向切面编程(AOP,Aspect Oriented Program)的。就是在你的Service或者一个方法前调用一个方法,或者在方法后调用一个方法。比如动态代理就是拦截器的简单实现,在你调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在你调用方法后打印出字符串,甚至在你抛出异常的时候做业务逻辑的操作。
出自:拦截器与过滤器的区别_Tang-CSDN博客_拦截器与过滤器的区别
过滤器中,需要对response使用代理类,才能拿到响应的返回值。
/**
* @desc 返回值输出代理类,只能配合过滤器使用 parameterFilter
* @param
* @author xfchen12
* @date 2021/6/21 14:59
*/
@Component
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream bytes = new ByteArrayOutputStream();
private HttpServletResponse response;
private PrintWriter pwrite;
public ResponseWrapper(HttpServletResponse response) {
super(response);
this.response = response;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new MyServletOutputStream(bytes); // 将数据写到 byte 中
}
/**
* 重写父类的 getWriter() 方法,将响应数据缓存在 PrintWriter 中
*/
@Override
public PrintWriter getWriter() throws IOException {
try{
pwrite = new PrintWriter(new OutputStreamWriter(bytes, "utf-8"));
} catch(UnsupportedEncodingException e) {
e.printStackTrace();
}
return pwrite;
}
/**
* 获取缓存在 PrintWriter 中的响应数据
* @return
*/
public byte[] getBytes() {
if(null != pwrite) {
pwrite.close();
return bytes.toByteArray();
}
if(null != bytes) {
try {
bytes.flush();
} catch(IOException e) {
e.printStackTrace();
}
}
return bytes.toByteArray();
}
class MyServletOutputStream extends ServletOutputStream {
private ByteArrayOutputStream ostream ;
public MyServletOutputStream(ByteArrayOutputStream ostream) {
this.ostream = ostream;
}
@Override
public void write(int b) throws IOException {
ostream.write(b); // 将数据写到 stream 中
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
}
@SneakyThrows
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest r = (HttpServletRequest) servletRequest;
System.out.println(r.getMethod());
Map<String, String[]> parameterMap = servletRequest.getParameterMap();
System.out.println(parameterMap);
Iterator<String> iter = parameterMap.keySet().iterator();
StringBuffer stringBuffer = new StringBuffer();
while (iter.hasNext()) {
String key = iter.next();
String[] strings = parameterMap.get(key);
String string = strings[0];
stringBuffer.append("," + key + ":" + string);
}
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletRequest req2 = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
ResponseWrapper mResp = new ResponseWrapper(resp); // 包装响应对象 resp 并缓存响应数据
filterChain.doFilter(req, mResp);
byte[] bytes = mResp.getBytes(); // 获取响应数据
String s = String.valueOf(stringBuffer);
if (s.startsWith(",")) {
s = s.substring(1);
}
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String d = sdf.format(date);
date = sdf.parse(d);
StringBuffer requestURL = req.getRequestURL();
String paramter = "date:" + date + "; " +
"request:" + requestURL + "; " +
"paramters:" + s + "; " +
"response:" + new String(bytes, "utf-8");
System.out.println(paramter);
EsParam esParam = new EsParam("","",String.valueOf(new Date()), "111",String.valueOf(requestURL),"", s, new String(bytes, "utf-8"),"");
EsClientUtil.createDoc(esParam);
JSONObject json = GetRequestJsonUtils.getRequestJsonObject(req2);
System.out.println(json);
}
拿到入参出参后,我们在测试环境下载es、kibana 包,并解压。
ES: https://www.elastic.co/cn/downloads/elasticsearch
Kibana: https://www.elastic.co/cn/downloads/kibana
Logstash: https://www.elastic.co/cn/downloads/logstash
Filebeat: https://www.elastic.co/cn/downloads/beats/filebeat
进入es解压后目录。
修改config下yml配置文件
cluster.name: MyES #集群名称
node.name: node01 #本节点名称
network.host: 0.0.0.0 #所有机器都可监听
http.port: 9200 #默认端口
cluster.initial_master_nodes: ["node01"] #主节点名称,与上面配置的保持一致
启动es目录下 bin/es ,如果启动报错 jvm报错,修改config下jvm.options,配置启动内存大小
-Xms512m
-Xmx512m
启动后访问 http://ip:9200 端口,出现如下版本信息则启动成功。
解压kibana,进入config目录,修改yml配置文件
server.port: 5601
server.host: "172.31.131.12"
elasticsearch.hosts: ["http://172.31.131.12:9200"] #ES所在的ip
elasticsearch.username: "elk"
elasticsearch.password: "1qaz@WSX_1qaz@wsx"
启动 kibana/bin/kibana,访问5601端口
es和kibana启动完毕。
写入es工具类,根据esip和端口修改连接配置。
public static RestHighLevelClient getClient() {
//创建HttpHost
HttpHost host = new HttpHost(HOST, PORT);
// 创建RestClientBuilder
RestClientBuilder builder = RestClient.builder(host);
// 创建RestHighLevelClient
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
public static void createDoc(EsParam esTestVo) throws IOException {
//准备一个json数据
long time = System.currentTimeMillis();
esTestVo.setId(String.valueOf(time));
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(esTestVo);
//准备request对象
IndexRequest request = new IndexRequest(esTestVo.getIndex(), "string", String.valueOf(time));
request.source(json, XContentType.JSON);
//通过client对象连接es
RestHighLevelClient client = getClient();
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
//输出
System.out.println(request.toString());
System.out.println(response.getResult());
}
这时考虑到一个问题,请求信息写入es,如果采用同步的方式,会造成接口响应延迟,因为考虑采用异步写入的方式。因过滤器对异步支持不太友好,此时再次转变思路,使用注解定义切点,在切面中可拿到请求值、响应值,对异步写入请求信息到es处理也更加方便。
自定义注解
/**
* @desc es写入参数切面注解
* @param
* @author xfchen12
* @date 2021/7/1 15:46
*/
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented
public @interface EsLog {
String operModul() default ""; // 操作模块
String operDesc() default ""; // 操作说明
String index() default ""; // es索引目录
}
切面
/**
* @desc 正常返回通知,连接点正常执行完成后执行,如果连接点抛出异常,则不会执行
* @param joinPoint 切入点
* @param result 返回结果
* 需要异步处理,不阻塞主线程
* @author xfchen12
* @date 2021/7/2 11:20
*/
@AfterReturning(value = "operLogPoinCut()", returning = "result")
public void saveOperLog(JoinPoint joinPoint, Object result) {
try {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes
.resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 需要异步处理,不阻塞主线程
esClientUtil.logEs(joinPoint, result, request);
} catch (Exception e) {
e.printStackTrace();
}
}
其中切面中的util.loges方法采用异步调用,方法加上@Async注解,启动类加上开启异步的注解@EnableAsync(proxyTargetClass = true)
执行切点方法后,控制台打印信息,显示写入es成功。
2021-07-02 17:32:48.451 [I/O dispatcher 25] WARN org.elasticsearch.client.RestClient -request [PUT http://172.31.131.12:9200/pomp-sap/string/1625218367116?timeout=1m] returned 2 warnings: [299 Elasticsearch-7.13.2-4d960a0733be83dd2543ca018aa4ddc42e956800 "Elasticsearch built-in security features are not enabled. Without authentication, your cluster could be accessible to anyone. See https://www.elastic.co/guide/en/elasticsearch/reference/7.13/security-minimal-setup.html to enable security."],[299 Elasticsearch-7.13.2-4d960a0733be83dd2543ca018aa4ddc42e956800 "[types removal] Specifying types in document index requests is deprecated, use the typeless endpoints instead (/{index}/_doc/{id}, /{index}/_doc, or /{index}/_create/{id})."]
index {[pomp-sap][string][1625218367116], source[n/a, actual length: [20.3kb], max length: 2kb]}
CREATED
这时我们进入kibana界面,选择discover页签,就可以看到我们写入的index了
选择左侧kibana下的index patterns,将这些索引添加至kibana,再点击overview,选择discover,就可以看到我们通过接口写入的索引内容了。