项目实战 - 前后端分离、后端校验、Swagger

  • Layui
  • 同源策略
  • SpringMVC 实现 CORS
  • 后端校验 - hibernate-validator
  • 方法的 Model 参数校验
  • 方法的非 Model 参数校验
  • 常见注解
  • 自定义校验
  • 快速失败
  • Swagger
  • 基本配置与使用
  • 常用注解
  • Bug:NumberFormatException
  • 全局异常处理
  • 驾考 - 前后端分离
  • MyBatis 的 QueryWrapper 的增强实现
  • mapstruct
  • 前后端分离使用 Session
  • 企业中的文件上传(图片、视频)


早期的前后端协作模式:

  • 前端:切图仔、页面仔
  • 后台:动态模板技术组装成

以前的协作模式的问题:

  1. 前端地位比较低,大部分工作都在后台
  2. 调试、修改页面比较麻烦,需要前端、后台充分配合
  3. 浪费流量(每次请求都会返回整个页面,这就意味着返回了很多重复的内容)

前后端分离:

  • 前端:切图、页面、交互、路由、业务逻辑
  • 后台:返回 JSON

前后端分离:

  1. 页面保持使用静态页面即可(html)
  2. 静态页面(浏览器)发送异步请求(AJAX)给服务器,服务器返回 JSON
  3. 浏览器解析 JSON 数据,动态生成对应的 HTML 标签,显示到用户眼前
  4. 前端一个项目、后端一个项目,分开开发(跨域)
  5. 前端一个服务器、后端一个服务器,分开部署

Layui

Layui 是一款前端 UI 框架,可以帮助开发者快速搭建后台管理系统的前端页面

  • 免费版:https://www.layui.com/
  • LayuiMini(更强大的免费版):http://layuimini.99php.cn/

本课程使用的是单页版:v2-onepage

基本使用:下载后解压,将以下内容放到项目中

javaee前后端分离作业_Swagger

同源策略

浏览器有个 同源策略 (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 用于允许跨域。

javaee前后端分离作业_后端校验_02

如果不设置允许跨域,则会出现下面情况:

javaee前后端分离作业_Swagger_03

在返回响应时要设置允许跨域,即可跨域访问资源。

javaee前后端分离作业_Swagger_04

设置跨域后,即可成功访问页面。

javaee前后端分离作业_javaee前后端分离作业_05

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的语法

常用注解

javaee前后端分离作业_java_06

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 跟随表单数据一起提交