基于缓存的统一请求日志实现

2023-05-16

前言

        昨天说的会分享方案的具体实现,今天就来分享想核心代码吧。


一、思路

        我们用的是springboot组服务,每个链路通过http请求。所以要实现全链路的日志记录,也都是围绕sprinboot做的处理。
        这里用的就是过滤器Filter、拦截器Interceptor、配合重写RequestWrapper、统一分发处理、redis缓存机制实现。

二、使用步骤

1.引入库

        引库就不说了,都是springboot的,没有第三方依赖,也就是redis、mysql要引入依赖。

2.重写Request

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.util.*;

/**
 * 自定义request对象
 *
 * @author zhengwen
 */
@Slf4j
public class MyRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 请求参数
     */
    private Map<String, String[]> params = new HashMap<String, String[]>();

    private Map<String, String> headerMap = new HashMap<>();
    /**
     * 请求体body
     */
    private String body;


    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public MyRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = new String(readBytes(request.getInputStream()));

        //
        /*if ("POST".equals(request.getMethod().toUpperCase())) {
            paramsMap = getParamMapFromPost(this);
        } else {
            paramsMap = getParamMapFromGet(this);
        }*/

    }


    public MyRequestWrapper(HttpServletRequest request, byte[] encodeBody) throws IOException {
        super(request);
        body = new String(encodeBody);
        //
        /*if ("POST".equals(request.getMethod().toUpperCase())) {
            paramsMap = getParamMapFromPost(this);
        } else {
            paramsMap = getParamMapFromGet(this);
        }*/
    }

    public MyRequestWrapper(HttpServletRequest request, Map<String, String[]> extendParams) throws IOException {
        super(request);
        body = new String(readBytes(request.getInputStream()));
        addAllParameters(extendParams);
    }

    public void MyRequestWrapper(HttpServletRequest request) {
        // 将request交给父类,以便于调用对应方法的时候,将其输出,其实父亲类的实现方式和第一种new的方式类似
        //super(request);
        //将参数表,赋予给当前的Map以便于持有request中的参数
        this.params.putAll(request.getParameterMap());
    }

    private String getRequestBody(InputStream stream) {
        String line = "";
        StringBuilder body = new StringBuilder();
        int counter = 0;

        // 读取POST提交的数据内容
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
        try {
            while ((line = reader.readLine()) != null) {
                if (counter > 0) {
                    body.append("rn");
                }
                body.append(line);
                counter++;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return body.toString();
    }

    private HashMap<String, String[]> getParamMapFromPost(HttpServletRequest request) {

        String body = "";
        try {
            body = getRequestBody(request.getInputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
        HashMap<String, String[]> result = new HashMap<String, String[]>();

        if (null == body || 0 == body.length()) {
            return result;
        }

        return parseQueryString(body);
    }


    public HashMap<String, String[]> parseQueryString(String s) {
        String valArray[] = null;
        if (s == null) {
            throw new IllegalArgumentException();
        }
        HashMap<String, String[]> ht = new HashMap<String, String[]>();
        StringTokenizer st = new StringTokenizer(s, "&");
        while (st.hasMoreTokens()) {
            String pair = (String) st.nextToken();
            int pos = pair.indexOf('=');
            if (pos == -1) {
                continue;
            }
            String key = pair.substring(0, pos);
            String val = pair.substring(pos + 1, pair.length());
            if (ht.containsKey(key)) {
                String oldVals[] = (String[]) ht.get(key);
                valArray = new String[oldVals.length + 1];
                for (int i = 0; i < oldVals.length; i++) {
                    valArray[i] = oldVals[i];
                }
            } else {
                valArray = new String[1];
            }
            ht.put(key, valArray);
        }
        return ht;
    }

    private Map<String, String[]> getParamMapFromGet(HttpServletRequest request) {
        return parseQueryString(request.getQueryString());
    }


    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes());
        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 arg0) {

            }
        };
    }

    private static byte[] readBytes(InputStream in) throws IOException {
        BufferedInputStream bufin = new BufferedInputStream(in);
        int buffSize = 1024;
        ByteArrayOutputStream out = new ByteArrayOutputStream(buffSize);

        byte[] temp = new byte[buffSize];
        int size = 0;
        while ((size = bufin.read(temp)) != -1) {
            out.write(temp, 0, size);
        }
        bufin.close();

        byte[] content = out.toByteArray();
        return content;
    }


    /**
     * add a header with given name and value
     *
     * @param name
     * @param value
     */
    public void addHeader(String name, String value) {
        headerMap.put(name, value);
    }

    @Override
    public String getHeader(String name) {
        log.info("getHeader --->{}", name);
        String headerValue = super.getHeader(name);
        if (headerMap.containsKey(name)) {
            headerValue = headerMap.get(name);
        }
        return headerValue;
    }

    /**
     * get the Header names
     */
    @Override
    public Enumeration<String> getHeaderNames() {
        List<String> names = Collections.list(super.getHeaderNames());
        for (String name : headerMap.keySet()) {
            names.add(name);
        }
        return Collections.enumeration(names);
    }

    @Override
    public Enumeration<String> getHeaders(String name) {
        log.info("getHeaders --->>>>>>{}", name);
        List<String> values = Collections.list(super.getHeaders(name));
        log.info("getHeaders --->>>>>>{}", values);
        if (headerMap.containsKey(name)) {
            log.info("getHeaders --->{}", headerMap.get(name));
            values = Arrays.asList(headerMap.get(name));
        }
        return Collections.enumeration(values);
    }

    @SuppressWarnings("unchecked")


    @Override
    public String getParameter(String name) {
        //重写getParameter,代表参数从当前类中的map获取
        String[] values = params.get(name);
        if (values == null || values.length == 0) {
            return null;
        }
        return values[0];
    }

    public String[] getParameterValues(String name) {
        //同上
        return params.get(name);
    }

    public void addAllParameters(Map<String, String[]> otherParams) {
        //增加多个参数
        for (Map.Entry<String, String[]> entry : otherParams.entrySet()) {
            addParameter(entry.getKey(), entry.getValue());
        }
    }


    public void addParameter(String name, Object value) {
        //增加参数
        if (value != null) {
            if (value instanceof ArrayList) {
                String value1 = String.valueOf(value).substring(1, String.valueOf(value).length());
                value = value1.substring(0, value1.length() - 1);
                params.put(name, new String[]{(String) value});
            } else if (value instanceof String[]) {
                params.put(name, (String[]) value);
            } else if (value instanceof String) {
                params.put(name, new String[]{(String) value});
            } else {
                params.put(name, new String[]{String.valueOf(value)});
            }
        }
    }

}


3.自定义过滤器

import com.xx.MyRequestWrapper;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;

/**
 * 自定义拦截器
 *
 * @author zhengwen
 */
@WebFilter
public class MyWebFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        ServletRequest request = null;
        //普通http请求使用重写的request
        if (servletRequest instanceof HttpServletRequest) {
            String url = servletRequest.getParameter("url");
            Map<String, String[]> parameterMap = servletRequest.getParameterMap();
            request = new MyRequestWrapper((HttpServletRequest) servletRequest, parameterMap);
        }

        if (request == null) {
            chain.doFilter(servletRequest, servletResponse);
        } else {
            chain.doFilter(request, servletResponse);
        }
    }

    @Override
    public void destroy() {
    }
}

        过滤器里的核心就是将原先的servletRequest替换为自定义的RequestWrapper,执行chain.doFilter(request, servletResponse);继续走下去。

4.拦截器配置

import cn.hutool.core.thread.ThreadUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.core.JsonParser;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.service.spi.ServiceException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;


import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;

/**
 * Spring MVC 配置
 * @author zhengwen
 */
@Configuration
@Slf4j
public class MyWebMvcConfigurer implements WebMvcConfigurer {



    /**
     * 当前激活的配置文件
     */
    @Value("${spring.profiles.active:dev}")
    private String env;

    @Value("${base_service.interface.profix:/api}")
    private String baseInterfaceProfix;

    @Resource
    RedisUtil redisUtil;


    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(stringHttpMessageConverter());
        converters.add(mappingJackson2HttpMessageConverter());
    }

    @Bean
    StringHttpMessageConverter stringHttpMessageConverter() {
        return new StringHttpMessageConverter(Charset.forName("UTF-8"));
    }

    @Bean
    MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        return new MappingJackson2HttpMessageConverter(customObjectMapper());
    }

    @Bean
    CustomObjectMapper customObjectMapper() {
        CustomObjectMapper objectMapper = new CustomObjectMapper();

        //传参允许注释
        objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);

        return objectMapper;
    }



    /**
     * 统一异常处理
     *
     * @param exceptionResolvers
     */
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        exceptionResolvers.add(new HandlerExceptionResolver() {
            @Override
            public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
                //TODO 统一异常可自行扩展
                RestMessage restMessage = null;
                if (e instanceof ServiceException) {
                    restMessage = RestBuilders.failureMessage().setCode(ResponseCodeI18n.SERVICE_EXCEPTION.getCode()).setMessage(ResponseCodeI18n.SERVICE_EXCEPTION.getMsg());
                    log.error(e.getMessage());
                } else if (e instanceof NoHandlerFoundException) {
                    restMessage = RestBuilders.failureMessage().setCode(ResponseCodeI18n.NOT_FOND.getCode()).setMessage("接口 [" + request.getRequestURI() + "] 不存在");
                }else if (e instanceof IllegalArgumentException){
                    restMessage = RestBuilders.failureMessage().setCode(ResponseCodeI18n.ILLEGAL_ARGUMENT.getCode()).setMessage(e.getMessage());
                }else {
                    String respError = "接口 [" + request.getRequestURI() + "] 内部错误,请联系管理员。";
                    if (StringUtils.isNotBlank(e.getMessage())) {
                        respError += "错误原因[" + e.getMessage() + "]";
                    }
                    restMessage = RestBuilders.failureMessage().setCode(ResponseCodeI18n.SERVICE_EXCEPTION.getCode()).setMessage(respError);
                    String message;
                    if (handler instanceof HandlerMethod) {
                        HandlerMethod handlerMethod = (HandlerMethod) handler;
                        message = String.format("接口 [%s] 出现异常,方法:%s.%s,异常摘要:%s",
                                request.getRequestURI(),
                                handlerMethod.getBean().getClass().getName(),
                                handlerMethod.getMethod().getName(),
                                e.getMessage());
                    } else {
                        message = e.getMessage();
                    }
                    log.error(message, e);
                }
                //开启线程,记录异常日志
                RestMessage finalRestMessage = restMessage;
                ThreadUtil.execAsync(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            //更新接口请求异常信息,保存日志
                            RequestUtil.updateInterfaceLogException(request, finalRestMessage,redisUtil);
                        } catch (IOException e) {
                            log.error("开启线程保存接口请求日志,发生异常,原因:{}",e.getMessage());
                        }
                    }
                });
                responseResult(response, restMessage);
                return new ModelAndView();
            }

        });
    }

    /**
     * response结果
     *
     * @param response    response
     * @param restMessage 结果
     */
    private void responseResult(HttpServletResponse response, RestMessage restMessage) {
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-type", "application/json;charset=UTF-8");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type,x-requested-with,X-Token");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setStatus(200);
        try {
            response.getWriter().write(JSON.toJSONString(restMessage, SerializerFeature.WriteNullStringAsEmpty));
        } catch (IOException ex) {
            log.error(ex.getMessage());
        }
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/templates/")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/resources/");
        WebMvcConfigurer.super.addResourceHandlers(registry);
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
        WebMvcConfigurer.super.addViewControllers(registry);
    }

    /**
     * 解决跨域问题
     *
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*")
                .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
                .allowedHeaders("*")
                .maxAge(3600);
    }


    /**
     * 添加拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new HandlerInterceptorAdapter() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                log.info("---------------------preHandle---------------------");
                //开发环境忽略签名认证
                /*if ("dev".equals(env)) {
                    return true;
                }*/
                //前面过滤器里已经定义了用自己重写的request往下走,这里就可以直接转换request对象
                MyRequestWrapper requestWrapper = null;
                if (request instanceof MyRequestWrapper){
                    requestWrapper = (MyRequestWrapper) request;
                }

                //查询接口信息
                BaseInterfaceInfo interfaceInfo = RequestUtil.getInterfaceInfoFrom(baseInterfaceProfix,requestWrapper,redisUtil);

                //TODO 这里接口信息为空,说明没有配置,是否允许继续访问呢?

                //初始化请求日志
                RequestUtil.initInterfaceRequestLogAndCache(requestWrapper, interfaceInfo,redisUtil);


                boolean isPass = true;
                //TODO 以下就是根据接口信息处理权限校验、限流、幂等性等
                //TODO 校验token后可以更新日志的clientId
                //请求机制校验:授权认证检查、接口权限检查、接口防伪检查、限流检查、幂等性
                //请求日志初始化
                //接口信息获取


                //接口权限检查
                //
                //限流检查
                //幂等性

                return isPass;
            }

            @Override
            public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
                //super.postHandle(request, response, handler, modelAndView);
                log.info("---------------------postHandle---------------------");
                //TODO 开线程去保存日志,提高性能
                //这里有response的advice处理,早于这个方法执行,因为response处理麻烦(其实重写的Response我也提供了,就是懒得验),所以就在那边处理,request其实也有advice方法,但是如果用感觉晚了一点
                /*ThreadUtil.execAsync(new Runnable() {
                    @Override
                    public void run() {
                        //转换request对象
                        try {
                            BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
                            //保存请求日志
                            //RequestUtil.saveInterfaceRequestLog(requestWrapper,redisUtil);
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });*/


            }
        });

    }


    /**
     * 是否OPTIONS请求
     *
     * @param request
     * @return
     */
    private boolean isOptionRequest(HttpServletRequest request) {
        String method = request.getMethod();
        if ("OPTIONS".equals(method)) {
            return true;
        }
        return false;
    }



}

        注释很清楚,可以仔细看

5.拦截Response

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;

import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;


import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author zhengwen
 */
@ControllerAdvice
public class InterceptResponse implements ResponseBodyAdvice<Object> {

    @Resource
    private RedisUtil redisUtil;

    @Resource
    private BaseInterfaceLogMapper baseInterfaceLogMapper;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Nullable
    @Override
    public Object beforeBodyWrite(@Nullable Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        ServletServerHttpResponse responseTemp = (ServletServerHttpResponse) serverHttpResponse;
        HttpServletResponse response = responseTemp.getServletResponse();
        String requestId = null;
        Object requestIdObj = null;
        if (serverHttpRequest instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest sshr = (ServletServerHttpRequest) serverHttpRequest;
            HttpServletRequest req = sshr.getServletRequest();
            requestIdObj = req.getAttribute("requestId");

        }
        if (serverHttpRequest instanceof MyRequestWrapper) {
            MyRequestWrapper sshr = (MyRequestWrapper) serverHttpRequest;
            requestIdObj = sshr.getAttribute("requestId");
        }
        if (requestIdObj != null) {
            requestId = "requestId-" + requestIdObj.toString();
        }

        String finalRequestId = requestId;
        ThreadUtil.execAsync(() -> {
            if (body instanceof SimpleRestMessage) {
                SimpleRestMessage result = (SimpleRestMessage) body;
                if (result != null) {
                    //TODO 是不是还有方法获取response的大小?根据大小来处理data是否放弃存储是不是更好?
                    String dataStr = RequestUtil.formatJsonStr(JSONUtil.toJsonStr(result.getData()));
                    if(dataStr.length() > 8000){
                        result.setData(null);
                        result.setMessage("data数据过长,已抛弃存储");
                    }
                    String resStr = JSONUtil.toJsonStr(result);
                    if (StringUtils.isNotBlank(resStr)) {
                        JSONObject resJson = JSONUtil.parseObj(resStr);
                        String code = resJson.getStr("code");
                        boolean success = resJson.getBool("success");
                        //记录日志等操作
                        Object interfaceLogObj = redisUtil.get(finalRequestId);
                        if (interfaceLogObj != null) {
                            BaseInterfaceLog interfaceLog = null;
                            if (interfaceLogObj instanceof BaseInterfaceLog) {
                                interfaceLog = (BaseInterfaceLog) interfaceLogObj;
                            } else {
                                interfaceLog = JSONUtil.toBean(JSONUtil.toJsonStr(interfaceLogObj.toString()), BaseInterfaceLog.class);
                            }
                            if (interfaceLog != null) {
                                interfaceLog.setRequestOutTime(DateUtil.date());

                                interfaceLog.setResponseResult(resStr);
                                interfaceLog.setResponseCode(code);
                                interfaceLog.setRequestState(success ? 1 : 0);
                                long useMs = DateUtil.betweenMs(interfaceLog.getRequestInTime(), interfaceLog.getRequestOutTime());
                                interfaceLog.setUseTime(useMs);
                                interfaceLog.setCreateTime(DateUtil.date());
                                baseInterfaceLogMapper.insert(interfaceLog);
                                redisUtil.del(finalRequestId);
                            }

                        }
                    }
                }
            }
        });

        return body;
    }
}

6.RequestUtil工具类

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;


import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author zhengwen
 */
@Component
public class RequestUtil {

    /**
     * 获取真实IP
     *
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 如果是多级代理,那么取第一个ip为客户端ip
        if (ip != null && ip.indexOf(",") != -1) {
            ip = ip.substring(0, ip.indexOf(",")).trim();
        }

        return ip;
    }


    /**
     * 获取请求的接口信息
     *
     * @param baseInterfaceProfix
     * @param request
     * @param redisUtil
     * @return
     * @throws Exception
     */
    public static BaseInterfaceInfo getInterfaceInfoFrom(String baseInterfaceProfix, MyRequestWrapper request, RedisUtil redisUtil) throws Exception {
        if (request == null) {
            return null;
        }
        //TODO 这里还可以优化
        String url = null;
        BaseInterfaceInfo baseInterfaceInfo = null;
        String requestURI = request.getRequestURI();
        String method = request.getMethod();
        String path = request.getServletPath();
        String interfaceKey = path + "-" + method;
        Map<String, String[]> paramsMap = request.getParameterMap();
        if(CollectionUtil.isNotEmpty(paramsMap) && paramsMap.containsKey("url")){
            url = paramsMap.get("url")[0];
        }
        boolean isOutInterface = false;
        String interfaceUrl = "";
        if (StringUtils.isNotBlank(url) && !"null".equals(url)) {
            //对外接口
            if (url.startsWith("/")) {
                interfaceUrl = baseInterfaceProfix + url.trim().substring(1);
                interfaceKey = interfaceUrl + "-" + method;
            } else {
                interfaceUrl = baseInterfaceProfix + "/" + url.trim();
                interfaceKey = interfaceUrl + "-" + method;
            }
            isOutInterface = true;
        } else {
            //内部接口
            if (!requestURI.equals(path)) {
                String requestUrl = request.getRequestURL().toString();
                if (requestUrl.contains("?")) {
                    requestUrl = requestUrl.substring(0, requestUrl.indexOf("?") - 1);
                }
                //interfaceKey = url.replace(baseInterfaceProfix, "") + "-" + method;
                if(StringUtils.isBlank(url)){
                    //interfaceKey = requestURI.substring(0,requestURI.lastIndexOf("/")) + "-" + method;
                    interfaceKey = requestURI + "-" + method;
                }else{
                    interfaceKey = url.trim() + "-" + method;
                }
            }
        }

        Object interfaceInfoObj = redisUtil.get(interfaceKey);
        if (interfaceInfoObj != null) {
            baseInterfaceInfo = JSONUtil.toBean(interfaceInfoObj.toString(), BaseInterfaceInfo.class);
        } else {
            //接口管理提供界面后,这里应该几乎不会走到
            if (isOutInterface) {
                QueryWrapper<BaseInterfaceInfo> qw = new QueryWrapper<>();
                qw.eq("interface_url", interfaceUrl);
                qw.eq("method_type", method.trim().toUpperCase());
                qw.eq("is_deleted", 1);
                qw.orderByDesc("id");
                baseInterfaceInfo = SpringUtil.getBean(BaseInterfaceInfoMapper.class).selectOne(qw);
                if (baseInterfaceInfo != null) {
                    redisUtil.set(interfaceKey, JSONUtil.toJsonStr(baseInterfaceInfo));
                }
            }
        }

        return baseInterfaceInfo;
    }

    /**
     * 初始化接口请求日志
     *
     * @param requestWrapper
     * @param interfaceInfo
     * @param redisUtil
     * @return
     * @throws Exception
     */
    public static BaseInterfaceLog initInterfaceRequestLogAndCache(MyRequestWrapper requestWrapper, BaseInterfaceInfo interfaceInfo, RedisUtil redisUtil) throws Exception {
        //请求id:使用雪花算法工具生成,参数1为终端ID,参数2为数据中心ID
        Snowflake snowflake = IdUtil.getSnowflake(1, 1);
        long requestId = snowflake.nextId();

        BaseInterfaceLog baseInterfaceLog = new BaseInterfaceLog();
        baseInterfaceLog.setRequestId(String.valueOf(requestId));
        baseInterfaceLog.setRequestIdCode(baseInterfaceLog.getRequestId());
        baseInterfaceLog.setRequestTime(DateUtil.date());
        if (interfaceInfo != null) {
            baseInterfaceLog.setInterfaceUrl(interfaceInfo.getInterfaceUrl());
            baseInterfaceLog.setInterfaceId(interfaceInfo.getId());
            baseInterfaceLog.setApplicationId(interfaceInfo.getApplicationId());
            baseInterfaceLog.setApplicationCode(interfaceInfo.getApplicationCode());
        }

        //获取请求id
        baseInterfaceLog.setRequestIp(RequestUtil.getIpAddress(requestWrapper));

        //baseInterfaceLog.setCleintId();
        baseInterfaceLog.setRequestInTime(baseInterfaceLog.getRequestTime());
        baseInterfaceLog.setIsDeleted(1);

        //补充请求id
        repairRequestInfo(requestWrapper, requestId, baseInterfaceLog);

        if (baseInterfaceLog != null) {
            //这里要不要设置有效期呢?,先设置个3分钟吧
            redisUtil.set("requestId-" + requestId, baseInterfaceLog, 60 * 3);
        }

        return baseInterfaceLog;
    }

    /**
     * 补全请求信息
     *
     * @param requestWrapper
     * @param requestId
     * @param baseInterfaceLog
     */
    private static void repairRequestInfo(MyRequestWrapper requestWrapper, long requestId, BaseInterfaceLog baseInterfaceLog) {
        if (requestWrapper == null) {
            return;
        }
        //补充requestId后,重新设置body
        String body = requestWrapper.getBody();
        String params = requestWrapper.getQueryString();
        if (StringUtils.isNotBlank(body)) {
            if (JSONUtil.isJson(body)) {
                JSONObject bodyJson = JSONUtil.parseObj(body);
                if (bodyJson.containsKey("customQueryParams")) {
                    JSONObject customQueryParamsJson = bodyJson.getJSONObject("customQueryParams");
                    if (customQueryParamsJson != null) {
                        customQueryParamsJson.putOpt("requestId", requestId);
                        customQueryParamsJson.putOpt("requestApplicationId", baseInterfaceLog.getApplicationId());
                        customQueryParamsJson.putOpt("requestApplicationCode", baseInterfaceLog.getApplicationCode());
                        bodyJson.putOpt("customQueryParams", customQueryParamsJson);
                    }
                } else {
                    bodyJson.putOpt("requestId", requestId);
                    bodyJson.putOpt("requestApplicationId", baseInterfaceLog.getApplicationId());
                    bodyJson.putOpt("requestApplicationCode", baseInterfaceLog.getApplicationCode());
                }
                String bodyJsonStr = JSONUtil.toJsonStr(bodyJson);
                requestWrapper.setBody(bodyJsonStr);
            }
        } else {
            //非body传参方式,是补充传参param,还是attr呢?
            params = JSONUtil.toJsonStr(requestWrapper.getParameterMap());
            requestWrapper.addParameter("requestId", requestId);
            requestWrapper.addParameter("requestApplicationId", baseInterfaceLog.getApplicationId());
            requestWrapper.addParameter("requestApplicationCode", baseInterfaceLog.getApplicationCode());
        }
        //补充attr,后面在response中获取
        requestWrapper.setAttribute("requestId", requestId);
        requestWrapper.setAttribute("requestApplicationId", baseInterfaceLog.getApplicationId());
        requestWrapper.setAttribute("requestApplicationCode", baseInterfaceLog.getApplicationCode());

        //日志记录2个参数域
        baseInterfaceLog.setRequestParam(params);
        //格式化body
        String fmtBodyStr = formatJsonStr(body);
        baseInterfaceLog.setRequestBody(fmtBodyStr);
    }

    /**
     * 更新接口异常日志,保存并删除缓存
     *
     * @param request
     * @param finalRestMessage
     * @param redisUtil
     * @throws IOException
     */
    public static void updateInterfaceLogException(HttpServletRequest request, RestMessage finalRestMessage, RedisUtil redisUtil) throws IOException {
        //MyRequestWrapper requestWrapper = new MyRequestWrapper(request);
        MyRequestWrapper requestWrapper = null;
        if (request instanceof MyRequestWrapper) {
            requestWrapper = (MyRequestWrapper) request;
        } else {
            requestWrapper = new MyRequestWrapper(request);
        }
        Object requestIdObj = requestWrapper.getAttribute("requestId");
        if (requestIdObj != null) {
            String finalRequestId = "requestId-" + requestIdObj.toString();
            Object interfaceLogObj = redisUtil.get(finalRequestId);
            if (interfaceLogObj != null) {
                BaseInterfaceLog interfaceLog = null;
                if (interfaceLogObj instanceof BaseInterfaceLog) {
                    interfaceLog = (BaseInterfaceLog) interfaceLogObj;
                } else {
                    interfaceLog = JSONUtil.toBean(JSONUtil.toJsonStr(interfaceLogObj), BaseInterfaceLog.class);
                }
                if (interfaceLog != null) {
                    interfaceLog.setRequestOutTime(DateUtil.date());
                    interfaceLog.setResponseCode(finalRestMessage.getCode());
                    String dataStr = formatJsonStr(JSONUtil.toJsonStr(finalRestMessage.getData()));
                    if (dataStr.length() > 8000) {
                        finalRestMessage.setData(null);
                        finalRestMessage.setMessage("data数据过长,已抛弃存储");
                    }
                    String fmtMsgStr = formatJsonStr(JSONUtil.toJsonStr(finalRestMessage));
                    interfaceLog.setResponseResult(fmtMsgStr);
                    interfaceLog.setRequestException(finalRestMessage.getMessage());
                    interfaceLog.setRequestOutTime(DateUtil.date());
                    //请求失败
                    interfaceLog.setRequestState(0);
                    long useMs = DateUtil.betweenMs(interfaceLog.getRequestInTime(), interfaceLog.getRequestOutTime());
                    interfaceLog.setUseTime(useMs);
                    interfaceLog.setCreateTime(DateUtil.date());

                    BaseInterfaceLogMapper baseInterfaceLogMapper = SpringUtil.getBean(BaseInterfaceLogMapper.class);

                    baseInterfaceLogMapper.insert(interfaceLog);
                    redisUtil.del(finalRequestId);
                }
            }
        }
    }

    public static String formatJsonStr(String jsonStr) {
        String result = null;
        if (StringUtils.isNotBlank(jsonStr)) {
            //去转义
            jsonStr = StringEscapeUtils.unescapeJavaScript(jsonStr);
            //正则表达式格式化成一行
            String regex = "\\r\\n[ ]+|\\r|\\n|\\t";
            //编译正则表达式
            Pattern pattern = Pattern.compile(regex);
            //检索匹配器对象
            Matcher matcher = pattern.matcher(jsonStr);
            //用单个空格替换所有空格字符
            result = matcher.replaceAll("");
        }
        return result;
    }

}

  • 接口信息、接口日志等常规的CRUD一套不用我提供吧,呵呵

3.表结构

接口信息

CREATE TABLE `base_interface_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `interfact_name` varchar(200) DEFAULT NULL COMMENT '接口名称',
  `interface_url` varchar(500) DEFAULT NULL COMMENT '接口url',
  `application_id` bigint(20) DEFAULT NULL COMMENT '接口提供方,来源application主键',
  `application_code` varchar(50) DEFAULT NULL COMMENT '应用编码,来源base_application编码,冗余,但是必填',
  `controller_name` varchar(100) DEFAULT NULL COMMENT 'controller名称',
  `method_name` varchar(100) DEFAULT NULL COMMENT '方法名称',
  `method_type` varchar(10) DEFAULT NULL COMMENT '方法类型,get、post、delete、put',
  `business_type` int(1) DEFAULT NULL COMMENT '业务类型,1新增、2删除、3修改、4查询',
  `interface_state` int(1) DEFAULT NULL COMMENT '接口状态,0停用、1可用',
  `memo` varchar(2000) DEFAULT NULL COMMENT '接口描述',
  `open_time` datetime DEFAULT NULL COMMENT '开放时间',
  `close_time` datetime DEFAULT NULL COMMENT '关闭时间',
  `limit_rate` int(11) DEFAULT NULL COMMENT '请求频率,1s次',
  `check_auth` int(1) DEFAULT NULL COMMENT '校验授权,0否,1是',
  `check_flow` int(1) DEFAULT NULL COMMENT '校验限流,0否,1是',
  `check_black_list` int(1) DEFAULT NULL COMMENT '校验黑名单,0否,1是',
  `check_idempotent` int(1) DEFAULT NULL COMMENT '校验幂等,0否,1是',
  `check_safe` int(1) DEFAULT NULL COMMENT '校验安全,0否,1是',
  `creator` varchar(100) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `modifier` varchar(100) DEFAULT NULL COMMENT '修改人',
  `modify_time` datetime DEFAULT NULL COMMENT '修改时间',
  `is_deleted` int(1) DEFAULT NULL COMMENT '是否删除,0删除,1存在',
  PRIMARY KEY (`id`),
  KEY `base_interface_info_application_id_IDX` (`application_id`) USING BTREE
) ENGINE=InnoDB COMMENT='接口信息';

接口日志

CREATE TABLE `base_interface_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `request_id_code` varchar(100) DEFAULT NULL COMMENT '请求id码',
  `request_time` datetime DEFAULT NULL COMMENT '请求时间',
  `interface_url` varchar(100) DEFAULT NULL COMMENT '接口url',
  `interface_id` bigint(20) DEFAULT NULL COMMENT '接口id',
  `request_ip` varchar(100) DEFAULT NULL COMMENT '请求ip',
  `request_body` longtext COMMENT '请求入参body',
  `request_param` longtext COMMENT '请求入参param',
  `response_result` longtext COMMENT '请求出参',
  `response_code` varchar(100) DEFAULT NULL COMMENT '响应码',
  `request_state` int(1) DEFAULT NULL COMMENT '请求状态,0失败,1成功',
  `request_exception` varchar(2000) DEFAULT NULL COMMENT '请求异常',
  `business_exception` varchar(2000) DEFAULT NULL COMMENT '业务异常',
  `use_time` bigint(20) DEFAULT NULL COMMENT '用时,单位毫秒',
  `client_id` bigint(20) DEFAULT NULL COMMENT '客户端id,来源客户端信息主键',
  `business_msg` longtext COMMENT '业务接口补充信息',
  `request_in_time` datetime DEFAULT NULL COMMENT '请求进时间',
  `request_out_time` datetime DEFAULT NULL COMMENT '请求出时间',
  `application_id` bigint(20) DEFAULT NULL COMMENT '应用id,来源base_application主键,冗余',
  `application_code` varchar(50) DEFAULT NULL COMMENT '应用编码,来源base_application编码,冗余',
  `creator` varchar(100) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `modifier` varchar(100) DEFAULT NULL COMMENT '修改人',
  `modify_time` datetime DEFAULT NULL COMMENT '修改时间',
  `is_deleted` int(1) DEFAULT NULL COMMENT '是否删除,0删除,1存在',
  PRIMARY KEY (`id`),
  KEY `NewTable_interface_id_IDX` (`interface_id`) USING BTREE,
  KEY `NewTable_cleint_id_IDX` (`client_id`) USING BTREE,
  KEY `NewTable_create_time_IDX` (`create_time`) USING BTREE,
  KEY `base_interface_log_request_time_IDX` (`request_time`) USING BTREE,
  KEY `base_interface_log_application_id_IDX` (`application_id`) USING BTREE
) ENGINE=InnoDB COMMENT='接口请求日志';

客户端信息

CREATE TABLE `base_client_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `client_name` varchar(50) DEFAULT NULL COMMENT '客户端名称',
  `client_logo` varchar(100) DEFAULT NULL COMMENT '客户端标识',
  `api_key` varchar(100) DEFAULT NULL COMMENT '授权apikey',
  `api_secret` varchar(100) DEFAULT NULL COMMENT '授权apisecret',
  `auth_start_time` datetime DEFAULT NULL COMMENT '授权有效开始时间',
  `auth_end_time` datetime DEFAULT NULL COMMENT '授权有效结束时间',
  `auth_state` int(11) DEFAULT NULL COMMENT '授权状态,0停用,1启用',
  `super_name` varchar(50) DEFAULT NULL COMMENT '超管账号,冗余',
  `password_salt` varchar(30) DEFAULT NULL COMMENT '超管账号密码盐值',
  `creator` varchar(100) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `modifier` varchar(100) DEFAULT NULL COMMENT '修改人',
  `modify_time` datetime DEFAULT NULL COMMENT '修改人时间',
  `is_deleted` int(11) DEFAULT NULL COMMENT '是否删除,0删除,1存在',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='客户端信息';

接口服务应用信息

CREATE TABLE `base_application` (
  `id_` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键,自增',
  `name_` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '应用名称',
  `code_` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '应用代码(应用在base的唯一标识)',
  `url_` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '应用访问地址(预留字段,也可以配置在环境变量中)',
  `api_url_` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '应用接口访问地址(预留字段,也可以配置在环境变量中)',
  `type_` int(10) DEFAULT NULL COMMENT '类别(1内部服务 2外部服务)',
  `remark_` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id_`) USING BTREE
) ENGINE=InnoDB COLLATE=utf8_bin;

总结

        各个核心代码注释的很清楚,大家可以仔细看几遍,其实核心思想就是:

  • preHandle、postHandle处理
  • pre里处理将请求日志(校验不通过也记录),统一请求标识作为key放redis缓存
  • 统一请求标识想办法塞入请求参数
  • 具体执行Controller分方法,从请求参数拿到日志,补充业务日志
  • postHandle做出参记录处理(需要重写Response,懒得写,我这里就直接用的spring提供的ResponseBodyAdvice处理的,自定义一个实现它就行)
  • 开启线程异步去记录日志
            其实还有一套Controller统一分发到其他子应用的请求没有分享,没有子系统的服务的其实也不需要(要是没有子系统其实日志可以直接用aop就够了),我先说下思路:
  • controller的post、get、等常规方法统一都从传参取到url(对于子应用的接口url),
  • 通过http+url请求子应用
  • 对子应用的返回做统一转换,(不然我们这边的ResponseBodyAdvice怕不认识出参对象)
  • 子系统里也是可以取到统一请求标识的,根据这个标识取redis可以取出日志记录补充日志,再放回缓存就行

        好,就分享到这里吧,至于各个验证机制,大家自己根据业务实现吧,我这里其实也还没写实现呢,写好了可以交流下。希望能帮到大家,uping

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于缓存的统一请求日志实现 的相关文章

  • 用C语言求n阶魔方阵

    给出n值 xff0c 输出n阶魔方阵 xff08 每一行 xff0c 每一列和对角线之和都相等 xff09 1放在第一行中间从2开始以后的数按以下规则 xff1a 每个数比前一个数行数减一 xff0c 列数加一当上一个数行数为1 xff0c
  • 解决 System has not been booted with systemd as init system (PID 1). Can‘t operate.

    docker 容器使用systemctl 以ubuntu做为参考 拉取容器 直接拉取 使用 docker pull ubuntu 直接拉取 通过Dockerfile拉取 Dockerfile 基础镜像 FROM ubuntu 18 04 b
  • SQLServer批量更新、批量插入数据

    https blog csdn net fengkang511 article details 51778984 utm medium 61 distribute pc aggpage search result none task blo
  • 使用powershell 开启或关闭NLA

    关闭NLA 96 span class token variable registryPath span 61 span class token string 34 HKLM SYSTEM CurrentControlSet Control
  • SVN异常之svn:E230001 Server SSL certificate verification failed

    起因 某一天上班 xff0c 使用idea更新代码 xff0c 发现出现如下问题 xff1a Error svn E230001 Unable to connect to a repository at URL 39 https xxx x
  • 在Linux下,禁止某一个显示屏的输出

    今天接到了一个任务 xff0c 实现显示屏的关和开 理了下思路 xff1a 1 需求 xff1a 实现终端的开关屏 xff0c 要求仅仅是终端的屏幕关掉了 xff0c 但是系统仍旧在跑 xff0c 可以用遥控器唤醒 2 方向 xff1a 2
  • 十分钟搞懂字符编码

    编码方式描述ANSI编码常见于window xff0c 与windows操作系统所面向的国家地区有关在简体中文Windows操作系统中 xff0c ANSI 编码代表 GBK 编码在繁体中文Windows操作系统中 xff0c ANSI 编
  • python数字运算

    加法 num1 span class token operator 61 span span class token builtin input span span class token punctuation span span cla
  • k8s集群运维之master节点无法调度问题

    k8s集群创建完成后 xff0c 在部署应用的时候发现master节点无法部署pod 解决步骤如下 查看节点名称 master01 k8s git span class token punctuation span master span
  • Debian 9安装Clion 2018

    现在去官网直接下载 https www jetbrains com clion 解压缩 tar zxvf CLion 2018 1 5 tar gz cd clion 2018 1 5 bin clion sh CLion是收费 的 xff
  • 从用户email信息分析是否为qq邮箱并截取qq号的sql语句

    思路 xff1a 1 email是qq邮箱 2 qq邮箱也可以自己注册的 xff0c 39 64 39 前面有可能是随意填写的字符串 xff0c 过滤 39 64 39 前面不是数字的 3 截取符合邮箱 39 64 39 前面部分即为qq号
  • 解决Ubuntu虚拟机NAT不能上网的几种方法

    vmware安装ubuntu虚拟机后 xff0c 网络经常抽风 也不知道具体是什么原因导致的 有时候开机就不能上网 xff0c 有时候 xff0c 是突然不能上网 这个时候 xff0c 尝试重启虚拟机后者电脑 xff0c 看看能否解决 或者
  • 显示https不安全的原因及解决办法

    很多人在部署了https证书之后 xff0c 有的时候仍然会出现https 不安全 的提示 xff0c 这就比较纳闷了 明明说部署了https证书会受到浏览器的信任的 xff0c 怎么还会出现这种情况 xff1f 安信SSL帮大家分析一下原
  • 小程序input输入限制小数位数

    小程序input组件本身没有自带这个校验属性 xff0c 但有一个maxlength属性 xff0c 可以通过是否输入了小数点来动态计算设置maxlength的方法达到限制输入的目的 保留一位小数 lt view class 61 34 l
  • js数组对象去重

    removeID span class token operator 61 span span class token punctuation span arr span class token punctuation span span
  • 解决echarts刷新不重绘

    切换筛选条件重新查询时候Echart不重新绘制 需要绘制之前初始化Echart echarts span class token punctuation span span class token function init span sp
  • 判断是否有值,0也是有值的情况

    span class token operator span span class token function isNaN span span class token punctuation span span class token f
  • 基于高德地图SDK进行搜索

    高德地图SDK使用地址http lbs amap com 地图设置 define GDMAPKEY 64 34 key 34 import 34 ViewController h 34 import lt MapKit MapKit h g
  • Microsoft Visual C++ Build Tools.exe安装包损坏

    Python3安装支持库的过程中经常会遇到 Microsoft Visual C 14 0 is required 此时就需要安装Visual C build tools生成工具 在运行build tool安装时 提示安装包损坏 翻墙也无效
  • debian图形界面安装

    安装GNOME中文桌面环境 安装基本的X系统 apt get install x window system core 安装GNOME桌面环境 apt get install gnome 到现在为止 xff0c 我们已成功安装完成gnome

随机推荐

  • Qt 调试时 程序异常结束

    在调试时 xff0c 关闭窗口 xff0c 应用程序输出窗口提示 Qt 调试时 程序异常结束 21 20 48 程序异常结束 21 20 48 The process was ended forcefully 21 20 48 G proj
  • c#webservice的简单示例

    是webservice 就概念上来说 xff0c 可能比较复杂 xff0c 不过我们可以有个宏观的了解 xff1a webservice就是个对外的接口 xff0c 里面有 函数可供外部客户调用 xff08 注意 xff1a 里面同样有客户
  • 实现常规厂家&品牌&型号业务对接物联网平台(snack3加json赋能)

    前言 之前介绍过通过snack3快速对接物模型 xff0c 不知道大家还有没有影响 记得还留了一个作业给大家想想 xff0c 就是这么兼容多型号 多版本 xff0c 这次就来跟大家分享下这么集成多型号 一 物模型文件调整 上次是利用snac
  • 组合OSS服务实现打包业务文件zip下载

    前言 实现文件打包成zip下载 xff0c 支持zip包含目录 文件 废话不多说 xff0c 直接上码 一 设计思路 后端组织文件 xff0c 打包成zip上传到OSS存储返回文件名称给前端前端根据返回的文件名称 xff08 url xff
  • Spring Boot + Disruptor = 王炸!!

    01 背景 工作中遇到项目使用Disruptor做消息队列 对你没看错 不是Kafka 也不是rabbitmq Disruptor有个最大的优点就是快 还有一点它是开源的哦 下面做个简单的记录 02 Disruptor介绍 Disrupto
  • Dbeaver连接ES问题一站解决

    前言 最近几天一直做ES的TPS测试 xff0c 每次看数据ES的数据都在嫌麻烦 xff08 在postman指定索引通过url请求查看数据 xff09 最后决定还是整整Dbeaver连接ES 一 当前境况 1 ES版本比较老 xff0c
  • Dbeaver连接TDengine时序数据库

    前言 还是结合上一阶段的工作 xff0c 为TPS满足合同里的要求 xff0c 预研数据库切换为TDengine 所以查看数据的工具我得能连上去看 xff0c 习惯了Dbeaver xff0c 所以先把Dbeaver整的能连接使用 一 Db
  • idea+ApifoxUploader+Apifox真是内外双修,香

    前言 最近部门为整合后端组 前端组 测试组 需求组 产品组等组之间的工作流程 xff0c 旨在提高协调与高效 xff0c 其中之一就是希望开发组 xff08 后端 前端 xff09 开发的接口能及时更新 xff0c 测试组能做接口测试 xf
  • SpringBoot3+最新MybatisPlus+Mysql与TDengine双数据源

    前言 昨天写的idea 43 Apifox uploader插件 43 apifox新年第一天上班就上榜了 xff0c 真是不错 今天来补一篇 xff0c 本来应该是在前一篇之前发的 实际上就是最新的springBoot集成最新的mybat
  • 业务平台扩展支持TDengine时序数据库方案

    1 场景与架构 1 1业务架构 这里涉及项目隐私 xff0c 架构图不方便公开 大致情况就是 xff1a 应用层的园区畅行 生态宜居 安全守护是我方要交付的系统 平台层的物联网感知中台是我方平台 1 2数据架构 从数据架构看 xff0c 园
  • 物联网平台+业务平台基本架构设计与优化想法

    前言 目前的交付底座有点老 xff0c 而且集成的有点杂 xff0c 计划是要升级下 xff0c 先说想法 xff0c 看领导做不做 1 业务平台定位 我们的愿景 xff1a 通过物联平台赋能 xff0c 让数据产生价值 为客户提供可视化的
  • Qt 正则表达式匹配失败的一个原因

    在Qt中做正则表达式时 xff0c 遇到一个很坑爹的问题 xff0c 还是经验不足导致 在正则表达式中 xff0c 有很多需要元字符 xff0c 是需要使用普通字符加转义符号搭配使用的 比如 w xff0c s 对于这类字符 xff0c 在
  • C++对象模型(整理)

    C 43 43 对象模型 1 何为C 43 43 对象模型 xff1f C 43 43 对象模型可以概括为以下2部分 xff1a 语言中直接支持面向对象程序设计的部分 面向对象程序设计部分 xff1a 如构造函数 析构函数 虚函数 继承 x
  • MybatisPlus多表查询之零sql编写实现

    1 前言 年初节奏还没有快起来 xff0c 适合做做技术前瞻 xff0c 无论是对个人还是团队都是好事 真要说分享 xff0c 其实感觉也没啥好分享的 xff0c 就像接手新项目一样 xff0c 代码都是自己看 xff0c 别人讲的再多 x
  • 物联网设备流水入库TDengine改造方案

    1 存储机制改造 针对提出的流水查询需求 xff0c 结合设备上报数据讨论后 xff0c 初步定以下几种方案 1 1 ES gt TDengine 1 ES入库有延时 2 ES没有提供数据监听API xff08 不能主动监听 xff0c 实
  • SpringBoot3集成Kafka优雅实现信息消费发送

    前言 首先 xff0c 你的JDK是否已经是8 43 了呢 xff1f 其次 xff0c 你是否已经用上SpringBoot3了呢 xff1f 最后 xff0c 这次分享的是SpringBoot3下的kafka发信息与消费信息 一 场景说明
  • SpringBoot3集成TDengine自适应裂变存储

    前言 首先很遗憾的告诉大家 xff0c 今天这篇分享要关注才可以看了 原因是穷啊 xff0c 现在基本都是要人民币玩家了 xff0c 就比如chatGPT copilot xff0c 这些AI虽然都是可以很好的辅助编码 xff0c 但是都是
  • Github Copilot编码神剑

    前言 今天跟大家分享的其实是现在比较火的Github copilot xff0c 另外 xff0c 就是分享下它的优雅使用 其实知道用这个以后 xff0c 瑟瑟发抖 xff0c 感觉就要失业了 不过真正用过后 xff0c 其实发现这要完全取
  • 一个基于缓存的业务链路日志记录设计方案

    前言 一个简单的全接口日志设计文档 xff0c 先分享设计文档 xff0c 后面分享核心设计代码 1 对外接口设计原则 1 1 基本原则 对外接口要遵守的基本原则 xff1a 1 开闭原则 2 依赖倒置原则 3 单一职责原则 4 接口隔离原
  • 基于缓存的统一请求日志实现

    前言 昨天说的会分享方案的具体实现 xff0c 今天就来分享想核心代码吧 一 思路 我们用的是springboot组服务 xff0c 每个链路通过http请求 所以要实现全链路的日志记录 xff0c 也都是围绕sprinboot做的处理 这