最全面的SpringMVC教程(二)——SpringMVC核心技术篇

时间:2022-11-28 15:53:14

前言

最全面的SpringMVC教程(二)——SpringMVC核心技术篇

本文为 【SpringMVC教程】核心技术篇 相关详细介绍,具体将对视图和模型拆分重定向与转发RequestMapping与其衍生注解URL 模式匹配牛逼的传参设定字符集返回json数据(序列化)获取请求中的json数据数据转化数据校验视图解析器详解全局异常捕获处理资源拦截器全局配置类等SpringMVC相关核心技术进行详尽介绍~

????博主主页:小新要变强 的主页
????Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
????算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
????Java微服务开源项目可参考:企业级Java微服务开源项目(开源框架,用于学习、毕设、公司项目、私活等,减少开发工作,让您只关注业务!)

↩️本文上接:最全面的SpringMVC教程(一)——SpringMVC简介


目录

最全面的SpringMVC教程(二)——SpringMVC核心技术篇

一、视图和模型拆分

视图和模型相伴相生,但是springmvc给我们提供了更好的,更优雅的解决方案:

  • Model会在调用handler时通过参数的形式传入
  • View可以简化为字符串形式返回

这样的解决方案也是企业开发中最常用的:

@RequestMapping("/test1")
public String testAnnotation(Model model){
    model.addAttribute("hello","hello annotationMvc as string");
    return "annotation";
}

二、重定向与转发

在返回的字符串中,默认使用视图解析器进行视图跳转。

springmvc给我们提供了更好的解决【重定向和转发】的方案:

????返回视图字符串加前缀redirect就可以进行重定向

redirect:/redirectController/redirectTest
redirect:https://www.baidu.com

????返回视图字符串加前缀forward就可以进行请求转发,而不走视图解析器

// 会将请求转发至/a/b
forward:/a/b

三、RequestMapping与其衍生注解

  • @RequestMapping这个注解很关键,他不仅仅是一个方法级的注解,还是一个类级注解。
  • 如果放在类上,相当于给每个方法默认都加上一个前缀url。
@Controller
@RequestMapping("/user/")
public class AnnotationController {

    @RequestMapping("register")
    public String register(Model model){
        ......
        return "register";
    }

    @RequestMapping("login")
    public String login(){
        ......
        return "register";
    }
}

????好处

  • 一个类一般处理一类业务,可以统一加上前缀,好区分
  • 简化书写复杂度

????RequestMapping注解有六个属性

value: 指定请求的实际地址,指定的地址可以是URI Template 模式(后面将会说明);
method: 指定请求的method类型, GET、POST、PUT、DELETE等;
consumes: 指定处理中的请求的内容类型(Content-Type),例如application/json;
produces: 指定返回响应的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;

@GetMapping(value = "{id}",produces = {"application/json;charset=utf-8"})

params: 指定request中必须包含某些参数值处理器才会继续执行;
headers: 指定request中必须包含某些指定的header值处理器才会继续执行。

@RequestMapping(value = "add",method = RequestMethod.POST,
                consumes = "application/json",produces = "text/plain",
                headers = "name",params = {"age","times"}
               )
@ResponseBody
public String add(Model model){
    model.addAttribute("user","add user");
    return "user";
}

@RequestMapping还有几个衍生注解,用来处理特定方法的请求:

@GetMapping("getOne")
public String getOne(){
    return "user";
}

@PostMapping("insert")
public String insert(){
    return "user";
}

@PutMapping("update")
public String update(){
    return "user";
}

@DeleteMapping("delete")
public String delete(){
    return "user";
}

源码中能看带GetMapping注解中有@RequestMapping作为元注解修饰:

@RequestMapping(method = {RequestMethod.GET})
public @interface GetMapping {

}

四、URL 模式匹配

@RequestMapping可以支持【URL模式匹配】,为此,spring提供了两种选择(两个类):

  • PathPatternPathPattern是 Web 应用程序的推荐解决方案,也是 Spring WebFlux 中的唯一选择,比较新。
  • AntPathMatcher:使用【字符串模式与字符串路径】匹配。这是Spring提供的原始解决方案,用于选择类路径、文件系统和其他位置上的资源。

小知识: 二者目前都存在于Spring技术栈内,做着相同的事。虽说现在还鲜有同学了解到PathPattern,我认为淘汰掉AntPathMatcher只是时间问题(特指web环境哈),毕竟后浪总归有上岸的一天。但不可否认,二者将在较长时间内共处,那么它俩到底有何区别呢?

  • (1)出现时间,AntPathMatcher是一个早在2003年(Spring的第一个版本)就已存在的路径匹配器,而PathPattern是Spring
    5新增的,旨在用于替换掉较为“古老”的AntPathMatcher。
  • (2)功能差异,PathPattern去掉了Ant字样,但保持了很好的向下兼容性:除了不支持将**写在path中间之外,其它的匹配规则从行为上均保持和AntPathMatcher一致,并且还新增了强大的{*pathVariable}的支持,他能匹配最后的多个路劲,并获取路径的值。
  • (3)性能差异,Spring官方说PathPattern的性能优于AntPathMatcher。

????一些模式匹配的示例

  • /resources/ima?e.png” - 匹配路径段中的一个字符
  • /resources/*.png” - 匹配路径段中的零个或多个字符
  • /resources/**” - 匹配多个路径段
  • /projects/{project}/versions” - 匹配路径段并将其【捕获为变量】
  • /projects/{project:[a-z]+}/versions” - 使用正则表达式匹配并【捕获变量】

捕获的 URI 变量可以使用@PathVariable注解,示例例如:

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}

还可以在类和方法级别声明 URI 变量,如以下示例所示:

@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

    @GetMapping("/pets/{petId}")
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}

????一个url可以匹配到多个路由的情况

有时候会遇到一个url可以匹配到多个路由的情况,这个时候就是由Spring的AntPatternComparator完成优先级处理,大致规律如下:

比如:有两个匹配规则一个是 /a/**,一个是 /a/b/**,还有一个是/a/b/*,如果访问的url是/a/b/c,其实这三个路由都能匹配到,在匹配优先级中,有限级如下:

匹配方式 优先级
全路径匹配,例如:配置路由/a/b/c 第一优先级
带有{}路径的匹配,例如:/a/{b}/c 第二优先级
正则匹配,例如:/a/{regex:\d{3}}/c 第三优先级
带有*路径的匹配,例如:/a/b/* 第四优先级
带有**路径的匹配,例如:/a/b/** 第五优先级
仅仅是双通配符:/** 最低优先级

注意:

  • 当有多个*和多个‘{}'时,命中单个路径多的,优先越高;
  • 多’*’的优先级高于‘**’,会优先匹配带有*

????我们还可以从一个类中看出,当一个url匹配了多个处理器时,优先级是如何考虑的,这个类是AntPathMatcher的一个内部类

protected static class AntPatternComparator implements Comparator<String> {

    @Override
    public int compare(String pattern1, String pattern2) {
        PatternInfo info1 = new PatternInfo(pattern1);
        PatternInfo info2 = new PatternInfo(pattern2);
		.....

        boolean pattern1EqualsPath = pattern1.equals(this.path);
        boolean pattern2EqualsPath = pattern2.equals(this.path);
        // 完全相等,是无法比较的
        if (pattern1EqualsPath && pattern2EqualsPath) {
            return 0;
        }
        // pattern1和urlequals,返回负数 1胜出
        else if (pattern1EqualsPath) {
            return -1;
        }
        // pattern2和urlequals,返回正数,2胜出
        else if (pattern2EqualsPath) {
            return 1;
        }

        // 都是前缀匹配,长的优先   /a/b/**  /a/**
        if (info1.isPrefixPattern() && info2.isPrefixPattern()) {
            return info2.getLength() - info1.getLength();
        }
        // 非前缀匹配的优先级高
        else if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) {
            return 1;
        }
        else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) {
            return -1;
        }

        // 匹配数越少,优先级越高
        if (info1.getTotalCount() != info2.getTotalCount()) {
            return info1.getTotalCount() - info2.getTotalCount();
        }

        // 路径越短越好
        if (info1.getLength() != info2.getLength()) {
            return info2.getLength() - info1.getLength();
        }

        // 单通配符个数,数量越少优先级越高
        if (info1.getSingleWildcards() < info2.getSingleWildcards()) {
            return -1;
        }
        else if (info2.getSingleWildcards() < info1.getSingleWildcards()) {
            return 1;
        }
        // url参数越少越优先
        if (info1.getUriVars() < info2.getUriVars()) {
            return -1;
        }
        else if (info2.getUriVars() < info1.getUriVars()) {
            return 1;
        }

        return 0;
    }
}

源码中我们看到的信息如下:

  • (1)完全匹配者,优先级最高
  • (2)都是前缀匹配(/a/**), 匹配路由越长,优先级越高
  • (3)前缀匹配优先级,比非前缀的低
  • (4)需要匹配的数量越少,优先级越高,this.uriVars + this.singleWildcards + (2 * this.doubleWildcards);
  • (5)路劲越短优先级越高
  • (6)*越少优先级越高
  • (7){}越少优先级越高

五、牛逼的传参

在学习servlet时,我们是这样获取请求参数的:

@PostMapping("insert")
public String insert(HttpServletRequest req){
    String username = req.getParameter("username");
    String password = req.getParameter("password");
    // 其他操作
    return "success";
}

有了springmvc之后,我们不再需要使用getParamter一个一个获取参数:

@Controller
@RequestMapping("/user/")
public class LoginController {

    @RequestMapping("login")
    public String login(String username,String password){
        System.out.println(username);
        System.out.println(password);
        return "login";
    }
}

如果一个表单几十个参数怎么获取啊?更牛的传参方式如下:

需要提前定义一个User对象:

public class User {
    
    private String username;
    private String password;
    private int age;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

直接在参数中申明user对象:

@Controller
@RequestMapping("/user/")
public class LoginController {

    @RequestMapping("register")
    public String register(User user){
        System.out.println(user);
        return "register";
    }

    @RequestMapping("login")
    public String login(String username,String password){
        System.out.println(username);
        System.out.println(password);
        return "login";
    }
}

????(1)@RequestParam

可以使用@RequestParam注解将【请求参数】(即查询参数或表单数据)绑定到控制器中的方法参数。

@Controller
@RequestMapping("/pets")
public class EditPetForm {

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { 
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }
}

默认情况下,使用此注解的方法参数是必需的,但我们可以通过将@RequestParam注解的【required标志设置】为 false来指定方法参数是可选的。如果目标方法参数类型不是String,则应用会自动进行类型转换,这个后边会讲。

请注意,使用@RequestParam是可选的。默认情况下,任何属于简单值类型且未被任何其他参数解析器解析的参数都被视为使用【@RequestParam】。

????(2)@RequestHeader

可以使用@RequestHeader注解将请求的首部信息绑定到控制器中的方法参数中:

假如我们的请求header如下:

Host localhost:8080 
Accept text/html,application/xhtml+xml,application/xml;q=0.9 
Accept-Language fr,en-gb;q=0.7,en;q=0.3 
Accept-Encoding gzip,deflate 
Accept-Charset ISO -8859-1,utf-8;q=0.7,*;q=0.7 
Keep-Alive 300

以下示例获取Accept-EncodingKeep-Alive标头的值:

@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, 
        @RequestHeader("Keep-Alive") long keepAlive) { 
    //...
}

小知识:当@RequestHeader注解上的使用Map<String, String>MultiValueMap<String, String>HttpHeaders参数,则map会被填充有所有header的值。当然,我们依然可以使用requied的属性来执行该参数不是必须的。

????(3)@CookieValue

可以使用@CookieValue注解将请求中的 cookie 的值绑定到控制器中的方法参数。

假设我们的请求中带有如下cookie:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下示例显示了如何获取 cookie 值:

@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { 
    //...
}

????(4)@ModelAttribute

可以使用@ModelAttribute注解在方法参数上来访问【模型中的属性】,或者在不存在的情况下对其进行实例化。模型的属性会覆盖来自 HTTP Servlet 请求参数的值,其名称与字段名称匹配,这称为数据绑定,它使您不必【处理解析】和【转换单个查询参数】和表单字段。以下示例显示了如何执行此操作:

@RequestMapping("/register")
public String register(@ModelAttribute("user") UserForm user) {
    ...
}

还有一个例子:

@ModelAttribute@RequestMapping 注解同时应用在方法上时,有以下作用:

  • 方法的【返回值】会存入到 Model 对象中,keyModelAttributevalue 属性值。
  • 方法的返回值不再是方法的访问路径,访问路径会变为 @RequestMappingvalue值,例如:@RequestMapping(value = "/index") 跳转的页面是 index.jsp 页面。
@Controller
public class ModelAttributeController {
    // @ModelAttribute和@RequestMapping同时放在方法上
    @RequestMapping(value = "/index")
    @ModelAttribute("name")
    public String model(@RequestParam(required = false) String name) {
        return name;
    }
}

????(5)@SessionAttribute

如果您需要访问全局管理的预先存在的会话属性,并且可能存在或可能不存在,您可以@SessionAttribute在方法参数上使用注解,如下所示示例显示:

@RequestMapping("/")
public String handle(@SessionAttribute User user) { 
    // ...
}

????(6)@RequestAttribute

@SessionAttribute一样,可以使用@RequestAttribute注解来访问先前创建的存在与请求中的属性(例如,由 ServletFilter HandlerInterceptor)创建或在请求转发中添加的数据:

@GetMapping("/")
public String handle(@RequestAttribute Client client) { 
    // ...
}

????(7)@SessionAttributes

@SessionAttributes注解应用到Controller上面,可以将Model中的属性同步到session当中:

@Controller
@RequestMapping("/Demo.do")
@SessionAttributes(value={"attr1","attr2"})
public class Demo {
    
    @RequestMapping(params="method=index")
    public ModelAndView index() {
        ModelAndView mav = new ModelAndView("index.jsp");
        mav.addObject("attr1", "attr1Value");
        mav.addObject("attr2", "attr2Value");
        return mav;
    }
    
    @RequestMapping(params="method=index2")
    public ModelAndView index2(@ModelAttribute("attr1")String attr1, @ModelAttribute("attr2")String attr2) {
        ModelAndView mav = new ModelAndView("success.jsp");
        return mav;
    }
}

附加一个注解使用的案例:

@RequestMapping("insertUser")
    public String insertUser(
            @RequestParam(value = "age",required = false) Integer age,
            @RequestHeader(value = "Content-Type",required = false) String contentType,
            @RequestHeader(required = false) String name,
            @CookieValue(value = "company",required = false) String company,
            @SessionAttribute(value = "username",required = false) String onlineUser,
            @RequestAttribute(required = false) Integer count,
            @ModelAttribute("date") Date date,
            @SessionAttribute(value = "date",required = false) Date sessionDate
    ) {
        System.out.println("sessionDate = " + sessionDate);
        System.out.println("date = " + date);
        System.out.println("count = " + count);
        System.out.println("onlineUser = " + onlineUser);
        System.out.println("age = " + age);
        System.out.println("contentType = " + contentType);
        System.out.println("name = " + name);
        System.out.println("company = " + company);
        return "user";
    }

????(8)数组的传递

在类似批量删除的场景中,我们可能需要传递一个id数组,此时我们仅仅需要将方法的参数指定为数组即可:

@GetMapping("/array")
public String testArray(@RequestParam("array") String[] array) throws Exception {
    System.out.println(Arrays.toString(array));
    return "array";
}

我们可以发送如下请求,可以是多个名称相同的key,也可以是一个key,但是值以逗号分割的参数:

http://localhost:8080/app/hellomvc?array=1,2,3,4

或者

http://localhost:8080/app/hellomvc?array=1&array=3

结果都是没有问题的:
最全面的SpringMVC教程(二)——SpringMVC核心技术篇

????(9)复杂参数的传递

当然我们在进行参数接收的时候,其中可能包含很复杂的参数,一个请求中可能包含很多项内容,比如以下表单:

当然我们要注意表单中的name(参数中key)的写法:

<form action="user/queryParam" method="post">
    排序字段:<br>
    <input type="text" name="sortField">
    <hr>
    数组:<br>
    <input type="text" name="ids[0]"> <br>
    <input type="text" name="ids[1]">
    <hr>
    user对象:<br>
    <input type="text" name="user.username" placeholder="姓名"><br>
    <input type="text" name="user.password" placeholder="密码">
    <hr>
    list集合<br>
    第一个元素:<br>
    <input type="text" name="userList[0].username" placeholder="姓名"><br>
    <input type="text" name="userList[0].password" placeholder="密码"><br>
    第二个元素: <br>
    <input type="text" name="userList[1].username" placeholder="姓名"><br>
    <input type="text" name="userList[1].password" placeholder="密码">
    <hr>
    map集合<br>
    第一个元素:<br>
    <input type="text" name="userMap['user1'].username" placeholder="姓名"><br>
    <input type="text" name="userMap['user1'].password" placeholder="密码"><br>
    第二个元素:<br>
    <input type="text" name="userMap['user2'].username" placeholder="姓名"><br>
    <input type="text" name="userMap['user2'].password" placeholder="密码"><br>
    <input type="submit" value="提交">
</form>

然后我们需要搞一个实体类用来接收这个表单的参数:

@Data
public class QueryVo {
    private String sortField;
    private User user;
    private Long[] ids;
    private List<User> userList;
    private Map<String, User> userMap;
}

编写接口进行测试,我们发现表单的数据已经尽数传递了进来:

@PostMapping("queryParam")
public String queryParam(QueryVo queryVo) {
    System.out.println(queryVo);
    return "user";
}

????拓展知识

  • VO(View Object): 视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
  • DTO(Data Transfer Object): 数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
  • DO(Domain Object): 领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
  • PO(Persistent Object): 持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。

下面以一个时序图建立简单模型来描述上述对象在三层架构应用中的位置:
最全面的SpringMVC教程(二)——SpringMVC核心技术篇

大致流程如下:

  • 用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO;
  • 展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层;
  • 服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务;
  • 服务层把DO转换为持久层对应的PO(可以使用ORM工具,也可以不用),调用持久层的持久化方法,把PO传递给它,完成持久化操作;
  • 数据传输顺序:VO => DTO => DO => PO

相对来说越是靠近显示层的概念越不稳定,复用度越低。分层的目的,就是复用和相对稳定性。

小知识: 一般的简单工程中,并不会进行这样的设计,我们可能有一个User类就可以了,并不需要什么VO、DO啥的。但是,随着项目工程的复杂化,简单的对象已经没有办法在各个层的使用,项目越是复杂,就需要越是复杂的设计方案,这样才能满足高扩展性和维护性。

六、设定字符集

springmvc内置了一个统一的字符集处理过滤器,我们只要在web.xml中配置即可:

<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

七、返回json数据(序列化)

我们经常需要使用ajax请求后台获取数据,而不需要访问任何的页面,这种场景在前后分离的项目当中尤其重要。

这种做法其实很简单,大致步骤如下:

  • 将我们的对象转化为json字符串。
  • 将返回的内容直接写入响应体,不走视图解析器。
  • 然后将Content-Type设置为application/json即可。

为了实现这个目的,我们可以引入fastjson:

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>1.2.68</version>
</dependency>
// produces指定了响应的Content-Type
@RequestMapping(value = "getUsers",produces = {"application/json;charset=utf-8"})
@ResponseBody  // 将返回的结果直接写入响应体,不走视图解析器
public String getUsers(){
    List<User> users =  new ArrayList<User>(){{
        add(new User("Tom","2222"));
        add(new User("jerry","333"));
    }};
    return JSONArray.toJSONString(users);
}

测试: 成功!

注意:@ResponseBody能将返回的结果直接放在响应体中,不走视图解析器。

最全面的SpringMVC教程(二)——SpringMVC核心技术篇

浏览器中添加插件json viewer可以有如上显示。

最全面的SpringMVC教程(二)——SpringMVC核心技术篇

当然springmvc也考虑到了,每次这样写也其实挺麻烦,我们还可以向容器注入一个专门处理消息转换的bean。

这个转化器的作用就是:当不走视图解析器时,如果发现【返回值是一个对象】,就会自动将返回值转化为json字符序列:

<mvc:annotation-driven >
        <mvc:message-converters>
            <bean id="fastjson" class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <!-- 这里顺序不能反,一定先写text/html,不然ie下会出现下载提示 -->
    				<value>text/html;charset=UTF-8</value>
					<value>application/json;charset=UTF-8</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
</mvc:annotation-driven>

以后我们的controller就可以写成下边的样子了:

@RequestMapping(value = "getUsersList")
@ResponseBody
public List<User> getUsersList(){
    return   new ArrayList<User>(){{
        add(new User("邸智伟","2222"));
        add(new User("刘展鹏","333"));
    }};
}

当然我们还可以使用一个更加流行的组件jackson来处理,他的工作和fastjson一致

首先需要引入以下依赖:

<!--jackson-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

我们还可以对序列化的过程进行额外的一些配置:

public class CustomObjectMapper extends ObjectMapper {

    public CustomObjectMapper() {
        super();
        //去掉默认的时间戳格式
        configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        //设置为东八区
        setTimeZone(TimeZone.getTimeZone("GMT+8"));
        //设置日期转换yyyy-MM-dd HH:mm:ss
        setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        // 设置输入:禁止把POJO中值为null的字段映射到json字符串中
        configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
        // 空值不序列化
        setSerializationInclusion(JsonInclude.Include.NON_NULL);
        // 反序列化时,属性不存在的兼容处理
        getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        // 序列化枚举是以toString()来输出,默认false,即默认以name()来输出
        configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
    }
}

编写配置文件:

<mvc:annotation-driven>

    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <!-- 自定义Jackson的objectMapper -->
            <property name="objectMapper" ref="customObjectMapper" />
            <property name="supportedMediaTypes">
                <list>
                    <value>text/plain;charset=UTF-8</value>
                    <value>application/json;charset=UTF-8</value>
                </list>
            </property>
        </bean>
    </mvc:message-converters>

</mvc:annotation-driven>
<!--注入我们写的对jackson的配置的bean-->
<bean name="customObjectMapper" class="com.ydlclass.CustomObjectMapper"/>

测试: 成功!

八、获取请求中的json数据

在前端发送的数据中可能会如如下情况,Contetn-Type是application/json,请求体中是json格式数据:

最全面的SpringMVC教程(二)——SpringMVC核心技术篇

@RequestBody注解可以【直接获取请求体的数据】。

如果我们配置了消息转化器,消息转化器会将请求体中的json数据反序列化成目标对象,如下所示:

@PostMapping("insertUser")
public String insertUser(@RequestBody User user) {
    System.out.println(user);
    return "user";
}

当然,我们可以把消息转化器注解掉,直接使用一个String来接收请求体的内容。

九、数据转化

假如有如下场景,前端传递过来一个日期字符串,但是后端需要使用Date类型进行接收,这时就需要一个类型转化器进行转化。

自定义的类型转化器只支持从requestParam获取的参数进行转化,我们可以定义如下,其实学习spring时我们已经接触过这个Converter接口:

public class StringToDateConverter implements Converter<String, Date> {
    @Override
    public Date convert(String source) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy_MM_dd hh,mm,ss");
        try {
            return simpleDateFormat.parse(source);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }
}

然后,我们需要在配置文件中进行配置:

<!-- 开启mvc的注解 -->
<mvc:annotation-driven conversion-service="conversionService" />

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean id="stringToDateConverter" class="cn.itnanls.convertors.StringToDateConverter"/>
        </set>
    </property>
</bean>

对于时间类型的处理,springmvc给我们提供了一个比较完善的解决方案,使用注解@DateTimeFormat,同时配合jackson提供的@JsonFormat注解几乎可以满足我们的所有需求。

  • @DateTimeFormat:当从requestParam中获取string参数并需要转化为Date类型时,会根据此注解的参数pattern的格式进行转化。
  • @JsonFormat:当从请求体中获取json字符序列,需要反序列化为对象时,时间类型会按照这个注解的属性内容进行处理。

这两个注解需要加在实体类的对应字段上即可:

// 对象和json互相转化的过程当中按照此转化方式转哈
@JsonFormat(
            pattern = "yyyy年MM月dd日",
            timezone = "GMT-8"
    )
// 从requestParam中获取参数并且转化
@DateTimeFormat(pattern = "yyyy年MM月dd日")
private Date birthday;

处理的过程大致如下:

最全面的SpringMVC教程(二)——SpringMVC核心技术篇

十、数据校验

  • JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它包含在 JavaEE 6.0 中。
  • JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean
    进行验证。
Constraint 详细信息
@Null 被注解的元素必须为 null
@NotNull 被注解的元素必须不为 null
@AssertTrue 被注解的元素必须为 true
@AssertFalse 被注解的元素必须为 false
@Min(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注解的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注解的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注解的元素必须是一个过去的日期
@Future 被注解的元素必须是一个将来的日期
@Pattern(value) 被注解的元素必须符合指定的正则表达式

????Hibernate Validator 扩展注解

Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解(Hibernate Validator 附加的 constraint):

Constraint 详细信息
@Email 被注解的元素必须是电子邮箱地址
@Length 被注解的字符串的大小必须在指定的范围内
@NotEmpty 被注解的字符串的必须非空
@Range 被注解的元素必须在合适的范围内

????Spring MVC 数据校验

Spring MVC 可以对表单参数进行校验,并将结果保存到对应的【BindingResult】或 【Errors 】对象中。

要实现数据校验,需要引入已下依赖:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.9.Final</version>
</dependency>

并在实体类加上特定注解:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserVO {

    @NotNull(message = "用户名不能为空")
    private String username;

    @NotNull(message = "用户名不能为空")
    private String password;

    @Min(value = 0, message = "年龄不能小于{value}")
    @Max(value = 120,message = "年龄不能大于{value}")
    private int age;

    @JsonFormat(
            pattern = "yyyy-MM-dd",
            timezone = "GMT-8"
    )
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Past(message = "生日不能大于今天")
    private Date birthday;

    @Pattern(regexp = "^1([358][0-9]|4[579]|66|7[0135678]|9[89])[0-9]{8}$", message = "手机号码不正确")
    private String phone;

    @Email
    private String email;
}

在配置文件中配置如下内容,增加hibernate校验:

<bean id="localValidator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>
<!--注册注解驱动-->
<mvc:annotation-driven validator="localValidator"/>

controller使用@Validated标识验证的对象,紧跟着的BindingResult获取错误信息

@PostMapping("insert")
public String insert(@Validated UserVO user, BindingResult br) {
    List<ObjectError> allErrors = br.getAllErrors();
    Iterator<ObjectError> iterator = allErrors.iterator();
    // 打印以下错误结果
    while (iterator.hasNext()){
        ObjectError error = iterator.next();
        log.error("user数据校验错误:{}",error.getDefaultMessage());
    }

    if(allErrors.size() > 0){
        return "error";
    }

    System.out.println(user);
    return "user";
}

注意: 永远不要相信用户的输入,我们开发的系统凡是涉及到用户输入的地方,都要进行校验,这里的校验分为前台校验和后台校验,前台校验通常由javascript来完成,后台校验主要由java来负责,这里我们可以通过spring mvc+hibernate validator完成。

十一、视图解析器详解

我们默认的视图解析器是如下的配置,它主要是处理jsp页面的映射渲染:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"  id="internalResourceViewResolver">
    <!-- 前缀 -->
    <property name="prefix" value="/WEB-INF/page/" />
    <!-- 后缀 -->
    <property name="suffix" value=".jsp" />
</bean>

如果我们想添加新的视图解析器,则需要给旧的新增一个order属性,或者直接删除原有的视图解析器:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver">
    <!-- 前缀 -->
    <property name="prefix" value="/WEB-INF/page/" />
    <!-- 后缀 -->
    <property name="suffix" value=".jsp" />
    <property name="order" value="10"/>
</bean>
  • 这里的order表示视图解析的【优先级】,数字越小优先级越大(即:0为优先级最高,所以优先进行处理视图),InternalResourceViewResolver在项目中的优先级一般要设置为最低,也就是order要最大。不然它会影响其他视图解析器。
  • 当处理器返回逻辑视图时(也就是return “string”),要经过视图解析器链,如果前面的解析器能处理,就不会继续往下传播。如果不能处理就要沿着解析器链继续寻找,直到找到合适的视图解析器。

如下图所示:

最全面的SpringMVC教程(二)——SpringMVC核心技术篇
然后,我们可以配置一个新的Tymeleaf视图解析器,order设置的低一些,这样两个视图解析器都可以生效:

<!--thymeleaf的视图解析器-->
<bean id="templateResolver"
      class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
    <property name="prefix" value="/WEB-INF/templates/" />
    <property name="suffix" value=".html" />
    <property name="templateMode" value="HTML" />
    <property name="cacheable" value="true" />
</bean>
<!--thymeleaf的模板引擎配置-->
<bean id="templateEngine"
      class="org.thymeleaf.spring4.SpringTemplateEngine">
    <property name="templateResolver" ref="templateResolver" />
    <property name="enableSpringELCompiler" value="true" />
</bean>
<bean id="viewResolver" class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
    <property name="order" value="1"/>
    <property name="characterEncoding" value="UTF-8"/>
    <property name="templateEngine" ref="templateEngine"/>
</bean>

添加两个相关依赖:

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>3.0.14.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf-spring4 -->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring4</artifactId>
    <version>3.0.14.RELEASE</version>
</dependency>

模板中需要添加对应的命名空间:

<html xmlns:th="http://www.thymeleaf.org" >

thymeleaf官网地址:https://www.thymeleaf.org/

十二、全局异常捕获

????(1)HandlerExceptionResolver

在Java中,对于异常的处理一般有两种方式:

  • 一种是当前方法捕获处理(try-catch),这种处理方式会造成业务代码和异常处理代码的耦合。
  • 另一种是自己不处理,而是抛给调用者处理(throws),调用者再抛给它的调用者,也就是一直向上抛,指导传递给浏览器。

最全面的SpringMVC教程(二)——SpringMVC核心技术篇

被异常填充的页面是长这个样子的:

最全面的SpringMVC教程(二)——SpringMVC核心技术篇
在这种方法的基础上,衍生出了SpringMVC的异常处理机制。系统的dao、service、controller都通过throws Exception向上抛出,最后由springmvc前端控制器交由异常处理器进行异常处理,如下图:

小知识: service层尽量不要处理异常,如果自己捕获并处理了,异常就不生效了。特别是不要生吞异常。

最全面的SpringMVC教程(二)——SpringMVC核心技术篇
Spring MVC的Controller出现异常的默认处理是响应一个500状态码,再把错误信息显示在页面上,如果用户看到这样的页面,一定会觉得你这个网站太LOW了。

要解决Controller的异常问题,当然也不能在每个处理请求的方法中加上异常处理,那样太繁琐了。

通过源码我们得知,需要写一个HandlerExceptionResolver,并实现其方法:

public class GlobalExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request,
                                         HttpServletResponse response, Object handler, Exception ex) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("error", ex.getMessage());
        modelAndView.setViewName("error");
        return modelAndView;
    }
}
<bean id="globalExecptionResovler" class="com.lagou.exception.GlobalExecptionResovler"></bean>
@Component
public class GlobalExecptionResovler implements HandlerExceptionResolver {}

小知识: 在web中我们也能对异常进行统一处理:

<!--处理500异常-->
<error-page>
    <error-code>500</error-code>
    <location>/500.jsp</location>
</error-page>
<!--处理404异常-->
<error-page>
    <error-code>404</error-code>
    <location>/404.jsp</location>
</error-page>

????(2)@ControllerAdvice

该注解同样能实现异常的全局统一处理,而且实现起来更加简单优雅,当然使用这个注解有一下三个功能:

  • 处理全局异常
  • 预设全局数据
  • 请求参数预处理

我们主要学习其中的全局异常处理,@ControllerAdvice 配合 @ExceptionHandler 实现全局异常处理:

@Slf4j
@ControllerAdvice
public class GlobalExceptionResolverController  {

    @ExceptionHandler(ArithmeticException.class)
    public String processArithmeticException(ArithmeticException ex){
        log.error("发生了数学类的异常:",ex);
        return "error";
    }

    @ExceptionHandler(BusinessException.class)
    public String processBusinessException(BusinessException ex){
        log.error("发生了业务相关的异常:",ex);
        return "error";
    }

    @ExceptionHandler(Exception.class)
    public String processException(Exception ex){
        log.error("发生了其他的异常:",ex);
        return "error";
    }
}

十三、处理资源

当我们使用了springmvc后,所有的请求都会交给springmvc进行管理,当然也包括静态资源,比如/static/js/index.js,这样的请求如果走了*处理器,必然会抛出异常,因为没有与之对应的controller,这样我们可以使用一下配置进行处理:

<mvc:resources mapping="/js/**" location="/static/js/"/>
<mvc:resources mapping="/css/**" location="/static/css/"/>
<mvc:resources mapping="/img/**" location="/static/img/"/>

十四、拦截器

  • (1)SpringMVC提供的拦截器类似于JavaWeb中的过滤器,只不过SpringMVC拦截器只拦截被前端控制器拦截的请求,而过滤器拦截从前端发送的【任意】请求。
  • (2)熟练掌握SpringMVC拦截器对于我们开发非常有帮助,在没使用权限框架(shiro,spring security)之前,一般使用拦截器进行认证和授权操作。
  • (3)SpringMVC拦截器有许多应用场景,比如:登录认证拦截器,字符过滤拦截器,日志操作拦截器等等。

最全面的SpringMVC教程(二)——SpringMVC核心技术篇
????(1)自定义拦截器

SpringMVC拦截器的实现一般有两种方式:

  • (1)自定义的Interceptor类要实现了Spring的HandlerInterceptor接口。
  • (2)继承实现了HandlerInterceptor接口的类,比如Spring已经提供的实现了HandlerInterceptor接口的抽象类HandlerInterceptorAdapter。
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {}
}

????(2)拦截器拦截流程

最全面的SpringMVC教程(二)——SpringMVC核心技术篇

????(3)拦截器规则

我们可以配置多个拦截器,每个拦截器中都有三个方法。下面将总结多个拦截器中的方法执行规律。

  • preHandle: Controller方法处理请求前执行,根据拦截器定义的顺序,正向执行。
  • postHandle: Controller方法处理请求后执行,根据拦截器定义的顺序,逆向执行。需要所有的preHandle方法都返回true时才会调用。
  • afterCompletion: View视图渲染后处理方法:根据拦截器定义的顺序,逆向执行。preHandle返回true也会调用。

????(4)登录拦截器

接下来编写一个登录拦截器,这个拦截器可以实现认证操作。就是当我们还没有登录的时候,如果发送请求访问我们系统资源时,拦截器不放行,请求失败。只有登录成功后,拦截器放行,请求成功。登录拦截器只要在preHandle()方法中编写认证逻辑即可,因为是在请求执行前拦截。代码实现如下:

/**
 *  登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
    
    /**
        在执行Controller方法前拦截,判断用户是否已经登录,
        登录了就放行,还没登录就重定向到登录页面
    */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        HttpSession session = request.getSession();
        User user = session.getAttribute("user");
        if (user == null){
            //还没登录,重定向到登录页面
            response.sendRedirect("/toLogin");
        }else {
            //已经登录,放行
            return true;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {}
}

编写完SpringMVC拦截器,我们还需要在springmvc.xml配置文件中,配置我们编写的拦截器,配置代码如下:

  • 配置需要拦截的路径
  • 配置不需要拦截的路径
  • 配置我们自定义的拦截器类
<mvc:interceptors>
    <mvc:interceptor>
        <!--
            mvc:mapping:拦截的路径
            /**:是指所有文件夹及其子孙文件夹
            /*:是指所有文件夹,但不包含子孙文件夹
            /:Web项目的根目录
        -->
        <mvc:mapping path="/**"/>
        <!--
                mvc:exclude-mapping:不拦截的路径,不拦截登录路径
                /toLogin:跳转到登录页面
                /login:登录操作
            -->
        <mvc:exclude-mapping path="/toLogin"/>
        <mvc:exclude-mapping path="/login"/>
        <!--class属性就是我们自定义的拦截器-->
        <bean id="loginInterceptor" class="com.ydlclass.interceptor.LoginInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

十五、全局配置类

springmvc有一个可用作用于做全局配置的接口,这个接口是WebMvcConfigurer,在这个接口中有很多默认方法,每一个默认方法都可以进行一项全局配置,这些配置可以和我们配置文件的配置一一对应:这些配置在全局的xml中也可以进行配置。

????列举几个xml的配置

<!--处理静态资源-->
<mvc:resources mapping="/js/**" location="/static/js/"/>
<mvc:resources mapping="/css/**" location="/static/css/"/>
<mvc:resources mapping="/./image/**" location="/static/./image/"/>

<!--配置页面跳转-->
<mvc:view-controller path="/toGoods" view-name="goods"/>
<mvc:view-controller path="/toUpload" view-name="upload"/>
<mvc:view-controller path="/websocket" view-name="websocket"/>

<mvc:cors>
    <mvc:mapping path="/goods/**" allowed-methods="*"/>
</mvc:cors>

????列举几个常用的WebMvcConfigurer的配置

@Configuration
@EnableWebMvc
public class MvcConfiguration implements WebMvcConfigurer {
    
    // 拦截器进行配置
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns(List.of("/toLogin","/login"))
                .order(1);
    }

    // 资源的配置
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/js/**").addResourceLocations("/static/js/");
        registry.addResourceHandler("/css/**").addResourceLocations("/static/css/");
    }

    // 跨域的全局配置
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("*")
                .allowedMethods("GET","POST","PUT","DELETE")
                .maxAge(3600);
    }

    // 页面跳转的配置
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/index").setViewName("index");
    }
    
}

后记

最全面的SpringMVC教程(二)——SpringMVC核心技术篇

????Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
????算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~