新增:新增系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等),新增操作日志引擎,记录 HTTP 请求信息

This commit is contained in:
2022-12-25 13:16:15 +08:00
parent 78e84e8941
commit 727850933f
28 changed files with 1523 additions and 12 deletions

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>continew-admin</artifactId>
<groupId>top.charles7c</groupId>
<version>${revision}</version>
</parent>
<artifactId>continew-admin-monitor</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等)</description>
<dependencies>
<!-- 公共模块(存放公共工具类,公共配置等) -->
<dependency>
<groupId>top.charles7c</groupId>
<artifactId>continew-admin-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,41 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.annotation;
import java.lang.annotation.*;
/**
* 操作日志注解(用于接口方法或类上)
*
* @author Charles7c
* @since 2022/12/23 20:00
*/
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
/**
* 操作日志描述
*/
String value() default "";
/**
* 是否忽略日志记录
*/
boolean ignore() default false;
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.charles7c.cnadmin.monitor.interceptor.LogInterceptor;
/**
* 监控模块 Web MVC 配置
*
* @author Charles7c
* @since 2022/12/24 23:15
*/
@EnableWebMvc
@Configuration
@RequiredArgsConstructor
public class WebMvcMonitorConfiguration implements WebMvcConfigurer {
private final LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.config.properties;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 操作日志配置属性
*
* @author Charles7c
* @since 2022/12/24 23:04
*/
@Data
@Component
@ConfigurationProperties(prefix = "logging.operation")
public class LogProperties {
/**
* 是否启用操作日志
*/
private Boolean enabled = false;
/**
* 脱敏字段
*/
private List<String> desensitize = new ArrayList<>();
/**
* 不记录操作日志的请求方式
*/
private List<String> excludeMethods = new ArrayList<>();
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 操作日志级别枚举
*
* @author Charles7c
* @since 2022/12/25 9:09
*/
@Getter
@RequiredArgsConstructor
public enum LogLevelEnum {
/** 普通 */
INFO("普通"),
/** 错误 */
ERROR("错误"),;
/** 描述 */
private final String description;
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.filter;
import java.io.IOException;
import java.util.Objects;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
/**
* 操作日志过滤器(缓存请求和响应体过滤器)
*
* <p>
* 由于 requestBody 和 responseBody 分别对应的是 InputStream 和 OutputStream由于流的特性读取完之后就无法再被使用了。 所以,需要额外缓存一次流信息。
* </p>
*
* @author Charles7c
* @since 2022/12/24 21:16
*/
@Component
public class LogFilter extends OncePerRequestFilter implements Ordered {
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 包装流,可重复读取
if (!(request instanceof ContentCachingRequestWrapper)) {
request = new ContentCachingRequestWrapper(request);
}
if (!(response instanceof ContentCachingResponseWrapper)) {
response = new ContentCachingResponseWrapper(response);
}
filterChain.doFilter(request, response);
updateResponse(response);
}
/**
* 更新响应(不操作这一步,会导致接口响应空白)
*
* @param response
* 响应对象
* @throws IOException
* /
*/
private void updateResponse(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
Objects.requireNonNull(responseWrapper).copyBodyToResponse();
}
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.interceptor;
import java.util.Date;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import io.swagger.v3.oas.annotations.Operation;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import top.charles7c.cnadmin.common.model.dto.OperationLog;
import top.charles7c.cnadmin.common.util.IpUtils;
import top.charles7c.cnadmin.common.util.ServletUtils;
import top.charles7c.cnadmin.common.util.helper.LoginHelper;
import top.charles7c.cnadmin.common.util.holder.LogContextHolder;
import top.charles7c.cnadmin.monitor.annotation.Log;
import top.charles7c.cnadmin.monitor.config.properties.LogProperties;
import top.charles7c.cnadmin.monitor.enums.LogLevelEnum;
import top.charles7c.cnadmin.monitor.model.entity.SysLog;
/**
* 操作日志拦截器
*
* @author Charles7c
* @since 2022/12/24 21:14
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class LogInterceptor implements HandlerInterceptor {
private final LogProperties operationLogProperties;
@Override
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
@NotNull Object handler) {
if (!checkIsNeedRecord(handler, request)) {
return true;
}
// 记录操作时间
this.logCreateTime();
return true;
}
@Override
public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
@NotNull Object handler, Exception e) {
// 记录请求耗时及异常信息
SysLog sysLog = this.logElapsedTimeAndException();
if (sysLog == null) {
return;
}
// 记录描述
this.logDescription(sysLog, handler);
// 记录请求信息
this.logRequest(sysLog, request);
// 记录响应信息
this.logResponse(sysLog, response);
// 保存操作日志
SpringUtil.getApplicationContext().publishEvent(sysLog);
}
/**
* 记录操作时间
*/
private void logCreateTime() {
OperationLog operationLog = new OperationLog();
operationLog.setCreateUser(LoginHelper.getUserId());
operationLog.setCreateTime(new Date());
LogContextHolder.set(operationLog);
}
/**
* 记录请求耗时及异常信息
*
* @return 日志信息
*/
private SysLog logElapsedTimeAndException() {
OperationLog operationLog = LogContextHolder.get();
if (operationLog != null) {
LogContextHolder.remove();
SysLog sysLog = new SysLog();
sysLog.setCreateTime(operationLog.getCreateTime());
sysLog.setElapsedTime(System.currentTimeMillis() - sysLog.getCreateTime().getTime());
sysLog.setLogLevel(LogLevelEnum.INFO);
// 记录异常信息
Exception exception = operationLog.getException();
if (exception != null) {
sysLog.setLogLevel(LogLevelEnum.ERROR);
sysLog.setException(ExceptionUtil.stacktraceToString(operationLog.getException(), -1));
}
return sysLog;
}
return null;
}
/**
* 记录日志描述
*
* @param sysLog
* 日志信息
* @param handler
* 处理器
*/
private void logDescription(@NotNull SysLog sysLog, Object handler) {
HandlerMethod handlerMethod = (HandlerMethod)handler;
Operation methodOperation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Operation.class);
Log methodLog = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Log.class);
if (methodOperation != null) {
sysLog.setDescription(
StrUtil.isNotBlank(methodOperation.summary()) ? methodOperation.summary() : "请在该接口方法上指定操作日志描述");
}
// 例如:@Log("获取验证码") -> 获取验证码
if (methodLog != null && StrUtil.isNotBlank(methodLog.value())) {
sysLog.setDescription(methodLog.value());
}
}
/**
* 记录请求信息
*
* @param sysLog
* 日志信息
* @param request
* 请求对象
*/
private void logRequest(@NotNull SysLog sysLog, @NotNull HttpServletRequest request) {
sysLog.setRequestUrl(StrUtil.isBlank(request.getQueryString()) ? request.getRequestURL().toString()
: request.getRequestURL().append("?").append(request.getQueryString()).toString());
sysLog.setRequestMethod(request.getMethod());
sysLog.setRequestHeader(this.desensitize(ServletUtil.getHeaderMap(request)));
String requestBody = this.getRequestBody(request);
if (StrUtil.isNotBlank(requestBody)) {
sysLog.setRequestBody(this.desensitize(
JSONUtil.isTypeJSON(requestBody) ? JSONUtil.parseObj(requestBody) : ServletUtil.getParamMap(request)));
}
sysLog.setRequestIp(ServletUtil.getClientIP(request));
sysLog.setLocation(IpUtils.getCityInfo(sysLog.getRequestIp()));
sysLog.setBrowser(ServletUtils.getBrowser(request));
sysLog.setCreateUser(sysLog.getCreateUser() == null ? LoginHelper.getUserId() : sysLog.getCreateUser());
}
/**
* 记录响应信息
*
* @param sysLog
* 日志信息
* @param response
* 响应对象
*/
private void logResponse(SysLog sysLog, HttpServletResponse response) {
sysLog.setStatusCode(response.getStatus());
sysLog.setResponseHeader(this.desensitize(ServletUtils.getHeaderMap(response)));
// 响应体(不记录非 JSON 响应数据)
String responseBody = this.getResponseBody(response);
if (StrUtil.isNotBlank(responseBody) && JSONUtil.isTypeJSON(responseBody)) {
sysLog.setResponseBody(responseBody);
}
}
/**
* 数据脱敏
*
* @param waitDesensitizeData
* 待脱敏数据
* @return 脱敏后的 JSON 字符串数据
*/
private String desensitize(Map waitDesensitizeData) {
String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData);
try {
if (CollUtil.isEmpty(waitDesensitizeData)) {
return desensitizeDataStr;
}
for (String desensitizeProperty : operationLogProperties.getDesensitize()) {
waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> "****************");
waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> "****************");
waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> "****************");
}
return JSONUtil.toJsonStr(waitDesensitizeData);
} catch (Exception ignored) {
}
return desensitizeDataStr;
}
/**
* 获取请求体
*
* @param request
* 请求对象
* @return 请求体
*/
private String getRequestBody(HttpServletRequest request) {
String requestBody = "";
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
requestBody = StrUtil.utf8Str(wrapper.getContentAsByteArray());
}
return requestBody;
}
/**
* 获取响应体
*
* @param response
* 响应对象
* @return 响应体
*/
private String getResponseBody(HttpServletResponse response) {
String responseBody = "";
ContentCachingResponseWrapper wrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (wrapper != null) {
responseBody = StrUtil.utf8Str(wrapper.getContentAsByteArray());
}
return responseBody;
}
/**
* 检查是否要记录操作日志
*
* @param handler
* /
* @param request
* /
* @return true 需要记录false 不需要记录
*/
private boolean checkIsNeedRecord(Object handler, HttpServletRequest request) {
// 1、未启用时不需要记录操作日志
if (!(handler instanceof HandlerMethod) || Boolean.FALSE.equals(operationLogProperties.getEnabled())) {
return false;
}
// 2、排除不需要记录日志的接口
HandlerMethod handlerMethod = (HandlerMethod)handler;
Log methodLog = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Log.class);
// 2.1 请求方式不要求记录且请求上没有 @Log 注解,则不记录操作日志
if (operationLogProperties.getExcludeMethods().contains(request.getMethod()) && methodLog == null) {
return false;
}
// 2.2 如果接口上既没有 @Log 注解,也没有 @Operation 注解,则不记录操作日志
Operation methodOperation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Operation.class);
if (methodLog == null && methodOperation == null) {
return false;
}
// 2.3 如果接口被隐藏,不记录操作日志
if (methodOperation != null && methodOperation.hidden()) {
return false;
}
// 2.4 如果接口上有 @Log 注解,但是要求忽略该接口,则不记录操作日志
return methodLog == null || !methodLog.ignore();
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import top.charles7c.cnadmin.monitor.model.entity.SysLog;
/**
* 操作日志 Mapper
*
* @author Charles7c
* @since 2022/12/22 21:47
*/
public interface LogMapper extends BaseMapper<SysLog> {}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.model.entity;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import top.charles7c.cnadmin.monitor.enums.LogLevelEnum;
/**
* 操作日志实体
*
* @author Charles7c
* @since 2022/12/25 9:11
*/
@Data
@TableName("sys_log")
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 日志ID
*/
@TableId
private Long logId;
/**
* 日志级别
*/
private LogLevelEnum logLevel;
/**
* 日志描述
*/
private String description;
/**
* 请求 URL
*/
private String requestUrl;
/**
* 请求方式
*/
private String requestMethod;
/**
* 请求头
*/
private String requestHeader;
/**
* 请求体
*/
private String requestBody;
/**
* 状态码
*/
private Integer statusCode;
/**
* 响应头
*/
private String responseHeader;
/**
* 响应体
*/
private String responseBody;
/**
* 请求耗时ms
*/
private Long elapsedTime;
/**
* 请求IP
*/
private String requestIp;
/**
* 操作地址
*/
private String location;
/**
* 浏览器
*/
private String browser;
/**
* 异常
*/
private String exception;
/**
* 操作人
*/
private Long createUser;
/**
* 操作时间
*/
private Date createTime;
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.service;
/**
* 操作日志业务接口
*
* @author Charles7c
* @since 2022/12/23 20:12
*/
public interface LogService {
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import top.charles7c.cnadmin.monitor.mapper.LogMapper;
import top.charles7c.cnadmin.monitor.model.entity.SysLog;
import top.charles7c.cnadmin.monitor.service.LogService;
/**
* 操作日志业务实现类
*
* @author Charles7c
* @since 2022/12/23 20:12
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class LogServiceImpl implements LogService {
private final LogMapper logMapper;
@Async
@EventListener
public void save(SysLog sysLog) {
logMapper.insert(sysLog);
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="top.charles7c.cnadmin.monitor.mapper.LogMapper">
</mapper>