当前位置: 首页 > article >正文

瑞吉外卖项目学习笔记(二)Swagger、logback、表单校验和参数打印功能的实现

瑞吉外卖项目学习笔记(一)准备工作、员工登录功能实现

文章目录

  • 3 项目组件优化
    • 3.1 实现Swagger文档输出
    • 3.2 实现logback日志打印
    • 3.3 实现表单校验功能
    • 3.4 实现请求参数和响应参数的打印

3 项目组件优化

3.1 实现Swagger文档输出

  • 1)在application.yml中增加knife4j配置
spring:
  mvc:
    pathmatch:
      matching-strategy: ANT_PATH_MATCHER
knife4j:
  enable: true
  title: 瑞吉外卖
  group: ruiji_takeout
  description: 瑞吉外卖
  version: 1.0
  name: itweid
  url:
  email:
  base-package: com.itweid.takeout.controller
  • 2)创建配置类SwaggerProperties类接收配置
@Data
@ConfigurationProperties(prefix = "knife4j")
public class SwaggerProperties {

    private String title = ""; //标题
    private String group = ""; //组名
    private String description = ""; //描述
    private String version = ""; //版本
    private String name = ""; // 联系人
    private String url = ""; // 联系人url
    private String email = ""; // 联系人email
    private String basePackage = ""; //swagger会解析的包路径
    private List<String> basePath = new ArrayList<>(); //swagger会解析的url规则
    private List<String> excludePath = new ArrayList<>(); //在basePath基础上需要排除的url
    // 如果没有填写组名,则直接用标题作为组名
    public String getGroup() {
        if (group == null || group.isEmpty()) {
            return title;
        }
        return group;
    }
}
  • 3)创建自动配置类SwaggerAutoConfiguration进行初始化
@Configuration
@ConditionalOnProperty(name = "knife4j.enable", havingValue = "true", matchIfMissing = true)
@EnableSwagger2
@EnableConfigurationProperties(SwaggerProperties.class)
public class SwaggerAutoConfiguration implements BeanFactoryAware {

    @Autowired
    private SwaggerProperties swaggerProperties;
    @Autowired
    private BeanFactory beanFactory;

    @Bean
    @ConditionalOnMissingBean
    public List<Docket> createRestApi(){
        ConfigurableBeanFactory configurableBeanFactory = (ConfigurableBeanFactory) beanFactory;
        List<Docket> docketList = new LinkedList<>();
        ApiInfo apiInfo = new ApiInfoBuilder()
                // 页面标题
                .title(swaggerProperties.getTitle())
                // 创建人
                .contact(new Contact(swaggerProperties.getName(),
                        swaggerProperties.getUrl(),
                        swaggerProperties.getEmail()))
                // 版本号
                .version(swaggerProperties.getVersion())
                // 描述
                .description(swaggerProperties.getDescription())
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .groupName(swaggerProperties.getGroup())
                .select()
                // 为当前包路径
                .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()))
                .paths(PathSelectors.any())
                .build();
        configurableBeanFactory.registerSingleton(swaggerProperties.getGroup(), docket);
        docketList.add(docket);
        return docketList;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
}
  • 4)重新启动项目,在浏览器访问http://localhost:8081/doc.html,即可查看Swagger文档:


在Swagger文档的调试功能中,可以直接进行测试:

3.2 实现logback日志打印

  • 1)引入依赖
<!--logback-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>
  • 2)在resources目录下创建配置文件

  • logback-base.xml

<?xml version="1.0" encoding="UTF-8"?>
<included>
    <contextName>logback</contextName>
    <!--
		name的值是变量的名称,value的值时变量定义的值
		定义变量后,可以使“${}”来使用变量
	-->
    <property name="log.path" value="logs" />

    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule
            conversionWord="clr"
            converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule
            conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!--输出到控制台-->
    <appender name="LOG_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--输出到文件-->
    <appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/Business.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
    </appender>
</included>
  • logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--引入其他配置文件-->
    <include resource="logback-base.xml" />
    <!--
    <logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
    <logger>仅有一个name属性,一个可选的level和一个可选的addtivity属性。
        name:用来指定受此logger约束的某一个包或者具体的某一个类。
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
            如果未设置此属性,那么当前logger将会继承上级的级别。
        addtivity:是否向上级logger传递打印信息。默认是true。
    -->

    <!--开发环境-->
    <springProfile name="dev">
        <logger name="com.itweid.takeout" additivity="false" level="debug">
            <appender-ref ref="LOG_CONSOLE"/>
        </logger>
    </springProfile>
    <!--生产环境-->
    <springProfile name="pro">
        <logger name="com.itweid.takeout" additivity="false" level="info">
            <appender-ref ref="LOG_FILE"/>
        </logger>
    </springProfile>

    <!--
    root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
    level:设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF 默认是DEBUG
    可以包含零个或多个元素,标识这个appender将会添加到这个logger。
    -->
    <root level="info">
        <appender-ref ref="LOG_CONSOLE" />
        <appender-ref ref="LOG_FILE" />
    </root>
</configuration>
  • 3)重新启动项目,可以看到根目录下生成了logs文件夹及日志文件:

3.3 实现表单校验功能

员工登录时,必须输入用户名和密码,虽然前端JS进行了校验,但对于后端来说,前端传来的数据是不可信的。

前端很容易获取到后端的接口,如果有人直接调用接口,就可能会出现非法数据,因此服务端也要数据校验。总的来说:

  • 前端校验:主要是提高用户体验
  • 后端校验:主要是保证数据安全可靠

Hibernate Validator框架可以以很优雅的方式实现参数的校验,让业务代码和校验逻辑分开,不再编写重复的校验逻辑。

更详细的用法可参考:后台管理系统的通用权限解决方案(五)SpringBoot整合hibernate-validator实现表单校验

  • 1)首先,在LoginForm类中加入表单校验的注解,如字符串类型的参数则用@NotBlank
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel("登录表单")
public class LoginForm {
    @ApiModelProperty("用户名")
    @NotBlank(message = "用户名不能为空")
    private String username;
    @ApiModelProperty("密码")
    @NotBlank(message = "密码不能为空")
    private String password;
}
  • 2)在EmployeeController类中使用@Validated注解开启校验功能:

  • 3)需要特别注意的是,在2.3.0版本之前,spring-boot-starter-web是集成了validation检验的,但是在2.3.0开始就去掉了该依赖,所以根据实际版本决定是否添加依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • 4)重启服务,发起登录请求,如果用户名为空,则会报错:


但此时前端提示不太友好(报400)。我们还要继续完善一下,对异常进行统一处理。

  • 5)自定义一个CustomException异常类来统一处理已知的异常。未来在业务逻辑中,使用try...catch...捕获异常后,再抛出一个CustomException异常:
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class CustomException extends RuntimeException {

    private BaseResult result;

    /**
    * 指定一个是否追踪信息栈的异常
    */
    public CustomException(BaseResult result, boolean writableStackTrace) {
        super(result.getMsg(), null, false, writableStackTrace);
        this.result = result;
    }

    /**
    * 指定一个不追踪信息栈的异常
    */
    public CustomException(BaseResult result) {
        super(result.getMsg(), null, false, false);
        this.result = result;
    }

    /**
     * 指定一个不追踪栈信息的异常
     */
    public CustomException(ErrorCode errorCode) {
        super(errorCode.getMsg(), null, false, false);
        this.result = BaseResult.error(errorCode);
    }
}
  • 6)创建一个全局异常处理类GlobalExceptionHandler,对自定义异常和参数绑定异常进行统一处理:
@ControllerAdvice(annotations = { RestController.class, Controller.class })
@Slf4j
public class GlobalExceptionHandler {

    /**
    * 自定义异常的处理
    */
    @ExceptionHandler(CustomException.class)
    @ResponseBody
    public BaseResult customExceptionHandler(CustomException customException) {
        log.error("捕获自定义异常:{}", customException.getResult().getMsg(), customException);
        return customException.getResult();
    }

    /**
    * 参数绑定异常的处理
    */
    @ExceptionHandler({ConstraintViolationException.class, BindException.class})
    @ResponseBody
    public String validateException(Exception e, HttpServletRequest request) {
        log.error("捕获参数异常:{}", e.getMessage(), e);
        String msg = null;
        if (e instanceof ConstraintViolationException) {
            ConstraintViolationException constraintViolationException =
                    (ConstraintViolationException) e;
            Set<ConstraintViolation<?>> violations =
                    constraintViolationException.getConstraintViolations();
            ConstraintViolation<?> next = violations.iterator().next();
            msg = next.getMessage();
        } else if (e instanceof BindException) {
            BindException bindException = (BindException) e;
            msg = bindException.getBindingResult().getFieldError().getDefaultMessage();
        }
        log.error("参数异常信息:{}", msg);
        return msg;
    }

}
  • 7)重启服务,再次调用登录请求,当参数不符合要求时,则会返回更加友好的提示:

3.4 实现请求参数和响应参数的打印

页面的每个请求都有请求参数和响应参数,如果每个请求都单独打印这些参数,则显得非常冗余。

为此我们可以基于注解和切面编程,实现请求参数和响应参数的打印。

  • 1)引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--hutool工具-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.1.0</version>
</dependency>
  • 2)创建切面类OptLogAspect,配置切入点拦截规则,拦截所有Controller方法
@Aspect
@Slf4j
public class OptLogAspect {
    /**
     * 定义Controller切入点拦截规则,拦截 @OptLog 注解的方法
     */
    @Pointcut("execution(public * com.itweid.takeout.controller.*Controller.*(..))")
    public void optLogAspect() {
    }
}
  • 3)在OptLogAspect的前置通知方法中,打印请求参数信息
/**
 * 前置通知
 */
@Before(value = "optLogAspect()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
    HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
    // 请求参数
    Object[] args = joinPoint.getArgs();
    String strArgs = "";
    try {
        if (!request.getContentType().contains("multipart/form-data")) {
            strArgs = JSONUtil.toJsonStr(args);
        }
    } catch (Exception e) {
        try {
            strArgs = Arrays.toString(args);
        } catch (Exception ex) {
            log.warn("解析参数异常", ex);
        }
    }
    log.info("请求参数:{}", StrUtil.sub(strArgs, 0, 65535));
}
  • 4)在成功返回通知方法和异常返回通知中,打印响应参数信息
/**
 * 成功返回通知
 */
@AfterReturning(returning = "ret", pointcut = "optLogAspect()")
public void doAfterReturning(Object ret) {
    BaseResult baseResult = Convert.convert(BaseResult.class, ret);
    log.info("响应参数:{}", baseResult);
}

/**
 * 异常返回通知
 */
@AfterThrowing(throwing = "e", pointcut = "optLogAspect()")
public void doAfterThrowable(Throwable e) {
    log.info("响应异常:{}", getStackTrace(e));
}

public static String getStackTrace(Throwable throwable) {
    StringWriter sw = new StringWriter();
    try (PrintWriter pw = new PrintWriter(sw)) {
        throwable.printStackTrace(pw);
        return sw.toString();
    }
}
  • 5)在WebMvcConfig配置类中注册切面类为Bean
@Bean
public OptLogAspect optLogAspect() {
    return new OptLogAspect();
}
  • 6)重启服务,测试登录功能


可见,请求参数和响应参数成功打印。后续还可以将请求IP、操作员等信息收集起来存到数据库,就可以实现常说的审计功能。

本节完,更多内容查阅:瑞吉外卖项目实战


http://www.kler.cn/a/445909.html

相关文章:

  • 机动车油耗计算API集成指南
  • 免费GIS工具箱:轻松将glb文件转换成3DTiles文件
  • golang断言
  • C语言进阶(2) ---- 指针的进阶
  • 服务器防火墙设置某个端口号只允许固定 ip地址访问
  • SLAAC如何工作?
  • 提炼关键词的力量:AI驱动下的SEO优化策略
  • RAGFlow(3):VScode端口转发在在本机浏览(比内网穿透好用)
  • R语言处理XML文件
  • C语言:以数据块的形式读写文件
  • Docker 清理命令
  • 使用C#调用SAP的WebService接口
  • 【工作记录】springboot应用中使用Jasypt 加密配置文件@20241216
  • 微信小程序:轻应用的未来与无限可能
  • Fortify 24.2.0版本最新版 win/mac/linux
  • 网络和IP地址计算器方案
  • JVM 详解
  • 大数据-252 离线数仓 - Airflow 任务调度 Crontab简介 任务集成部署 入门案例
  • 中间件 redis安装
  • 洛谷 B3644 【模板】拓扑排序 / 家谱树 C语言
  • git部分命令的使用
  • Hmsc包开展群落数据联合物种分布模型分析通用流程(Pipelines)
  • 如何快速构建Jmeter脚本
  • oracle AES CBC,128位密钥加解密方法
  • 【C++ DFS 图论】1519. 子树中标签相同的节点数|1808
  • 解决 Ubuntu 20.04 上因 postmaster.pid 文件残留导致的 PostgreSQL 启动失败问题