記錄透傳日誌功能設計代碼(升級版本)

設計思想:
  很簡單請求到方法執行攔截器,存到表裏,表有兩個,一個字典表table1,記錄請求接口和請求實體的屬性值和ext擴展表的列的對應關係。第二個擴展表table2,存真正的業務數據,so easy
 
使用方法:
  在方法上增加 註解@AutoLog 支持多個實體,類屬性和參數上增加@AutoLogField註解:

新版本支持的功能場景:

1.支持controller,service等spring管理的bean

2.支持多實體重名:A:name,B:name

3.支持實體類和參數上增加註解

4.支持post,put,get,delete等請求

5.自動搜索所有參數實體的@AutoLogField註解

 
原理:配置相關注解以後,第一次請求時會自動把相關字段存字典表,設置了緩存,減少請求次數,如果後續請求參數增加屬性需要入庫需要執行插入sql插入mysql
舉例:RequestVo增加了一個字段  name,name上增加註解@AutoLogField,數據庫需要執行:
select * from  table1 where service_name ='**.getResultByIdNo'; 到方法
insert table1(service_name,column_name,ext_column_name) value(**.getResultByIdNo","name","ext3");
ext3來自table2還未使用的字段。
 
優點:和業務邏輯代碼解耦,提高開發效率,異步線程池入庫不影響原來邏輯,
     日誌參數可視化(相較於直接存json),擴展表可以增加索引查詢(相較於直接存json),
     避免了json大字段,打印了log日誌兜底
 
缺點:1.新增字段需要執行sql(也可以做成自動取校驗,那樣就要控制查表得頻率,訪問字典表得情況就會變多)
     2.考慮到mysql表的字段大小,目前支持20個擴展字段,根據反射的字段順序截取
     3.針對map,list,業務對象屬性目前還是保存的json
 
 
注意:下面代碼只支持controller和get post請求,如果需要升級版本的代碼請聯繫我1076472549
代碼和表結構:
部分包路徑屏蔽了
 
1.攔截器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 自動日誌攔截器
 *
 * @author yuJie
 * @since 2020/5/16
 */
@Component
public class AutoLogInterceptor implements HandlerInterceptor {
    private static final ExecutorService threadPool
            = new ThreadPoolExecutor(4, 10, 10000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));
    @Autowired
    private AutomaticLoggingService automaticLoggingService;

    /**
     * 預處理
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        AutoLog methodAnnotation = method.getAnnotation(AutoLog.class);
        if (methodAnnotation != null) {
            RequestLogVo requestLogVo = RequestLogVo.builder()
                    .reqParam(RequestHandleUtil.getReqParam(request))
                    .uri(request.getRequestURI())
                    .methodName(method.getName())
                    .requestService(handlerMethod.getBeanType())
                    .requestVO(methodAnnotation.value())
                    .build();

            threadPool.submit(() -> {
                automaticLoggingService.insertLog(requestLogVo);
            });
        }
    }

}

實體類:

ExtEntity(table2):
public static final int EXT_COLUMN_SIZE = 30;
    @TableId(type = IdType.AUTO)
    private Long id;
    private String serviceName;
    private String ext0;
    private String ext1;
    private String ext2;
    private String ext3;
    private String ext4;
    private String ext5;
    private String ext6;
    private String ext7;
    private String ext8;
    private String ext9;
    private String ext10;
    private String ext11;
    private String ext12;
    private String ext13;
    private String ext14;
    private String ext15;
    private String ext16;
    private String ext17;
    private String ext18;
    private String ext19;
    private String ext20;

 

2.字典表:

 @TableId(type = IdType.AUTO)
    private Long id;

    private String uri;
    /**
     * 唯一,service接口的全限定名
     */
    private String serviceName;

    private String columnName;

    private String extColumnName;

    public FieldDictEntity(String uri, String serviceName, String columnName, String extColumnName) {
        this.uri = uri;
        this.serviceName = serviceName;
        this.columnName = columnName;
        this.extColumnName = extColumnName;
    }

攔截請求後封裝類:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class RequestLogVo {

    private String uri;

    private String methodName;
    private Class<?> requestService;
    private Class<?>[] requestVO;
    private Map<String, String> reqParam;
}

兩個註解類:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoLog {
    Class<?>[] value();
}

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoLogField {
}
WebMvcConfigurationSupport配置:
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(AutoLogInterceptor);
    registry.addInterceptor(autoIdempotentInterceptor);
    super.addInterceptors(registry);
}
核心業務處理類:
AutomaticLoggingServiceImpl
package com.ddmc.das.utils.businesslog.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author yuJie
 * @since 2020/5/25
 */
@Slf4j
@Service
public class AutomaticLoggingServiceImpl implements AutomaticLoggingService {

    private static final String AUTO_LOG_PREX = "**:";
    @Autowired
    public ExtMapper extMapper;

    @Autowired
    public MasterLogMapper masterLogMapper;

    @Autowired
    public FieldDictServiceImpl fieldDictServiceImpl;
    @Autowired
    private RedisTemplate<String, Object> solarRedisTemplate;

    @Override
    @SysLogPoint
    @Transactional(rollbackFor = Exception.class)
    public void insertLog(RequestLogVo requestLogVo) {
        // TODO 增加緩存
         if (!solarRedisTemplate.hasKey(AUTO_LOG_PREX + getMethodName(requestLogVo))) {
        initLog(requestLogVo);
        }
        //入庫
        insertExtLog(requestLogVo);
    }

    private void insertExtLog(RequestLogVo requestLogVo) {
        try {
            QueryWrapper<FieldDictEntity> Wrapper = new QueryWrapper();
            Wrapper.eq("service_name", getMethodName(requestLogVo));
            List<FieldDictEntity> fieldDictList = fieldDictServiceImpl.list(Wrapper);
            Assert.notEmpty(fieldDictList, "自定記錄日誌失敗字典表爲空:" + requestLogVo.getMethodName());
            Map<String, String> columnMap = fieldDictList.stream().collect(Collectors.toMap(FieldDictEntity::getColumnName, FieldDictEntity::getExtColumnName));
            ExtEntity extEntity = generateExtValue(columnMap, requestLogVo.getReqParam());
            extEntity.setServiceName(getMethodName(requestLogVo));
            extMapper.insert(extEntity);
        } catch (Exception e) {
            log.error("插入ExtLog失敗:{}", e);
        }
    }

    /**
     * columnMap:{"columnName":"extColumnName"}
     * valueMap:{"columnName":value}
     * 設置ext表的擴展屬性
     *
     * @param columnMap
     * @param valueMap
     * @return
     */
    private final ExtEntity generateExtValue(final Map<String, String> columnMap, final Map<String, String> valueMap) {
        try {
            Class clazz = ExtEntity.class;
            Field[] fields = clazz.getDeclaredFields();
            ExtEntity extEntity = (ExtEntity) clazz.newInstance();
            for (Map.Entry<String, String> entry : valueMap.entrySet()) {
                String extColumnName = columnMap.get(entry.getKey());
                for (Field f : fields) {
                    f.setAccessible(true);
                    if (f.getName().equals(extColumnName)) {
                        f.set(extEntity, entry.getValue());
                    }
                }
            }
            return extEntity;
        } catch (Exception e) {
            log.error("生成自動日誌擴展表失敗:{}", e);
        }
        return null;
    }

    /**
     * 根據訪問接口的數據初始化日誌主表和字典表
     *
     * @param requestLogVo
     */
    private void initLog(RequestLogVo requestLogVo) {
        try {
            QueryWrapper<FieldDictEntity> Wrapper = new QueryWrapper();
            Wrapper.eq("service_name", getMethodName(requestLogVo));
            List<FieldDictEntity> fieldDictList = fieldDictServiceImpl.list(Wrapper);
            if (CollectionUtils.isEmpty(fieldDictList)) {
                List<String> columns = new LinkedList<>();
                for (Class<?> cla : requestLogVo.getRequestVO()) {
                    generateField(cla, columns);
                }
                List<FieldDictEntity> dictEntityList = ConvertToFieldDictList(columns, requestLogVo);
                BusinessAssert.OBJECT_NOT_NULL.assertNotEmpty(dictEntityList);
                fieldDictServiceImpl.saveBatch(dictEntityList);
            }
            solarRedisTemplate.opsForValue().setIfAbsent(AUTO_LOG_PREX + getMethodName(requestLogVo), "exist");
        } catch (Exception e) {
            log.error("初始化日誌字典表失敗:{}", e);
        }
    }

    private final List<FieldDictEntity> ConvertToFieldDictList(List<String> columns, RequestLogVo requestLogVo) {
        int length = Math.min(columns.size(), ExtEntity.EXT_COLUMN_SIZE);
        List<FieldDictEntity> list = new ArrayList<>();
        for (int i = 0; i < length; i++) {
            list.add(new FieldDictEntity(requestLogVo.getUri(), getMethodName(requestLogVo), columns.get(i), "ext" + i));
        }
        return list;
    }

    private static String getMethodName( RequestLogVo requestLogVo){
        return requestLogVo.getRequestService().getName() + "." + requestLogVo.getMethodName();
    }

    /***
     * @param vo
     * @param columnMap
     */
    private void generateField(Class vo, List<String> columnMap) {
        Field[] fs = vo.getDeclaredFields();
        for (Field f : fs) {
            //PUBLIC: 1 PRIVATE: 2 PROTECTED: 4
            if (f.getModifiers() == 1 || f.getModifiers() == 2 || f.getModifiers() == 4) {
                AutoLogField autoLogField = (AutoLogField) f.getAnnotation(AutoLogField.class);
                if (autoLogField != null) {
                    columnMap.add(f.getName());
                }
            }
        }
    }
}
RequestHandleUtil:
package com.ddmc.utils.http;


import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 類作用描述
 *
 * @author yuJie
 * @since 2020/5/18
 */
public class RequestHandleUtil {

    public static final String METHOD_POST = "POST";
    public static final String METHOD_GET = "GET";
    public static final String CONTENT_TYPE_JSON = "application/json";

    /**
     * 獲取請求參數
     *
     * @param req
     * @return 請求參數格式key-value
     */
    public static String getReqParam(HttpServletRequest req, String key) {
        String method = req.getMethod();
        Map<String, String> reqMap = new HashMap<String, String>();
        if (METHOD_GET.equals(method)) {
            reqMap = doGet(req);
        } else if (METHOD_POST.equals(method)) {
            reqMap = doPost(req);
        } else {
            //其他請求方式暫不處理
            return "";
        }
        return reqMap.get(key);
    }

    /**
     * 獲取請求參數
     *
     * @param req
     * @return 請求參數格式key-value
     */
    public static Map<String, String> getReqParam(HttpServletRequest req) {
        String method = req.getMethod();
        Map<String, String> reqMap = new HashMap<String, String>();
        if (METHOD_GET.equals(method)) {
            reqMap = doGet(req);
        } else if (METHOD_POST.equals(method)) {
            reqMap = doPost(req);
        } else {
            //其他請求方式暫不處理
            return new HashMap<>();
        }
        return reqMap;
    }

    private static Map<String, String> doGet(HttpServletRequest req) {
        String param = req.getQueryString();
        return getParamsToMap(param);
    }

    private static Map<String, String> doPost(HttpServletRequest request) {
        Map<String, String> map = new HashMap<>();
        if (request instanceof BodyReaderHttpServletRequestWrapper) {
            map = (Map) JSON.parse(((BodyReaderHttpServletRequestWrapper) request).getBodyStr());
        }
        return map;
    }

    public static Map<String, String> getParamsToMap(String params) {
        Map<String, String> map = new LinkedHashMap<>();
        if (StringUtils.isNotBlank(params)) {
            String[] array = params.split("&");
            for (String pair : array) {
                if ("=".equals(pair.trim())) {
                    continue;
                }
                String[] entity = pair.split("=");
                if (entity.length == 1) {
                    map.put(decode(entity[0]), null);
                } else {
                    map.put(decode(entity[0]), decode(entity[1]));
                }
            }
        }
        return map;
    }

    public static Map<String, String> jsonParamsToMap(String params) {
        Map<String, String> map = new LinkedHashMap<>();
        if (StringUtils.isNotBlank(params)) {
            String[] array = params.split(",");
            for (String pair : array) {
                String[] entity = pair.split(":");
                if (entity.length == 1) {
                    map.put(decode(entity[0]), null);
                } else {
                    map.put(decode(entity[0]), decode(entity[1]));
                }
            }
        }
        return map;
    }

    /**
     * 編碼格式轉換
     *
     * @param content
     * @return
     */
    public static String decode(String content) {
        try {
            return URLDecoder.decode(content, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return "";
    }
}

另外還需要解析post請求的body流

BodyReaderHttpServletRequestWrapper
package com.ddmc.utils.http;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;

/**
 * 類作用描述
 *
 * @author yuJie
 * @since 2020/5/18
 */
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;
    private String bodyStr;

    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        String bodyString = getBodyString(request);
        body = bodyString.getBytes(Charset.forName("UTF-8"));
        bodyStr = bodyString;
    }

    public String getBodyStr() {
        return bodyStr;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);

        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }


    public 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();
    }
}

表:

CREATE TABLE `table1` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '租戶號',
  `uri` varchar(200) NOT NULL DEFAULT '' COMMENT '請求地址',
  `service_name` varchar(100) NOT NULL DEFAULT '' COMMENT '接口名',
  `column_name` varchar(20) DEFAULT NULL COMMENT '列名',
  `ext_column_name` varchar(20) DEFAULT NULL COMMENT '擴展表的列名',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COMMENT='table1'

CREATE TABLE `table2` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '租戶號',
  `service_name` varchar(200) NOT NULL DEFAULT '' COMMENT '接口名',
`ext0` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
  `ext1` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext2` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext3` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext4` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext5` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext6` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext7` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext8` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext9` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext10` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext11` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext12` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext13` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext14` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext15` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext16` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext17` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext18` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext19` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
 `ext20` varchar(200) NOT NULL DEFAULT '' COMMENT '擴展字段',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COMMENT='table2'

 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章