spring-mvc第二期:让Controller没有秘密 (http,@RestController,Multipart,@PathVariable,@MatrixVariavle)

上期回顾:链接

源码clone地址:链接 (spring-mvc模块)

SpringMVC的底层细节不可不知,但在日常开发的大部分时间里,我们还是要专注于业务逻辑的开发,因此详细了解接口的 "管家"——Controller自然很重要:(还是先摆出这张图片,然后根据官方文档来讨论)

目录

1.简单回顾一下 HTTP

2.@Controller Or @RestController ?

3.@RquestMapping

4.Handler methods

4.1.参数

4.1.1.@PathVariable & @MatrixVariable

4.1.2. @RequestParam & @RequestHeader

4.1.3.@CookieValue

4.1.4.Model & @ModelAttribute

 4.1.5.Multipart

4.1.6.@RequestBody

4.2.返回值

4.2.1.@ResposeBody & ResponseEntity

4.2.2.@JsonIgnore & @JsonView

4.2.3.@JsonAlias & @JsonProperty

5.Exceptions

6.Controller Advice


1.简单回顾一下 HTTP

Http报头分为通用报头,请求报头,响应报头和实体报头。
请求方的http报头结构:通用报头+请求报头+实体报头
响应方的http报头结构:通用报头+响应报头+实体报头

而这里我们讨论两个常用报头信息:

  • 请求报头 Accept ( 例如 (Accept:application/json) 代表客户端希望接受的数据类型是 json 类型)
  • 实体报头 Content-Type (Content-Type代表发送端(客户端或者服务器)发送的实体数据的数据类型)

2.@Controller Or @RestController ?

在上一期的基础上,我们可以直接开始新建一个Controller类了

@Controller
public class BoringController {
}

@Controller 或 @RestController 注解可谓是Controller的灵魂,至于他们两个有啥区别捏,先给出官方解释:读完此文你会更加明白 

@RestController is a composed annotation that is itself meta-annotated with @Controller and @ResponseBody to indicate a controller whose every method inherits the type-level @ResponseBody annotation and, therefore, writes directly to the response body versus view resolution and rendering with an HTML template. 

当让配置了这些还不够,你还得让ServletWebApplicationContext 发现这个Bean(@Controller 中  包含@Component 注解),还记得我们在上一期中说到SevletWebApplicationContext 是由MvcConfig得到的,因此这个组件扫描也应当陪在这里:

3.@RquestMapping

requestMapping 即浏览器请求的接口路径,和下面的 Handler methods可以说是对好恋人,而HandlerMapping则是他们的介绍人(上一期有提到)

    @RequestMapping(name = "boring1", value = {"/1/{id}", "/101/{id}"})
    public String boring1(@PathVariable Long id) {
        return "home";
    }

@RequetsMapping 注解有如下几个修饰:

  • name 此接口的名字,和 <servlet-name>同义
  • value : 这个就很重要啦,是请求的路径,可以同时配多个,而且支持模糊匹配,如下表

可以使用@PathVariable 接受请求传入的参数,甚至这样:(这里要注意请求的参数和方法传入的参数类型要匹配,否则会抛出TypeMismatchException 异常)

或者这样:

  • method: 请求方式
@RequestMapping(value = {"/2"}, method = RequestMethod.POST)

以上等价于@PostMapping 注解, 其他的 GET PUT DELETE同理

  •  consumes 与 produce :
@RequestMapping(name = "boring1", value = {"/1/{id}", "/101/{id}"}, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)

如果写成如上形式,表面该请求需满足:

  •  params参数
@GetMapping(value = {"/3"}, params = {"name","age"})

如上写法,则表示请求要为如下才可以:

4.Handler methods

当使用@RequestMapping 为handler规范了url后,我们就来专心讨论请求的处理方法 Handler

4.1.参数

4.1.1.@PathVariable & @MatrixVariable

为了让url的参数传入更灵活,spring支持以上两种方式从url中获取数据,@PathVariable 上文已讲,这里着重说一下 @MatrixVariable

    /**
     * GET http://localhost:8080/boring/4/swing;a=blue;b=yellow/api/12;b=red;c=black
     */
    @GetMapping(value = {"/4/{name}/api/{age}"})
    public String boring4(@PathVariable String name,
                          @MatrixVariable(pathVar = "name", name = "a", required = false) String a,
                          @PathVariable String age,
                          @MatrixVariable(pathVar = "age", name = "b", required = false) String b,
                          @MatrixVariable(pathVar = "name") MultiValueMap<String, String> multiValueMap,
                          @MatrixVariable MultiValueMap<String, String> map) {
        //swing
        System.out.println(name);
        //blue
        System.out.println(a);
        //12
        System.out.println(age);
        //red
        System.out.println(b);
        //{a=[blue], b=[yellow]}
        System.out.println(multiValueMap);
        //{a=[blue], b=[yellow, red], c=[black]}
        System.out.println(map);
        return "home";
    }

以上基本是官网列出的所有用法,分析: ( @MatrixVariable(pathVar = "name", name = "a", required = false) String a ) 表示在 url 中name部分寻找一个a的值,并将其付给 参数a

注意要想实现这个注解,必须在mvcConfig中增加如下配置(将urlPathHelper.setRemoveSemicolonContent设置为false)

    @Bean
    public UrlPathHelper urlPathHelper() {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setRemoveSemicolonContent(false);
        return urlPathHelper;
    }

4.1.2. @RequestParam & @RequestHeader

    /**
     * GET http://localhost:8080/boring/5?name=swing
     * Accept: multipart/form-data
     */
    @GetMapping(value = {"/5"})
    public String boring5(@RequestHeader(value = "Accept",required = false) String accept, @RequestParam(value = "name",required = false) String name) {
        //multipart/form-data
        System.out.println(accept);
        //swing
        System.out.println(name);
        return "home";
    }

4.1.3.@CookieValue

    /**
     * GET http://localhost:8080/boring/6
     * Cookie: sentence=swing
     */
    @GetMapping(value = {"/6"})
    public String boring6(@CookieValue("sentence") String sentence) {
        //swing
        System.out.println(sentence.toString());
        return "home";
    }

4.1.4.Model & @ModelAttribute

上一期有讲到,Model用来将handler处理后的数据传到视图层进行渲染,数据是以键值对的形式存储起来的

@ModelAttribute注解用于将方法的参数或方法的返回值绑定到指定的模型属性上,并返回给Web视图

当作用在方法上时:

    /**
     * GET http://localhost:8080/boring/8
     */
    @GetMapping(value = {"/8"})
    public String boring8(Model model) {
        return "home";
    }

    @ModelAttribute(name = "message")
    public String initName() {
        return "swing world";
    }

表示在请求到达handler之前,先将 <"message":"swing world">放入 Model中

当作用在参数上时:

    /**
     * GET http://localhost:8080/boring/9
     */
    @GetMapping(value = {"/9"})
    public String boring9(@ModelAttribute UserDO userDO, Model model) {
        userDO.setUsername("swing");
        userDO.setPassword("312312");
        userDO.setAge(11);
        userDO.setId(10L);
        return "home";
    }

此时如果Model 里不存在 userDO属性,则创建一个,并放入Model中(注意这时候这个UserDO类一定要有无参数的构造函数)

 4.1.5.Multipart

文件上传是一个很常用的功能,而从前端传入的二进制文件数据,便是通过MultipartFile 参数传入Handler

首先我们咋们先增加个依赖:

    <!--文件的上传与下载-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.3</version>
            <!--排除其中与本项目重复的包-->
            <exclusions>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>javax.servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

然后在mvcConfig中配置一下 MultipartResolver 用来解析文件

    /**
     * 配置文件上传解析器
     */
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
        multipartResolver.setMaxUploadSize(10485760);
        multipartResolver.setDefaultEncoding("UTF-8");
        return multipartResolver;
    }

 简单写个上传页面:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Welcome!</title>
</head>
<body>
<h1>Welcome ${message}</h1>
<form action="/boring/upLoad" method="post" enctype="multipart/form-data">
    File:
    <input type="file" name="file"/>
    <input type="submit" value="UpLoad"/>
    <input type="reset" value="Reset"/>
</form>
</body>
</html>

最后开始我们的handler

    /**
     * 文件的上传
     */
    @RequestMapping("/upLoad")
    public String upLoadFile(@RequestParam("file") MultipartFile uploadFile, HttpServletResponse response) {
        System.out.println(uploadFile.getName());
        System.out.println(uploadFile.getSize());
        return "home";
    }

4.1.6.@RequestBody

这个参数可以获取客户端传来的请求体,由于Get的请求参数是放入url中,因此这个注解自然是实用于POST请求

当请求体是json格式,那么我我们有如下两种获取方式,使用字符串或数据传输对象:

/**
     * POST http://localhost:8080/swing/1
     * Content-Type: application/json
     * {
     * "id": 12,
     * "username": "swing"
     * }
     */
    @PostMapping("/1")
    public String swing1(@RequestBody String jsonString) {
//        {
//            "id": 12,
//            "username": "swing"
//        }
        log.info(jsonString);
        return "home";
    }

    /**
     * POST http://localhost:8080/swing/2
     * Content-Type: application/json
     * {
     * "id": 12,
     * "username": "swing"
     * }
     */
    @PostMapping("/2")
    public String swing2(@RequestBody UserDTO user) {
        //UserDTO(id=12, username=swing)
        log.info(user.toString());
        return "home";
    }

当然和上面提到的 @RequestParam 一起使用也是没问题的

/**
     * POST http://localhost:8080/swing/3?age=45
     * Content-Type: application/json
     * {
     * "id": 12,
     * "username": "swing"
     * }
     */
    @PostMapping("/3")
    public String swing3(@RequestBody UserDTO user, @RequestParam Integer age) {
        //UserDTO(id=12, username=swing)
        log.info(user.toString());
        //45
        log.info(age.toString());
        return "home";
    }

4.2.返回值

4.2.1.@ResposeBody & ResponseEntity

处理完了请求,我们自然要来考虑:如何将我们的结果返回呢?常用的两种模式如下

  • 返回为ModelAndView 然后交给视图解析器渲染出对应的页面,然后以html的形式传给浏览器显示
  • 第二种是基于前后端分离的模式,后端的程序员不必再去纠结如何渲染页面,只需要处理请求,然后将处理结果以约定的数据格式传送到前端(通常是JSON或XML格式),然后交由前端自行渲染

之前我们使用的都是基于第一种方式的数据返回,服务端视图渲染,现在我们着重来说一下第二种模式,这里就不得不提到@ResposeBody注解啦:

它作用在handler上时候表示将handler的返回值以字符串的形式返回给前端,如下演示:

    /**
     * 请求:
     * GET http://localhost:8080/swing/4
     * 结果:
     * {
     * "id": 20,
     * "username": "swing"
     * }
     */
    @GetMapping("/4")
    @ResponseBody
    public UserDTO swing4() {
        UserDTO userDTO = new UserDTO();
        userDTO.setId(20L);
        userDTO.setUsername("swing");
        return userDTO;
    }

 如果@ResponseBody作用在Controller上,则对该类中的所有handler起作用

而@RestController和@Controller的区别也是应为前者多了一个@ResponseBody注解,因此在前后端分离模式的开发时,我们常采用@RestController

另外官方还提供一个和@ResponseBody作用相似的类 ResponseEntity 但是它多两个属性,status,headers

/**
     * 请求:
     * GET http://localhost:8080/swing/5
     * 结果:
     * {
     * "id": 20,
     * "username": "swing"
     * }
     */
    @GetMapping("/5")
    public ResponseEntity<UserDTO> swing5() {
        UserDTO userDTO = new UserDTO();
        userDTO.setId(20L);
        userDTO.setUsername("swing");
        return ResponseEntity.ok(userDTO);
    }

OK!既然已经搞清楚了返回数据的格式,那么我们便来讨论一下返回数据的内容,试想一下,如果一个接口返回的内容是根据业务随便改变,一会儿三个字段,一会儿十个字段,那怕是会被前端的小伙伴喷成筛子,所以,如果是使用前后端分离模式,接口的响应数据一定要做到规范统一。

4.2.2.@JsonIgnore & @JsonView

既然接口可以返回JOSN类型的数据,那么我们就不得不考虑一个数据隐私的问题,例如像 password这样的字段,可不能随随便便的返回给前端,于是我们便可以使用@JsonIgnore注解来让对象在序列化为Json时候忽略password字段,如下:

public class UserDO implements Serializable {
    private Long id;

    private String username;
    @JsonIgnore
    private String password;

    private Integer age;

    private static final long serialVersionUID = 1L;
}


/**
     * 请求:
     * GET http://localhost:8080/swing/6
     * 结果:
     * {
     * "id": 12,
     * "username": "swing",
     * "age": 18
     * }
     */
    @GetMapping("/6")
    @ResponseBody
    public UserDO swing6() {
        UserDO user = new UserDO();
        user.setId(12L);
        user.setAge(18);
        user.setUsername("swing");
        user.setPassword("42423423");
        return user;
    }

不过新的问题又来了,一个字段可能并不是一直都不需要,如果在某个业务场景下,我们需要json将密码返回,那可咋办,于是@JsonView 便来了:

@Data
public class UserDO implements Serializable {
    /**
     * 没有密码的视图
     */
    public interface WithoutPasswordView {
    }

    /**
     * 有密码的视图
     */
    public interface WithPasswordView extends WithoutPasswordView {
    }

    @JsonView(WithoutPasswordView.class)
    private Long id;

    @JsonView(WithoutPasswordView.class)
    private String username;

    @JsonView(WithPasswordView.class)
    private String password;

    private Integer age;

    private static final long serialVersionUID = 1L;
}

 注意:没有被@JsonView注解的字段不会被序列化

/**
     * 请求:
     * GET http://localhost:8080/swing/7
     * 结果:
     * {
     * "id": 12,
     * "username": "swing"
     * }
     */
    @GetMapping("/7")
    @ResponseBody
    @JsonView(UserDO.WithoutPasswordView.class)
    public UserDO swing7() {
        UserDO user = new UserDO();
        user.setId(12L);
        user.setAge(18);
        user.setUsername("swing");
        user.setPassword("42423423");
        return user;
    }

    /**
     * 请求:
     * GET http://localhost:8080/swing/8
     * 结果:
     * {
     * "id": 12,
     * "username": "swing",
     * "password": "42423423"
     * }
     */
    @GetMapping("/8")
    @ResponseBody
    @JsonView(UserDO.WithPasswordView.class)
    public UserDO swing8() {
        UserDO user = new UserDO();
        user.setId(12L);
        user.setAge(18);
        user.setUsername("swing");
        user.setPassword("42423423");
        return user;
    }

4.2.3.@JsonAlias & @JsonProperty

这两个注解让我们可以适当地对 json 的序列化和反序列化进行一下设置:

  • @JsonAlias:JSON反序列化时起作用, 给属性一个别名(即json的key名)(注意:使用这个注解后原来的字段名便不可以再作为json的key名)
  • @JsonProperty:JSON序列化时起作用,设置字段的key名
@Data
public class UserDTO {
    private Long id;

    @JsonAlias(value = {"myName", "testName"})
    @JsonProperty("myName")
    private String username;
}


    /**
     * 请求:
     * POST http://localhost:8080/swing/9
     * Content-Type: application/json
     * {
     * "id": 12,
     * "testName": "swing"
     * }
     * 结果:
     * {
     * "id": 12,
     * "myName": "swing"
     * }
     */
    @PostMapping("/9")
    @ResponseBody
    public UserDTO swing9(@RequestBody UserDTO user) {
        return user;
    }

5.Exceptions

在阿里巴巴代码规范中的工程结构模块,对异常的处理有如下建议:

(分层异常处理规约)在DAO层,产生的异常类型有很多,无法用细粒度的异常进行catch,使用catch(Exceptione)方式,并thrownewDAOException(e),不需要打印日志,因为日志在Manager/Service层一定需要捕获并打印到日志文件中去,如果同台服务再打日志,浪费性能和存储。在Service层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息,相当于保护案发现场。如果Manager层与Service同机部署,日志方式与DAO层处理一致,如果是单独部署,则采用与Service一致的处理方式。Web层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳 Java开发手册38/44转到友好错误页面,加上用户容易理解的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回

既然web层的一场不能再往上抛了,Spring为我们提供了@ExceptonHandler方法,用来捕捉抛到最上层(这里指web层)的异常,我们来拿程序员的最常见的“小伙伴”NPE 来举个栗子

/**
 * @author swing
 */
@Slf4j
@Controller
@RequestMapping(value = {"/ex"})
public class ExceptionController {

    @ExceptionHandler({NullPointerException.class})
    public ResponseEntity<String> handle(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Sorry!NullPointerException!");
    }

    /**
     * 请求:
     * POST http://localhost:8080/ex/1
     * Content-Type: application/json
     *
     * {
     *   "id": 12,
     *   "testName": "钱骞",
     *   "birthday": ""
     * }
     * 结果:
     * Sorry!NullPointerException?
     * Response code: 500; Time: 50ms; Content length: 27 bytes
     */
    @PostMapping("/1")
    @ResponseBody
    public UserDTO swing9(@RequestBody UserDTO user) {
        System.out.println(user.getBirthday().getTime());
        return user;
    }
}

6.Controller Advice

上文中我们介绍了@ExceptionHandler @ModelAttribute 等注解,但是他们有一个不足:只会在定义他们的Controller中作用,很明显,当项目中有超级多的Controller时,我们需要寻找一个新的定义办法,首先想到的当然是Spring AOP的思想,做一个切面,SpringMVC为我们提供了 @ControllerAdvice 和 @RestControllerAdvice 用来实现这个功能:

如下

/**
 * @author swing
 */
@ControllerAdvice(assignableTypes = {SwingController.class})
public class AdviceController {
    @ExceptionHandler({NullPointerException.class})
    public ResponseEntity<String> handle(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Sorry!NullPointerException!");
    }
}

如果需要精确的作用与某一些Controller,ControllerAdvice提供如下几种定位

//所用以RestController注解的Controller
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// 该包下的所有Controller
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// 详细的Controller类
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

猜你喜欢

转载自blog.csdn.net/qq_42013035/article/details/106558696