SpringBoot实现自定义日志注解

版本信息

  • JDK21
  • SpringBoot3.3.0
  • Knife4.5.0
  • Shiro2.0.1
  • Jackson2.17.1
  • Maven3.9.6
  • IDEA2024.1

一、需求

    通过自定义日志注解,实现对请求信息的记录。按照请求方法(GET/POST等),请求地址、请求人员、请求来源等不同来进行记录,方便日后进行日志审计。

二、依赖

    在SpringBoot2.x与SpringBoot3.x中只针对于javax...jakarta...的引用的不同,其余均可复用,这里以最新当前版本信息为准。
    以下仅列出该项目涉及到的部分核心依赖

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.0</version>
    <relativePath/>
</parent>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>21</java.version>
    <!-- 更新log4j2版本包:https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
    <log4j2.version>2.23.1</log4j2.version>
    <!-- 打包时跳过测试环境 -->
    <skipTests>true</skipTests>
    ...
    <!--
        SpringBoot3.x集成knife4j
        knife4j-openapi3-jakarta-spring-boot-starter:https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter
    -->
    <knife4j.version>4.5.0</knife4j.version>
    <!--
        SpringBoot集成shiro:https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring
        shiro-ehcache:https://mvnrepository.com/artifact/org.apache.shiro/shiro-ehcache
    -->
    <shiro-spring.version>2.0.1</shiro-spring.version>
    <!-- jackson:https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
    <jackson.version>2.17.1</jackson.version>
    ...
</properties>
<dependencies>
    ...
    <!-- Knife4j -->
    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
        <version>${knife4j.version}</version>
    </dependency>

    <!-- shiro 依赖 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <classifier>jakarta</classifier>
        <version>${shiro-spring.version}</version>
        <!-- 排除仍使用了javax.servlet的依赖 -->
        <exclusions>
            <exclusion>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-core</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-web</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>${shiro-spring.version}</version>
    </dependency>
    <!-- 引入适配jakarta的依赖包 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <classifier>jakarta</classifier>
        <version>${shiro-spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <classifier>jakarta</classifier>
        <version>${shiro-spring.version}</version>
        <exclusions>
            <exclusion>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!--jackson-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>${jackson.version}</version>
    </dependency>
    ...
</dependencies>

三、步骤

1.自定义常量扩展类<ConstantsExpand.java>

package cn.keyidea.common.constant;

/**
 * 服务端自定义变量(扩展)
 *
 * @author qyd
 * @date 2022-10-17
 */
public final class ConstantsExpand
{

    /**
     * 标记类是否进行日志记录
     * 说明:不记录的情况主要是针对在操作日志类本身的查询
     */
    public interface SysLogFlag
    {
        int FLASE = 0;  // 不记录
        int TRUE = 1;   // 记录
    }

    /**
     * 业务来源
     */
    public interface SourceType
    {
        int INSIDE_SYS = 0;     // 系统日志(系统内日志,系统产生,如定时任务、告警等)
        int INSIDE_USER = 1;    // 用户日志(系统内日志,用户行为日志,用户操作产生的日志)
        int OUTSIDE = 2;        // 系统外日志(第三方日志,可根据不同第三方继续细化日志来源)
    }

    /**
     * 业务类型
     */
    public interface ServiceType
    {
        int ADD = 1;       // 新增
        int DELETE = 2;    // 删除
        int UPDATE = 3;    // 更新
        int QUERY = 4;     // 查询
        int LOGIN = 5;     // 登陆
        int LOGOUT = 6;    // 登出
        int EXPORT = 7;    // 导出
        int IMPORT = 8;    // 导入
        int SYNC = 9;      // 同步
        int UPLOAD = 10;   // 上传
        int DOWNLOAD = 11; // 下载
        int OTHER = 99;    // 其他
    }

}

2.日志表设计

日志类实体<SysLog.java>

package cn.keyidea.sys.entity;

import cn.keyidea.common.constant.ConstantsExpand;
import cn.keyidea.common.constant.StatusCode;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;

import java.io.Serial;
import java.io.Serializable;
import java.util.Date;

/**
 * 日志记录
 *
 * @author qyd
 * @date 2022-10-17
 */
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@TableName("sys_log")
@Schema(name = "SysLog", description = "系统日志")
@Entity
@Table(name = "sys_log", indexes = {
        @Index(name = "index_user_name", columnList = "user_name"),
        @Index(name = "index_module", columnList = "module"),
        @Index(name = "index_service_type", columnList = "service_type"),
        @Index(name = "index_source_type", columnList = "source_type"),
        @Index(name = "index_request_method", columnList = "request_method"),
        @Index(name = "index_ip", columnList = "ip"),
        @Index(name = "index_create_date", columnList = "create_date"),
        @Index(name = "index_response_code", columnList = "response_code"),
})
@Comment("系统日志表")
public class SysLog implements Serializable
{
    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * GeneratedValuez注解中strategy属性:提供四种值
     * -AUTO主键由程序控制, 是默认选项 ,不设置就是这个
     * -IDENTITY 主键由数据库生成, 采用数据库自增长, Oracle不支持这种方式
     * -SEQUENCE 通过数据库的序列产生主键, MYSQL  不支持
     * -Table 提供特定的数据库产生主键, 该方式更有利于数据库的移植
     */
    @Schema(name = "id", description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @Id
    @TableId(value = "id", type = IdType.AUTO)
    @Column(name = "id", columnDefinition = "bigint(20) unsigned not null COMMENT '主键ID'")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * 用户名称,如果是第三方调用,不走验证,可以缺省
     */
    @Schema(name = "userName", description = "用户名称", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "user_name", columnDefinition = "varchar(20) COMMENT '用户名称'")
    private String userName;

    @Schema(name = "module", description = "模块名称", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "module", columnDefinition = "varchar(50) not null COMMENT '模块名称'")
    private String module;

    @Schema(name = "serviceDesc", description = "业务描述", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "service_desc", columnDefinition = "varchar(100) not null COMMENT '业务描述'")
    private String serviceDesc;

    /**
     * 业务类型,参见{@link ConstantsExpand.ServiceType}
     */
    @Schema(name = "serviceType", description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "service_type", columnDefinition = "int(10) not null COMMENT '业务类型'")
    private Integer serviceType;

    /**
     * 业务来源,参见{@link ConstantsExpand.SourceType}
     * 0-系统日志(系统内日志,系统产生,如定时任务、告警等)
     * 1-用户日志(系统内日志,用户行为日志,用户操作产生的日志)
     * 2-系统外日志(第三方日志,可根据不同第三方继续细化日志来源)
     */
    @Schema(name = "sourceType", description = "业务来源:0-系统日志(系统内日志,系统产生,如定时任务、告警等)、1-用户日志(系统内日志,用户行为日志,用户操作产生的日志)、2-系统外日志(第三方日志,可根据不同第三方继续细化日志来源)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    @Column(name = "source_type", columnDefinition = "int(2) not null COMMENT '业务来源:0-系统日志(系统内日志,系统产生,如定时任务、告警等)、1-用户日志(系统内日志,用户行为日志,用户操作产生的日志)、2-系统外日志(第三方日志,可根据不同第三方继续细化日志来源)'")
    private Integer sourceType;

    @Schema(name = "requestMethod", description = "请求方式", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "request_method", columnDefinition = "varchar(10) not null COMMENT '请求方式'")
    private String requestMethod;

    @Schema(name = "className", description = "类名", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "class_name", columnDefinition = "varchar(200) not null COMMENT '类名'")
    private String className;

    @Schema(name = "methodName", description = "方法名称", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "method_name", columnDefinition = "varchar(100) not null COMMENT '方法名称'")
    private String methodName;

    @Schema(name = "payload", description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "payload", columnDefinition = "varchar(1000) COMMENT '请求参数'")
    private String payload;

    @Schema(name = "ip", description = "IP地址", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "ip", columnDefinition = "varchar(20) not null COMMENT 'IP地址'")
    private String ip;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @Schema(name = "createDate", description = "执行时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "create_date", columnDefinition = "datetime not null COMMENT '执行时间'")
    private Date createDate;

    @Schema(name = "responseTime", description = "响应时间,单位:毫秒", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "response_time", columnDefinition = "int(10) not null COMMENT '响应时间,单位:毫秒'")
    private Integer responseTime;

    /**
     * 执行状态,参见:{@link StatusCode}
     */
    @Schema(name = "responseCode", description = "执行状态:1000-成功、1001-非法字段、1002-系统忙、1003-无接口访问权限、1004-请求失败、1005-未授权、2001-TOKEN失效", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "response_code", columnDefinition = "int(2) not null COMMENT '执行状态:1000-成功、1001-非法字段、1002-系统忙、1003-无接口访问权限、1004-请求失败、1005-未授权、2001-TOKEN失效'")
    private Integer responseCode;

    /**
     * 说明,desc与describe是MySQL关键字
     */
    @Schema(name = "descInfo", description = "执行描述", requiredMode = Schema.RequiredMode.REQUIRED)
    @Column(name = "desc_info", columnDefinition = "varchar(2000) COMMENT '执行描述'")
    private String descInfo;
}

日志表

CREATE TABLE `sys_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `user_name` varchar(20) DEFAULT NULL COMMENT '用户名称',
  `module` varchar(50) NOT NULL COMMENT '模块名称',
  `service_desc` varchar(100) NOT NULL COMMENT '业务描述',
  `service_type` int(10) NOT NULL COMMENT '业务类型',
  `source_type` int(2) NOT NULL COMMENT '业务来源:0-系统日志(系统内日志,系统产生,如定时任务、告警等)、1-用户日志(系统内日志,用户行为日志,用户操作产生的日志)、2-系统外日志(第三方日志,可根据不同第三方继续细化日志来源)',
  `request_method` varchar(10) NOT NULL COMMENT '请求方式',
  `class_name` varchar(200) NOT NULL COMMENT '类名',
  `method_name` varchar(100) NOT NULL COMMENT '方法名称',
  `payload` varchar(1000) DEFAULT NULL COMMENT '请求参数',
  `ip` varchar(20) NOT NULL COMMENT 'IP地址',
  `create_date` datetime NOT NULL COMMENT '执行时间',
  `response_time` int(10) NOT NULL COMMENT '响应时间,单位:毫秒',
  `response_code` int(2) NOT NULL COMMENT '执行状态:1000-成功、1001-非法字段、1002-系统忙、1003-无接口访问权限、1004-请求失败、1005-未授权、2001-TOKEN失效',
  `desc_info` varchar(2000) DEFAULT NULL COMMENT '执行描述',
  PRIMARY KEY (`id`),
  KEY `index_user_name` (`user_name`),
  KEY `index_module` (`module`),
  KEY `index_service_type` (`service_type`),
  KEY `index_source_type` (`source_type`),
  KEY `index_request_method` (`request_method`),
  KEY `index_ip` (`ip`),
  KEY `index_create_date` (`create_date`),
  KEY `index_response_code` (`response_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统日志表';

3.自定义注解类<SysLogAnnotation.java>

package cn.keyidea.common.annotation;

import cn.keyidea.common.constant.ConstantsExpand;

import java.lang.annotation.*;

/**
 * 自定义系统日志注解,实现日志记录功能
 *
 * @author qyd
 * @date 2022-10-17
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLogAnnotation
{
    /**
     * 执行模块
     */
    String module() default "";

    /**
     * 业务描述
     */
    String serviceDesc() default "";

    /**
     * 业务来源,参见{@link ConstantsExpand.SourceType}
     * 默认为:1-用户日志
     */
    int sourceType() default 1;

    /**
     * 业务类型,参看{@link ConstantsExpand.ServiceType}
     * 默认为:99-其他
     */
    int serviceType() default 99;

    /**
     * 是否进行日志记录,0-不记录 1-记录(默认值为1)
     * 参看{@link ConstantsExpand.SysLogFlag}
     */
    int flag() default 1;
}

4.自定义注解处理类<SysLogAnnotationAspect.java>

  • 其中涉及统一返回封装类<BaseRes.java>和<BaseResponse.java>,由于历史代码遗留问题,在此进行了兼容,前者与后者主要区别
    • 后者继承了HashMap中put方法,可以实现HashMap的自定义返回<k,v>属性值,不过也因此会造成在集成Knife4j下,前端无法准备理解正常的返回对象属性描述,不推荐使用;
    • 前者定义了范式数据约束,因此在和Knife4j结合后,可以在前端进行完整的呈现返回实体的描述,推荐使用。
  • 其余涉及如工具类<JacksonUtils.java>和<ShiroUtils.java>等,见文末附录。
package cn.keyidea.common.annotation;

import cn.keyidea.common.bean.BaseRes;
import cn.keyidea.common.bean.BaseResponse;
import cn.keyidea.common.constant.ConstantsExpand;
import cn.keyidea.common.constant.StatusCode;
import cn.keyidea.common.util.JacksonUtils;
import cn.keyidea.common.util.ShiroUtils;
import cn.keyidea.sys.entity.SysLog;
import cn.keyidea.sys.entity.SysUser;
import cn.keyidea.sys.mapper.SysLogMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 系统日志切面类
 * <p>
 * 自定义日志标记类(参看{@link SysLogAnnotation})
 *
 * @author qyd
 * @date 2022-10-19
 */
@Order(2)
@Aspect
@Component
public class SysLogAnnotationAspect
{

    private final static Logger logger = LoggerFactory.getLogger(SysLogAnnotationAspect.class);

    @Autowired
    private SysLogMapper sysLogMapper;

    /**
     * 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
     * 在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
     * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(本类采用这)
     * 方式二:execution:一般用于指定方法的执行
     */
    @Pointcut("@annotation(cn.keyidea.common.annotation.SysLogAnnotation)")
    private void controllerAspect()
    {

    }

    /**
     * 环绕方法,可自定义目标执行的时机
     *
     * @param pjp JoinPoint的子接口,添加了两个方法
     *            Object proceed() throws Throwable; 执行目标方法
     *            Object proceed(Object[] var1) throws Throwable; 传入的新的参数去执行目标方法
     * @return 此方法需要返回值,返回值视为目标方法的返回值
     * @throws Throwable
     */
    @Around("controllerAspect()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable
    {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 获取请求方式
        String requestMethod = request.getMethod();
        // 获取登录用户账户;用户登陆时,从参数中获取userName
        String userName = null;
        // 参数列表
        String payload = null;
        // 获取系统ip
        String ip = request.getRemoteAddr();
        // 拦截的实体类,就是当前正在执行的controller
        Object target = pjp.getTarget();
        // 拦截的方法名称,当前正在执行的方法,示例:login
        String methodName = pjp.getSignature().getName();
        // 目标方法所属类的类名,示例:cn.keyidea.sys.controller.SysLoginController
        String className = pjp.getSignature().getDeclaringTypeName();
        // 目标方法所属类的简单类名,示例:SysLoginController
        String simpleName = pjp.getSignature().getDeclaringType().getSimpleName();
        // 拦截的参数类型
        Signature sig = pjp.getSignature();
        // 获取参数列表
        Map<String, Object> paramsMap = getParams(pjp);

        // 方法通知前获取时间,为什么要记录这个时间呢?当然是用来计算模块执行时间的
        long start = System.currentTimeMillis();

        if (!(sig instanceof MethodSignature))
        {
            throw new IllegalArgumentException("该注解只能用于方法");
        }
        Class<?>[] parameterTypes = ((MethodSignature) sig).getMethod().getParameterTypes();

        // 获得被拦截的方法
        Method method = null;
        try
        {
            method = target.getClass().getMethod(methodName, parameterTypes);
        }
        catch (NoSuchMethodException e1)
        {
            e1.printStackTrace();
        }
        catch (SecurityException e1)
        {
            e1.printStackTrace();
        }

        if (null == method)
        {
            return pjp.proceed();
        }
        // 判断是否包含自定义的注解
        if (!method.isAnnotationPresent(SysLogAnnotation.class))
        {
            return pjp.proceed();
        }

        // 获取自定义注解中的内容
        SysLogAnnotation sysLogAnnotation = method.getAnnotation(SysLogAnnotation.class);
        String module = sysLogAnnotation.module();
        String serviceDesc = sysLogAnnotation.serviceDesc();
        int sourceType = sysLogAnnotation.sourceType();
        int serviceType = sysLogAnnotation.serviceType();
        int flag = sysLogAnnotation.flag();

        // 根据业务来源,设置:1)用户名,2)参数列表(用户登陆时重新获取)
        switch (sourceType)
        {
            case ConstantsExpand.SourceType.INSIDE_SYS:
            case ConstantsExpand.SourceType.INSIDE_USER:
                // 如果是用户登录进行特殊处理,1)用户名从参数中获取(由于此刻用户尚未登录,Shiro尚未管理到用户),2)用户密码脱敏
                if (serviceType == ConstantsExpand.ServiceType.LOGIN && paramsMap.containsKey("sysUser"))
                {
                    SysUser sysUser = (SysUser) paramsMap.get("sysUser");
                    userName = sysUser.getLoginName();
                    SysUser entity = new SysUser();
                    entity.setLoginName(sysUser.getLoginName());
                    entity.setPassword("xxx");
                    payload = entity.toString();
                }
                else
                {
                    if (ShiroUtils.getUserEntity() != null)
                    {
                        userName = ShiroUtils.getUserEntity().getLoginName();
                    }
                }
                break;
            default:
                userName = "第三方请求";
        }

        // 封装日志实体对象
        SysLog sysLog = new SysLog();
        sysLog.setUserName(userName);
        sysLog.setRequestMethod(requestMethod);
        sysLog.setClassName(className);
        sysLog.setMethodName(methodName);
        if (paramsMap.containsKey("response"))
        {
            paramsMap.remove("response");
        }
        sysLog.setPayload(payload == null ? JacksonUtils.toJson(paramsMap) : payload);
        sysLog.setCreateDate(new Date());
        sysLog.setIp(ip);
        sysLog.setModule(module);
        sysLog.setServiceDesc(serviceDesc);
        sysLog.setServiceType(serviceType);
        sysLog.setSourceType(sourceType);

        // 默认执行成功
        sysLog.setResponseCode(StatusCode.SUCCESS.getCodeValue());
        sysLog.setDescInfo(StatusCode.SUCCESS.getMsg());

        Object object = pjp.proceed();
        long end = System.currentTimeMillis();
        sysLog.setResponseTime((int) (end - start));
        // 如果方法体返回的是BaseRes或者BaseResponse,则获取其在失败后的自定义描述文字
        if (object instanceof BaseRes)
        {
            BaseRes res = (BaseRes) object;
            if (res.getCode() != StatusCode.SUCCESS.getCodeValue())
            {
                sysLog.setResponseCode(res.getCode());
                sysLog.setDescInfo(res.getMsg());
            }
        }
        else if (object instanceof BaseResponse)
        {
            BaseResponse res = (BaseResponse) object;
            int code = (int) res.get("code");
            String msg = (String) res.get("msg");
            if (code != StatusCode.SUCCESS.getCodeValue())
            {
                sysLog.setResponseCode(code);
                sysLog.setDescInfo(msg);
            }
        }
        // 保存进数据库
        logSave(sysLog, flag);
        return object;
    }

    /**
     * 进行日志保存
     *
     * @param log  日志实体类
     * @param flag 参看{@link SysLogAnnotation#flag}
     */
    private void logSave(SysLog log, int flag)
    {
        if (flag == 1)
        {
            sysLogMapper.insert(log);
        }
        else
        {
            logger.warn("日志不保存,进行打印:{}", log.toString());
        }
    }

    /**
     * 获取参数名和参数值
     *
     * @param pjp Proceedingjoinpoint继承自JoinPoint,在JoinPoint的基础上暴露出 proceed(), 这个方法是AOP代理链执行的方法
     * @return 返回参数名与参数值
     */
    private static Map<String, Object> getParams(ProceedingJoinPoint pjp)
    {
        Map<String, Object> map = new HashMap<String, Object>(32);
        Object[] values = pjp.getArgs();

        String[] parameterNames = ((CodeSignature) pjp.getSignature()).getParameterNames();
        for (int i = 0; i < parameterNames.length; i++)
        {
            if (values[i] instanceof MultipartFile)
            {
                // 对上传的文件做单独处理,获取文件名称作为value
                MultipartFile file = (MultipartFile) values[i];
                map.put(parameterNames[i], file.getOriginalFilename());
            }
            else
            {
                map.put(parameterNames[i], values[i]);
            }
        }
        return map;
    }
}

说明

  • 日志注解处理类中统一对传入的参数中包含response的进行了移除,原因在于保留此参数的引用对于日志审计毫无用处,因此移除

5.日志注解使用示例

这里以系统配置控制器<SysController.java>为例进行示例,在需要进行记录的接口上添加自定义注解@SysLogAnnotation,并进行参数描述即可。

package cn.keyidea.sys.controller;

import cn.keyidea.common.annotation.SysLogAnnotation;
import cn.keyidea.common.bean.BaseRes;
import cn.keyidea.common.config.InitConfigProperties;
import cn.keyidea.common.constant.ConstantsExpand;
import cn.keyidea.common.valid.GroupAdd;
import cn.keyidea.common.valid.GroupUpdate;
import cn.keyidea.sys.entity.SysConfig;
import cn.keyidea.sys.service.SysConfigService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 系统配置控制器
 *
 * @author qyd
 * @date 2022-10-17
 */
@ApiSupport(order = 7)
@Tag(name = "7-系统配置", description = "系统配置")
@RestController
@RequestMapping("/sys/sysConfig")
public class SysConfigController
{

    private final static Logger logger = LoggerFactory.getLogger(SysConfigController.class);

    @Autowired
    private SysConfigService sysConfigService;
    @Autowired
    private InitConfigProperties initConfigProperties;

    @SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-分页查询", serviceType = ConstantsExpand.ServiceType.QUERY)
    @ApiOperationSupport(author = "qyd", order = 1)
    @Operation(summary = "分页查询", description = "")
    @Parameters({
            @Parameter(name = "sysConfigName", description = "配置名称,支持模糊查询", required = false, example = "场景1", in = ParameterIn.QUERY),
            @Parameter(name = "sysConfigCode", description = "配置key值,支持模糊查询", required = false, example = "0", in = ParameterIn.QUERY),
            @Parameter(name = "current", description = "当前页", required = true, example = "1", in = ParameterIn.QUERY),
            @Parameter(name = "size", description = "分页数", required = true, example = "15", in = ParameterIn.QUERY)
    })
    @GetMapping("listPage")
    public BaseRes<BaseRes.DataList<SysConfig>> listPage(@RequestParam(value = "sysConfigName", required = false) String sysConfigName,
                                                         @RequestParam(value = "sysConfigCode", required = false) String sysConfigCode,
                                                         @RequestParam(value = "current", required = true) Integer pageNumber,
                                                         @RequestParam(value = "size", required = true) Integer pageSize)
    {
        QueryWrapper<SysConfig> wrapper = new QueryWrapper<>();
        wrapper.lambda().like(StringUtils.isNotBlank(sysConfigName), SysConfig::getSysConfigName, sysConfigName)
                .like(StringUtils.isNotBlank(sysConfigCode), SysConfig::getSysConfigCode, sysConfigCode);
        Page<SysConfig> page = new Page<>(pageNumber, pageSize);
        IPage<SysConfig> iPage = sysConfigService.page(page, wrapper);
        return BaseRes.list(iPage.getTotal(), iPage.getRecords());
    }

    @SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-配置新增", serviceType = ConstantsExpand.ServiceType.ADD)
    @ApiOperationSupport(author = "qyd", order = 2, includeParameters = {
            "sysConfig.sysConfigName",
            "sysConfig.sysConfigCode",
            "sysConfig.sysConfigValue",
            "sysConfig.remark"
    })
    @Operation(summary = "配置新增", description = "")
    @PostMapping("add")
    public BaseRes add(@Validated(GroupAdd.class) @RequestBody SysConfig sysConfig)
    {
        return sysConfigService.add(sysConfig);
    }

    @SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-配置更新", serviceType = ConstantsExpand.ServiceType.UPDATE)
    @ApiOperationSupport(author = "qyd", order = 3, includeParameters = {
            "sysConfig.id",
            "sysConfig.sysConfigName",
            "sysConfig.sysConfigValue",
            "sysConfig.remark"
    })
    @Operation(summary = "配置更新", description = "配置key值一旦设置便不可更改,除非删除")
    @PutMapping("/update")
    public BaseRes update(@Validated(GroupUpdate.class) @RequestBody SysConfig sysConfig)
    {
        return sysConfigService.update(sysConfig);
    }

    @SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-配置删除", serviceType = ConstantsExpand.ServiceType.DELETE)
    @ApiOperationSupport(author = "qyd", order = 4)
    @Operation(summary = "删除", description = "")
    @Parameter(name = "id", description = "主键ID", required = true, example = "1", in = ParameterIn.PATH)
    @DeleteMapping("/delete/{id}")
    public BaseRes delete(@PathVariable(value = "id", required = true) Integer id)
    {
        return sysConfigService.delete(id);
    }


    @SysLogAnnotation(module = "配置管理", serviceDesc = "配置管理-配置列表", serviceType = ConstantsExpand.ServiceType.UPDATE)
    @ApiOperationSupport(author = "qyd", order = 5, includeParameters = {
            "sysConfig.sysConfigName",
            "sysConfig.sysConfigCode"
    })
    @Operation(summary = "配置列表", description = "name与code均支持模糊查询")
    @GetMapping("/listAll")
    public BaseRes listAll(SysConfig sysConfig)
    {
        List<SysConfig> list = sysConfigService.list(Wrappers.<SysConfig>lambdaQuery()
                .like(StringUtils.isNotBlank(sysConfig.getSysConfigName()), SysConfig::getSysConfigName, sysConfig.getSysConfigName())
                .like(StringUtils.isNotBlank(sysConfig.getSysConfigCode()), SysConfig::getSysConfigCode, sysConfig.getSysConfigCode())
                .orderByDesc(SysConfig::getId));
        return BaseRes.successData(list);
    }

}

附录

附录A<BaseRes.java>

package cn.keyidea.common.bean;

import cn.keyidea.common.constant.StatusCode;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serializable;

/**
 * 通用响应封装,范式返回(Swagger要求)
 *
 * @author qyd
 */
@Data
public class BaseRes<T> implements Serializable
{

    /**
     * 错误码
     */
    @Schema(name = "code", description = "错误码,当code为1000时返回正常,其余返回异常", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000")
    public Integer code;

    /**
     * 错误提示信息
     */
    @Schema(name = "msg", description = "错误提示信息,当code为非1000时返回提示信息")
    public String msg;

    /**
     * 附加返回数据
     */
    @Schema(name = "data", description = "附加返回数据,当code为1000时返回数据")
    public T data;

    public static class DataList<T>
    {
        /**
         * 记录总数
         */
        @Schema(name = "total", description = "记录总数")
        public Integer total;
        /**
         * 数据列表
         */
        @Schema(name = "list", description = "数据列表")
        public T list;

        public DataList(Integer total, T list)
        {
            this.total = total;
            this.list = list;
        }
    }

    /**
     * 给ObjectMapper用的,代码中不要调用
     */
    public BaseRes()
    {

    }

    /**
     * 自定义返回码和提示消息
     *
     * @param code 错误码
     * @param msg  提示文字
     */
    public BaseRes(int code, String msg)
    {
        this.code = code;
        this.msg = msg;
    }

    public BaseRes(int code, String msg, T data)
    {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    /**
     * 返回成功,但是没有附加数据
     *
     * @return BaseRes对象
     */
    public static BaseRes success()
    {
        return new BaseRes(StatusCode.SUCCESS.getCodeValue(), "请求成功");
    }

    /**
     * 返回成功,但是没有附加数据
     *
     * @param msg 成功时返回自定义msg
     * @return BaseRes对象
     */
    public static BaseRes success(String msg)
    {
        return new BaseRes(StatusCode.SUCCESS.getCodeValue(), msg);
    }

    /**
     * 返回成功,带附加数据
     *
     * @param data 附加数据
     * @return BaseRes对象
     */
    public static BaseRes successData(Object data)
    {
        BaseRes value = new BaseRes(StatusCode.SUCCESS.getCodeValue(), "请求成功");
        value.data = data;
        return value;
    }

    /**
     * 返回参数无效响应
     *
     * @return BaseRes对象
     */
    public static BaseRes invalidParam()
    {
        return new BaseRes(StatusCode.INVALID_PARAM.getCodeValue(), "参数无效");
    }

    /**
     * 返回参数无效响应,自定义错误提示
     *
     * @param msg 提示文字
     * @return BaseRes对象
     */
    public static BaseRes invalidParam(String msg)
    {
        return new BaseRes(StatusCode.INVALID_PARAM.getCodeValue(), msg);
    }

    /**
     * 返回系统忙无效响应
     *
     * @return BaseRes对象
     */
    public static BaseRes systemBusy()
    {
        return new BaseRes(StatusCode.SYSTEM_BUSY.getCodeValue(), "系统忙");
    }

    /**
     * 返回master key无效响应
     *
     * @return BaseRes对象
     */
    public static BaseRes invalidMasterkey()
    {
        return new BaseRes(StatusCode.INVALID_MASTER_KEY.getCodeValue(), "没有接口访问权限");
    }

    /**
     * 返回失败,附带说明
     *
     * @return BaseRes对象
     */
    public static BaseRes fail(String msg)
    {
        return new BaseRes(StatusCode.FAILURE.getCodeValue(), msg);
    }

    /**
     * 返回错误信息时,仍然返回数据
     *
     * @param data 数据集
     * @param msg  错误信息
     * @return BaseRes对象
     */
    public static BaseRes failData(Object data, String msg)
    {
        return new BaseRes(StatusCode.FAILURE.getCodeValue(), msg, data);
    }

    /**
     * 登录失效的错误
     *
     * @return BaseRes对象
     */
    public static BaseRes invalidToken()
    {
        return new BaseRes(StatusCode.INVALID_TOKEN.getCodeValue(), "请先登录");
    }

    /**
     * 检查响应处理是否成功
     *
     * @return 成功返回true,否则false
     */
    @JsonIgnore
    public boolean isSuccess()
    {

        return (this.code.equals(StatusCode.SUCCESS.getCodeValue()));
    }

    /**
     * 返回分页列表数据
     *
     * @param total 记录总数
     * @param list  列表数据
     * @return rsp
     */
    public static BaseRes list(long total, Object list)
    {
        DataList data = new DataList((int) total, list);

        return BaseRes.successData(data);
    }

}

附录B<BaseResponse.java>

package cn.keyidea.common.bean;

import cn.keyidea.common.constant.StatusCode;
import com.fasterxml.jackson.annotation.JsonIgnore;

import java.util.HashMap;

/**
 * 通用响应封装
 */
public class BaseResponse extends HashMap<String, Object> {

    public static class DataList {
        //记录总数
        public Long total;
        //数据列表
        public Object list;

        public DataList(long total, Object list) {
            this.total = total;
            this.list = list;
        }
    }

    /**
     * 给ObjectMapper用的,代码中不要调用
     */
    public BaseResponse() {

    }

    /**
     * 自定义返回码和提示消息
     *
     * @param code 错误码
     * @param msg  提示文字
     */
    public BaseResponse(int code, String msg) {
        put("code", code);
        put("msg", msg);
    }

    /**
     * 返回成功,但是没有附加数据
     *
     * @return BaseResponse对象
     */
    public static BaseResponse success() {
        return new BaseResponse(StatusCode.SUCCESS.getCodeValue(), "请求成功");
    }

    /**
     * 返回成功,带附加数据
     *
     * @param data 附加数据
     * @return BaseResponse对象
     */
    public static BaseResponse successData(Object data) {
        BaseResponse value = new BaseResponse(StatusCode.SUCCESS.getCodeValue(), "请求成功");
        value.put("data", data);
        return value;
    }

    /**
     * 返回参数无效响应
     *
     * @return BaseResponse对象
     */
    public static BaseResponse invalidParam() {
        return new BaseResponse(StatusCode.INVALID_PARAM.getCodeValue(), "参数无效");
    }

    /**
     * 返回参数无效响应,自定义错误提示
     *
     * @param msg 提示文字
     * @return BaseResponse对象
     */
    public static BaseResponse invalidParam(String msg) {
        return new BaseResponse(StatusCode.INVALID_PARAM.getCodeValue(), msg);
    }

    /**
     * 返回系统忙无效响应
     *
     * @return BaseResponse对象
     */
    public static BaseResponse systemBusy() {
        return new BaseResponse(StatusCode.SYSTEM_BUSY.getCodeValue(), "系统忙");
    }

    /**
     * 返回master key无效响应
     *
     * @return BaseResponse对象
     */
    public static BaseResponse invalidMasterkey() {
        return new BaseResponse(StatusCode.INVALID_MASTER_KEY.getCodeValue(), "没有接口访问权限");
    }

    /**
     * 返回失败,附带说明
     *
     * @return BaseResponse对象
     */
    public static BaseResponse fail(String msg) {
        return new BaseResponse(StatusCode.FAILURE.getCodeValue(), msg);
    }

    /**
     * 登录失效的错误
     *
     * @return BaseResponse对象
     */
    public static BaseResponse invalidToken() {
        return new BaseResponse(StatusCode.INVALID_TOKEN.getCodeValue(), "请先登录");
    }

    /**
     * 返回分页列表数据
     *
     * @param total 记录总数
     * @param list  列表数据
     * @return rsp
     */
    public static BaseResponse list(long total, Object list) {
        DataList data = new DataList(total, list);

        return BaseResponse.successData(data);
    }

    /**
     * 检查响应处理是否成功
     *
     * @return 成功返回true,否则false
     */
    @JsonIgnore
    public boolean isSuccess() {
        return (get("code").equals(StatusCode.SUCCESS.getCode()));
    }


    @Override
    public BaseResponse put(String key, Object value) {
        super.put(key, value);
        return this;
    }

}

附录C<StatusCode.java>

package cn.keyidea.common.constant;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 状态码枚举定义
 *
 * @author qyd
 * @date 2022-10-13
 */
public enum StatusCode
{

    SUCCESS(1000, "请求成功"),
    INVALID_PARAM(1001, "非法字段"),
    SYSTEM_BUSY(1002, "系统忙"),
    INVALID_MASTER_KEY(1003, "无接口访问权限"),
    FAILURE(1004, "请求失败"),
    UNAUTHORIZED(1005, "未授权"),
    INVALID_TOKEN(2001, "TOKEN失效"),
    CONNECT_TIMED_OUT(3001, "请求超时"),
    HTTP_REQ_ERROR(3002, "HTTP请求出错");

    /**
     * 错误码
     */
    private final int code;
    /**
     * 错误描述信息
     */
    private final String msg;

    StatusCode(int code, String msg)
    {
        this.code = code;
        this.msg = msg;
    }

    public String getMsg()
    {
        return this.msg;
    }

    public String getCode()
    {
        return this.code + "";
    }

    public int getCodeValue()
    {
        return this.code;
    }

    /**
     * 转为Map集合数据
     *
     * @return 枚举对象Map集合
     */
    public static Map<Integer, String> toMap()
    {
        Map<Integer, String> map = new HashMap<>(32);
        for (StatusCode value : StatusCode.values())
        {
            map.put(value.getCodeValue(), value.getMsg());
        }
        return map;
    }

    /**
     * 转为List集合数据
     *
     * @return 枚举对象List集合
     */
    public static List<Map<String, String>> toList()
    {
        List<Map<String, String>> list = new ArrayList<>(32);
        Map<String, String> map = null;
        for (StatusCode item : StatusCode.values())
        {
            map = new HashMap<>();
            map.put("code", item.getCode());
            map.put("msg", item.getMsg());
            list.add(map);
        }
        map = null;
        return list;
    }

}

附录D<JacksonUtils.java>

package cn.keyidea.common.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Jackson工具类
 *
 * @author qyd
 * @date 2022-10-14
 */
public class JacksonUtils
{

    private static final ObjectMapper MAPPER;

    static
    {
        MAPPER = new ObjectMapper();
        // 忽略不存在的字段
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 设置日期格式化
        MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }

    /**
     * 将对象转换成json数据
     */
    public static String toJson(Object o)
    {
        try
        {
            return MAPPER.writeValueAsString(o);
        }
        catch (JsonProcessingException e)
        {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json数据转化成对象
     */
    public static <T> T toEntity(String jsonData, Class<T> beanType)
    {
        try
        {
            return MAPPER.readValue(jsonData, beanType);
        }
        catch (JsonProcessingException e)
        {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json转化成List
     */
    public static <T> List<T> toList(String jsonData, Class<? extends List> collectionClass, Class<T> elementClass)
    {
        JavaType javaType = MAPPER.getTypeFactory().constructCollectionType(collectionClass, elementClass);
        try
        {
            return MAPPER.readValue(jsonData, javaType);
        }
        catch (JsonProcessingException e)
        {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将json转化成Map
     */
    public static <K, V> Map<K, V> toMap(String jsonData, Class<? extends Map> mapClass, Class<K> keyClass, Class<V> valueClass)
    {
        JavaType javaType = MAPPER.getTypeFactory().constructMapType(mapClass, keyClass, valueClass);
        try
        {
            return MAPPER.readValue(jsonData, javaType);
        }
        catch (JsonProcessingException e)
        {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 禁止调用无参构造
     */
    private JacksonUtils() throws IllegalAccessException
    {
        throw new IllegalAccessException("Can't create an instance!");
    }


    /**
     * 将JSON数据转换成列表
     */
    public static <T> List<T> getJsonToList(String jsonData, Class<T> beanType)
    {
        try
        {
            JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
            return MAPPER.readValue(jsonData, javaType);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将JSON数据转换成Set集合
     */
    public static <E> Set<E> getJsonToSet(String jsonData, Class<E> elementType)
    {
        try
        {
            JavaType javaType = MAPPER.getTypeFactory().constructCollectionType(Set.class, elementType);
            return MAPPER.readValue(jsonData, javaType);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将JSON数据转换成Map集合
     */
    public static <K, V> Map<K, V> getJsonToMap(String jsonData, Class<K> keyType, Class<V> valueType)
    {
        try
        {
            JavaType javaType = MAPPER.getTypeFactory().constructMapType(Map.class, keyType, valueType);
            return MAPPER.readValue(jsonData, javaType);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return null;
    }

}

附录E<ShiroUtils.java>

package cn.keyidea.common.util;

import cn.keyidea.common.config.BusinessException;
import cn.keyidea.sys.entity.SysUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;

/**
 * Shiro工具类
 *
 * @author qyd
 */
@Component
public class ShiroUtils
{

    public static Session getSession()
    {
        return SecurityUtils.getSubject().getSession();
    }

    public static Subject getSubject()
    {
        return SecurityUtils.getSubject();
    }

    public static SysUser getUserEntity()
    {
        return (SysUser) SecurityUtils.getSubject().getPrincipal();
    }

    public static Integer getUserId()
    {
        return getUserEntity().getId();
    }

    public static void setSessionAttribute(Object key, Object value)
    {
        getSession().setAttribute(key, value);
    }

    public static Object getSessionAttribute(Object key)
    {
        return getSession().getAttribute(key);
    }

    public static boolean isLogin()
    {
        return SecurityUtils.getSubject().getPrincipal() != null;
    }

    public static String getKaptcha(String key)
    {
        Object kaptcha = getSessionAttribute(key);
        if (kaptcha == null)
        {
            throw new BusinessException("验证码已失效");
        }
        getSession().removeAttribute(key);
        return kaptcha.toString();
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容