SpringMVC(4.2):Controller接口控制器详解(2)
原文出处: 张开涛
4.5、ServletForwardingController
将接收到的请求转发到一个命名的servlet,具体示例如下:
1 package cn.javass.chapter4.web.servlet; 2 public class ForwardingServlet extends HttpServlet { 3 @Override 4 protected void doGet(HttpServletRequest req, HttpServletResponse resp) 5 throws ServletException, IOException { 6 7 resp.getWriter().write("Controller forward to Servlet"); 8 9 } 10 }
1 <servlet> 2 <servlet-name>forwarding</servlet-name> 3 <servlet-class>cn.javass.chapter4.web.servlet.ForwardingServlet</servlet-class> 4 </servlet>
1 <!— 在chapter4-servlet.xml配置处理器 --> 2 <bean name="/forwardToServlet" 3 class="org.springframework.web.servlet.mvc.ServletForwardingController"> 4 <property name="servletName" value="forwarding"></property> 5 </bean>
当我们请求/forwardToServlet时,会被转发到名字为“forwarding”的servlet处理,该sevlet的servlet-mapping标签配置是可选的。
4.6、BaseCommandController
命令控制器通用基类,提供了以下功能支持:
1、数据绑定:请求参数绑定到一个command object(命令对象,非GoF里的命令设计模式),这里的命令对象是指绑定请求参数的任何POJO对象;
commandClass:表示命令对象实现类,如UserModel;
commandName:表示放入请求的命令对象名字(默认command),request.setAttribute(commandName, commandObject);
2、验证功能:提供Validator注册功能,注册的验证器会验证命令对象属性数据是否合法;
validators:通过该属性注入验证器,验证器用来验证命令对象属性是否合法;
该抽象类没有没有提供流程功能,只是提供了一些公共的功能,实际使用时需要使用它的子类。
4.7、AbstractCommandController
命令控制器之一,可以实现该控制器来创建命令控制器,该控制器能把自动封装请求参数到一个命令对象,而且提供了验证功能。
1、创建命令类(就是普通的JavaBean类/POJO)
1 package cn.javass.chapter4.model; 2 public class UserModel { 3 private String username; 4 private String password; 5 //省略setter/getter 6 }
2、实现控制器
1 package cn.javass.chapter4.web.controller; 2 //省略import 3 public class MyAbstractCommandController extends AbstractCommandController { 4 public MyAbstractCommandController() { 5 //设置命令对象实现类 6 setCommandClass(UserModel.class); 7 } 8 @Override 9 protected ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { 10 //将命令对象转换为实际类型 11 UserModel user = (UserModel) command; 12 ModelAndView mv = new ModelAndView(); 13 mv.setViewName("abstractCommand"); 14 mv.addObject("user", user); 15 return mv; 16 } 17 }
1 <!— 在chapter4-servlet.xml配置处理器 --> 2 <bean name="/abstractCommand" 3 class="cn.javass.chapter4.web.controller.MyAbstractCommandController"> 4 <!-- 也可以通过依赖注入 注入命令实现类 --> 5 <!-- property name="commandClass" value="cn.javass.chapter4.model.UserModel"/--> 6 </bean>
1 <!— WEB-INF/jsp/abstractCommand.jsp视图下的主要内容 --> 2 3 ${user.username }-${user.password }
当我们在浏览器中输入“http://localhost:9080/springmvc-chapter4/abstractCommand?username=123&password=123”,会自动将请求参数username和password绑定到命令对象;绑定时按照JavaBean命名规范绑定;
4.8、AbstractFormController
用于支持带步骤的表单提交的命令控制器基类,使用该控制器可以完成:
1、定义表单处理(表单的渲染),并从控制器获取命令对象构建表单;
2、提交表单处理,当用户提交表单内容后,AbstractFormController可以将用户请求的数据绑定到命令对象,并可以验证表单内容、对命令对象进行处理。
1 @Override 2 rotected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) 3 throws Exception { 4 //1、是否是表单提交? 该方法实现为("POST".equals(request.getMethod())),即POST表示表单提交 5 if (isFormSubmission(request)) { 6 try { 7 Object command = getCommand(request); 8 ServletRequestDataBinder binder = bindAndValidate(request, command); 9 BindException errors = new BindException(binder.getBindingResult()); 10 //表单提交应该放到该方法实现 11 return processFormSubmission(request, response, command, errors); 12 } 13 catch (HttpSessionRequiredException ex) { 14 //省略部分代码 15 return handleInvalidSubmit(request, response); 16 } 17 } 18 else { 19 //2、表示是表单展示,该方法又转调showForm方法,因此我们需要覆盖showForm来完成表单展示 20 return showNewForm(request, response); 21 }
bindOnNewForm:是否在进行表单展示时绑定请求参数到表单对象,默认false,不绑定;
sessionForm:session表单模式,如果开启(true)则会将表单对象放置到session中,从而可以跨越多次请求保证数据不丢失(多步骤表单常使用该方式,详解AbstractWizardFormController),默认false;
Object formBackingObject(HttpServletRequest request) :提供给表单展示时使用的表单对象(form object表单要展示的默认数据),默认通过commandName暴露到请求给展示表单;
Map referenceData(HttpServletRequest request, Object command, Errors errors):展示表单时需要的一些引用数据(比如用户注册,可能需要选择工作地点,这些数据可以通过该方法提供),如:
1 protected Map referenceData(HttpServletRequest request) throws Exception { 2 Map model = new HashMap(); 3 model.put("cityList", cityList); 4 return model; 5 }
这样就可以在表单展示页面获取cityList数据。
SimpleFormController继承该类,而且提供了更简单的表单流程控制。
4.9、SimpleFormController
提供了更好的两步表单支持:
1、准备要展示的数据,并到表单展示页面;
2、提交数据数据进行处理。
第一步,展示:
第二步,提交表单:
接下来咱们写一个用户注册的例子学习一下:
(1、控制器
1 package cn.javass.chapter4.web.controller; 2 //省略import 3 public class RegisterSimpleFormController extends SimpleFormController { 4 public RegisterSimpleFormController() { 5 setCommandClass(UserModel.class); //设置命令对象实现类 6 setCommandName("user");//设置命令对象的名字 7 } 8 //form object 表单对象,提供展示表单时的表单数据(使用commandName放入请求) 9 protected Object formBackingObject(HttpServletRequest request) throws Exception { 10 UserModel user = new UserModel(); 11 user.setUsername("请输入用户名"); 12 return user; 13 } 14 //提供展示表单时需要的一些其他数据 15 protected Map referenceData(HttpServletRequest request) throws Exception { 16 Map map = new HashMap(); 17 map.put("cityList", Arrays.asList("山东", "北京", "上海")); 18 return map; 19 } 20 protected void doSubmitAction(Object command) throws Exception { 21 UserModel user = (UserModel) command; 22 //TODO 调用业务对象处理 23 System.out.println(user); 24 } 25 }
setCommandClass和setCommandName:分别设置了命令对象的实现类和名字;
formBackingObject和referenceData:提供了表单展示需要的视图;
doSubmitAction:用于执行表单提交动作,由onSubmit方法调用,如果不需要请求/响应对象或进行数据验证,可以直接使用doSubmitAction方法进行功能处理。
(2、spring配置(chapter4-servlet.xml)
1 <bean name="/simpleForm" 2 class="cn.javass.chapter4.web.controller.RegisterSimpleFormController"> 3 <property name="formView" value="register"/> 4 <property name="successView" value="redirect:/success"/> 5 </bean> 6 <bean name="/success" class="cn.javass.chapter4.web.controller.SuccessController"/>
formView:表示展示表单时显示的页面;
successView:表示处理成功时显示的页面;“redirect:/success”表示成功处理后重定向到/success控制器;防止表单重复提交;
“/success” bean的作用是显示成功页面,此处就不列举了。
(3、视图页面
1 <!-- register.jsp 注册展示页面--> 2 <form method="post"> 3 username:<input type="text" name="username" value="${user.username}"><br/> 4 password:<input type="password" name="username"><br/> 5 city:<select> 6 <c:forEach items="${cityList }" var="city"> 7 <option>${city}</option> 8 </c:forEach> 9 </select><br/> 10 <input type="submit" value="注册"/> 11 </form>
此处可以使用${user.username}获取到formBackingObject设置的表单对象、使用${cityList}获取referenceData设置的表单支持数据;
到此一个简单的两步表单到此结束,但这个表单有重复提交表单的问题,而且表单对象到页面的绑定是通过手工绑定的,后边我们会学习spring标签库(提供自动绑定表单对象到页面)。
4.10、CancellableFormController
一个可取消的表单控制器,继承SimpleFormController,额外提供取消表单功能。
1、表单展示:和SimpleFormController一样;
2、表单取消:和SimpleFormController一样;
3、表单成功提交:取消功能处理方法为:onCancel(Object command),而且默认返回cancelView属性指定的逻辑视图名。
那如何判断是取消呢?如果请求中有参数名为“_cancel”的参数,则表示表单取消。也可以通过cancelParamKey来修改参数名(如“_cancel.x”等)。
示例:
(1、控制器
复制RegisterSimpleFormController一份命名为CanCancelRegisterSimpleFormController,添加取消功能处理方法实现:
1 @Override 2 protected ModelAndView onCancel(Object command) throws Exception { 3 UserModel user = (UserModel) command; 4 //TODO 调用业务对象处理 5 System.out.println(user); 6 return super.onCancel(command); 7 }
onCancel:在该功能方法内实现取消逻辑,父类的onCancel方法默认返回cancelView属性指定的逻辑视图名。
(2、spring配置(chapter4-servlet.xml)
1 <bean name="/canCancelForm" 2 class="cn.javass.chapter4.web.controller.CanCancelRegisterSimpleFormController"> 3 <property name="formView" value="register"/> 4 <property name="successView" value="redirect:/success"/> 5 <property name="cancelView" value="redirect:/cancel"/> 6 </bean> 7 <bean name="/cancel" class="cn.javass.chapter4.web.controller.CancelController"/>
(3、视图页面(修改register.jsp)
1 <input type="submit" name="_cancel" value="取消"/>
该提交按钮的作用是取消,因为name="_cancel",即请求后会有一个名字为_cancel的参数,因此会执行onCancel功能处理方法。
(4、测试:
在浏览器输入“http://localhost:9080/springmvc-chapter4/canCancelForm”,则首先到展示视图页面,点击“取消按钮”将重定向到“http://localhost:9080/springmvc-chapter4/cancel”,说明取消成功了。
实际项目可能会出现比如一些网站的完善个人资料都是多个页面(即多步),那应该怎么实现呢?接下来让我们看一下spring Web MVC提供的对多步表单的支持类AbstractWizardFormController。
4.11、AbstractWizardFormController
向导控制器类提供了多步骤(向导)表单的支持(如完善个人资料时分步骤填写基本信息、工作信息、学校信息等)
假设现在做一个完善个人信息的功能,分三个页面展示:
1、页面1完善基本信息;
2、页面2完善学校信息;
3、页面3完善工作信息。
这里我们要注意的是当用户跳转到页面2时页面1的信息是需要保存起来的,还记得AbstractFormController中的sessionForm吗? 如果为true则表单数据存放到session中,哈哈,AbstractWizardFormController就是使用了这个特性。
向导中的页码从0开始;
PARAM_TARGET = “_target”:
用于选择向导中的要使用的页面参数名前缀,如“_target0”则选择第0个页面显示,即图中的“wizard/baseInfo”,以此类推,如“_target1”将选择第1页面,要得到的页码为去除前缀“_target”后的数字即是;
PARAM_FINISH = “_finish”:
如果请求参数中有名为“_finish”的参数,表示向导成功结束,将会调用processFinish方法进行完成时的功能处理;
PARAM_CANCEL = “_cancel”:
如果请求参数中有名为“_cancel”的参数,表示向导被取消,将会调用processCancel方法进行取消时的功能处理;
向导中的命令对象:
向导中的每一个步骤都会把相关的参数绑定到命令对象,该表单对象默认放置在session中,从而可以跨越多次请求得到该命令对象。
接下来具体看一下如何使用吧。
(1、修改我们的模型数据以支持多步骤提交:
1 public class UserModel { 2 private String username; 3 private String password; 4 private String realname; //真实姓名 5 private WorkInfoModel workInfo; 6 private SchoolInfoModel schoolInfo; 7 //省略getter/setter 8 } 9 10 11 public class SchoolInfoModel { 12 private String schoolType; //学校类型:高中、中专、大学 13 private String schoolName; //学校名称 14 private String specialty; //专业 15 //省略getter/setter 16 } 17 18 19 public class WorkInfoModel { 20 private String city; //所在城市 21 private String job; //职位 22 private String year; //工作年限 23 //省略getter/setter 24 }
(2、控制器
1 package cn.javass.chapter4.web.controller; 2 //省略import 3 public class InfoFillWizardFormController extends AbstractWizardFormController { 4 public InfoFillWizardFormController() { 5 setCommandClass(UserModel.class); 6 setCommandName("user"); 7 } 8 protected Map referenceData(HttpServletRequest request, int page) throws Exception { 9 Map map = new HashMap(); 10 if(page==1) { //如果是填写学校信息页 需要学校类型信息 11 map.put("schoolTypeList", Arrays.asList("高中", "中专", "大学")); 12 } 13 if(page==2) {//如果是填写工作信息页 需要工作城市信息 14 map.put("cityList", Arrays.asList("济南", "北京", "上海")); 15 } 16 return map; 17 } 18 protected void validatePage(Object command, Errors errors, int page) { 19 //提供每一页数据的验证处理方法 20 } 21 protected void postProcessPage(HttpServletRequest request, Object command, Errors errors, int page) throws Exception { 22 //提供给每一页完成时的后处理方法 23 } 24 protected ModelAndView processFinish(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { 25 //成功后的处理方法 26 System.out.println(command); 27 return new ModelAndView("redirect:/success"); 28 } 29 protected ModelAndView processCancel(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception { 30 //取消后的处理方法 31 System.out.println(command); 32 return new ModelAndView("redirect:/cancel"); 33 } 34 }
page页码:是根据请求中以“_target”开头的参数名来确定的,如“_target0”,则页码为0;
referenceData:提供每一页需要的表单支持对象,如完善学校信息需要学校类型,page页码从0开始(而且根据请求参数中以“_target”开头的参数来确定当前页码,如_target1,则page=1);
validatePage:验证当前页的命令对象数据,验证应根据page页码来分步骤验证;
postProcessPage:验证成功后的后处理;
processFinish:成功时执行的方法,此处直接重定向到/success控制器(详见CancelController);
processCancel:取消时执行的方法,此处直接重定向到/cancel控制器(详见SuccessController);
其他需要了解:
allowDirtyBack和allowDirtyForward:决定在当前页面验证失败时,是否允许向导前移和后退,默认false不允许;
onBindAndValidate(HttpServletRequest request, Object command, BindException errors, int page):允许覆盖默认的绑定参数到命令对象和验证流程。
(3、spring配置文件(chapter4-servlet.xml)
1 <bean name="/infoFillWizard" 2 class="cn.javass.chapter4.web.controller.InfoFillWizardFormController"> 3 <property name="pages"> 4 <list> 5 <value>wizard/baseInfo</value> 6 <value>wizard/schoolInfo</value> 7 <value>wizard/workInfo</value> 8 </list> 9 </property> 10 </bean>
(4、向导中的每一步视图
(4.1、基本信息页面(第一步) baseInfo.jsp:
1 <form method="post"> 2 真实姓名:<input type="text" name="realname" value="${user.realname}"> 3 <input type="submit" name="_target1" value="下一步"/> 4 </form>
(4.2、学校信息页面(第二步) schoolInfo.jsp:
1 <form method="post"> 2 学校类型:<select name="schoolInfo.schoolType"> 3 <c:forEach items="${schoolTypeList }" var="schoolType"> 4 <option value="${schoolType }" 5 <c:if test="${user.schoolInfo.schoolType eq schoolType}"> 6 selected="selected" 7 </c:if> 8 > 9 ${schoolType} 10 </option> 11 </c:forEach> 12 </select> 13 学校名称:<input type="text" name="schoolInfo.schoolName" value="${user.schoolInfo.schoolName}"/> 14 专业:<input type="text" name="schoolInfo.specialty" value="${user.schoolInfo.specialty}"/> 15 <input type="submit" name="_target0" value="上一步"/> 16 <input type="submit" name="_target2" value="下一步"/> 17 </form>
(4.3、工作信息页面(第三步) workInfo.jsp:
1 <form method="post"> 2 所在城市:<select name="workInfo.city"> 3 <c:forEach items="${cityList }" var="city"> 4 <option value="${city }" 5 <c:if test="${user.workInfo.city eq city}">selected="selected"</c:if> 6 > 7 ${city} 8 </option> 9 </c:forEach> 10 </select> 11 职位:<input type="text" name="workInfo.job" value="${user.workInfo.job}"/> 12 工作年限:<input type="text" name="workInfo.year" value="${user.workInfo.year}"/> 13 <input type="submit" name="_target1" value="上一步"/> 14 <input type="submit" name="_finish" value="完成"/> 15 <input type="submit" name="_cancel" value="取消"/> 16 </form> 17
当前页码为2;
name=”_target1″:上一步,表示向导上一步要显示的页面的页码为1;
name=”_finish”:向导完成,表示向导成功,将会调用向导控制器的processFinish方法;
name=”_cancel”:向导取消,表示向导被取消,将会调用向导控制器的processCancel方法;
到此向导控制器完成,此处的向导流程比较简单,如果需要更复杂的页面流程控制,可以选择使用Spring Web Flow框架。
4.12、ParameterizableViewController
参数化视图控制器,不进行功能处理(即静态视图),根据参数的逻辑视图名直接选择需要展示的视图。
1 <bean name="/parameterizableView" 2 class="org.springframework.web.servlet.mvc.ParameterizableViewController"> 3 <property name="viewName" value="success"/> 4 </bean>
该控制器接收到请求后直接选择参数化的视图,这样的好处是在配置文件中配置,从而避免程序的硬编码,比如像帮助页面等不需要进行功能处理,因此直接使用该控制器映射到视图。
4.13、AbstractUrlViewController
提供根据请求URL路径直接转化为逻辑视图名的支持基类,即不需要功能处理,直接根据URL计算出逻辑视图名,并选择具体视图进行展示:
urlDecode:是否进行url解码,不指定则默认使用服务器编码进行解码(如Tomcat默认ISO-8859-1);
urlPathHelper:用于解析请求路径的工具类,默认为org.springframework.web.util.UrlPathHelper。
UrlFilenameViewController是它的一个实现者,因此我们应该使用UrlFilenameViewController。
4.14、UrlFilenameViewController
将请求的URL路径转换为逻辑视图名并返回的转换控制器,即不需要功能处理,直接根据URL计算出逻辑视图名,并选择具体视图进行展示:
根据请求URL路径计算逻辑视图名;
1 <bean name="/index1/*" 2 class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/> 3 <bean name="/index2/**" 4 class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/> 5 <bean name="/*.html" 6 class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/> 7 <bean name="/index3/*.html" 8 class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
/index1/*:可以匹配/index1/demo,但不匹配/index1/demo/demo,如/index1/demo逻辑视图名为demo;
/index2/**:可以匹配/index2路径下的所有子路径,如匹配/index2/demo,或/index2/demo/demo,“/index2/demo”的逻辑视图名为demo,而“/index2/demo/demo”逻辑视图名为demo/demo;
/*.html:可以匹配如/abc.html,逻辑视图名为abc,后缀会被删除(不仅仅可以是html);
/index3/*.html:可以匹配/index3/abc.html,逻辑视图名也是abc;
上述模式为Spring Web MVC使用的Ant-style 模式进行匹配的:
1 2 ? 匹配一个字符,如/index? 可以匹配 /index1 , 但不能匹配 /index 或 /index12 3 * 匹配零个或多个字符,如/index1/*,可以匹配/index1/demo,但不匹配/index1/demo/demo 4 ** 匹配零个或多个路径,如/index2/**:可以匹配/index2路径下的所有子路径,如匹配/index2/demo,或/index2/demo/demo 5 6 如果我有如下模式,那Spring该选择哪一个执行呢?当我的请求为“/long/long”时如下所示: 7 /long/long 8 /long/**/abc 9 /long/** 10 /** 11 Spring的AbstractUrlHandlerMapping使用:最长匹配优先; 12 如请求为“/long/long” 将匹配第一个“/long/long”,但请求“/long/acd” 则将匹配 “/long/**”,如请求“/long/aa/abc”则匹配“/long/**/abc”,如请求“/abc”则将匹配“/**”
UrlFilenameViewController还提供了如下属性:
prefix:生成逻辑视图名的前缀;
suffix:生成逻辑视图名的后缀;
1 protected String postProcessViewName(String viewName) { 2 return getPrefix() + viewName + getSuffix(); 3 }
1 <bean name="/*.htm" class="org.springframework.web.servlet.mvc.UrlFilenameViewController"> 2 <property name="prefix" value="test"/> 3 <property name="suffix" value="test"/> 4 </bean>
当prefix=“test”,suffix=“test”,如上所示的/*.htm:可以匹配如/abc.htm,但逻辑视图名将变为testabctest。
4.15、MultiActionController
之前学过的控制器如AbstractCommandController、SimpleFormController等一般对应一个功能处理方法(如新增),如果我要实现比如最简单的用户增删改查(CRUD Create-Read-Update-Delete),那该怎么办呢?
4.15.1 解决方案
1、每一个功能对应一个控制器,如果是CRUD则需要四个控制器,但这样我们的控制器会暴增,肯定不可取;
2、使用spring Web MVC提供的MultiActionController,用于支持在一个控制器里添加多个功能处理方法,即将多个请求的处理方法放置到一个控制器里,这种方式不错。
4.15.2 问题
1、 MultiActionController如何将不同的请求映射不同的请求的功能处理方法呢?
Spring Web MVC提供了MethodNameResolver(方法名解析器)用于解析当前请求到需要执行的功能处理方法的方法名。默认使用InternalPathMethodNameResolver实现类,另外还提供了ParameterMethodNameResolver和PropertiesMethodNameResolver,当然我们也可以自己来实现,稍候我们仔细研究下它们是如何工作的。
2、那我们的功能处理方法应该怎么写呢?
public (ModelAndView | Map | String | void) actionName(HttpServletRequest request, HttpServletResponse response, [,HttpSession session] [,AnyObject]);
哦,原来如此,我们只需要按照如上格式写我们的功能处理方法即可;此处需要注意一下几点:
1、返回值:即模型和视图部分;
ModelAndView:模型和视图部分,之前已经见过了;
Map:只返回模型数据,逻辑视图名会根据RequestToViewNameTranslator实现类来计算,稍候讨论;
String:只返回逻辑视图名;
void:表示该功能方法直接写出response响应(如果其他返回值类型(如Map)返回null则和void进行相同的处理);
2、actionName:功能方法名字;由methodNameResolver根据请求信息解析功能方法名,通过反射调用;
3、形参列表:顺序固定,“[]”表示可选,我们来看看几个示例吧:
1 //表示到新增页面 2 3 public ModelAndView toAdd(HttpServletRequest request, HttpServletResponse response); 4 5 //表示新增表单提交,在最后可以带着命令对象 6 7 public ModelAndView add(HttpServletRequest request, HttpServletResponse response, UserModel user); 8 9 //列表,但只返回模型数据,视图名会通过RequestToViewNameTranslator实现来计算 10 11 public Map list(HttpServletRequest request, HttpServletResponse response); 12 13 //文件下载,返回值类型为void,表示该功能方法直接写响应 14 15 public void fileDownload(HttpServletRequest request, HttpServletResponse response) 16 17 //第三个参数可以是session 18 19 public ModelAndView sessionWith(HttpServletRequest request, HttpServletResponse response, HttpSession session); 20 21 //如果第三个参数是session,那么第四个可以是命令对象,顺序必须是如下顺序 22 23 public void sessionAndCommandWith(HttpServletRequest request, HttpServletResponse response, HttpSession session, UserModel user) 24
4、异常处理方法,MultiActionController提供了简单的异常处理,即在请求的功能处理过程中遇到异常会交给异常处理方法进行处理,式如下所示:
1 public ModelAndView anyMeaningfulName(HttpServletRequest request, HttpServletResponse response, ExceptionClass exception) 2 3 MultiActionController会使用最接近的异常类型来匹配对应的异常处理方法,示例如下所示: 4 5 //处理PayException 6 7 public ModelAndView processPayException(HttpServletRequest request, HttpServletResponse response, PayException ex) 8 9 //处理Exception 10 11 public ModelAndView processException(HttpServletRequest request, HttpServletResponse response, Exception ex) 12
4.15.3 MultiActionController类实现
类定义:public class MultiActionController extends AbstractController implements LastModified ,继承了AbstractController,并实现了LastModified接口,默认返回-1;
核心属性:
delegate:功能处理的委托对象,即我们要调用请求处理方法所在的对象,默认是this;
methodNameResolver:功能处理方法名解析器,即根据请求信息来解析需要执行的delegate的功能处理方法的方法名。
核心方法:
1 //判断方法是否是功能处理方法 2 private boolean isHandlerMethod(Method method) { 3 //得到方法返回值类型 4 Class returnType = method.getReturnType(); 5 //返回值类型必须是ModelAndView、Map、String、void中的一种,否则不是功能处理方法 6 if (ModelAndView.class.equals(returnType) || Map.class.equals(returnType) || String.class.equals(returnType) || 7 void.class.equals(returnType)) { 8 Class[] parameterTypes = method.getParameterTypes(); 9 //功能处理方法参数个数必须>=2,且第一个是HttpServletRequest类型、第二个是HttpServletResponse 10 //且不能Controller接口的handleRequest(HttpServletRequest request, HttpServletResponse response),这个方法是由系统调用 11 return (parameterTypes.length >= 2 && 12 HttpServletRequest.class.equals(parameterTypes[0]) && 13 HttpServletResponse.class.equals(parameterTypes[1]) && 14 !("handleRequest".equals(method.getName()) && parameterTypes.length == 2)); 15 } 16 return false; 17 }
1 //是否是异常处理方法 2 private boolean isExceptionHandlerMethod(Method method) { 3 //异常处理方法必须是功能处理方法 且 参数长度为3、第三个参数类型是Throwable子类 4 return (isHandlerMethod(method) && 5 method.getParameterTypes().length == 3 && 6 Throwable.class.isAssignableFrom(method.getParameterTypes()[2])); 7 } 8 9 10 11 12 13 14 private void registerHandlerMethods(Object delegate) { 15 //缓存Map清空 16 this.handlerMethodMap.clear(); 17 this.lastModifiedMethodMap.clear(); 18 this.exceptionHandlerMap.clear(); 19 20 //得到委托对象的所有public方法 21 Method[] methods = delegate.getClass().getMethods(); 22 for (Method method : methods) { 23 //验证是否是异常处理方法,如果是放入exceptionHandlerMap缓存map 24 if (isExceptionHandlerMethod(method)) { 25 registerExceptionHandlerMethod(method); 26 } 27 //验证是否是功能处理方法,如果是放入handlerMethodMap缓存map 28 else if (isHandlerMethod(method)) { 29 registerHandlerMethod(method); 30 registerLastModifiedMethodIfExists(delegate, method); 31 } 32 } 33 } 34 35 36 37 38 39 protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) 40 throws Exception { 41 try { 42 //1、使用methodNameResolver 方法名解析器根据请求解析到要执行的功能方法的方法名 43 String methodName = this.methodNameResolver.getHandlerMethodName(request); 44 //2、调用功能方法(通过反射调用,此处就粘贴代码了) 45 return invokeNamedMethod(methodName, request, response); 46 } 47 catch (NoSuchRequestHandlingMethodException ex) { 48 return handleNoSuchRequestHandlingMethod(ex, request, response); 49 } 50 } 51 52 53 54
接下来,我们看一下MultiActionController如何使用MethodNameResolver来解析请求到功能处理方法的方法名。
4.15.4 MethodNameResolver
1、InternalPathMethodNameResolver:
MultiActionController的默认实现,提供从请求URL路径解析功能方法的方法名,从请求的最后一个路径(/)开始,并忽略扩展名;如请求URL是“/user/list.html”,则解析的功能处理方法名为“list”,即调用list方法。该解析器还可以指定前缀和后缀,通过prefix和suffix属性,如指定prefix=”test_”,则功能方法名将变为test_list;
2、ParameterMethodNameResolver:
提供从请求参数解析功能处理方法的方法名,并按照如下顺序进行解析:
(1、
methodParamNames:
根据请求的参数名解析功能方法名(功能方法名和参数名同名);
1 <property name="methodParamNames" value="list,create,update"/>
如上配置时,如果请求中含有参数名list、create、update时,则功能处理方法名为list、create、update,这种方式的可以在当一个表单有多个提交按钮时使用,不同的提交按钮名字不一样即可。
ParameterMethodNameResolver也考虑到图片提交按钮提交问题:
<input type="image" name="list"> 和submit类似可以提交表单,单击该图片后会发送两个参数“list.x=x轴坐标”和“list.y=y轴坐标”(如提交后会变为list.x=7&list.y=5);因此我们配置的参数名(如list)在会加上“.x” 和 “.y”进行匹配。
1 for (String suffix : SUBMIT_IMAGE_SUFFIXES) {//SUBMIT_IMAGE_SUFFIXES {“.x”, “.y”} 2 if (request.getParameter(name + suffix) != null) {// name是我们配置的methodParamNames 3 return true; 4 } 5 }
(2、paramName:
根据请求参数名的值解析功能方法名,默认的参数名是action,即请求的参数中含有“action=query”,则功能处理方法名为query;
(3、logicalMappings:
逻辑功能方法名到真实功能方法名映射,如下所示:
1 <property name="logicalMappings"> 2 <props> 3 <prop key="doList">list</prop> 4 </props> 5 </property>
即如果步骤1或2解析出逻辑功能方法名为doList(逻辑的),将会被重新映射为list功能方法名(真正执行的)。
(4、defaultMethodName:
默认的方法名,当以上策略失败时默认调用的方法名。
3、PropertiesMethodNameResolver:
提供自定义的从请求URL解析功能方法的方法名,使用一组用户自定义的模式到功能方法名的映射,映射使用
Properties对象存放,具体配置示例如下:
1 <bean id="propertiesMethodNameResolver" 2 class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver"> 3 <property name="mappings"> 4 <props> 5 <prop key="/create">create</prop> 6 <prop key="/update">update</prop> 7 <prop key="/delete">delete</prop> 8 <prop key="/list">list</prop> 9 <!-- 默认的行为 --> 10 <prop key="/**">list</prop> 11 </props> 12 </property> 13 </bean>
对于/create请求将调用create方法,Spring内部使用PathMatcher进行匹配(默认实现是AntPathMatcher)。
4.15.5 RequestToViewNameTranslator
用于直接将请求转换为逻辑视图名。默认实现为DefaultRequestToViewNameTranslator。
1、DefaultRequestToViewNameTranslator:
将请求URL转换为逻辑视图名,默认规则如下:
http://localhost:9080/web上下文/list -------> 逻辑视图名为list
http://localhost:9080/web上下文/list.html -------> 逻辑视图名为list(默认删除扩展名)
http://localhost:9080/web上下文/user/list.html -------> 逻辑视图名为user/list
4.15.6 示例
(1、控制器UserController
1 package cn.javass.chapter4.web.controller; 2 //省略import 3 public class UserController extends MultiActionController { 4 //用户服务类 5 private UserService userService; 6 //逻辑视图名 通过依赖注入方式注入,可配置 7 private String createView; 8 private String updateView; 9 private String deleteView; 10 private String listView; 11 private String redirectToListView; 12 //省略setter/getter 13 14 public String create(HttpServletRequest request, HttpServletResponse response, UserModel user) { 15 if("GET".equals(request.getMethod())) { 16 //如果是get请求 我们转向 新增页面 17 return getCreateView(); 18 } 19 userService.create(user); 20 //直接重定向到列表页面 21 return getRedirectToListView(); 22 } 23 public ModelAndView update(HttpServletRequest request, HttpServletResponse response, UserModel user) { 24 if("GET".equals(request.getMethod())) { 25 //如果是get请求 我们转向更新页面 26 ModelAndView mv = new ModelAndView(); 27 //查询要更新的数据 28 mv.addObject("command", userService.get(user.getUsername())); 29 mv.setViewName(getUpdateView()); 30 return mv; 31 } 32 userService.update(user); 33 //直接重定向到列表页面 34 return new ModelAndView(getRedirectToListView()); 35 } 36 37 public ModelAndView delete(HttpServletRequest request, HttpServletResponse response, UserModel user) { 38 if("GET".equals(request.getMethod())) { 39 //如果是get请求 我们转向删除页面 40 ModelAndView mv = new ModelAndView(); 41 //查询要删除的数据 42 mv.addObject("command", userService.get(user.getUsername())); 43 mv.setViewName(getDeleteView()); 44 return mv; 45 } 46 userService.delete(user); 47 //直接重定向到列表页面 48 return new ModelAndView(getRedirectToListView()); 49 } 50 51 public ModelAndView list(HttpServletRequest request, HttpServletResponse response) { 52 ModelAndView mv = new ModelAndView(); 53 mv.addObject("userList", userService.list()); 54 mv.setViewName(getListView()); 55 return mv; 56 } 57 58 //如果使用委托方式,命令对象名称只能是command 59 protected String getCommandName(Object command) { 60 //命令对象的名字 默认command 61 return "command"; 62 } 63 }
增删改:如果是GET请求方法,则表示到展示页面,POST请求方法表示真正的功能操作;
getCommandName:
表示是命令对象名字,默认command,对于委托对象实现方式无法改变,因此我们就使用默认的吧。
(2、spring配置文件chapter4-servlet.xml
1 <bean id="userService" class="cn.javass.chapter4.service.UserService"/> 2 <bean name="/user/**" class="cn.javass.chapter4.web.controller.UserController"> 3 <property name="userService" ref="userService"/> 4 <property name="createView" value="user/create"/> 5 <property name="updateView" value="user/update"/> 6 <property name="deleteView" value="user/delete"/> 7 <property name="listView" value="user/list"/> 8 <property name="redirectToListView" value="redirect:/user/list"/> 9 <!-- 使用PropertiesMethodNameResolver来解析功能处理方法名 --> 10 <!--property name="methodNameResolver" ref="propertiesMethodNameResolver"/--> 11 </bean>
userService:用户服务类,实现业务逻辑;
依赖注入:对于逻辑视图页面通过依赖注入方式注入,redirectToListView表示增删改成功后重定向的页面,防止重复表单提交;
默认使用InternalPathMethodNameResolver解析请求URL到功能方法名。
(3、视图页面
(3.1、list页面(WEB-INF/jsp/user/list.jsp)
1 <a href="${pageContext.request.contextPath}/user/create">用户新增</a> 2 <table border="1" width="50%"> 3 <tr> 4 <th>用户名</th> 5 <th>真实姓名</th> 6 <th>操作</th> 7 </tr> 8 <c:forEach items="${userList}" var="user"> 9 <tr> 10 <td>${user.username }</td> 11 <td>${user.realname }</td> 12 <td> 13 <a href="${pageContext.request.contextPath}/user/update?username=${user.username}">更新</a> 14 | 15 <a href="${pageContext.request.contextPath}/user/delete?username=${user.username}">删除</a> 16 </td> 17 </tr> 18 </c:forEach> 19 </table>
(3.2、update页面(WEB-INF/jsp/user/update.jsp)
1 <form action="${pageContext.request.contextPath}/user/update" method="post"> 2 用户名: <input type="text" name="username" value="${command.username}"/> 3 真实姓名:<input type="text" name="realname" value="${command.realname}"/> 4 <input type="submit" value="更新"/> 5 </form>
(4、测试:
默认的InternalPathMethodNameResolver将进行如下解析:
http://localhost:9080/springmvc-chapter4/user/list————>list方法名;
http://localhost:9080/springmvc-chapter4/user/create————>create方法名;
http://localhost:9080/springmvc-chapter4/user/update————>update功能处理方法名;
http://localhost:9080/springmvc-chapter4/user/delete————>delete功能处理方法名。
我们可以将默认的InternalPathMethodNameResolver改为PropertiesMethodNameResolver:
1 <bean id="propertiesMethodNameResolver" 2 class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver"> 3 <property name="mappings"> 4 <props> 5 <prop key="/user/create">create</prop> 6 <prop key="<span style="font-size: 1em; line-height: 1.5;">/user/</span><span style="font-size: 1em; line-height: 1.5;">update">update</prop></span> 7 <prop key="<span style="font-size: 1em; line-height: 1.5;">/user/</span><span style="font-size: 1em; line-height: 1.5;">delete">delete</prop></span> 8 <prop key="<span style="font-size: 1em; line-height: 1.5;">/user/</span><span style="font-size: 1em; line-height: 1.5;">list">list</prop></span> 9 <prop key="/**">list</prop><!-- 默认的行为 --> 10 </props> 11 </property> 12 <property name="alwaysUseFullPath" value="false"/><!-- 不使用全路径 --> 13 </bean> 14 <bean name="/user/**" class="cn.javass.chapter4.web.controller.UserController"> 15 <!—省略其他配置,详见配置文件--> 16 <!-- 使用PropertiesMethodNameResolver来解析功能处理方法名 --> 17 <property name="methodNameResolver" ref="propertiesMethodNameResolver"/> 18 </bean>
/**表示默认解析到list功能处理方法。
如上配置方式可以很好的工作,但必须继承MultiActionController,Spring Web MVC提供给我们无需继承MultiActionController实现方式,即使有委托对象方式,继续往下看吧。
4.15.7、委托方式实现
(1、控制器UserDelegate
将UserController复制一份,改名为UserDelegate,并把继承MultiActionController去掉即可,其他无需改变。
(2、spring配置文件chapter4-servlet.xml
1 <!—委托对象--> 2 <bean id="userDelegate" class="cn.javass.chapter4.web.controller.UserDelegate"> 3 <property name="userService" ref="userService"/> 4 <property name="createView" value="user2/create"/> 5 <property name="updateView" value="user2/update"/> 6 <property name="deleteView" value="user2/delete"/> 7 <property name="listView" value="user2/list"/> 8 <property name="redirectToListView" value="redirect:/user2/list"/> 9 </bean> 10 <!—控制器对象--> 11 <bean name="/user2/**" 12 class="org.springframework.web.servlet.mvc.multiaction.MultiActionController"> 13 <property name="delegate" ref="userDelegate"/> 14 <property name="methodNameResolver" ref="parameterMethodNameResolver"/> 15 </bean>
delegate:控制器对象通过
delegate属性指定委托对象,即实际调用delegate委托对象的功能方法。
methodNameResolver:此处我们使用ParameterMethodNameResolver解析器;
1 <!—ParameterMethodNameResolver --> 2 <bean id="parameterMethodNameResolver" 3 class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver"> 4 <!-- 1、根据请求参数名解析功能方法名 --> 5 <property name="methodParamNames" value="create,update,delete"/> 6 <!-- 2、根据请求参数名的值解析功能方法名 --> 7 <property name="paramName" value="action"/> 8 <!-- 3、逻辑方法名到真实方法名的映射 --> 9 <property name="logicalMappings"> 10 <props> 11 <prop key="doList">list</prop> 12 </props> 13 </property> 14 <!—4、默认执行的功能处理方法 --> 15 <property name="defaultMethodName" value="list"/> 16 </bean>
1、
methodParamNames:create,update,delete,当请求中有参数名为这三个的将被映射为功能方法名,如“<input type=”submit” name=”create” value=”新增”/>”提交后解析得到的功能方法名为create;
2、paramName:
当请求中有参数名为action,则将值映射为功能方法名,如“<input type=”hidden”name=”action” value=”delete”/>”,提交后解析得到的功能方法名为delete;
3、logicalMappings:
逻辑功能方法名到真实功能方法名的映射,如:
http://localhost:9080/springmvc-chapter4/user2?action=doList;
首先请求参数“action=doList”,则第二步解析得到逻辑功能方法名为doList;
本步骤会把doList再转换为真实的功能方法名list。
4、defaultMethodName:
以上步骤如果没有解析到功能处理方法名,默认执行的方法名。
(3、视图页面
(3.1、list页面(WEB-INF/jsp/user2/list.jsp)
1 <a href="${pageContext.request.contextPath}/user2?action=create">用户新增</a> 2 <table border="1" width="50%"> 3 <tr> 4 <th>用户名</th> 5 <th>真实姓名</th> 6 <th>操作</th> 7 </tr> 8 <c:forEach items="${userList}" var="user"> 9 <tr> 10 <td>${user.username }</td> 11 <td>${user.realname }</td> 12 <td> 13 <a href="${pageContext.request.contextPath}/user2?action=update&username=${user.username}">更新</a> 14 | 15 <a href="${pageContext.request.contextPath}/user2?action=delete&username=${user.username}">删除</a> 16 </td> 17 </tr> 18 </c:forEach> 19 </table>
(3.2、update页面(WEB-INF/jsp/user2/update.jsp)
1 <form action="${pageContext.request.contextPath}/user2" method="post"> 2 <input type="hidden" name="action" value="update"/> 3 用户名: <input type="text" name="username" value="${command.username}"/> 4 真实姓名:<input type="text" name="realname" value="${command.realname}"/> 5 <input type="submit" value="更新"/> 6 </form>
通过参数
name=“action” value=“update”来指定要执行的功能方法名update。
(3.3、create页面(WEB-INF/jsp/user2/create.jsp)
1 <form action="${pageContext.request.contextPath}/user2" method="post"> 2 用户名: <input type="text" name="username" value="${command.username}"/> 3 真实姓名:<input type="text" name="realname" value="${command.realname}"/> 4 <input type="submit" name="create" value="新增"/> 5 </form>
(4、测试:
使用ParameterMethodNameResolver将进行如下解析:
http://localhost:9080/springmvc-chapter4/user2?create ————>create功能处理方法名(参数名映射);
http://localhost:9080/springmvc-chapter4/user2?action=create————>create功能处理方法名(参数值映射);
http://localhost:9080/springmvc-chapter4/user2?update ————>update功能处理方法名;
http://localhost:9080/springmvc-chapter4/user2?action=update————>update功能处理方法名;
http://localhost:9080/springmvc-chapter4/user2?delete ————>delete功能处理方法名;
http://localhost:9080/springmvc-chapter4/user2?action=delete————>delete功能处理方法名;
http://localhost:9080/springmvc-chapter4/user2?doList ————>通过logicalMappings解析为list功能处理方法。
http://localhost:9080/springmvc-chapter4/user2?action=doList————>通过logicalMappings解析为list功能处理方法。
http://localhost:9080/springmvc-chapter4/user2————>默认的功能处理方法名list(默认)。
4.16、数据类型转换和数据验证
流程:
1、首先创建数据绑定器,在此此会创建ServletRequestDataBinder类的对象,并设置messageCodesResolver(错误码解析器);
2、提供第一个扩展点,初始化数据绑定器,在此处我们可以覆盖该方法注册自定义的PropertyEditor(请求参数——>命令对象属性的转换);
3、进行数据绑定,即请求参数——>命令对象的绑定;
4、提供第二个扩展点,数据绑定完成后的扩展点,此处可以实现一些自定义的绑定动作;
5、验证器对象的验证,验证器通过validators注入,如果验证失败,需要把错误信息放入Errors(此处使用BindException实现);
6、提供第三个扩展点,此处可以实现自定义的绑定/验证逻辑;
7、将errors传入功能处理方法进行处理,功能方法应该判断该错误对象是否有错误进行相应的处理。
4.16.1、数据类型转换
请求参数(String)——>命令对象属性(可能是任意类型)的类型转换,即数据绑定时的类型转换,使用PropertyEditor实现绑定时的类型转换。
一、spring内建的PropertyEditor如下所示:
二、Spring内建的PropertyEditor支持的属性(符合JavaBean规范)操作:
三、示例:
接下来我们写自定义的属性编辑器进行数据绑定:
(1、模型对象:
1 package cn.javass.chapter4.model; 2 //省略import 3 public class DataBinderTestModel { 4 private String username; 5 private boolean bool;//Boolean值测试 6 private SchoolInfoModel schooInfo; 7 private List hobbyList;//集合测试,此处可以改为数组/Set进行测试 8 private Map map;//Map测试 9 private PhoneNumberModel phoneNumber;//String->自定义对象的转换测试 10 private Date date;//日期类型测试 11 private UserState state;//String——>Enum类型转换测试 12 //省略getter/setter 13 } 14 15 package cn.javass.chapter4.model; 16 //如格式010-12345678 17 public class PhoneNumberModel { 18 private String areaCode;//区号 19 private String phoneNumber;//电话号码 20 //省略getter/setter 21 }
(2、PhoneNumber属性编辑器
前台输入如010-12345678自动转换为PhoneNumberModel。
1 package cn.javass.chapter4.web.controller.support.editor; 2 //省略import 3 public class PhoneNumberEditor extends PropertyEditorSupport { 4 Pattern pattern = Pattern.compile("^(\\d{3,4})-(\\d{7,8})$"); 5 @Override 6 public void setAsText(String text) throws IllegalArgumentException { 7 if(text == null || !StringUtils.hasLength(text)) { 8 setValue(null); //如果没值,设值为null 9 } 10 Matcher matcher = pattern.matcher(text); 11 if(matcher.matches()) { 12 PhoneNumberModel phoneNumber = new PhoneNumberModel(); 13 phoneNumber.setAreaCode(matcher.group(1)); 14 phoneNumber.setPhoneNumber(matcher.group(2)); 15 setValue(phoneNumber); 16 } else { 17 throw new IllegalArgumentException(String.format("类型转换失败,需要格式[010-12345678],但格式是[%s]", text)); 18 } 19 } 20 @Override 21 public String getAsText() { 22 PhoneNumberModel phoneNumber = ((PhoneNumberModel)getValue()); 23 return phoneNumber == null ? "" : phoneNumber.getAreaCode() + "-" + phoneNumber.getPhoneNumber(); 24 } 25 } 26
PropertyEditorSupport:一个PropertyEditor的支持类;
setAsText:表示将String——>PhoneNumberModel,根据正则表达式进行转换,如果转换失败抛出异常,则接下来的验证器会进行验证处理;
getAsText:表示将PhoneNumberModel——>String。
(3、控制器
需要在控制器注册我们自定义的属性编辑器。
此处我们使用AbstractCommandController,因为它继承了BaseCommandController,拥有绑定流程。
1 package cn.javass.chapter4.web.controller; 2 //省略import 3 public class DataBinderTestController extends AbstractCommandController { 4 public DataBinderTestController() { 5 setCommandClass(DataBinderTestModel.class); //设置命令对象 6 setCommandName("dataBinderTest");//设置命令对象的名字 7 } 8 @Override 9 protected ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { 10 //输出command对象看看是否绑定正确 11 System.out.println(command); 12 return new ModelAndView("bindAndValidate/success").addObject("dataBinderTest", command); 13 } 14 @Override 15 protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { 16 super.initBinder(request, binder); 17 //注册自定义的属性编辑器 18 //1、日期 19 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 20 CustomDateEditor dateEditor = new CustomDateEditor(df, true); 21 //表示如果命令对象有Date类型的属性,将使用该属性编辑器进行类型转换 22 binder.registerCustomEditor(Date.class, dateEditor); 23 //自定义的电话号码编辑器 24 binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()); 25 } 26 }
initBinder:第一个扩展点,初始化数据绑定器,在此处我们注册了两个属性编辑器;
CustomDateEditor:自定义的日期编辑器,用于在String<——>日期之间转换;
binder.registerCustomEditor(Date.class, dateEditor):表示如果命令对象是Date类型,则使用dateEditor进行类型转换;
PhoneNumberEditor:自定义的电话号码属性编辑器用于在String<——> PhoneNumberModel之间转换;
binder.registerCustomEditor(PhoneNumberModel.class, newPhoneNumberEditor()):表示如果命令对象是PhoneNumberModel类型,则使用PhoneNumberEditor进行类型转换;
(4、spring配置文件chapter4-servlet.xml
1 <bean name="/dataBind" 2 class="cn.javass.chapter4.web.controller.DataBinderTestController"/>
(5、视图页面(WEB-INF/jsp/bindAndValidate/success.jsp)
1 EL phoneNumber:${dataBinderTest.phoneNumber} 2 EL state:${dataBinderTest.state} 3 EL date:${dataBinderTest.date}
视图页面的数据没有预期被格式化,如何进行格式化显示呢?请参考【第七章 注解式控制器的数据验证、类型转换及格式化】。
(6、测试:
1、在浏览器地址栏输入请求的URL,如
http://localhost:9080/springmvc-chapter4/dataBind?username=zhang&bool=yes&schooInfo.specialty=computer&hobbyList[0]=program&hobbyList[1]=music&map[key1]=value1&map[key2]=value2&phoneNumber=010-12345678&date=2012-3-18 16:48:48&state=blocked
2、控制器输出的内容:
DataBinderTestModel [username=zhang, bool=true, schooInfo=SchoolInfoModel [schoolType=null, schoolName=null, specialty=computer], hobbyList=[program, music], map={key1=value1, key2=value2}, phoneNumber=PhoneNumberModel [areaCode=010, phoneNumber=12345678], date=Sun Mar 18 16:48:48 CST 2012, state=锁定]
类型转换如图所示:
四、注册PropertyEditor
1、使用WebDataBinder进行控制器级别注册PropertyEditor(控制器独享)
如“【三、示例】”中所使用的方式,使用WebDataBinder注册控制器级别的PropertyEditor,这种方式注册的PropertyEditor只对当前控制器独享,即其他的控制器不会自动注册这个PropertyEditor,如果需要还需要再注册一下。
2、使用WebBindingInitializer批量注册
PropertyEditor
如果想在多个控制器同时注册多个相同的PropertyEditor时,可以考虑使用WebBindingInitializer。
示例:
(1、实现WebBindingInitializer
1 package cn.javass.chapter4.web.controller.support.initializer; 2 //省略import 3 public class MyWebBindingInitializer implements WebBindingInitializer { 4 @Override 5 public void initBinder(WebDataBinder binder, WebRequest request) { 6 //注册自定义的属性编辑器 7 //1、日期 8 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 9 CustomDateEditor dateEditor = new CustomDateEditor(df, true); 10 //表示如果命令对象有Date类型的属性,将使用该属性编辑器进行类型转换 11 binder.registerCustomEditor(Date.class, dateEditor); 12 //自定义的电话号码编辑器 13 binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()); 14 } 15 }
通过实现WebBindingInitializer并通过binder注册多个PropertyEditor。
(2、修改【三、示例】中的DataBinderTestController,注释掉initBinder方法;
(3、修改chapter4-servlet.xml配置文件:
1 <!-- 注册WebBindingInitializer实现 --> 2 <bean id="myWebBindingInitializer" class="cn.javass.chapter4.web.controller.support.initializer.MyWebBindingInitializer"/> 3 <bean name="/dataBind" class="cn.javass.chapter4.web.controller.DataBinderTestController"> 4 <!-- 注入WebBindingInitializer实现 --> 5 <property name="webBindingInitializer" ref="myWebBindingInitializer"/> 6 </bean>
(4、尝试访问“【三、示例】”中的测试URL即可成功。
使用WebBindingInitializer的好处是当你需要在多个控制器中需要同时使用多个相同的PropertyEditor可以在WebBindingInitializer实现中注册,这样只需要在控制器中注入WebBindingInitializer即可注入多个PropertyEditor。
3、全局级别注册PropertyEditor(全局共享)
只需要将我们自定义的PropertyEditor放在和你的模型类同包下即可,且你的Editor命名规则必须是“模型类名Editor”,这样Spring会自动使用标准JavaBean架构进行自动识别,如图所示:
此时我们把“DataBinderTestController”的“binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor());”注释掉,再尝试访问“【三、示例】”中的测试URL即可成功。
这种方式不仅仅在使用Spring时可用,在标准的JavaBean等环境都是可用的,可以认为是全局共享的(不仅仅是Spring环境)。
PropertyEditor被限制为只能String<——>Object之间转换,不能Object<——>Object,Spring3提供了更强大的类型转换(TypeConversion)支持,它可以在任意对象之间进行类型转换,不仅仅是String
<——>Object。
4.16.2、数据验证
1、数据绑定失败:比如需要数字却输入了字母;
2、数据不合法:可以认为是业务错误,通过自定义验证器验证,如用户名长度必须在5-20之间,我们却输入了100个字符等;
3、错误对象:当我们数据绑定失败或验证失败后,错误信息存放的对象,我们叫错误对象,在spring Web MVC中Errors是具体的代表者;线程不安全对象;
4、错误消息:是硬编码,还是可配置?实际工作应该使用配置方式,我们只是把错误码(errorCode)放入错误对象,在展示时读取相应的错误消息配置文件来获取要显示的错误消息(errorMessage);
4.16.2.1、验证流程
1、首先进行数据绑定验证,如果验证失败会通过MessageCodesResolver生成错误码放入Errors错误对象;
2、数据不合法验证,通过自定义的验证器验证,如果失败需要手动将错误码放入Errors错误对象;
4.16.2.2、错误对象和错误消息
错误对象的代表者是Errors接口,并且提供了几个实现者,在Spring Web MVC中我们使用的是如下实现:
相关的错误方法如下:
Errors:存储和暴露关于数据绑定错误和验证错误相关信息的接口,提供了相关存储和获取错误消息的方法:
1 package org.springframework.validation; 2 public interface Errors { 3 //=========================全局错误消息(验证/绑定对象全局的)============================= 4 //注册一个全局的错误码() 5 void reject(String errorCode); 6 //注册一个全局的错误码,当根据errorCode没有找到相应错误消息时,使用defaultMessage作为错误消息 7 void reject(String errorCode, String defaultMessage); 8 //注册一个全局的错误码,当根据errorCode没有找到相应错误消息时(带错误参数的),使用defaultMessage作为错误消息 9 void reject(String errorCode, Object[] errorArgs, String defaultMessage); 10 //=========================全局错误消息(验证/绑定整个对象的)============================= 11 //=========================局部错误消息(验证/绑定对象字段的)============================= 12 //注册一个对象字段的错误码,field指定验证失败的字段名 13 void rejectValue(String field, String errorCode); 14 void rejectValue(String field, String errorCode, String defaultMessage); 15 void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage); 16 //=========================局部错误消息(验证/绑定对象字段的)============================= 17 boolean hasErrors(); ////是否有错误 18 boolean hasGlobalErrors(); //是否有全局错误 19 boolean hasFieldErrors(); //是否有字段错误 20 Object getFieldValue(String field); //返回当前验证通过的值,或验证失败时失败的值; 21 }
getFieldValue:可以得到验证失败的失败值,这是其他Web层框架很少支持的,这样就可以给用户展示出错时的值(而不是空或其他的默认值等)。
BindingResult:代表数据绑定的结果,继承了Errors接口。
BindException:代表数据绑定的异常,它继承Exception,并实现了BindingResult,这是内部使用的错误对象。
示例:
(1、控制器
1 package cn.javass.chapter4.web.controller; 2 //省略import 3 public class ErrorController extends AbstractCommandController { 4 public ErrorController() { 5 setCommandClass(DataBinderTestModel.class); 6 setCommandName("command"); 7 } 8 @Override 9 protected ModelAndView handle(HttpServletRequest req, HttpServletResponse resp, Object command, BindException errors) throws Exception { 10 //表示用户名不为空 11 errors.reject("username.not.empty"); 12 //带有默认错误消息 13 errors.reject("username.not.empty1", "用户名不能为空1"); 14 //带有参数和默认错误消息 15 errors.reject("username.length.error", new Object[]{5, 10}); 16 17 //得到错误相关的模型数据 18 Map model = errors.getModel(); 19 return new ModelAndView("bindAndValidate/error", model); 20 } 21 }
errors.reject(“username.not.empty”):注册全局错误码“username.not.empty”,我们必须提供messageSource来提供错误码“username.not.empty”对应的错误信息(如果没有会抛出NoSuchMessageException异常);
errors.reject(“username.not.empty1″, “用户名不能为空1″):注册全局错误码“username.not.empty1”,如果从messageSource没没有找到错误码“username.not.empty1”对应的错误信息,则将显示默认消息“用户名不能为空1”;
errors.reject(“username.length.error”, new Object[]{5, 10}):错误码为“username.length.error”,而且错误信息需要两个参数,如我们在我们的配置文件中定义“用户名长度不合法,长度必须在{0}到{1}之间”,则实际的错误消息为“用户名长度不合法,长度必须在5到10之间”
errors.getModel():当有错误信息时,一定将errors.getModel()放入我们要返回的ModelAndView中,以便使用里边的错误对象来显示错误信息。
(2、spring配置文件chapter4-servlet.xml
1 <bean id="messageSource" 2 class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> 3 <property name="basename" value="classpath:messages"/> 4 <property name="fileEncodings" value="utf-8"/> 5 <property name="cacheSeconds" value="120"/> 6 </bean> 7 8 <bean name="/error" class="cn.javass.chapter4.web.controller.ErrorController"/>
messageSource:用于获取错误码对应的错误消息的,而且bean名字默认必须是messageSource。
messages.properties(需要执行NativeToAscii)
1 username.not.empty=用户名不能为空 2 username.length.error=用户名长度不合法,长度必须在{0}到{1}之间
(3、视图页面(WEB-INF/jsp/bindAndValidate/error.jsp)
1 <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> 2 <!-- 表单的默认命令对象名为command --> 3 <form:form commandName="command"> 4 <form:errors path="*"></form:errors> 5 </form:form>
form标签库:此处我们使用了spring的form标签库;
<form:form commandName=”command”>:表示我们的表单标签,commandName表示绑定的命令对象名字,默认为command;
<form:errors path=”*”></form:errors>:表示显示错误信息的标签,如果path为“*”表示显示所有错误信息。
接下来我们来看一下 数据绑定失败和数据不合法时,如何处理。
4.16.2.3、数据绑定失败
如我们的DataBinderTestModel类:
bool:boolean类型,此时如果我们前台传入非兼容的数据,则会数据绑定失败;
date:Date类型,此时如果我们前台传入非兼容的数据,同样会数据绑定失败;
phoneNumber:自定义的PhoneNumberModel类型,如果如果我们前台传入非兼容的数据,同样会数据绑定失败。
示例:
(1、控制器,DataBinderErrorTestController。
1 package cn.javass.chapter4.web.controller; 2 //省略import 3 public class DataBinderErrorTestController extends SimpleFormController { 4 public DataBinderErrorTestController() { 5 setCommandClass(DataBinderTestModel.class); 6 setCommandName("dataBinderTest"); 7 } 8 @Override 9 protected ModelAndView showForm(HttpServletRequest request, HttpServletResponse response, BindException errors) throws Exception { 10 //如果表单提交有任何错误都会再回到表单展示页面 11 System.out.println(errors); 12 return super.showForm(request, response, errors); 13 } 14 @Override 15 protected void doSubmitAction(Object command) throws Exception { 16 System.out.println(command); //表单提交成功(数据绑定成功)进行功能处理 17 } 18 @Override 19 protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { 20 super.initBinder(request, binder); 21 //注册自定义的属性编辑器 22 //1、日期 23 DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 24 CustomDateEditor dateEditor = new CustomDateEditor(df, true); 25 //表示如果命令对象有Date类型的属性,将使用该属性编辑器进行类型转换 26 binder.registerCustomEditor(Date.class, dateEditor); 27 28 //自定义的电话号码编辑器 29 binder.registerCustomEditor(PhoneNumberModel.class, new PhoneNumberEditor()); 30 } 31 }
此处我们使用SimpleFormController;
showForm:展示表单,当提交表单有任何数据绑定错误会再回到该方法进行表单输入(在此处我们打印错误对象);
doSubmitAction:表单提交成功,只要当表单的数据到命令对象绑定成功时,才会执行;
(2、spring配置文件chapter4-servlet.xml
1 <bean name="/dataBindError" 2 class="cn.javass.chapter4.web.controller.DataBinderErrorTestController"> 3 <property name="formView" value="bindAndValidate/input"/> 4 <property name="successView" value="bindAndValidate/success"/> 5 </bean>
(3、视图页面(WEB-INF/jsp/bindAndValidate/ input.jsp)
1 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> 3 <!-- 表单的命令对象名为dataBinderTest --> 4 <form:form commandName="dataBinderTest"> 5 <form:errors path="*" cssStyle="color:red"></form:errors> 6 bool:<form:input path="bool"/> 7 phoneNumber:<form:input path="phoneNumber"/> 8 date:<form:input path="date"/> 9 <input type="submit" value="提交"/> 10 </form:form>
此处一定要使用form标签库,借此我们可以看到它的强大支持(别的大部分Web框架所不具备的,展示用户验证失败的数据)。
<form:form commandName=“dataBinderTest”>:指定命令对象为dataBinderTest,默认command;
<form:errors path=“*” cssStyle=“color:red”></form:errors>:显示错误消息,当提交表单有错误时展示错误消息(数据绑定错误/数据不合法);
<form:input path=“bool”/>:等价于(<input type=’text’>),但会从命令对象中取出bool属性进行填充value属性,或如果表单提交有错误会从错误对象取出之前的错误数据(而非空或默认值);
<input type=“submit” value=“提交“/>:spring没有提供相应的提交按钮,因此需要使用html的。
(4、测试
在地址栏输入如下地址:http://localhost:9080/springmvc-chapter4/dataBindError
全部是错误数据,即不能绑定到我们的命令对象;
当提交表单后,我们又回到表单输入页面,而且输出了一堆错误信息
1、错误消息不可读;
2、表单元素可以显示之前的错误的数据,而不是默认值/空;
(5、问题
这里最大的问题是不可读的错误消息,如何让这些错误消息可读呢?
首先我们看我们的showForm方法里输出的“errors”错误对象信息:
1 org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 3 errors 2 3 Field error in object 'dataBinderTest' on field 'bool': rejected value [www]; codes [typeMismatch.dataBinderTest.bool,typeMismatch.bool,typeMismatch.boolean,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dataBinderTest.bool,bool]; arguments []; default message [bool]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'boolean' for property 'bool'; nested exception is java.lang.IllegalArgumentException: Invalid boolean value [www]] 4 5 Field error in object 'dataBinderTest' on field 'date': rejected value [123]; codes [typeMismatch.dataBinderTest.date,typeMismatch.date,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dataBinderTest.date,date]; arguments []; default message [date]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'date'; nested exception is java.lang.IllegalArgumentException: Could not parse date: Unparseable date: "123"] 6 7 Field error in object 'dataBinderTest' on field 'phoneNumber': rejected value [123]; codes [typeMismatch.dataBinderTest.phoneNumber,typeMismatch.phoneNumber,typeMismatch.cn.javass.chapter4.model.PhoneNumberModel,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dataBinderTest.phoneNumber,phoneNumber]; arguments []; default message [phoneNumber]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'cn.javass.chapter4.model.PhoneNumberModel' for property 'phoneNumber'; nested exception is java.lang.IllegalArgumentException: 类型转换失败,需要格式[010-12345678],但格式是[123]]
数据绑定失败(类型不匹配)会自动生成如下错误码(错误码对应的错误消息按照如下顺序依次查找):
1、typeMismatch.命令对象名.属性名
2、typeMismatch.属性名
3、typeMismatch.属性全限定类名(包名.类名)
4、typeMismatch
⊙内部使用MessageCodesResolver解析数据绑定错误到错误码,默认DefaultMessageCodesResolver,因此想要详细了解如何解析请看其javadoc;
⊙建议使用第1个进行错误码的配置。
因此修改我们的messages.properties添加如下错误消息(需要执行NativeToAscii):
1 typeMismatch.dataBinderTest.date=您输入的数据格式错误,请重新输入(格式:2012-03-19 22:17:17) 2 #typeMismatch.date=2 3 #typeMismatch.java.util.Date=3 4 #typeMismatch=4
到此,数据绑定错误我们介绍完了,接下来我们再看一下数据不合法错误。
4.16.2.4、数据不合法
1、比如用户名长度必须在5-20之间,而且必须以字母开头,可包含字母、数字、下划线;
2、比如注册用户时 用户名已经存在或邮箱已经存在等;
3、比如去一些论坛经常会发现,您发的帖子中包含×××屏蔽关键字等。
还有很多数据不合法的场景,在此就不罗列了,对于数据不合法,Spring Web MVC提供了两种验证方式:
◆编程式验证器验证
◆声明式验证
先从编程式验证器开始吧。
4.16.2.4.1、编程式验证器
一、验证器接口
1 package org.springframework.validation; 2 public interface Validator { 3 boolean supports(Class<?> clazz); 4 void validate(Object target, Errors errors); 5 }
Validator接口:验证器,编程实现数据验证的接口;
supports方法:当前验证器是否支持指定的clazz验证,如果支持返回true即可;
validate方法:验证的具体方法,target参数表示要验证的目标对象(如命令对象),errors表示验证出错后存放错误信息的错误对象。
示例:
(1、验证器实现
1 package cn.javass.chapter4.web.controller.support.validator; 2 //省略import 3 public class UserModelValidator implements Validator { 4 private static final Pattern USERNAME_PATTERN = Pattern.compile("[a-zA-Z]\\w{4,19}"); 5 private static final Pattern PASSWORD_PATTERN = Pattern.compile("[a-zA-Z0-9]{5,20}"); 6 private static final Set<String> FORBINDDDEN_WORD_SET = new HashSet<String>(); 7 static { 8 FORBINDDDEN_WORD_SET.add("fuc k"); //删掉空格 9 FORBINDDDEN_WORD_SET.add("admin"); 10 } 11 @Override 12 public boolean supports(Class<?> clazz) { 13 return UserModel.class == clazz;//表示只对UserModel类型的目标对象实施验证 14 } 15 @Override 16 public void validate(Object target, Errors errors) { 17 //这个表示如果目标对象的username属性为空,则表示错误(简化我们手工判断是否为空) 18 ValidationUtils.rejectIfEmpty(errors, "username", "username.not.empty"); 19 20 UserModel user = (UserModel) target; 21 22 if(!USERNAME_PATTERN.matcher(user.getUsername()).matches()) { 23 errors.rejectValue("username", "username.not.illegal");//如果用户名不合法 24 } 25 26 for(String forbiddenWord : FORBINDDDEN_WORD_SET) { 27 if(user.getUsername().contains(forbiddenWord)) { 28 errors.rejectValue("username", "username.forbidden", new Object[]{forbiddenWord}, "您的用户名包含非法关键词");//用户名包含屏蔽关键字 29 break; 30 } 31 } 32 if(!PASSWORD_PATTERN.matcher(user.getPassword()).matches()) { 33 errors.rejectValue("password","password.not.illegal", "密码不合法");//密码不合法 34 } 35 } 36 }
supports方法:表示只对UserModel类型的对象验证;
validate方法:数据验证的具体方法,有如下几个验证:
1、用户名不合法(长度5-20,以字母开头,随后可以是字母、数字、下划线)
USERNAME_PATTERN.matcher(user.getUsername()).matches() //使用正则表达式验证
errors.rejectValue(“username”, “username.not.illegal”);//验证失败为username字段添加错误码
2、屏蔽关键词:即用户名中含有不合法的数据(如admin)
user.getUsername().contains(forbiddenWord) //用contains来判断我们的用户名中是否含有非法关键词
errors.rejectValue(“username”, “username.forbidden”, new Object[]{forbiddenWord}, “您的用户名包含非法关键词”);//验证失败为username字段添加错误码(参数为当前屏蔽关键词)(默认消息为”您的用户名包含非法关键词”)
3、密码不合法
在此就不罗列代码了;
4、ValidationUtils
ValidationUtils.rejectIfEmpty(errors, “username”, “username.not.empty”);
表示如果目标对象的username属性数据为空,则添加它的错误码;
内部通过(value == null || !StringUtils.hasLength(value.toString()))实现判断value是否为空,从而简化代码。
(2、spring配置文件chapter4-servlet.xml
1 <bean id="userModelValidator" 2 class="cn.javass.chapter4.web.controller.support.validator.UserModelValidator"/> 3 <bean name="/validator" 4 class="cn.javass.chapter4.web.controller.RegisterSimpleFormController"> 5 <property name="formView" value="registerAndValidator"/> 6 <property name="successView" value="redirect:/success"/> 7 <property name="validator" ref="userModelValidator"/> 8 </bean>
此处使用了我们第4.9节创建的RegisterSimpleFormController。
(3、错误码配置(messages.properties),需要执行NativeToAscii
1 username.not.empty=用户名不能为空 2 username.not.illegal=用户名错误,必须以字母开头,只能出现字母、数字、下划线,并且长度在5-20之间 3 username.forbidden=用户名中包含非法关键词【{0}】 4 password.not.illegal=密码长度必须在5-20之间 5
(4、视图页面(/WEB-INF/jsp/registerAndValidator.jsp)
1 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> 3 <form:form commandName="user"> 4 5 <form:errors path="*" cssStyle="color:red"></form:errors> 6 7 username:<form:input path="username"/> 8 <form:errors path="username" cssStyle="color:red"></form:errors> 9 10 11 password:<form:password path="password"/> 12 <form:errors path="password" cssStyle="color:red"></form:errors> 13 14 <input type="submit" value="注册"/> 15 </form:form> 16
form:errors path=“username”:表示只显示username字段的错误信息;
(5、测试
地址:http://localhost:9080/springmvc-chapter4/validator
当我们输入错误的数据后,会报错(form:errors path=“*”显示所有错误信息,而form:errors path=“username”只显示该字段相关的)。
问题:
如MultiActionController控制器相关方法没有提供给我们errors对象(Errors),我们应该怎么进行错误处理呢?
此处给大家一个思路,errors本质就是一个Errors接口实现,而且在页面要读取相关的错误对象,该错误对象应该存放在模型对象里边,因此我们可以自己创建个errors对象并将其添加到模型对象中即可。
此处我们复制4.15节的UserController类为UserAndValidatorController,并修改它的create(新增)方法添加如下代码片段:
1 BindException errors = new BindException(user, getCommandName(user)); 2 //如果用户名为空 3 if(!StringUtils.hasLength(user.getUsername())) { 4 errors.rejectValue("username", "username.not.empty"); 5 } 6 if(errors.hasErrors()) { 7 return new ModelAndView(getCreateView()).addAllObjects(errors.getModel()); 8 }
√ new BindException(user, getCommandName(user)):使用当前的命令对象,和命令对象的名字创建了一个BindException作为errors;
√StringUtils.hasLength(user.getUsername()):如果用户名为空就是用errors.rejectValue(“username”, “username.not.empty”);注入错误码;
√errors.hasErrors():表示如果有错误就返回到新增页面并显示错误消息;
√ModelAndView(getCreateView()).addAllObjects(errors.getModel()):此处一定把errors对象的模型数据放在当前的ModelAndView中,作为当前请求的模型数据返回。
在浏览器地址栏输入:http://localhost:9080/springmvc-chapter4/userAndValidator/create 到新增页面
用户名什么都不输入,提交后又返回到新增页面 而且显示了错误消息说明我们的想法是正确的。
4.16.2.4.2、声明式验证器
从Spring3开始支持JSR-303验证框架,支持XML风格和注解风格的验证,目前在@RequestMapping时才能使用,也就是说基于Controller接口的实现不能使用该方式(但可以使用编程式验证,有需要的可以参考hibernatevalidator实现),我们将在第七章详细介绍。
到此Spring2风格的控制器我们就介绍完了,以上控制器从spring3.0开始已经不推荐使用了(但考虑到还有部分公司使用这些@Deprecated类,在此也介绍了一下),而是使用注解控制器实现(@Controller和@RequestMapping)。
学问:纸上得来终觉浅,绝知此事要躬行
为事:工欲善其事,必先利其器。
态度:道阻且长,行则将至;行而不辍,未来可期
.....................................................................
------- 桃之夭夭,灼灼其华。之子于归,宜其室家。 ---------------
------- 桃之夭夭,有蕡其实。之子于归,宜其家室。 ---------------
------- 桃之夭夭,其叶蓁蓁。之子于归,宜其家人。 ---------------
=====================================================================
* 博客文章部分截图及内容来自于学习的书本及相应培训课程以及网络其他博客,仅做学习讨论之用,不做商业用途。
* 如有侵权,马上联系我,我立马删除对应链接。 * @author Alan -liu * @Email no008@foxmail.com
转载请标注出处! ✧*꧁一品堂.技术学习笔记꧂*✧. ---> https://www.cnblogs.com/ios9/