SpringMVC框架中如何使用Filter实现请求日志打印
发表于:2025-02-23 作者:千家信息网编辑
千家信息网最后更新 2025年02月23日,这篇文章主要为大家展示了"SpringMVC框架中如何使用Filter实现请求日志打印",内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下"SpringMVC框架
千家信息网最后更新 2025年02月23日SpringMVC框架中如何使用Filter实现请求日志打印
这篇文章主要为大家展示了"SpringMVC框架中如何使用Filter实现请求日志打印",内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下"SpringMVC框架中如何使用Filter实现请求日志打印"这篇文章吧。
具体实现
日志记录过滤器
public class RequestFilter implements Filter{private static final String LOG_FORMATTER_IN = "请求路径:{%s},请求方法:{%s},参数:{%s},来源IP:{%s},请求开始时间{%s},返回:{%s},请求结束时间{%s},用时:{%s}ms,操作类型:{%s},操作人:{%s}";public static final String USER_TOKEN_REDIS_PREFIX = "token_prefix";private static final Logger log = LoggerFactory.getLogger(RequestFilter.class);//request拦截的conten-type列表private List
contentTypes;@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; //请求路径 String path = httpServletRequest.getRequestURI(); String method = httpServletRequest.getMethod(); //所有请求参数的Map Map paramMap = new HashMap<>(); //请求的真实IP String requestedIP = RequestUtils.getRealIP(httpServletRequest); //是否拦截并包装请求,如果需要拦截则会获取RequestBody,一般为application/json才拦截 boolean filterRequestFlag = checkFilter(request.getContentType()); if (filterRequestFlag) { httpServletRequest = new MyRequestBodyReaderWrapper(httpServletRequest); } //获取所有queryString和requestBody Map requestParamMap = RequestUtils.getRequestParamMap(httpServletRequest); if (requestParamMap != null && !requestParamMap.isEmpty()){ paramMap.putAll(requestParamMap); } //获取header参数 Map headerMap = RequestUtils.getHeaders(httpServletRequest); if (headerMap != null && !headerMap.isEmpty()){ paramMap.putAll(headerMap); } //获取路径参数 Map uriTemplateMap = RequestUtils.getUriTemplateVar(httpServletRequest); if (uriTemplateMap != null && !uriTemplateMap.isEmpty()){ paramMap.putAll(uriTemplateMap); } //包装Response,重写getOutputStream()和getWriter()方法,并用自定义的OutputStream和Writer来拦截和保存ResponseBody MyResponseWrapper responseWrapper = new MyResponseWrapper(httpServletResponse); //请求开始时间 Long dateStart = System.currentTimeMillis(); //Spring通过DispatchServlet处理请求 chain.doFilter(httpServletRequest, responseWrapper); //请求结束时间 Long dateEnd = System.currentTimeMillis(); String responseBody; if (responseWrapper.getMyOutputStream() == null){ if (responseWrapper.getMyWriter() != null){ responseBody = responseWrapper.getMyWriter().getContent(); //一定要flush,responseBody会被复用 responseWrapper.getMyWriter().myFlush(); } }else { responseBody = responseWrapper.getMyOutputStream().getBuffer(); //一定要flush,responseBody会被复用 responseWrapper.getMyOutputStream().myFlush(); } String params = JSONObject.toJSONString(paramMap); log.info(String.format(LOG_FORMATTER_IN, path, method, params, requestedIP, dateStart, responseBody, dateEnd,(dateEnd - dateStart));}/** * 判断请求/返回是否为application/json * 是则进行拦截, * 否则退出 * @param contentType 请求/响应类型 */private boolean checkFilter(String contentType) { boolean filterFlag = false;//是否继续拦截 for (String p : getContentTypes()) { if (StringUtils.contains(contentType, p)){ filterFlag = true; } } if (StringUtils.isEmpty(contentType)){ filterFlag = true; } return filterFlag;}} Request包装器
/*** HttpServletRequest的包装器,为了在拦截器阶段获取requestBody且不妨碍SpringMVC再次获取requestBody*/@Slf4jpublic class MyRequestBodyReaderWrapper extends HttpServletRequestWrapper {//存放JSON数据主体private final byte[] body;public MyRequestBodyReaderWrapper(HttpServletRequest request) throws IOException { super(request); body = getBody(request).getBytes(Charset.forName("UTF-8"));}@Overridepublic ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return byteArrayInputStream.read(); } };}@Overridepublic BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream()));}/** * 获取请求Body */public static String getBody(ServletRequest request) { StringBuilder sb = new StringBuilder(); InputStream inputStream = null; BufferedReader reader = null; try { inputStream = request.getInputStream(); reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { log.error("MyRequestBodyReaderWrapper.getBody()异常-->",e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { log.error("MyRequestBodyReaderWrapper.getBody()异常-->",e); } } if (reader != null) { try { reader.close(); } catch (IOException e) { log.error("MyRequestBodyReaderWrapper.getBody()异常-->",e); } } } return sb.toString();}}
RequestUtils
/*** 请求工具类*/public class RequestUtils {private static final Logger logger = LoggerFactory.getLogger(RequestUtils.class);/** * 获取所有的请求头 * @param request * @return */public static Map
getHeaders(HttpServletRequest request){ Map headerMap = new HashMap<>(); List headers = getCommonHeaders(); headers.add("Postman-Token"); headers.add("Proxy-Connection"); headers.add("X-Lantern-Version"); headers.add("Cookie"); Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()){ String headerName = headerNames.nextElement(); if (headers.contains(headerName)){ continue; } headerMap.put(headerName,request.getHeader(headerName)); } return headerMap;}/** * 获取请求的路径参数 * @param request * @return */public static Map getUriTemplateVar(HttpServletRequest request) { NativeWebRequest webRequest = new ServletWebRequest(request); Map uriTemplateVars = (Map ) webRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); return uriTemplateVars;}/** * 获取请求的真实IP * @param request * @return */public static String getRealIP(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) { //多次反向代理后会有多个ip值,第一个ip才是真实ip int index = ip.indexOf(","); if (index != -1) { return ip.substring(0, index); } else { return ip; } } ip = request.getHeader("X-Real-IP"); if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) { return ip; } return request.getRemoteAddr();}/** * 从Request中获取所有的请求参数,包括GET/POST/PATCH等请求,不包括路径参数 * @param request * @return */public static Map getRequestParamMap(HttpServletRequest request) { Map paramMap = new HashMap<>(); //获取QueryString中的参数,GET方式 或application/x-www-form-urlencoded Map queryParamMap = RequestUtils.getUriQueryVar(request); if (queryParamMap != null){ paramMap.putAll(queryParamMap); } //获取Body中的参数,POST/PATCH等方式,application/json Map bodyParamMap = null; try { //当为POST请求且 application/json时,request被RequestFilter处理为wrapper类 if (!(request instanceof MyRequestBodyReaderWrapper)){ return paramMap; } MyRequestBodyReaderWrapper readerWrapper = (MyRequestBodyReaderWrapper) request; String requestBody = new String(readerWrapper.getBody(), "UTF-8"); if (com.zhongan.health.common.utils.StringUtils.isNotBlank(requestBody)){ /** * 该方法为了避免 fastJson在 反序列化多层json时,改变对象顺序 */ bodyParamMap = JSONObject.parseObject(requestBody, new TypeReference >(){}, Feature.OrderedField); } } catch (Exception e) { logger.error("获取请求Body异常-->",e); } if (bodyParamMap != null){ paramMap.putAll(bodyParamMap); } return paramMap;}private static List getCommonHeaders(){ List headers = new ArrayList<>(); Class clazz = HttpHeaders.class; Field[] fields = clazz.getFields(); for (Field field : fields) { field.setAccessible(true); if (field.getType().toString().endsWith("java.lang.String") && Modifier.isStatic(field.getModifiers())){ try { headers.add((String) field.get(HttpHeaders.class)); } catch (IllegalAccessException e) { logger.error("反射获取属性值异常-->",e); } } } return headers;}} Response包装器
/***该包装器主要是重写getOutputStream()和getWriter()方法,给调用者返回自定义的OutputStream和Writer,以便参与输出的过程并记录保存responseBody。*/public class MyResponseWrapper extends HttpServletResponseWrapper {private ResponsePrintWriter writer;private MyServletOutputStream out;public MyResponseWrapper(HttpServletResponse response) { super(response);}@Overridepublic ServletOutputStream getOutputStream() throws IOException { //一定要先判断当前out为空才能去新建out对象,否则一次请求会出现多个out对象 if (out == null){ out = new MyServletOutputStream(super.getOutputStream()); } return out;}@Overridepublic PrintWriter getWriter() throws IOException { //一定要先判断当前writer为空才能去新建writer对象,否则一次请求会出现多个writer对象 if (writer == null){ writer = new ResponsePrintWriter(super.getWriter()); } return writer;}public ResponsePrintWriter getMyWriter() { return writer;}public MyServletOutputStream getMyOutputStream(){ return out;}}
自定义Writer
/***自定义Writer,重写write方法,并记录保存ResponseBody*/public class ResponsePrintWriter extends PrintWriter{private StringBuffer buffer;public ResponsePrintWriter(PrintWriter out) { super(out); buffer = new StringBuffer();}public String getContent(){ return buffer == null ? null : buffer.toString();}@Overridepublic void flush() { super.flush();}//清空buffer,以便下一次重新使用public void myFlush(){ buffer = null;}@Overridepublic void write(char[] buf, int off, int len) { super.write(buf, off, len); char[] destination = new char[len]; System.arraycopy(buf,off,destination,0,len); buffer.append(destination);}@Overridepublic void write(String s) { super.write(s); buffer.append(s);}}
自定义OutputStream
/*** 自定义输出流包装器,重写write方法,并记录保存ResponseBody*/public class MyServletOutputStream extends ServletOutputStream {private ServletOutputStream outputStream;private StringBuffer buffer;public MyServletOutputStream(ServletOutputStream outputStream) { this.outputStream = outputStream; buffer = new StringBuffer();}@Overridepublic void write(int b) throws IOException { outputStream.write(b);}@Overridepublic void write(byte[] b, int off, int len) throws IOException { outputStream.write(b, off, len); byte[] bytes = new byte[len]; System.arraycopy(b, off, bytes, 0, len); buffer.append(new String(bytes,"UTF-8"));}@Overridepublic void write(byte[] b) throws IOException { outputStream.write(b);}@Overridepublic void flush() throws IOException { super.flush();}//清空buffer,以便下一次重新使用public void myFlush(){ outputStream = null; buffer = null;}public String getBuffer() { if (buffer != null){ return buffer.toString(); } return null;}}
总结
之前利用HttpServletRequest.getInputStream()和RequestWrapper实现了请求的requestBody获取,现在提出将一个请求的RequestBody和ResponseBody都提出来并打印日志&落入数据库,以便统计和查找问题。
查找资料后确定两种技术方案
1. 使用AOP对所有Controller的方法进行环绕通知处理;
2. 使用Filter拦截所有的Request和Response,并获取body。
最后选择了第二种方式,具体实现记录如下。
具体实现
日志记录过滤器
public class RequestFilter implements Filter{private static final String LOG_FORMATTER_IN = "请求路径:{%s},请求方法:{%s},参数:{%s},来源IP:{%s},请求开始时间{%s},返回:{%s},请求结束时间{%s},用时:{%s}ms,操作类型:{%s},操作人:{%s}";public static final String USER_TOKEN_REDIS_PREFIX = "token_prefix";private static final Logger log = LoggerFactory.getLogger(RequestFilter.class);//request拦截的conten-type列表private ListcontentTypes;@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; //请求路径 String path = httpServletRequest.getRequestURI(); String method = httpServletRequest.getMethod(); //所有请求参数的Map Map paramMap = new HashMap<>(); //请求的真实IP String requestedIP = RequestUtils.getRealIP(httpServletRequest); //是否拦截并包装请求,如果需要拦截则会获取RequestBody,一般为application/json才拦截 boolean filterRequestFlag = checkFilter(request.getContentType()); if (filterRequestFlag) { httpServletRequest = new MyRequestBodyReaderWrapper(httpServletRequest); } //获取所有queryString和requestBody Map requestParamMap = RequestUtils.getRequestParamMap(httpServletRequest); if (requestParamMap != null && !requestParamMap.isEmpty()){ paramMap.putAll(requestParamMap); } //获取header参数 Map headerMap = RequestUtils.getHeaders(httpServletRequest); if (headerMap != null && !headerMap.isEmpty()){ paramMap.putAll(headerMap); } //获取路径参数 Map uriTemplateMap = RequestUtils.getUriTemplateVar(httpServletRequest); if (uriTemplateMap != null && !uriTemplateMap.isEmpty()){ paramMap.putAll(uriTemplateMap); } //包装Response,重写getOutputStream()和getWriter()方法,并用自定义的OutputStream和Writer来拦截和保存ResponseBody MyResponseWrapper responseWrapper = new MyResponseWrapper(httpServletResponse); //请求开始时间 Long dateStart = System.currentTimeMillis(); //Spring通过DispatchServlet处理请求 chain.doFilter(httpServletRequest, responseWrapper); //请求结束时间 Long dateEnd = System.currentTimeMillis(); String responseBody; if (responseWrapper.getMyOutputStream() == null){ if (responseWrapper.getMyWriter() != null){ responseBody = responseWrapper.getMyWriter().getContent(); //一定要flush,responseBody会被复用 responseWrapper.getMyWriter().myFlush(); } }else { responseBody = responseWrapper.getMyOutputStream().getBuffer(); //一定要flush,responseBody会被复用 responseWrapper.getMyOutputStream().myFlush(); } String params = JSONObject.toJSONString(paramMap); log.info(String.format(LOG_FORMATTER_IN, path, method, params, requestedIP, dateStart, responseBody, dateEnd,(dateEnd - dateStart));}/** * 判断请求/返回是否为application/json * 是则进行拦截, * 否则退出 * @param contentType 请求/响应类型 */private boolean checkFilter(String contentType) { boolean filterFlag = false;//是否继续拦截 for (String p : getContentTypes()) { if (StringUtils.contains(contentType, p)){ filterFlag = true; } } if (StringUtils.isEmpty(contentType)){ filterFlag = true; } return filterFlag;}}
Request包装器
/*** HttpServletRequest的包装器,为了在拦截器阶段获取requestBody且不妨碍SpringMVC再次获取requestBody*/@Slf4jpublic class MyRequestBodyReaderWrapper extends HttpServletRequestWrapper {//存放JSON数据主体private final byte[] body;public MyRequestBodyReaderWrapper(HttpServletRequest request) throws IOException { super(request); body = getBody(request).getBytes(Charset.forName("UTF-8"));}@Overridepublic ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return byteArrayInputStream.read(); } };}@Overridepublic BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream()));}/** * 获取请求Body */public static String getBody(ServletRequest request) { StringBuilder sb = new StringBuilder(); InputStream inputStream = null; BufferedReader reader = null; try { inputStream = request.getInputStream(); reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); String line; while ((line = reader.readLine()) != null) { sb.append(line); } } catch (IOException e) { log.error("MyRequestBodyReaderWrapper.getBody()异常-->",e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { log.error("MyRequestBodyReaderWrapper.getBody()异常-->",e); } } if (reader != null) { try { reader.close(); } catch (IOException e) { log.error("MyRequestBodyReaderWrapper.getBody()异常-->",e); } } } return sb.toString();}}
RequestUtils
/*** 请求工具类*/public class RequestUtils {private static final Logger logger = LoggerFactory.getLogger(RequestUtils.class);/** * 获取所有的请求头 * @param request * @return */public static MapgetHeaders(HttpServletRequest request){ Map headerMap = new HashMap<>(); List headers = getCommonHeaders(); headers.add("Postman-Token"); headers.add("Proxy-Connection"); headers.add("X-Lantern-Version"); headers.add("Cookie"); Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()){ String headerName = headerNames.nextElement(); if (headers.contains(headerName)){ continue; } headerMap.put(headerName,request.getHeader(headerName)); } return headerMap;}/** * 获取请求的路径参数 * @param request * @return */public static Map getUriTemplateVar(HttpServletRequest request) { NativeWebRequest webRequest = new ServletWebRequest(request); Map uriTemplateVars = (Map ) webRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); return uriTemplateVars;}/** * 获取请求的真实IP * @param request * @return */public static String getRealIP(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) { //多次反向代理后会有多个ip值,第一个ip才是真实ip int index = ip.indexOf(","); if (index != -1) { return ip.substring(0, index); } else { return ip; } } ip = request.getHeader("X-Real-IP"); if (StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) { return ip; } return request.getRemoteAddr();}/** * 从Request中获取所有的请求参数,包括GET/POST/PATCH等请求,不包括路径参数 * @param request * @return */public static Map getRequestParamMap(HttpServletRequest request) { Map paramMap = new HashMap<>(); //获取QueryString中的参数,GET方式 或application/x-www-form-urlencoded Map queryParamMap = RequestUtils.getUriQueryVar(request); if (queryParamMap != null){ paramMap.putAll(queryParamMap); } //获取Body中的参数,POST/PATCH等方式,application/json Map bodyParamMap = null; try { //当为POST请求且 application/json时,request被RequestFilter处理为wrapper类 if (!(request instanceof MyRequestBodyReaderWrapper)){ return paramMap; } MyRequestBodyReaderWrapper readerWrapper = (MyRequestBodyReaderWrapper) request; String requestBody = new String(readerWrapper.getBody(), "UTF-8"); if (com.zhongan.health.common.utils.StringUtils.isNotBlank(requestBody)){ /** * 该方法为了避免 fastJson在 反序列化多层json时,改变对象顺序 */ bodyParamMap = JSONObject.parseObject(requestBody, new TypeReference >(){}, Feature.OrderedField); } } catch (Exception e) { logger.error("获取请求Body异常-->",e); } if (bodyParamMap != null){ paramMap.putAll(bodyParamMap); } return paramMap;}private static List getCommonHeaders(){ List headers = new ArrayList<>(); Class clazz = HttpHeaders.class; Field[] fields = clazz.getFields(); for (Field field : fields) { field.setAccessible(true); if (field.getType().toString().endsWith("java.lang.String") && Modifier.isStatic(field.getModifiers())){ try { headers.add((String) field.get(HttpHeaders.class)); } catch (IllegalAccessException e) { logger.error("反射获取属性值异常-->",e); } } } return headers;}}
Response包装器
/***该包装器主要是重写getOutputStream()和getWriter()方法,给调用者返回自定义的OutputStream和Writer,以便参与输出的过程并记录保存responseBody。*/public class MyResponseWrapper extends HttpServletResponseWrapper {private ResponsePrintWriter writer;private MyServletOutputStream out;public MyResponseWrapper(HttpServletResponse response) { super(response);}@Overridepublic ServletOutputStream getOutputStream() throws IOException { //一定要先判断当前out为空才能去新建out对象,否则一次请求会出现多个out对象 if (out == null){ out = new MyServletOutputStream(super.getOutputStream()); } return out;}@Overridepublic PrintWriter getWriter() throws IOException { //一定要先判断当前writer为空才能去新建writer对象,否则一次请求会出现多个writer对象 if (writer == null){ writer = new ResponsePrintWriter(super.getWriter()); } return writer;}public ResponsePrintWriter getMyWriter() { return writer;}public MyServletOutputStream getMyOutputStream(){ return out;}}
自定义Writer
/***自定义Writer,重写write方法,并记录保存ResponseBody*/public class ResponsePrintWriter extends PrintWriter{private StringBuffer buffer;public ResponsePrintWriter(PrintWriter out) { super(out); buffer = new StringBuffer();}public String getContent(){ return buffer == null ? null : buffer.toString();}@Overridepublic void flush() { super.flush();}//清空buffer,以便下一次重新使用public void myFlush(){ buffer = null;}@Overridepublic void write(char[] buf, int off, int len) { super.write(buf, off, len); char[] destination = new char[len]; System.arraycopy(buf,off,destination,0,len); buffer.append(destination);}@Overridepublic void write(String s) { super.write(s); buffer.append(s);}}
自定义OutputStream
/*** 自定义输出流包装器,重写write方法,并记录保存ResponseBody*/public class MyServletOutputStream extends ServletOutputStream {private ServletOutputStream outputStream;private StringBuffer buffer;public MyServletOutputStream(ServletOutputStream outputStream) { this.outputStream = outputStream; buffer = new StringBuffer();}@Overridepublic void write(int b) throws IOException { outputStream.write(b);}@Overridepublic void write(byte[] b, int off, int len) throws IOException { outputStream.write(b, off, len); byte[] bytes = new byte[len]; System.arraycopy(b, off, bytes, 0, len); buffer.append(new String(bytes,"UTF-8"));}@Overridepublic void write(byte[] b) throws IOException { outputStream.write(b);}@Overridepublic void flush() throws IOException { super.flush();}//清空buffer,以便下一次重新使用public void myFlush(){ outputStream = null; buffer = null;}public String getBuffer() { if (buffer != null){ return buffer.toString(); } return null;}}
以上是"SpringMVC框架中如何使用Filter实现请求日志打印"这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注行业资讯频道!
参数
包装
方法
对象
路径
时间
UTF-8
日志
多个
方式
处理
类型
复用
输出
框架
内容
数据
篇文章
主体
再次
数据库的安全要保护哪些东西
数据库安全各自的含义是什么
生产安全数据库录入
数据库的安全性及管理
数据库安全策略包含哪些
海淀数据库安全审计系统
建立农村房屋安全信息数据库
易用的数据库客户端支持安全管理
连接数据库失败ssl安全错误
数据库的锁怎样保障安全
删除数据库重复的数据库
局域网服务器安装错误
网络安全问题知多少手抄报
试述数据库管理员的职责
数据库传输加密
计算机网络技术应用毕业论文
重建服务器要多长时间
安卓如何连接数据库获取密码
台湾人名数据库
数据库审计部署方式拓扑
储存服务器哪个好
网络技术哪个方向最好
网络安全试题及答案ppt
最新网络安全产业
电子厂mes软件开发商
wsus是网络管理服务器
邮政社招软件开发笔试
福州龙卷风网络技术
基层减负数据库
js函数取数据库的字段
网络安全三角形
数据库只能存表情地址吗
自动售卖机的软件开发商
玖二柒互联网科技公司靠谱吗
联合国贸易数据库产品
南京网络安全审计系统咨询品牌
网络安全产业发展趋势
云服务器可以挂什么
软件开发计入营业成本吗
用什么软件查大数据库