项目实战 - 前后端分离、后端校验、Swagger
- Layui
- 同源策略
- SpringMVC 实现 CORS
- 后端校验 - hibernate-validator
- 方法的 Model 参数校验
- 方法的非 Model 参数校验
- 常见注解
- 自定义校验
- 快速失败
- Swagger
- 基本配置与使用
- 常用注解
- Bug:NumberFormatException
- 全局异常处理
- 驾考 - 前后端分离
- MyBatis 的 QueryWrapper 的增强实现
- mapstruct
- 前后端分离使用 Session
- 企业中的文件上传(图片、视频)
早期的前后端协作模式:
- 前端:切图仔、页面仔
- 后台:动态模板技术组装成
以前的协作模式的问题:
- 前端地位比较低,大部分工作都在后台
- 调试、修改页面比较麻烦,需要前端、后台充分配合
- 浪费流量(每次请求都会返回整个页面,这就意味着返回了很多重复的内容)
前后端分离:
- 前端:切图、页面、交互、路由、业务逻辑
- 后台:返回 JSON
前后端分离:
- 页面保持使用静态页面即可(html)
- 静态页面(浏览器)发送异步请求(AJAX)给服务器,服务器返回 JSON
- 浏览器解析 JSON 数据,动态生成对应的 HTML 标签,显示到用户眼前
- 前端一个项目、后端一个项目,分开开发(跨域)
- 前端一个服务器、后端一个服务器,分开部署
Layui
Layui 是一款前端 UI 框架,可以帮助开发者快速搭建后台管理系统的前端页面
- 免费版:https://www.layui.com/
- LayuiMini(更强大的免费版):http://layuimini.99php.cn/
本课程使用的是单页版:v2-onepage
基本使用:下载后解压,将以下内容放到项目中
同源策略
浏览器有个 同源策略 (Same-Origin Policy)
- 它规定了:默认情况下,AJAX请求只能发给同源的URL
- 同源是指3个相同:协议、域名(IP)、端口
- img、script、link、iframe、video、audio 等标签不受同源策略的约束
解决 AJAX 跨域请求的常用方法:
- CORS (Cross-Origin Resource Sharing),跨域资源共享
CORS 的实现需要客户端和服务器同时支持:
- 客户端:基本所有的浏览器都支持(IE 至少是 IE10 版本)
- 服务器:需要返回相应的响应头(比如 Access-Control-Allow-Origin),告知浏览器这是一个允许跨域的请求
Access-Control-Allow-Origin 用于允许跨域。如果不设置允许跨域,则会出现下面情况:
在返回响应时要设置允许跨域,即可跨域访问资源。
设置跨域后,即可成功访问页面。
SpringMVC 实现 CORS
局部设置:在某个 Controller 上使用 @CrossOrigin 注解,允许当前 Controller 被跨域访问
@RequestMapping("/plateRegions")
@CrossOrigin("http://localhost:63342")
public class PlateRegionController {
}
全局设置:
- 需要客户端设置后才会发送 Cookie
let xhr = new XMLHttpRequest()
xhr.withCredentials = true
- 服务器设置:
@Configuration
public class WebCfg implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// /**表示对所有的路径开放全局跨域访问权限
registry.addMapping("/**")
// 开放哪些IP、端口、域名的访问权限
.allowedOrigins("*")
// 是否允许发送Cookie信息
.allowCredentials(true)
// 哪些HTTP方法允许跨域访问
.allowedMethods("GET", "POST");
// 允许HTTP请求中携带的哪些Hedaer信息
.allowHeaders("*");
// 暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
// .exposeHeaders("*");
}
}
后端校验 - hibernate-validator
前端哪怕页面上的交互做好了校验,但是仍然可以通过 postman 等工具直接发送请求,因此前端校验是可以被跳过的,因此后端也应当有校验的功能;但是如果直接在 Controller 中写,又会增加大量代码,使得控制层十分的冗余,因此可以借助一些便利的框架来做后端校验。
hibernate-validator 是 Java 中常用的一款后端校验框架:
参考文档:
- https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/https://docs.jboss.org/hibernate/stable/validator/reference/en-US/pdf/hibernate_validator_reference.pdf
<!-- 后端校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
方法的 Model 参数校验
使用步骤: ① 在 Model 的 getter 或成员变量上加相关的校验注解
@NotBlank(message = "名称不能为空")
private String name;
@NotBlank(message = "车牌不能为空")
private String plate;
② 在 Model 参数上加 @Valid 注解
@PostMapping("/save")
public R save(@Valid T entity) {
if (getService().saveOrUpdate(entity)) {
return Rs.ok(CodeMsg.SAVE_SUCCESS);
} else {
return Rs.raise(CodeMsg.SAVE_ERROR);
}
}
校验失败时,会抛出异常:org.springframework.validation.BindException
- 可以通过 BindException.getBindResult().getAllErrors() 拿到所有的错误信息
方法的非 Model 参数校验
使用步骤: ① 在 Controller 上加 @Validated 注解 ② 在非 Model 参数上加相关的校验注解
@Validated
public abstract class BaseController<T> {
@PostMapping("/remove")
public R remove(@NotBlank(message = "id不能为空") String id) {
// code...
}
}
校验失败时,会抛出异常:javax.validation.ConstraintViolationException
- 可以通过 ConstraintViolationException.getConstraintViolations() 拿到所有的错误信息
常见注解
@NotNull:不能为 null,但可以为 empty@NotEmpty:不能为 null,而且长度必须大于 0@NotBlank:只能作用在 String 上,不能为 null,且去除空格后长度必须大于 0
@AssertFalse:必须为 false@AssertTrue:必须为 true
@Max,@DecimalMax:必须为一个不大于指定值的数字@Min,@DecimalMin:必须为一个不小于指定值的数字@Digits:必须为一个小数,且整数部分的位数不能超过 integer,小数部分的位数不能超过 fraction
@Email:必须是 Email,也可以通过正则表达式和 flag 指定自定义的 Email 格式
@Future:必须是一个将来的日期@Past:必须是一个过去的日期@Range:必须在合适的范围内,min 到 max 之间
@Size,@Length:长度必须在 min 到 max 之间
@Pattern:必须符合指定的正则表达式
自定义校验
示例代码:自定义校验
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = BoolNumber.BoolNumberValidator.class)
public @interface BoolNumber {
// 这3个方法必须实现,不然会报错
String message() default "只能是0和1";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
class BoolNumberValidator implements ConstraintValidator<BoolNumber, Short> {
@Override
public boolean isValid(Short num,
ConstraintValidatorContext constraintValidatorContext) {
return num == null || num == 0 || num == 1;
}
}
}
@BoolNumber(message = "disabled只能是0或1")
private Short disabled;
快速失败
默认情况下是检测完所有的错误后再统一抛出异常,也可以设置快速失败
- 快速失败:只要检测到一个错误,就直接抛出异常,不再往下检查
@Configuration
public class ValidatorCfg {
@Bean
public Validator validator() {
return Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true) // 快速失败
.buildValidatorFactory().getValidator();
}
}
Swagger
Swagger 可以快速生成接口文档,极大地节省了开发人员编写接口文档的时间
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
也可以使用官方的 starter 一步到位:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
基本配置与使用
代码:Swagger 的配置与使用
注:如果使用 starter,需要去掉 @EnableSwagger2
@Configuration
@EnableSwagger2 // 如果使用的 starter, 需要去掉这个注解
public class SwaggerCfg {
@Bean
public Docket docket(Environment environment) {
boolean enable = environment.acceptsProfiles(Profiles.of("dev", "test"));
return new Docket(DocumentationType.SWAGGER_2)
.enable(enable).apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("驾考接口接口文档")
.description("已经包含了所有可用的请求接口")
.version("1.0.0")
.build();
}
}
配置好后,可以通过 URL 去访问 Swagger 文档页面,访问路径:
- 如果使用了 starter:context_path/swagger-ui/index.html
- 如果没有使用 swagger:context_path/swagger-ui.html
被 Swagger 显示的 API 应当由我们自己来配置,API 选择:
- 以 dict 开头的包下的所有的接口:
new Docket(DocumentationType.SWAGGER_2)
.select()
.paths(PathSelectors.ant("/dict*/**"))
.build();
- 使用了 @RestController 注解的接口:
new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.build();
- 指定包下面的所有接口:
new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.mj.jk.controller"))
.build();
有时候某些参数不希望显示出来被其他人看见,可以配置 忽略参数:
new Docket(DocumentationType.SWAGGER_2)
.ignoredParameterTypes(
HttpSession.class,
HttpServletRequest.class,
HttpServletResponse.class);
可以将 API 分组后进行显示(根据下拉框选择某个分组,展示对应的 API),例如:将满足条件的 API 接口配置到 “元数据” 这一组:
new Docket(DocumentationType.SWAGGER_2)
.groupName("元数据")
.select()
.paths(PathSelectors.regex("/(dict.+|plateRegions.+)"))
.build();
全局参数:
Parameter token = new ParameterBuilder()
.name("token")
.description("用户登录令牌")
.parameterType("header") // 还可以是query
.modelRef(new ModelRef("String")) // 参数类型
.required(true) // 是否必要
.build();
new Docket(DocumentationType.SWAGGER_2)
.globalOperationParameters(List.of(token)); // List.of是jdk11的语法
常用注解
Bug:NumberFormatException
在使用 Swagger 2.9.2 版本时,可能经常在控制台看到一个异常:
- java.lang.NumberFormatException:For input string:""
- 这是 io.swagger:swagger-models:1.5.20 内部的一个 Bug
- 升级到最新版本的 io.swagger:swagger-models 即可解决问题
直接在 pom.xml 中,使用下面的 io.swagger 则会覆盖 starter 中的版本
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.6.2</version>
</dependency>
全局异常处理
@RestControllerAdvice
@Slf4j
public class CommonExceptionHandler {
@ExceptionHandler(Throwable.class)
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public JsonVo handle(Throwable t) {
log.error("handleThrowable", t);
return null;
}
}
驾考 - 前后端分离
MyBatis 的 QueryWrapper 的增强实现
使用原生的 MyBatis 对于多个关键字的模糊查询写法比较累赘,对使用方法进行增强:
public class MpLambdaQueryWrapper<T> extends LambdaQueryWrapper<T> {
// 返回值返回自身实现链式编程
@SafeVarargs
public final MpLambdaQueryWrapper<T> like(Object val, SFunction<T, ?>... funcs) {
if (val == null) return this;
String str = val.toString();
if (str.length() == 0) return this;
return (MpLambdaQueryWrapper<T>) nested((w) -> {
for (SFunction<T, ?> func : funcs) {
w.like(func, str).or();
}
});
}
}
增强后,使用起来就很方便:
// 查询条件
MpLambdaQueryWrapper<DictType> wrapper = new MpLambdaQueryWrapper<>();
// 根据关键字查询
wrapper.like(query.getKeyword(),
DictType::getName,
DictType::getValue,
DictType::getIntro);
mapstruct
参考文章:mapstruct 使用详解
前后端分离使用 Session
后台:
- allowCredentials(true)
- allowedOrigins(origins):origins 要指定特定主机地址,不能使用 *
前端:
$.ajax({
url: url,
xhrFields: {
withCredentials: true
}
})
企业中的文件上传(图片、视频)
1、文件数据跟随表单数据一起提交 2、文件数据先单独提交,从文件服务器获取一个文件的 uri,文件的 uri 跟随表单数据一起提交