开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第28天,点击查看活动详情
前言
本文将为大家对 【SpringMVC教程】核心技术 的相关内容进行介绍,具体将对视图和模型拆分,重定向与转发,RequestMapping与其衍生注解,URL 模式匹配,牛逼的传参等SpringMVC相关核心技术进行详尽介绍~
👉Java全栈学习路线可参考: 【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考: 算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
目录
一、视图和模型拆分
视图和模型相伴相生,但是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提供了两种选择(两个类):
PathPattern:PathPattern是 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-Encoding和Keep-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 对象中,
key为ModelAttribute的value属性值。 - 方法的返回值不再是方法的访问路径,访问路径会变为
@RequestMapping的value值,例如:@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
结果都是没有问题的:
🍀(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的一个(或若干个)属性。
下面以一个时序图建立简单模型来描述上述对象在三层架构应用中的位置:
大致流程如下:
- 用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO;
- 展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层;
- 服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务;
- 服务层把DO转换为持久层对应的PO(可以使用ORM工具,也可以不用),调用持久层的持久化方法,把PO传递给它,完成持久化操作;
- 数据传输顺序:VO => DTO => DO => PO
相对来说越是靠近显示层的概念越不稳定,复用度越低。分层的目的,就是复用和相对稳定性。
小知识: 一般的简单工程中,并不会进行这样的设计,我们可能有一个User类就可以了,并不需要什么VO、DO啥的。但是,随着项目工程的复杂化,简单的对象已经没有办法在各个层的使用,项目越是复杂,就需要越是复杂的设计方案,这样才能满足高扩展性和维护性。
后记
本文呢为大家对 【SpringMVC教程】核心技术 的相关内容进行了介绍,具体对视图和模型拆分,重定向与转发,RequestMapping与其衍生注解,URL 模式匹配,牛逼的传参等SpringMVC相关核心技术进行了详尽介绍~
希望本文的内容能够使你有所收获,如果你想继续深入的学习数据结构与算法相关的知识,或想深入的学习Java相关的知识与技术,可以参考:
👉Java全栈学习路线可参考: 【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~
👉算法刷题路线可参考: 算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~