Spring WebMvc进阶-mvc中的更多注解解析

从 0 开始深入学习 Spring 专栏目录总览

接着上一章的茬,咱把 WebMvc 中的一些其他的注解也都了解一下。这里面大多数都是日常使用不算很多的,小伙伴们不必全部都掌握。

1. @ControllerAdvice

上一章我们已经讲过 @ControllerAdvice 的基本使用,它配合 @ExceptionHandler 就可以完成 Controller 的统一异常处理。除此之外,它还可以配合 @InitBinder 、@ModelAttribute 注解,下面我们一一来讲。

1.1 @InitBinder【了解】

见名知意,它应该是做初始化绑定工作的,它绑定的是一个叫 WebDataBinder 的东西,而绑定的具体工作,可以让 DispatcherServlet 来处理页面表单 / 请求参数到我们编写的 Controller 中方法参数,这个过程中的参数解析可定制。

这么说好像有点抽象,我们可以换一种说法:之前我们在保存用户信息的时候,为了能让用户的生日能成功由字符串转为 Date ,我们不是自定义了一个类型转换器嘛,其实也可以采用别的办法,下面我们先来演示。

1.1.1 另一种方式处理String转Date

我们先把 <mvc:annotation-driven> 标签中的 conversion-service 属性去掉:

<mvc:annotation-driven validator="validatorFactory"/>

然后,我们新建一个 ConversionBinderAdvice 的类,并将其声明为 @ControllerAdvice :

@ControllerAdvice
public class ConversionBinderAdvice {

}

然后,在这里面声明一个方法,这个方法有两个特点:不能有返回值,方法入参必须为 WebDataBinder 。并且,给这个方法上标注一个 @InitBinder 注解:

@ControllerAdvice
public class ConversionBinderAdvice {
    
    @InitBinder
    public void addDateBinder(WebDataBinder dataBinder) {
        dataBinder.addCustomFormatter(new DateFormatter("yyyy年MM月dd日"));
    }
}

方法体中,我们可以调用 WebDataBinder 的 API ,向其中添加一个自定义的格式转换器,其实这个转换器就可以类比参数类型转换器。恰好,SpringWebMvc 中还真就内置了一个可以用于日期转换的 DateFormatter ,只需要给它传入转换格式,它就可以帮我们完成 String 到 Date 的转换。

这样写完之后,重启 Tomcat ,并尝试保存用户信息,可以发现依然可以正常保存、跳转页面,控制台上也能打印出转换后的日期:

User{id='09ec5fcea620c168936deee53a9cdcfb', username='zhangsan', name='张三', birthday=2020-11-11 00:00:00, department=Department{id='6ded6d3bdc8f4fc70bcc4347822a5ca3', name='null'}}

说明这种方式确实也是可行的。

1.1.2 @InitBinder的执行时机

或许各位会感到好奇,既然 @InitBinder 的配置,相较于自定义参数类型转换器,似乎更为简单,那为什么当初不讲这种呢?这样,我们在 addDateBinder 方法中打印一行控制台输出:

    @InitBinder
    public void addDateBinder(WebDataBinder dataBinder) {
        System.out.println("@InitBinder addDateBinder ......");
        dataBinder.addCustomFormatter(new DateFormatter("yyyy年MM月dd日"));
    }

重启 Tomcat ,我们多次编辑、保存用户信息,观察控制台的输出:

@InitBinder addDateBinder ......
@InitBinder addDateBinder ......
User{id='09ec5fcea620c168936deee53a9cdcfb', username='zhangsan', name='张三', birthday=2020-01-01 00:00:00, department=Department{id='6ded6d3bdc8f4fc70bcc4347822a5ca3', name='null'}}
@InitBinder addDateBinder ......
@InitBinder addDateBinder ......
User{id='31e944950285bdcec68008e404eab324', username='lisisi', name='李四', birthday=2020-11-11 00:00:00, department=Department{id='6ded6d3bdc8f4fc70bcc4347822a5ca3', name='null'}}

?????咋打印了这么多次呢?

实际上,被 @InitBinder 注解标注的方法,会在每一次 Controller 方法执行时都触发!注意是每一次!而对于页面跳转这种动作来讲,很明显是不需要的,所以用这种方法有点不大合适。也正是因为此,我们在讲解自定义参数转换的时候没有用这种方法。

1.2 @ModelAttribute【熟悉】

之前我们已经见过它了,它可以用来做数据回显是吧。除了可以用于数据回显,它还有两个比较有用的作用:公共数据暴露、请求数据处理

1.2.1 公共数据暴露

注意看 @ModelAttribute 注解的可标注位置:

@Target({ElementType.PARAMETER, ElementType.METHOD}) // 看这里
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {

这个注解不止可以标注在方法的参数上,也可以标注在方法上。而当 @ModelAttribute 标注到方法上时,它可以用来暴露一些特定的数据,可以通过返回值暴露,也可以通过传入 Model / ModelMap / HttpServletRequest 来手动 set 值。下面我们可以来简单测试一下。

我们在新写一个 DataModelAttributeAdvice 的类,在里面声明一个方法:(不要忘记标注 @ControllerAdvice )

@ControllerAdvice
public class DataModelAttributeAdvice {
    
    @ModelAttribute("publicMessage")
    public String publicMessage() {
        return "publicMessage-hahaha";
    }
}

这样编写完之后,相当于在每次 Controller 中的方法执行之前,都执行了一次 request.setAttribute("publicMessage", publicMessage()); 的代码。所以我们在 Controller 的任意位置,都能拿到这个 publicMessage ,同样在页面上也能取到。

我们可以试一下,在 userList 中添加一个 el 表达式,展示这个 publicMessage :

<body>
<h3>用户列表 ${publicMessage}</h3>

重启 Tomcat ,浏览器访问 http://localhost:8080/spring-webmvc/user/list ,可以发现 publicMessage() 方法返回的 publicMessage-hahaha 确实展示在页面上了:

Spring WebMvc进阶-mvc中的更多注解解析

使用 Model 或者 ModelMap 等方式原理相同,小伙伴可以自行测试。

1.2.2 请求数据处理

上面的例子中,入参是空的,我们可以理解为从无到有,那就是提供新的数据。除了这个之外,@ModelAttribute 还可以对请求的数据进行一些处理。

比如说,我们可以对表单传入的数据进行修改。来一个需求吧,带着目标去研究:篡改用户列表中,用户名模糊搜索的输入值。

我们在 DataModelAttributeAdvice 中再写一个方法:

    @ModelAttribute("username")
    public String processUsername(String username) {
        return username + "haha";
    }

执行这个方法,就相当于执行了下边这样的一段代码:

    public void processUsername(Model model) {
        String username = (String) model.getAttribute("username");
        username += "haha";
        model.addAttribute("username", username);
    }

当然,要想让篡改的 username 能在 Controller 方法中拿到,还需要在 UserController 的 list 方法中,username 的前面标注 @ModelAttribute :

    @GetMapping("/list")
    public String list(@ModelAttribute("username") String username, ModelMap map) {
        System.out.println(username);
        map.put("userList", userService.findAllUsers());
        return "user/userList";
    }

OK 这样就算编写完了。重启 Tomcat ,来到用户列表,我们来试一下用户名的模糊查询。

可以访问 http://localhost:8080/spring-webmvc/user/list ,并输在用户名的输入框中敲字,也可以直接访问 http://localhost:8080/spring-webmvc/user/list?username=zhang ,我们只需要观察控制台的打印,或者 Debug 一下观察就可以了:

Spring WebMvc进阶-mvc中的更多注解解析

果然被处理过了,说明 @ModelAttribute 确实可以做到请求数据的覆盖处理。

1.3 @SessionAttribute【了解】

@SessionAttribute 本来与 @ControllerAdvice 并无关系,但它与 @ModelAttribute 在功能上相似。@ModelAttribute 无论是取还是存,它都是在 request 域中工作,而 @SessionAttribute 就是在 session 域中工作了。

@SessionAttribute 还有另一个跟它长得特别像的注解:@SessionAttributes ,它们俩差一个 s ,作用也就刚好相反:

  • @SessionAttribute :从 session 中取数据
  • @SessionAttributes :向 session 中存数据

这两个注解的设计之初,是为了让我们开发者在使用 SpringWebMvc 的时候,尽量少的直接操作 Servlet 的 API 。然而实际上在使用时,可能体验并不是那么好,下面我们可以来写一个简单的例子。

1.3.1 向session存数据

这次我们就不拿现成的那些部门、员工的东西了,咱搞个新的 Controller 和新的页面吧。

Controller 中,定义一个方法,并传入 Model 或者 ModelMap ,注意这里不能传 HttpServletRequest 了,前面说了让我们避免使用 Servlet 的 API 。

之后,像给 request 域中填充数据那样,编写一个最简单的赋值、跳转页面的动作。

@Controller
public class SessionAttributeController {
    
    @GetMapping("/session/username")
    public String sessionUsername(Model model) {
        model.addAttribute("username", "hahaha");
        return "session";
    }
}

上面的内容本来都是非常非常简单的,我们已经再熟悉不过了。要想把 username 放入 session ,只需要在整个类上,标注一个 @SessionAttributes("username") 即可。对,这个 @SessionAttributes 注解只能标注在类上。

这样写完之后,接下来跳转到 jsp 页面,用 session 去取一下 username 试试:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>session数据测试</title>
</head>
<body>
<h3>session 中的 username :${pageContext.session.getAttribute("username")}</h3>
</body>
</html>

重启 Tomcat ,浏览器访问 http://localhost:8080/spring-webmvc/session/username ,发现 hahaha 确实存入到 session 了。

Spring WebMvc进阶-mvc中的更多注解解析

1.3.2 从session中取数据

只把数据存到 session 还不够,我们的目的肯定还是日后从 session 中取。接下来我们来看看取数据是怎么个取法:

    @GetMapping("/session/get")
    @ResponseBody
    public String getSessionUsername(@SessionAttribute("username") String username) {
        return username;
    }

看,很简单吧,只需要在方法上加 @SessionAttribute 注解就行,用法跟 @ModelAttribute 几乎一样。

重启 Tomcat ,此时如果浏览器直接访问 http://localhost:8080/spring-webmvc/session/get ,是会报 400 的,异常信息是 Missing session attribute 'username' of type String —— username 不存在。因为 Tomcat 重启后,之前的 session 已经没有了,需要我们先访问一次 /session/username 把数据存进 session 中,然后才可以取。

预先访问 /session/username 之后,再访问 /session/get ,发现浏览器可以响应出 hahaha ,说明取数据也是没有问题的:

Spring WebMvc进阶-mvc中的更多注解解析

好了,有关 @ControllerAdvice 以及相关的注解就这些,下面咱再介绍一个注解。

2. @CrossOrigin【熟悉】

可能有些小伙伴看到这个注解,能联想到一个概念:跨域,当然也可能有不少小伙伴不了解,咱先解释一下跨域。

2.1 同源策略与跨域问题

跨域是一种现象,跨域是由浏览器的同源策略引起的,我们先来说同源策略。

同源策略,是在浏览器上对资源的一种保护策略,最初同源策略只是用于保护网页的 cookie ,后演变的越来越严格,下面会提到。同源策略规定了三个相同:协议相同、域名 / 主机相同、端口相同。当这三个因素都相同时,则浏览器会认为访问的资源是同一个来源。

同源策略的保护主要是保护 cookie ,试想如果你在一个银行的网站上访问,当登录银行电子账户之后,同时你又在这个浏览器上开了一个新的页签,访问了别的网站,如果此时浏览器没有同源策略的保护,则浏览器中存放的银行网站的 cookie 会一并发送给这个别的网站。万一这个网站有啥危险的想法,那你的银行账户就岌岌可危了。因此,同源策略要保护我们的上网安全。

再来说跨域,简单地说,跨域就好比你在访问 juejin.cn/ 域名下的网页,这个网页中包含 juejin.im/ 的资源(不是图片,不是 css 、js ,可以是 ajax 请求),则此时就会构成跨域访问。因为当前的网页所在域名是 juejin.cn ,但页内访问的资源有不是当前域名的,所以就构成了跨域访问。

只要触发以下三种情况之一,都会引起跨域问题:

  • http 访问 https ,或者 https 访问 http
  • 不同域名 / 服务器主机之间的访问
  • 不同端口之间的访问

目前来讲,浏览器的同源策略,在处理跨域问题时的态度如下:

  • 非同源的 cookie 、localstorage 、indexedDB 无法访问
  • 非同源的 iframe 无法访问(防止加载其他网站的页面元素)
  • 非同源的 ajax 请求可以访问,但浏览器拒绝接收响应

了解了同源策略和跨域问题,接下来我们来搭建一个能触发跨域问题的工程环境,来实际体会一下跨域的效果和现象。

2.2 搭建触发跨域问题的工程环境

要搭建能触发跨域问题的工程环境,首先需要两个工程,和两个 Tomcat (模拟基于端口的跨域相对比较容易操作)。

2.2.1 准备工程和Tomcat

之前我们分别用 xml 的方式,以及注解驱动的方式,搭建了两个工程,正好我们来利用一下。

在 IDE 中,分别配置好两个 Tomcat:

Spring WebMvc进阶-mvc中的更多注解解析

之后,将基于注解驱动的工程,部署到下面的 8081 端口的 Tomcat 上:

Spring WebMvc进阶-mvc中的更多注解解析

2.2.2 准备测试代码

将 xml 工程中的 UserController ,复制粘贴到注解驱动的工程中,并保留其中的 batchDelete 方法,其余的可以不要了:

@Controller
@RequestMapping("/user")
public class UserController76 {
    
    @PostMapping("/batchDelete")
    @ResponseBody
    public String batchDelete(@RequestParam("ids[]") List<String> ids) {
        System.out.println(ids);
        return "success";
    }
}

然后,修改 xml 工程中,userList 中的异步请求方法地址:

    $("#batch-delete-button").click(function() {
        var selectedIds = $("[name='selectedId']:checked");
        var ids = [];
        for (var i = 0; i < selectedIds.length; i++) {
            ids.push(selectedIds[i].value);
        }
        $.post("http://localhost:8081/spring-webmvc/user/batchDelete", {ids: ids}, function(data) {
            alert(data)
        });
    });

这样工程代码就改好了。

2.2.3 测试跨域

将两个 Tomcat 都启动,浏览器访问 http://localhost:8080/spring-webmvc/user/list ,并勾选几个用户,点击批量删除的按钮。

点击,点击,,,嗯?咋没反应呢?打开浏览器的控制台,发现早都报一堆错误了:

Spring WebMvc进阶-mvc中的更多注解解析

Access to XMLHttpRequest at 'http://localhost:8081/spring-webmvc/user/batchDelete' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

错误中提示的这个 Access-Control-Allow-Origin ,就是告诉我们,访问出现跨域了。与此同时,我们会发现,Tomcat 中是有控制台输出的:

Spring WebMvc进阶-mvc中的更多注解解析

说明后端确实处理请求了,也响应回浏览器了,但是浏览器不理,它觉得这不安全,于是就拒绝了 Tomcat 的响应。

2.3 CORS解决跨域问题

针对跨域问题,W3C 制定了一个标准,就叫 CORS Cross-origin Resource Sharing 跨域资源共享。CORS 的实现,需要浏览器与服务端同时支持才可以,不过好在绝大多数浏览器都支持(恩,除了低版本 IE ),所以我们要实现的,就是如何让服务端实现,也就是在我们的代码中配置实现。

这个时候就需要 SpringWebMvc 的注解登场了,它提供的这个 @CrossOrigin 注解,标注在需要的 Controller 类或方法上,就可以实现跨域资源共享。

使用方式很简答,找到注解驱动的工程,在 UserController 上标注 @CrossOrigin 注解,就可以了。

重新启动 Tomcat ,浏览器上重新点击批量删除,可以发现浏览器可以成功处理响应,并弹出 alert 提示:

Spring WebMvc进阶-mvc中的更多注解解析

证明 @CrossOrigin 注解确实可以解决跨域。

2.4 @CrossOrigin注解的细节

我们注意一下 @CrossOrigin 注解的几个细节。

2.4.1 标注的位置

@CrossOrigin 注解不止可以标注在类上,也可以标注在方法上,这种写法比较像 @Transactional ,标注在一个类上,则整个 Controller 中标注了 @RequestMapping 注解的方法都支持跨域访问;标注在 Controller 的方法上,则对应的方法支持跨域访问。

2.4.2 允许跨域的范围

跨域是可以指定请求来源的范围的,默认情况下 @CrossOrigin 的允许跨域范围是 * ,也就是任意,我们可以自行声明可以跨域的域名 + 端口等等。

例如下面的写法,就只能限制从 localhost:8080 的请求才允许跨域访问:

@Controller
@RequestMapping("/user")
@CrossOrigin(origins = "http://localhost:8080")
public class UserController76 { ... }

2.4.3 @CrossOrigin做的工作

究其根本,@CrossOrigin 要干的事情,其实不用它也能干,因为解决跨域问题的核心,是给响应头中添加一些额外的信息,其中最重要的就是上面刚提到的,允许跨域的范围,也就是响应头中的这个东西:

Access-Control-Allow-Origin: *

所以 @CrossOrigin 干的事,其实就相当于我们用 HttpServletResponse 执行了这么一句代码:

response.addHeader("Access-Control-Allow-Origin", "*");

对于现阶段的我们,其实了解到这里就够了,如果有小伙伴想深入探究里面真正干的事情,可以跳转到 RequestMappingHandlerMapping ,那里面有关于 @CrossOrigin 注解的解析,以及对应的工作,由于这部分不是重点部分,小册就不展开讲解了。

好,到这里,SpringWebMvc 主要涉及到的注解就全部讲解完毕了。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring WebMvc进阶-mvc中的更多注解解析