概述
Spring MVC-05循序渐进之数据绑定和form标签库(上) 博文中我们学习了数据绑定和form标签库,那我们来写一个小demo练习下吧。
功能概述
假设有个Artisan管理页面,先抛开花里胡哨的前端,我们用最丑最简单的方式实现,来体会下Spring MVC数据绑定及表单的操作过程 。如下图
搭建SpringMVC Maven工程
pom.xml
添加Maven依赖,主要的依赖包是spring-webmvc-${version},这里我们采用4.3.9版本,同时使用JDK7来编译
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.artisan</groupId> <artifactId>chapter05a</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>chapter05a Maven Webapp</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.9.RELEASE</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies> <build> <finalName>chapter05a</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> <source>1.7</source> <target>1.7</target> <compilerArgument>-Xlint:all</compilerArgument> <showWarnings>true</showWarnings> <showDeprecation>true</showDeprecation> </configuration> </plugin> </plugins> </build> </project>
部署描述符web.xml
配置DispatcherServlet,指定SpringMVC配置文件的路径,同时为避免中文乱码配置filter ,指定CharacterEncodingFilter为UTF-8。
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/config/springmvc-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!-- 避免中文乱码 --> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
配置Spring MVC配置文件
通过context:component-scan 结合注解,扫描bean 。
同时配置静态资源文件过滤,以及视图解析器。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 扫描Controller --> <context:component-scan base-package="com.artisan.springmvc.controller"/> <!-- 扫描Service --> <context:component-scan base-package="com.artisan.springmvc.service"/> <!-- 静态资源文件 --> <mvc:annotation-driven/> <mvc:resources mapping="/css/**" location="/css/"/> <mvc:resources mapping="/*.jsp" location="/"/> <!-- 视图解析器 --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
日志配置文件
先简单配置下 ,启动Spring容器的时候不报错即可。
log4j.rootLogger=INFO,A1 log4j.logger.org.springframework=info log4j.appender.A1=org.apache.log4j.ConsoleAppender log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.appender.A1.layout.ConversionPattern=%d %5p [%t] (%F:%L) - %m%n
Domain类
根据我们的构想及页面原型,这个Demo中的domain类Artisan,应该有如下几个属性
private long id; private String name; private String code; private String sex; private Org org;
有个类型为Org 的org属性,其中 Org有如下2个属性
private int orgId; private String orgName;
package com.artisan.springmvc.domian; public class Artisan { private long id; private String name; private String code; private String sex; private Org org; /** * * 创建一个新的实例 Artisan. * * @param id * @param name * @param code * @param sex * @param org */ public Artisan(long id, String name, String code, String sex, Org org) { super(); this.id = id; this.name = name; this.code = code; this.sex = sex; this.org = org; } /** * * 默认构造函数 * */ public Artisan() { super(); } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public Org getOrg() { return org; } public void setOrg(Org org) { this.org = org; } }
package com.artisan.springmvc.domian; public class Org { private int orgId; private String orgName; /** * * 创建一个新的实例 Org. 默认的构造函数需要有,否则 org.apache.jasper.JasperException: * org.springframework.beans.NullValueInNestedPathException: Invalid * property 'org' of bean class [com.artisan.springmvc.domian.Artisan]: * Could not instantiate property type [com.artisan.springmvc.domian.Org] to * auto-grow nested property path; nested exception is * org.springframework.beans.BeanInstantiationException: Failed to * instantiate [com.artisan.springmvc.domian.Org]: Is it an abstract class?; * nested exception is java.lang.InstantiationException: * com.artisan.springmvc.domian.Org * * */ public Org() { super(); } /** * * 创建一个新的实例 Org. * * @param orgId * @param orgName */ public Org(int orgId, String orgName) { super(); this.orgId = orgId; this.orgName = orgName; } public int getOrgId() { return orgId; } public void setOrgId(int orgId) { this.orgId = orgId; } public String getOrgName() { return orgName; } public void setOrgName(String orgName) { this.orgName = orgName; } }
Controller类
第一步,首先获取一个Artisan列表, 个人习惯先开发Controller
按照设计输入http://ip:port/context/artisan/artisanList 可获取全部的Artisan数据
@Controller @RequestMapping("/artisan") public class ArtisanController { private static final Logger logger = Logger.getLogger(ArtisanController.class); private ArtisanService artisanService; public ArtisanService getArtisanService() { return artisanService; } /** * * @Title: setArtisanService * @Description: 通过 @Autowired注入ArtisanService * @param @param artisanService 参数 * @return void 返回类型 * @throws */ @Autowired public void setArtisanService(ArtisanService artisanService) { this.artisanService = artisanService; } @RequestMapping(value="/artisanList",method=RequestMethod.GET) public String getAllArtisans(Model model){ logger.info("getAllArtisans called...."); List<Artisan> artisanList = artisanService.getArtisans(); // 添加到Model中,以便前台能访问到 model.addAttribute("artisanList", artisanList); return "ArtisanList"; } }
通过在类上标注注解@Controller ,配合component-scan扫描,使其成为一个控制器,然后标注了@RequestMapping(“/artisan”),在类层级上标注了请求路径,这个控制器中所有的方法都基于/artisan。
通过@Autowired自动注入service,然后通过artisanService.getArtisans()获取模拟的artisanList
紧接着将数据添加到Model中,以便前台能访问到 model.addAttribute(“artisanList”, artisanList);
最后返回了一个视图ArtisanList,结合SpringMVC配置文件中的视图解析器,会转发到/WEB-INF/jsp/目录下的ArtisanList.jsp
Service类
目前只有一个获取全部数据的接口,后续根据功能逐个增加
package com.artisan.springmvc.service; import java.util.List; import com.artisan.springmvc.domian.Artisan; public interface ArtisanService { // 获取所有的Artisan List<Artisan> getArtisans(); }
接口实现类
package com.artisan.springmvc.service; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Service; import com.artisan.springmvc.domian.Artisan; import com.artisan.springmvc.domian.Org; /** * * @ClassName: ArtisanServiceImpl * @Description: 通过@Service标注的服务层 , * @author Mr.Yang * @date 2018年1月30日 * */ @Service public class ArtisanServiceImpl implements ArtisanService { /* * this implementation is not thread-safe */ List<Artisan> artisanList = null; String sex = null; /** * * 创建一个新的实例 ArtisanServiceImpl的同时初始化模拟数据 * */ public ArtisanServiceImpl() { super(); // 初始化模式数据 artisanList = new ArrayList<Artisan>(); for (int i = 0; i < 10; i++) { if (i%2 == 0) { sex = "男"; }else { sex="女"; } artisanList.add(new Artisan(i, "Artisan" + i, "ATSCode" + i, sex, new Org(i, "org" + i))); } } @Override public List<Artisan> getArtisans() { return artisanList; } }
视图
引入c标签,然后对后台Model中的artisanList进行遍历显示数据。 有CSS修饰样式。
ArtisanList.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML> <html> <head> <title>Artisan List</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <h1>Artisan List</h1> <p> <a href='<c:url value="/artisan/artisan_input"/>'>Add Artisan</a> </p> <table border="1" cellspacing="0"> <tr> <th align="center">OrgName</th> <th align="center">ArtisanName</th> <th align="center">Code</th> <th align="center">Sex</th> <th align="center" colspan="2">Operation</th> </tr> <!-- var要循环集合的别名 --> <c:forEach items="${artisanList}" var="artisan" varStatus="status"> <tr <c:if test="${status.count%2==0}">bgcolor="#7CCD7C"</c:if> align="center"> <td>${artisan.org.orgName}</td> <td>${artisan.name}</td> <td>${artisan.code}</td> <td>${artisan.sex}</td> <td><a href>Edit</a></td> </tr> </c:forEach> </table> </div> </body> </html>
artisan_list测试
启动tomcat,然后访问
http://localhost:8080/chapter05a/artisan/artisanList 即可获取全部的ArtisanList
artisan_add
我们来分析一下artisan_add的逻辑
1. 通过点击ArtisanList.jsp页面上的Add Artisan 超链接标签,使用JSTL标记的URL解决路径访问的问题,跳转到添加页面
2. 再添加页面中加载Org下拉列表,输入信息后,提交触发保存Artisan的操作
3. 后台保存完成后 ,重定向到ArtisanList,展示数据。
编写超链接标签中对应的uri
<a href='<c:url value="/artisan/artisan_input"/>'>Add Artisan</a>
使用JSTL标记的URL解决路径访问的问题, 因为我们在web.xml中配置拦截所有的请求,因此这个请求会被DispatcherServlet拦截,映射到如下的方法中
Controller映射方法
/** * * @Title: inputArtisan * @Description: 进入inputArtisan的页面 * @param @return 参数 * @return String 返回类型 * @throws */ @RequestMapping(value="/artisan_input") public String inputArtisan(Model model){ // 获取全部的org List<Org> orgs = artisanService.getAllOrgs(); // 加载org到Model中以便前台展示 model.addAttribute("orgs", orgs); // 前台form commandName为artisan,因此必须保证model中存在一个artisan model.addAttribute("artisan",new Artisan()); return "AddArtisan"; }
因为添加页面需要展示org列表,所以必须从后台加载全部的org,放到model中,确保前台页面可以通过表达式获取到对应的数据。 同时,前台添加Artisan的form ,打算加入commandName属性方便识别, 如下 form:form commandName="artisan" commandName 为artisan,如果该属性存在,则必须在返回包含该表单的视图的请求处理方法中添加对应的模型属性.
返回的字符串 AddArtisan,SpringMVC会根据视图解析器的配置规则
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean>
映射到/WEB-INF/jsp/AddArtisan.jsp
AddArtisan.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML> <html> <head> <title>Add Artisan Form</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <form:form commandName="artisan" action="artisan_add" method="post"> <fieldset> <legend>Add an Artisan</legend> <p> <label for="orgs">orgName: </label> <form:select id="org" path="org.orgId" items="${orgs}" itemValue="orgId" itemLabel="orgName"/> </p> <p> <label for="name">name: </label> <form:input id="name" path="name"/> </p> <p> <label for="code">code: </label> <form:input id="code" path="code"/> </p> <p> <label for="sex">sex: </label> <form:input id="sex" path="sex"/> </p> <p id="buttons"> <input id="reset" type="reset" tabindex="4"> <input id="submit" type="submit" tabindex="5" value="Add Artisan"> </p> </fieldset> </form:form> </div> </body> </html>
Org的下拉列表采用form的select标签,点击超链接跳转页面的方法中,调用后端的方法获取全部的orgList,同时存放到model中,便于前端展示。 同时绑定了path=”org.orgId” ,后端提供根据页面传入的orgId获取Org的接口及实现类
实现类如下
@Override public Org getOrg(int orgId) { for (Org org:orgList) { if (orgId == org.getOrgId()) { return org; } } return null; }
根据前端选择orgId, 返回对应的org实体类。
然后设置给artisan, 最后调用服务层的方法保存artisan到list中,最后重定向到list列表
代码如下
@RequestMapping(value="/artisan_add",method=RequestMethod.POST) public String addArtisan(@ModelAttribute Artisan artisan){ logger.info("addArtisan called..."); // 获取页面的数据 logger.info("orgId:" + artisan.getOrg().getOrgId()); logger.info("Name:" + artisan.getName()); logger.info("Code:" + artisan.getCode()); logger.info("Sex:" + artisan.getSex()); //根据前台传入绑定的orgId,获取Org Org org = artisanService.getOrg(artisan.getOrg().getOrgId()); // 设置org artisan.setOrg(org); // 保存artisan artisanService.addArtisan(artisan); // 跳转到list页面 return "redirect:/artisan/artisan_list"; }
测试结果
Edit Artisan
下面我们来梳理一下编辑的逻辑
1. 点击Edit按钮,进入编辑页面,这个页面需要将对应的数据加载显示,然后提供用户编辑
2. 用户点击UPDATE按钮后,提交到后端更新数据,然后重定向到list页面
编写uri
第一步展示list的时候,我们已经从后端加载了artisan的id ,所以编辑的时候根据artisan#id去编辑,这样href如下
<a href="artisan_edit/${artisan.id}">Edit</a>
编写映射方法
根据artisan_edit/${artisan.id} 映射到如下方法
/** * * @Title: editArtisan * @Description: 跳转到编辑Artisan页面 * @param @param model * @param @param id * @param @return 参数 * @return String 返回类型 * @throws */ @RequestMapping(value="/artisan_edit/{id}") public String editArtisan(Model model,@PathVariable long id){ logger.info("Artisan ID:" + id); // 加载Org全部数据 用于选择 List<Org> orgList = artisanService.getAllOrgs(); // 添加到model,以便前台访问 model.addAttribute("orgList", orgList); // 根据传入的id,获取对应的artisan信息 用于编辑页面展示Artisan信息 Artisan artisan = artisanService.getArtisanById(id); // 添加到model,以便前台访问 model.addAttribute("artisan", artisan); return "EditArtisan"; }
编写EditArtisan.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <% String path = request.getContextPath(); //获得本项目的地址(例如: http://localhost:8080/domain/)赋值给basePath变量 String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; // 将 "项目路径basePath" 放入pageContext中,待以后用EL表达式读出。 pageContext.setAttribute("basePath",basePath); %> <!DOCTYPE HTML> <html> <head> <title>Edit Artisan Form</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <form:form commandName="artisan" action="${pageScope.basePath}/artisan/artisan_update" method="post"> <fieldset> <legend>Edit Artisan</legend> <form:hidden path="id"/> <p> <label for="orgs">orgName: </label> <form:select id="org" path="org.orgId" items="${orgList}" itemValue="orgId" itemLabel="orgName"/> </p> <p> <label for="name">name: </label> <form:input id="name" path="name"/> </p> <p> <label for="code">code: </label> <form:input id="code" path="code"/> </p> <p> <label for="sex">sex: </label> <form:input id="sex" path="sex"/> </p> <p id="buttons"> <input id="reset" type="reset" tabindex="4"> <input id="submit" type="submit" tabindex="5" value="Update Artisan"> </p> </fieldset> </form:form> </div> </body> </html>
update映射方法
点击提交后,action=”${pageScope.basePath}/artisan/artisan_update” ,映射
@RequestMapping(value="/artisan_update",method=RequestMethod.POST) public String artisanUpdate(@ModelAttribute Artisan artisan){ logger.info("artisanUpdate called"); logger.info("artisan orgId:" + artisan.getOrg().getOrgId()); logger.info("artisan Id:" + artisan.getId()); logger.info("artisan Name:" + artisan.getName()); logger.info("artisan Sex:" + artisan.getSex()); logger.info("artisan Code:" + artisan.getCode()); // 根据orgId获取org Org org = artisanService.getOrg(artisan.getOrg().getOrgId()); logger.info("Org Name :" + org.getOrgName()); artisan.setOrg(org); // 更新数据 artisanService.updateArtisan(artisan); return "redirect:/artisan/artisan_list"; }
测试
修改一条数据,如下
总结
至此,一个简单的实例已经编写完毕,重点是体会思路及spring mvc 及form的应用。
源码
代码已提交到github