URL请求映射与Controller处理器参数绑定
还记得以前在配置参数绑定时,视图渲染一直404- -、,注解的映射器和适配器没问题,扫描Controller包配置也没问题,视图逻辑名,请求路径都检查过很多遍,最后发现,竟然是视图解析器没配置好!!崩溃!!!排除故障后,正确运行,算是当初踩过的一个大坑。
请求映射
在前面配置注解的处理器映射器和适配器时,我用的是简写方式,即使用标签:
<mvc:annotation-driven></mvc:annotation-driven>
该标签默认加载的时RequestMappingHandlerMapping和RequestMappingHandlerAdapter。当然你也可以选择手动配置映射器和适配器的方式:
<!-- 注解形式配置映射器和适配器 -->
<!-- 注解形式的处理器映射器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" />
<!-- 注解形式的处理器适配器 -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" />
由Spring MVC的各个组件来看,Handler处理器和View视图这两个模块是程序员需要编写的,尤其是进行业务逻辑处理的Handler。在使用了注解的处理器映射器和适配器配置后,我们就可以在自己编写的Handler处理器模块,即Controller类上面,使用@Controller注解,以此来标识这个类是一个Handler处理器类,在Handler中,我们就要编写处理请求的实现方法。
在前面的Spring MVC工作流中我们知道,前端控制器DispatcherServlet在请求完处理器映射器HandlerAdapter后,会带着一个返回的Handler(或者说执行链),去请求处理器适配器HandlerAdapter,处理器适配器通过自己的supports(Object handler)方法,判断是否支持这个类型的Handler。那么处理器适配器是根据什么来判断支不支持这个Handler的呢?来看看这个判断的过程:
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception;
long getLastModified(HttpServletRequest request, Object handler);
}
public boolean supports(Object handler) {
return getMethodResolver(handle).hasHandlerMethods();
}
public final boolean hasHandlerMethods() {
return !this.handlerMethods.isEmpty();
}
对于每一个处理器适配器,都要实现HandlerAdapter接口,由上面源码可以看到,supports()方法会去判断Handler处理器中是否至少存在一个实现方法,当存在至少一个时,则表明支持。
知道了处理器适配器是根据存在方法判断是否支持Handler处理器后,最后一个问题,处理器映射器是如何判断使用哪一个Handler处理器的呢?在注解的处理器映射器中,答案是使用@RequestMapping注解。
@RequestMapping
@RequestMapping注解的作用是将用户的HTTP请求,映射到可以处理该请求的Handler处理器中,即制定了某一个Handler可以处理那些URL请求。@RequestMapping注解提供的配置参数有很多,例如name属性指定映射器的名称,method属性指定请求方法的类型,params属性指定请求的参数,value和path属性指定映射路径等。这里我们拿value属性举例,因为它是一个十分常用到的属性配置。@RequestMapping注解可以放在Handler处理器类上,也可以放在类中的具体实现方法上,或者同时放在类与方法上都可以。
当@RequestMapping注解放到处理器类上时,表示的是一个前置请求路径,例如配置@RequestMapping(value=“/user”),那么我们就可以通过请求路径:(这是我的配置路径,以“.action”作为后缀)
http://localhost:8080/SpringMVC__Project/user.action
映射到该Handler进行处理。如果只把注解放在方法上,效果一样。而如果在Handler类和具体实现方法上都放置@RequestMapping注解,那么方法上的注解就会变成下一级URL路径。例如这种情况:
@RequestMapping(“/user”)
Handler类 {
@RequestMapping(“/findUser“)
具体方法
}
那么映射的请求路径就变为
http://localhost:8080/SpringMVC__Project/user/findUser.action
当@RequestMapping注解中只配置一个请求映射属性时,value字样可以省略。
前面说到,使用了annotation-driven标签配置的处理器映射器和适配器,还提供了数据绑定功能,即在HTTP请求路径中,可以进行数据参数的绑定。
处理器参数绑定
用户在前端页面发出一些请求时,通常会携带一些参数到后端进行处理,例如用户登录,注册和查询等操作。在Spring MVC中,数据参数绑定组件由处理器映射器负责,将HTTP请求中携带的数据参数绑定到Controller处理器中,接收参数的自然就是处理器类里具体实现方法的形参。在处理器类实现方法支持的参数绑定有HttpServletRequest、HttpServletResponse、HttpSession还有Model/ModelMap。这里使用Model/ModelMap举例,它可以实现视图和模型的分离,在后台完成数据的操作后,通过model对象将数据传达到前端页面。
基本类型的数据参数绑定
基本类型的数据绑定,即int、long、float、double、char,boolean等。来看一个例子,假如我们要查询id为1的用户信息,在处理器类中应该怎么写:
@Controller
@RequestMapping("/user")
public class SimpleParamBind {
private UserService userService = new UserService();
@RequestMapping(value="/queryUserById", method= {RequestMethod.GET})
public String queryUserById(Integer id, Model model) throws Exception {
// 查询用户信息
List<User> userList = userService.queryUserById(id);
// 通过形参中的model将数据传到页面
model.addAttribute("userList", userList);
return "users/userList"; // 返回视图
}
@RequestMapping注解中除了配置了URL映射路径,还配置了请求类型要为GET类型,也就是说该处理器方法只接受请求GET方法类型的HTTP请求。在进行queryUserById()方法进行用户查询时,我们为其提供一个参数Integer id,标识用户id,第19行并根据传入的id查询用户信息。第11行,将查询结果userList,通过model对象的addAttribute()方法传到前端视图中,最后返回逻辑视图名。按照这样的配置,在URL请求中我们可以传入id参数:
http://localhost:8080/SpringMVC__Project/user/queryUserById.action?id=1
查询id为1的用户信息:
@RequestParam注解自定义参数别名
在URL请求中,假如我们不给id参数,或者参数名字不为“id”,都会导致出错,例如我们在请求路径中把参数名改为userId的话:
就会出错,因为参数名不一致嘛。不过我们可以使用@RequestParam注解,来自定义基本类型的参数绑定,也就是不需要 URL请求中参数名与Controller控制器中具体方法的参数名一致。@RequestParam注解的具体使用是在Controller方法中:
@Controller
@RequestMapping("/user")
public class SimpleParamBind {
private UserService userService = new UserService();
@RequestMapping(value="/queryUserById", method= {RequestMethod.GET})
public String queryUserById(@RequestParam(value="userId")Integer id, Model model) throws Exception {
// 查询用户信息
List<User> userList = userService.queryUserById(id);
// 通过形参中的model将数据传到页面
model.addAttribute("userList", userList);
return "users/userList"; // 返回视图
}
第7行,我们在queryUserById()方法的参数id前加上@RequestParam注解,value=“userId”,这样就可以在URL请求中可以接收名为“userId”的参数传递:
如果我们在Controller方法中设置了参数传递,但在HTTP请求里又没有给予参数,显然这样会出错:
如果我们想在参数为空时,给予参数一个默认值,可以在@RequestParam注解中设置defaultValue属性,来指定某一个参数的默认值:
@Controller
@RequestMapping("/user")
public class SimpleParamBind {
private UserService userService = new UserService();
@RequestMapping(value="/queryUserById", method= {RequestMethod.GET})
public String queryUserById(@RequestParam(value="userId,
defaultValue="2")Integer id, Model model) throws Exception {
// 查询用户信息
List<User> userList = userService.queryUserById(id);
// 通过形参中的model将数据传到页面
model.addAttribute("userList", userList);
return "users/userList"; // 返回视图
}
第8行,我们设置defaultValue=“2”,为id参数设置一个默认值,当HTTP请求中没有id参数传递时,则默认查询id为2的用户信息:
除了基本类型的数据参数绑定外,还可以进行JavaBean或者包装类型的数据绑定,例如我们想要根据用户姓名和性别两个条件来查询数据表中这名用户的信息,怎么做?
包装类型的数据参数绑定
来看一个简单的测试用例,在前端页面传递Java包装类参数,与Controller处理器绑定参数后,根据参数条件查询用户信息。首先简单地创建一个查询包装类:
public class User {
private Integer id;
private String username;
private String password;
private String gender;
private String email;
private String province;
private String city;
private Date birthday;
private Integer age;
// 省略get()和set()方法
}
Service接口与实现类
接下来看具体的数据处理部分方法,在Spring MVC部分的日志中,我没有把它和MyBatis整合,打算放到以后做一个简单项目例子时在整合讲解,Spring MVC部分的日志我们就专心于Spring MVC,所以在数据处理部分,我们模拟数据库的数据初始化与查询。首先创建数据处理部分的接口:
package com.mvc.service;
import java.util.List;
import com.mvc.model.User;
public interface UserService {
public List<User> queryUserList(); // 查询所有顾客信息
public List<User> queryUserById(Integer id); // id查找
public List<User> queryUserByCondition(User user); // 条件查找
}
接口中定义了三个方法,分别查询所有的顾客用户信息,按id查找(上面的实现)以及接下来要用到的,根据包装类信息查找。来看queryUserByCondition()方法,根据Java包装类查询的实现:
@Override
public List<User> queryUserByCondition(User user) {
init();
String name = user.getUsername();
String gender = user.getGender();
List<User> queryList = new ArrayList<User>();
User u = null;
for(int i=0; i<userList.size(); i++) {
u = userList.get(i);
if((!name.equals("") && u.getUsername().contains(name)) &&
(!gender.equals("") && u.getGender().contains(gender))) {
queryList.add(u);
}
}
return queryList;
}
init()方法初始化用户数据,然后根据传入的参数user对象,首先获取姓名和性别属性,接着61到67行逐一判断,当条件不为空且在数据表中找到对应的用户信息,第65行就把该信息保存到查询结果集合中,最后返回出去。
Controller处理器类
数据处理部分完成,回到Controller处理器类,它负责调用数据处理方法,完成业务逻辑,也就是将前端页面发来的请求user对象参数,传递到数据处理方法中,进行条件查询:
package com.mvc.controller;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.mvc.model.User;
import com.mvc.service.UserServiceImpl;
@Controller
@RequestMapping("user")
public class PackParamBind {
private UserServiceImpl userService = new UserServiceImpl();
@RequestMapping("findUserByCondition")
public String queryUserByCondition(Model model, User user) {
List<User> userList = null;
if(user == null || (user.getUsername() == null && user.getGender() == null)) {
// 如果查询框中两个查询条件都为空,则默认查询所有顾客数据
userList = userService.queryUserList();
} else {
// 否则进行条件查询
userList = userService.queryUserByCondition(user);
}
// model数据传到页面
model.addAttribute("userList", userList);
return "users/queryUser"; // 返回视图
}
}
可以看到,具体方法中需要User类的对象作为传入参数,如果查询条件为空,则进行所有数据的查询,当查询条件不为空,调用方法进行条件查询。第28行调用addAttribute()方法,将查询结果集合userList传递到视图中进行视图渲染,最后返回的便是要使用的视图逻辑名。
前端视图
最后来看看我们的视图部分,接收到后端传来的数据,就要进行数据填充,完成视图渲染:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户查询列表</title>
</head>
<body>
<form action="findUserByCondition.action" method="post">
用户名:<input type="text" name="username" />
性别:<input type="text" name="gender" />
<input type="submit" value="查找" />
</form>
<hr/>
<h2>搜索结果</h2>
<table width="300px;" border=1>
<tr>
<td>顾客名</td>
<td>性别</td>
<td>电子邮箱</td>
<td>省会</td>
<td>城市</td>
</tr>
<c:forEach items="${userList }" var="user">
<tr>
<td>${user.username }</td>
<td>${user.gender }</td>
<td>${user.email }</td>
<td>${user.province }</td>
<td>${user.city }</td>
</tr>
</c:forEach>
</table>
</body>
</html>
第10行,form表单中的action制定了该页面请求地址要为findUserByCondition.action,即对应了我们Controller处理器类中的具体方法名。第11行和12行用户名文本框和性别文本框的name属性分别为username和gender,对应Java包装类中的属性,这样才能被处理器适配器解析,为其创建对应的Java实体类,通过set()方法将数据绑定到实体类的对象中进行参数传递。最后来看看运行结果:
当搜索框中没有填入查询条件时,默认查询所有用户的信息。当填入查询条件后,就调用条件查询的方法:
完整实现已上传GitHub:
https://github.com/justinzengtm/SSM-Framework/tree/master/SpringMVC_Project