在 Spring MVC 中控制器的主要作用就是绑定请求参数
、处理业务逻辑
、返回模型数据和视图
,要定义一个控制器也很简单,使用@Controller
注解标注一个类即可,但这还不够,还需要结合@RequestMapping
注解,它可以在类、方法上使用,用来指定请求 URL 可以由控制类中的那个方法来处理,这里我们主要学习以下两方面的内容:
- 绑定请求参数
- 模型数据和视图
- 重定向和转发
一、绑定请求参数
1、普通请求参数
在 Spring MVC 中,如果网络请求时传递的参数名称和控制器方法的形参名称一致,则对应的参数值会自动绑定到控制器方法的形参上。
先准备一个表单 RoleForm.jsp 来提交参数:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>RoleForm</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/role/commonParams" id="form" method="post">
<table>
<tr>
<td>名称</td>
<td><input id="roleName" name="roleName" value=""/></td>
</tr>
<tr>
<td>备注</td>
<td><input id="note" name="note" value=""/></td>
</tr>
<tr>
<td></td>
<td align="right"><input id="commit" type="submit" value="提交"/></td>
</tr>
</table>
</form>
</body>
</html>
要提交的两个参数名称分别为roleName
、name
,地址是http://localhost:8080/role/commonParams
,再看对应的控制器类:
@Controller
@RequestMapping("/role")
public class RoleController {
@RequestMapping("/roleForm")
public String roleForm() {
return "roleForm";
}
@RequestMapping("/commonParams")
public ModelAndView commonParams(String roleName, String note) {
ModelAndView mv = new ModelAndView();
Role role = new Role();
role.setRoleName(roleName);
role.setNote(note);
mv.addObject("role", role);
mv.setViewName("result");
return mv;
}
}
形参名和表单提交的参数名一致,这样当提交表单后,就可以直接通过形参拿到参数值,同时将请求转发到result.jsp
来展示提交的数据,结果如下:
如果提交的参数名称和控制器方法的形参不一致,则是无法将参数值绑定到形参上的,同时又无法修改提交的参数名称。例如将 RoleForm.jsp 的name="roleName"
改为name="role_name"
,上边的控制器方法自然是无法正常获取参数值的,要解决这个问题可以使用@RequestParam
注解,它的作用是将请求参数的值赋给形参:修改后的控制器方法如下:
@RequestMapping("/commonParams2")
public ModelAndView commonParams2(@RequestParam("role_name") String roleName, String note) {
return commonParams(roleName, note);
}
这样问题就解决了,要注意的是使用了@RequestParam("role_name")
则role_name
参数的值默认不能为空,否则会有异常,如果不能保证role_name
有值,可以配置required
属性:
@RequestMapping("/commonParams2")
public ModelAndView commonParams2(@RequestParam(value = "role_name", required = false) String roleName, String note) {
return commonParams(roleName, note);
}
如果表单提交的参数过多,那么控制器方法的形参也会很多,这样大大降到底了代码的可读性、增加了后期的维护成本,当然 Spring MVC 早已想到了这一点,我们可以将控制器的方法参数声明为一个对象,只要其包含的属性名称和请求参数的名称一致即可,就会得到一个属性值为提交参数的对像。
修改 RoleForm.jsp 的 action
属性为${pageContext.request.contextPath}/role/commonParams3
,对应的控制方法如下:
@RequestMapping("/commonParams3")
public ModelAndView commonParams3(Role role) {
ModelAndView mv = new ModelAndView();
mv.addObject("role", role);
mv.setViewName("result");
return mv;
}
2、URL 模板参数
在 RESTful API 的设计规范中,可以通过 URL 来传递参数,例如要通过 id 获得某个角色的信息,并用 JSON 格式返回,我们希望 URL 是这样的:http://localhost:8080/role/getRole/1
,其中 1 是角色 id,动态变化的。当然 Spring MVC 也支持这样的情况,通过@PathVariable
注解可以方便的获取请求 URL 的动态参数,对应的控制器方法如下:
@RequestMapping("/getRole/{id}")
@ResponseBody
public Role getRole(@PathVariable("id") long id) {
Role role = roleService.getRole(id);
return role;
}
@ResponseBody
注解可以将返回的对象转换成 JSON 格式,在浏览器测试效果如下:
3、JSON 格式参数
如果参数结构比较复杂,为了方便而使用 JSON 格式传递数据也是比较常见的,接下来实现 JSON 数据的提交和解析。首先准备 RoleForm2.jsp,主要的作用是将表单数据已 JSON 格式提交:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>RoleForm2</title>
<script src="${pageContext.request.contextPath}/js/jquery-3.4.1.min.js"></script>
<script>
$(function () {
$("#commit").click(function () {
var data = {
id: 1,
roleName: $("#roleName").val(),
note: $("#note").val()
}
$.post({
url: "./jsonParams",
contentType: "application/json",
data: JSON.stringify(data),
success: function (result) {
alert("名称:" + result.roleName + "\n" + "备注:" + result.note)
}
});
});
});
</script>
</head>
<body>
<form id="form">
<table>
<tr>
<td>名称</td>
<td><input id="roleName" name="roleName" value=""/></td>
</tr>
<tr>
<td>备注</td>
<td><input id="note" name="note" value=""/></td>
</tr>
<tr>
<td></td>
<td align="right"><input id="commit" type="button" value="提交"/></td>
</tr>
</table>
</form>
</body>
</html>
这里通过 jQuery 以 JSON 格式来提交数据到http://localhost:8080/role/jsonParams
,并在 success
回调中用一个弹窗回显提交的数据,控制器方法如下:
@Controller
@RequestMapping("/role")
public class RoleController {
@RequestMapping("/roleForm2")
public String roleForm2() {
return "roleForm2";
}
@RequestMapping("/jsonParams")
@ResponseBody
public Role jsonParams(@RequestBody Role role) {
return role;
}
}
这里的重点是jsonParams
方法中的@RequestBody
注解,它会将 JSON 串自动映射为对象的,当然 JSON 串中的属性名要和对象中属性名保持一致。同时jsonParams
方法将提交的数据以 JSON 格式返回,以便在弹窗中获取其属性值。测试效果如下:
同样的原理,我们可以将列表数据以 JSON 的格式提交,具体代码可参考 demo。
4、序列化参数
如果表单数据比较复杂,我们自己收集数据的话会比较麻烦,一种简便的方式就是将表单序列化。我们修改 RoleForm2.jsp 提交按钮的事件:
$("#commit").click(function () {
//提交表单
var data = $("form").serialize();
$.post({
url: "./serializeParams",
data: data,
success: function (result) {
var s = $.parseJSON(result);
alert("名称:" + s.roleName + "\n" + "备注:" + s.note)
}
});
});
其中$("form").serialize()
就是将表单的数据序列化,返回一个字符串,得到的参数如下:roleName=管理员¬e=666
。
对应的控制器方法为:
@RequestMapping("/serializeParams")
@ResponseBody
public Role serializeParams(Role role) {
return role;
}
执行效果和图4类似。
同样可以将表单数据序列化成 JSON 数组格式,使用如下 jQuery 方法即可:$("form").serializeArray()
,得到的 JSON 数据格式参数如下:
[{
"name": "roleName",
"value": "管理员"
}, {
"name": "note",
"value": "666"
}]
二、模型数据和视图
其实我们在绑定请求参数部分内容中,已经接触到了模型数据和视图,在 Spring MVC 中两者一般是搭配使用的,模型数据一般保存控制器方法中执行业务后需要展示或返回的数据,然后将模型数据渲染到视图中。
1、模型数据
常用的模型数据有Model
、ModelMap
、ModelAndView
。Spring MVC 在控制器方法被调用前会创建一个隐含的BindingAwareModelMap
类型的模型数据,这样如果在控制器方法中声明了Model
、ModelMap
、ModelAndView
其中某个类型的形参,则 Spring MVC 会将隐含的模型数据转换为声明的模型数据类型,即自动创建了形参的实例,当然你也可以选择手动创建。有了模型数据的实例,接下来就可以添加数据了,之后可以在视图页面得到添加的数据。模型数据以键值对的形式来添加数据。
2、视图
视图分为逻辑视图
和非逻辑视图
,怎么理解呢?
- 逻辑视图,需要通过视图解析器解析逻辑视图名,得到真实的视图,然后渲染模型数据,例如
InternalResourceView
,视图解析器以及逻辑视图的文件目录在dispatcher_servlet.xml
中已经配置过了。 - 非逻辑视图,无需视图解析器,直接渲染模型数据,例如
MappingJackson2JsonView
2.1
使用逻辑视图时,控制器方法的返回值可以是String
类型,即逻辑视图名,这样 Spring MVC 会根据逻辑视图名以及配置的视图解析器的视图文件目录和文件后缀名找到对应的视图文件,得到真实的视图,如果有模型数据则渲染数据,将最终的视图呈现给用户。看如下的例子:
@Controller
@RequestMapping("/role")
public class RoleController {
@Autowired
private RoleService roleService;
@RequestMapping("/getRoles2")
public String getRoles2(Model model) {
List<Role> roles = roleService.getAllRole();
model.addAttribute("roles", roles);
return "role";
}
}
控制器方法getRoles2
返回逻辑视图名role
,所以需要提前在/WEB-INF/JSP/
目录下定义role.jsp
文件。模型数据model
中保存了角色的集合数据。所以在jsp文件中根据角色数据渲染一个表格:
<c:forEach items="${roles}" var="role">
<tr>
<td><c:out value="${role.id}"/></td>
<td><c:out value="${role.roleName}"/></td>
<td><c:out value="${role.note}"/></td>
</tr>
</c:forEach>
运行后可以看到如下效果:
2.2
控制器方法除了返回String
,还可以返回ModelAndView
,ModelAndView
既可以添加数据也可以设置逻辑视图名,对应的控制器方法如下:
@RequestMapping("/getRoles")
public ModelAndView getRoles(ModelAndView mv) {
List<Role> roles = roleService.getAllRole();
mv.setViewName("role");
mv.addObject("roles", roles);
return mv;
}
运行效果和图5一致。
2.3
这里补充一点,如果在控制器方法中声明了Model
或者ModelMap
参数,同时声明了一个类的对象参数,例如Role
来接收请求参数,那么Model
或者ModelMap
参数会自动添加声明的Role
对象,添加数据时使用的默认 key 就是类首字母小写的字符串,即role
,是由框架自动推断出的。
2.4
使用非逻辑视图时,例如返回 JSON 格式的数据,由于 Spring MVC 默认使用 Jackson 处理 JSON 数据,所以可以先用MappingJackson2JsonView
视图来完成数据格式的转换:
@RequestMapping("/getRoles3")
public ModelAndView getRoles3() {
ModelAndView mv = new ModelAndView();
List<Role> roles = roleService.getAllRole();
mv.addObject("roles", roles);
mv.setView(new MappingJackson2JsonView());
return mv;
}
当然,要返回 JSON 格式数据,更简单的方式是使用@ResponseBody
,但本质上还是采用 Jackson 处理 JSON 数据,当然也可以整合其它的 JSON 处理框架,例如Fastjson,这样当使用@ResponseBody
、@RequestBody
注解时会使用 Fastjson 来处理 JSON 数据。整合的方式也很简单,使用 Fastjson 提供的FastJsonHttpMessageConverter
来替换 Spring MVC 默认的 HttpMessageConverter
即可,在dispatcher_servlet.xml
添加如下配置
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
使用@ResponseBody
返回 JSON 的控制器如下:
@RequestMapping("/getRoles4")
@ResponseBody
public List<Role> getRoles4() {
List<Role> roles = roleService.getAllRole();
return roles;
}
浏览器输入http://localhost:8080/role/getRoles4
,效果如下:
三、重定向和转发
首先通过一个表格来了解下重定向和转发之间的差别:
重定向(redirect) | 转发(forward) |
---|---|
执行重定向会重新发起新请求 | 执行转发依然是上次的请求 |
地址栏的请求URL会变为新请求的URL | 地址栏的请求URL不会改变 |
由于重新发起请求,原请求的请求参数、request范围内的属性全部丢失 | 原请求的请求参数、request范围内的属性依然存在 |
1、重定向(redirect)
在 Spring MVC 中,当控制器方法返回的字符串带有redirect:
前缀时,那么该字符串不是用来查找视图的逻辑视图名,而是要执行重定向,我们这里所说的重定向仅代表重定向到控制器的请求处理方法。例如"redirect:./result"
,代表重定向到./result
路径对应的控制器方法。其实要实现重定向很简单,关键是如何传递参数。
1.1
如果要传递参数到目标控制器方法,可以采用model.addAttribute
方式添加参数:
@RequestMapping("/addRole")
public String addRole(Model model, Role role) {
model.addAttribute("roleName", role.getRoleName());
model.addAttribute("note", role.getNote());
return "redirect:./result";
}
目标控制器方法很简单,仅仅是展示传递的参数:
@RequestMapping("/result")
public String result(Model model, Role role) {
return "result";
}
大致的流程是,通过表单将参数提交到addRole
控制器方法,然后传递参数重定向到result
控制器方法来展示提交的参数。在浏览器执行后发现,如果按照上述方式添加重定向参数,则在重定向时参数会以查询参数的形式拼接到 URL 上,例如:http://localhost:8080/role/result?roleName=%E7%AE%A1%E7%90%86%E5%91%98¬e=666
1.2
由于通过model.addAttribute
添加重定向参数时,参数会拼接在 URL 上,这样存在安全性问题。我们可以考虑另外一种方式,就是我们之前学习的通过 URL 模板传递参数,代码如下:
@RequestMapping("/addRole2")
public String addRole2(Model model, Role role) {
model.addAttribute("roleName", role.getRoleName());
model.addAttribute("note", role.getNote());
return "redirect:./result2/{roleName}";
}
model 中的roleName
会作为参数填充到 URL 中,note
由于和 URL 模板参数不匹配则会以查询参数的形式拼接到 URL 上。
要重定向到的控制器方法如下,用来接收 URL 模板参数和查询参数:
@RequestMapping("/result2/{roleName}")
public String result2(Model model, @PathVariable("roleName") String roleName, String note) {
model.addAttribute("roleName", roleName);
model.addAttribute("note", note);
return "result2";
}
重定向时的 URL 如下:http://localhost:8080/role/result2/%E7%AE%A1%E7%90%86%E5%91%98?note=666,可以和第一种方式对比下。
1.3
上边两种方式只能添加简单的基本类型参数,例如字符串、数字等,如要传递对象就无能为力了,当然 Spring MVC 自然有解决方案,我们可以采用另外一个模型数据RedirectAttributes
来添加 flash 类型的参数,在执行重定向前,会将参数复制到会话(Session)中,当重定向后,从会话中取出参数。
添加参数的方式如下:
@RequestMapping("/addRole3")
public String addRole3(RedirectAttributes ra, Role role) {
ra.addFlashAttribute("role", role);
return "redirect:./result3";
}
重定向后,参数值会自动定转存到 model 中,当然也可以定义 Role 对象来接收参数值。
@RequestMapping("/result3")
public String result3(Model model) {
return "result";
}
重定向的 URL 如下:http://localhost:8080/role/result3,由于是通过会话保存参数的,并不会将参数值暴露,所以也更加的安全。
1.4
上边的例子中,我们的重定向是在同一个控制器中进行的,更多的时候可能需要重定向到另一个控制器的请求处理方法,我们来修改1.3的例子,要重定向到的控制器类的方法如下:
@Controller
@RequestMapping("/result")
public class ResultController {
@RequestMapping("/success")
public String result(Role role) {
return "result";
}
}
编写addRole4
方法来重定向到上边的控制器方法:
@RequestMapping("/addRole4")
public String addRole4(RedirectAttributes ra, Role role) {
ra.addFlashAttribute("role", role);
return "redirect:/result/success";
}
注意返回值,即多了一个控制器类的根路径/result
,少了.
符号。
1.5
上边的例子重定向时都是返回以redirect:
开头的字符串,当然也可以使用ModelAndView
,例如:mv.setViewName("redirect:/result/success")
2、转发(forward)
2.1
和重定向类似, 在 Spring MVC 中,当控制器方法返回的字符串带有forward:
前缀时,那么就是要执行请求转发操作,我们之前接触到的返回一个逻辑视图名,来展示相应的jsp页面,就是一种转发,只是默认省略了forward:
。这里我们要讨论的控制器方法之间的转发。先看个例子:
@RequestMapping("/addRole5")
public String addRole5(Model model, Role role) {
model.addAttribute("roleName", role.getRoleName());
return "forward:./fail";
}
addRole5
控制器方法会直接将请求转发到当前控制下可以相应fail
请求的控制器方法,提示当前添加的角色已存在:
@RequestMapping("/fail")
public String fail() {
return "fail";
}
2.2
如果控制器方法返回ModelAndView
,也是可以进行请求转发的:
@RequestMapping("/addRole6")
public ModelAndView addRole6(ModelAndView mv, Role role) {
mv.addObject("roleName", role.getRoleName());
mv.setViewName("forward:./fail");
return mv;
}
2.3
当然也可以转发到另一个控制器方法:
@RequestMapping("/addRole7")
public ModelAndView addRole7(ModelAndView mv, Role role) {
mv.addObject("roleName", role.getRoleName());
mv.setViewName("forward:/result/fail");
return mv;
}
关键的代码就是mv.setViewName("forward:/result/fail")
,要转发的目标控制器方法如下:
@Controller
@RequestMapping("/result")
public class ResultController {
@RequestMapping("/fail")
public String fail() {
return "fail";
}
}
文中demo地址:https://github.com/SheHuan/MyJavaEE