瑞吉外卖项目学习笔记(一)准备工作、员工登录功能实现
文章目录
- 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、操作员等信息收集起来存到数据库,就可以实现常说的审计功能。
…
本节完,更多内容查阅:瑞吉外卖项目实战