SpringMVC
1. RESTful风格
1.1 概念
RESTful :是网络应用程序的接口(url)设计风格,基于HTTP协议,采用了以下要素构建网络应用程序访问路径。
- 设计要访问的资源【名词】路径,用唯一的 URI 表示
- 选择资源的展现的方式(数据交互):一般是 json 格式,也可以是其它格式
例:
-
查询编号为 10 的图书
-
更新编号为 10 的图书
-
删除编号为 10 的图书
-
新增的图书,无编号
1.2 路径参数
要实现【设计要访问的资源【名词】,用统一的 URI 表示】这一特性,我们发现 RESTful 风格中,唯一标识 id,并不是像之前一样从请求参数(即 ? 后)传递过来,而是此 id 就是路径的组成部分,因此我们需要从路径中获取参数
- Spring 提供了 @PathVariable 来解析资源路径中的参数信息
@PathVariable :路径变量,将路径后面占位符的值赋值给形参变量。如果占位符的名称和形参变量名不一样,需要在注解中指定占位符名称。
@RequestMapping("/brand/{a}")
public String selectById(@PathVariable("a") Integer id){
log.info("selectById--->id:{}",id);
return "selectById查询成功--->"+id;
}
要实现【用 HTTP Method 【动词】来转换资源状态】按 **请求方法(GET POST 等)**来区分请求
- 查询
- method = RequestMethod.GET
- 删除
- method = RequestMethod.DELETE
- 修改
- method = RequestMethod.PUT
- 新增
- method = RequestMethod.POST
查询
@RequestMapping(value = "/brand/{id}", method = RequestMethod.GET)
public String selectById(@PathVariable Integer id) {
log.info("selectById--->id:{}", id);
return "selectById查询成功--->" + id;
}
修改
@RequestMapping(value = "/brand",method = RequestMethod.PUT)
public String update(@RequestBody Brand brand){
log.info("update--->brand:{}",brand);
return "update修改成功";
}
1.3 衍生注解
SpringMVC提供了**@GetMapping**、@PostMapping、@PutMapping、@DeleteMapping四种不同的注解定义不同请求方式映射路径
- @GetMapping
@RestController
@Slf4j
@RequestMapping("/brand")
//根据id查询
public class BrandController {
@GetMapping("/{id}")
public String selectById(@PathVariable Integer id) {
log.info("selectById--->id:{}", id);
return "selectById查询成功--->" + id;
}
}
- @DeleteMapping
@RestController
@Slf4j
@RequestMapping("/brand")
public class BrandController {
//根据id删除
@DeleteMapping("/{id}")
public String delete(@PathVariable Integer id) {
log.info("delete--->id:{}", id);
return "delete删除成功--->" + id;
}
}
- @PutMapping
@RestController
@Slf4j
@RequestMapping("/brand")
public class BrandController {
//修改
@PutMapping
public String update(@RequestBody Brand brand){
log.info("update--->brand:{}",brand);
return "update修改成功";
}
}
- @PostMapping
@RestController
@Slf4j
@RequestMapping("/brand")
public class BrandController {
//新增
@PostMapping
public String add(@RequestBody Brand brand) {
log.info("add--->brand:{}", brand);
return "add添加成功..";
}
}
- 注意:衍生注解只能写在方法上,不能写在类上。
2. 进阶
2.1 参数校验
数据不校验,就可能导致非法、残缺数据入库,严重还可能引起系统漏洞。
- 添加依赖
<!--validation校验框架起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 添加校验规则
- @NotNull:要求值不为null
- @NotEmpty:要求值不为null和" "(空字符串),不能用于校验Integer类型的数据,只能用于校验字符串类型的数据
- @NotBlank:要求值不为null、“”、" "(多个空字符串),不能用于校验Integer类型的数据,只能用于检验字符串类型的数据
- @Min(18):设置最小值
- @Min(80):设置最大值
@Data
public class User {
//@NotNull //要求值不为null
@NotEmpty(message = "不能为空!") //要求值不为null和" "(空字符串),不能用于校验Integer类型的数据,只能用于校验字符串类型的数据
//@NotBlank //要求值不为null、""、" "(多个空字符串),不能用于校验Integer类型的数据,只能用于检验字符串类型的数据
private String name;
@NotNull //不为null
@Min(18)//最小值
@Min(80)//设置最大值
private Integer age;
}
- 控制器代码
/**
* 保存学生信息
* @Valid:表示开启校验
* BindingResult result:封装了校验结果,只能放到被检验对象的参数后面
* @param user
* @return
*/
@PostMapping
public String save(@Valid @RequestBody User user, BindingResult result){
//使用校验框架对user进行校验
if (result.hasErrors()) { //是否有错误
//获取所以错误集合
List<FieldError> fieldErrors = result.getFieldErrors();
log.info("fieldErrors = {}", fieldErrors);
//遍历所有错误的集合
for (FieldError fieldError : fieldErrors) {
//获取错误字段
String field = fieldError.getField();
//获取错误信息
String defaultMessage = fieldError.getDefaultMessage();
log.info("{}{}", field,defaultMessage);
}
return "参数不合法!";
}
log.info("save--->user:{}",user);
//判断校验是否成功,如果有不合法数据,打印错误信息
return "保存user成功";
}
2.2 文件上传
-
文件上传对我们的表单是由要求的,下面是我们平时用的普通表单,无法完成文件上传
-
文件上传表单
单文件上传
- controller代码
@RestController
@Slf4j
public class UploadController {
//需要在配置文件配置路径
@Value("${fileDir}")
private String fileDir;
/**
* 单文件上传
* @param name
* @param multipartFile
* @return
* @throws IOException
*/
@RequestMapping("/upload")
public String upload(String name, @RequestParam("file") MultipartFile multipartFile) throws IOException {
//1.判断是否有上传文件
if (multipartFile == null || multipartFile.isEmpty()) {
return "未上传文件!";
}
//2.获取文件名
String fileName = multipartFile.getOriginalFilename();
log.info("文件名:{}", fileName);
//3.获取其他的文件信息
log.info("文件名:{}", fileName); //获取文件大小,单位字节
log.info("文件大小:{}", multipartFile.getSize());//获取文件类型
//4.将上传的文件保存到服务器硬盘中
multipartFile.transferTo(new File(fileDir, fileName));
return "上传成功!";
}
}
- yml文件
servlet:
multipart:
max-file-size: 500MB #指定上传文件允许的最大大小。 默认值为1MB
max-request-size: 1GB #指定multipart/form-data请求允许的最大大小。 默认值为10MB。
fileDir: D:\\yyds\\javacode\\javaEE-framework\\springmvc_01\\src\\main\\resources\\static\\img #文件保存路径
多文件上传
- controller代码
@RestController
@Slf4j
public class UploadController {
//需要在配置文件配置路径
@Value("${fileDir}")
private String fileDir;
/**
* 多文件上传
* @param name
* @param multipartFiles
* @return
* @throws IOException
*/
@RequestMapping("/uploads")
public String upload2(String name, @RequestParam("file") MultipartFile[] multipartFiles) throws IOException {
for (MultipartFile multipartFile : multipartFiles) {
//1.判断是否有上传文件
if (multipartFile == null || multipartFile.isEmpty()) {
return "未上传文件!";
}
//2.获取文件名
String fileName = multipartFile.getOriginalFilename();
log.info("文件名:{}", fileName);
//3.获取其他的文件信息
log.info("文件名:{}", fileName); //获取文件大小,单位字节
log.info("文件大小:{}", multipartFile.getSize());//获取文件类型
//4.将上传的文件保存到服务器硬盘中
multipartFile.transferTo(new File(fileDir, fileName));
}
return "上传成功!";
}
}
2.3 统一响应结果
设置响应状态码和响应头
可以用 @ResponseStatus 来指定控制器方法的状态码
@GetMapping("/s2")
@ResponseStatus(HttpStatus.BAD_REQUEST)//响应400状态码
public void s2() {}
可以控制器方法中使用用 ResponseEntity类来封装响应头
@GetMapping("/s2")
public ResponseEntity s2() {
log.info("s2...");
//1.准备响应头
LinkedMultiValueMap header = new LinkedMultiValueMap();
header.add("header1", "a");
//2.准备响应体
Brand body = new Brand();
//3.创建ResponseEntity对象,封装响应信息(状态码、响应头、响应体)
ResponseEntity entity = new ResponseEntity(body, header, HttpStatus.OK);
//返回ResponseEntity对象
return entity;
}
自定义Result封装响应结果
响应状态码的不足
- 可以用状态码较为精确地描述此操作是成功失败、是客户端的问题、还是服务端的问题,是最为通用的描述,但状态码毕竟有限。
- 返回的响应体中,可能会表示正常的数据,也有可能包含错误提示,若不统一,就增加了前端解析成本
- 因此一般应用开发时,会定义一个 Result类统一响应格式
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Result {
//扩展的状态码,和具体业务有关
private Integer code;
//响应消息,可以保存成功的消息,也可以保存失败的消息
private String msg;
//响应结果,适用于查询
private Object data;
}
自定义应用(业务)状态码举例:在企业中由公司团队技术骨干制定:
- Controller
//统一响应结果Result类
@GetMapping("/{id}")
public Result selectByIds(@PathVariable Integer id) {
log.info("selectById-->id : {}", id);
return new Result(2001, "查询一个品牌成功", new Brand());
}
3. 高级
3.1 异常处理
全局异常处理
Spring MVC 提供了一种处理异常的方式,定义全局异常处理通知类来处理
- 定义一个异常通知类
//@ControllerAdvice //表示该类是一个异常处理类,类中的方法用来处理各种异常
@RestControllerAdvice //等价于@ControllerAdvice + @RequestBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理服务端异常
* @ExceptionHandler(Exception.class): 该方法是一个处理异常的方法
* 注解参数表示该方法要处理的异常类型,可以省略不写,如果不写就表示处理方法参数对应的异常
* @param e 要处理的异常对象,也就是接收到的异常对象
* @return
*/
// @ExceptionHandler(Exception.class)
@ExceptionHandler //处理和方法形参同类型的异常
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//响应状态码500
public Result doException(Exception e) {
log.info("接收到的异常是:{}", e.getMessage());
//向客户端返回结果
return new Result(500, "服务器正在维护,请耐心等待。", null);
}
}
- controller
@GetMapping("/{id}")
public String selectById(@PathVariable Integer id) {
log.info("selectById--->id:{}", id);
int i = 1 / 0;
return "selectById查询成功--->" + id;
}
- 结果
自定义异常
如果用户传入的参数不正确,我们通过全局异常处理器告知了前端,但是状态码是一个500的状态码是不合适的。
- 封装客户异常:只要是客户端导致的异常,统统抛出ClientException
/**
* 封装客户异常:只要是客户端导致的异常,统统抛出ClientException
*/
public class ClientException extends RuntimeException {
public ClientException(String message) {
super(message);
}
}
- 在GlobalExceptionHandler添加处理客户端异常方法
/**
* 处理客户端异常
* @param e
* @return
*/
@ExceptionHandler //处理和方法形参同类型的异常
@ResponseStatus(HttpStatus.BAD_REQUEST)//响应状态码400
public Result doClientException(ClientException e) {
log.info("接收到的异常是: {}", e.getMessage());
//向客户端返回结果
return new Result(400, e.getMessage(), null);
}
- controller
@PostMapping
public String save(@Valid @RequestBody User user, BindingResult result){
//使用校验框架对user进行校验
if (result.hasErrors()) { //是否有错误
//获取所以错误集合
List<FieldError> fieldErrors = result.getFieldErrors();
log.info("fieldErrors = {}", fieldErrors);
//遍历所有错误的集合
for (FieldError fieldError : fieldErrors) {
//获取错误字段
String field = fieldError.getField();
//获取错误信息
String defaultMessage = fieldError.getDefaultMessage();
log.info("{}{}", field,defaultMessage);
//校验不合法就抛出异常到处理器中
// throw new RuntimeException(field + defaultMessage);
throw new ClientException(field + defaultMessage);
}
// return "参数不合法!";
}
log.info("save--->user:{}",user);
//判断校验是否成功,如果有不合法数据,打印错误信息
return "保存user成功";
}
3.2 定义拦截器
拦截器:是一种动态拦截处理器方法的机制,可以在处理器方法执行前后执行,底层采用的是AOP。
定义拦截器并使用
- 编写拦截器
- 创建拦截器类实现HandlerInterceptor接口
- 重写preHandle方法
@Slf4j
@Component//添加到spring容器
public class LoginInterceptor implements HandlerInterceptor {
/**
* 前处理,在处理器方法执行前执行
* @param request 请求对象
* @param response 响应对象
* @param handler 当前方法对象,类似于aop中的连接点对象,封装了要执行的方法Method
* @return 返回true表示放行,返回false表示拦截,不继续执行
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle...前处理");
return true; //表示是否放行
}
- 配置拦截器
在启动类下进行配置或者自己新建一个类
@Autowired //注入拦截对象
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//addPathPatterns:添加拦截路径
registry.addInterceptor(loginInterceptor).addPathPatterns("/brand/**");//拦截路径
}
拦截器实现登录验证
需求:如果用户登录成功,就运行访问。如果没有登录,对客户端做出响应并阻止运行
- 在Controller中定义了login方法,访问之后模拟用户登录。
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/login")
public String login(HttpSession httpSession){
httpSession.setAttribute("user","user");
return "登录成功!";
}
}
- 修改拦截器preHandle方法。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle...");
//1.从session域中获取user信息
Object user = request.getSession().getAttribute("user");
//2.如果没有获取到,就表示用户没有登录,return false阻止访问
if (user == null) {
//抛出客户端异常,告诉客户端没有登录
throw new ClientException("你还没有登录,无权访问!");
}
return true; //返回true表示放行,返回false表示拦截,不继续执行
}
拦截器和过滤器的区别
- 归属不同: Filter属于Servlet技术,Interceptor属于SpringMVC技术
- 拦截内容不同: Filter是拦截请求和响应的,Interceptor拦截访问处理器中的方法,也就是拦截Controller中的方法调用。
- 执行顺序不同:先执行过滤器,后执行拦截器
3.3 SpringMVC的执行流程
前端控制器(DispatcherServlet):中间调度的作用,调度HandlerMapping、HandlerAdapter、ViewResolver工作。
处理器映射器(HandlerMapping):解析Controller中的路径,和我们客户端的路径进行匹配,找到要访问的Controller中的方法
处理器适配器(HandlerAdapter):负责去调用Controller中的对应的方法,执行该方法,将结果封装成ModelAndView对象。
试图解析器(ViewResolver):负责解析ModelAndView对象中的数据和页面,最终告诉前端控制器去响应。
处理器(Handler):也就是我们自己写的Controller对象。
- 执行步骤:
-
用户发送请求到前端控制器(DispatcherServlet)。
-
前端控制器 ( DispatcherServlet ) 收到请求调用处理器映射器 (HandlerMapping),去查找处理器(Handler)。
-
处理器映射器(HandlerMapping)找到具体的处理器(可以根据 xml 配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet。
-
前端控制器(DispatcherServlet)调用处理器适配器(HandlerAdapter)。
-
处理器适配器(HandlerAdapter)去调用自定义的处理器类(Controller)。
-
自定义的处理器类(Controller)将得到的参数进行处理并返回结果给处理器适配器(HandlerAdapter)。
-
处理器适配器 ( HandlerAdapter )将得到的结果返回给前端控制器 (DispatcherServlet),页面跳转返回ModelAndView,响应数据返回null给前端控制器 (DispatcherServlet) 。
-
前端控制器(DispatcherServlet )将 ModelAndView 传给视图解析器 (ViewReslover),如果是响应数据就没有这一步。
-
视图解析器(ViewReslover)将得到的参数从逻辑视图转换为物理视图并返回给前端控制器(DispatcherServlet) ,如果是响应数据就没有这一步。
-
前端控制器(DispatcherServlet)调用物理视图进行渲染并返回,如果是响应数据就没有这一步。
-
前端控制器(DispatcherServlet)将渲染后的结果返回,如果是响应数据直接通过response响应体传输。