之前看的那本《Spring揭秘》内容真多,还是没有全部啃完。现在先用网上一个史上最全最强的SpringMVC示例来练练手好了,感觉书上的罗列介绍都是理论内容,还是不太懂怎么用。所以就借着这个练手项目,把代码背后Spring MVC整理一下好了。好吧其实我看书都还没看到这部分呢,不过没关系,先接触一下到时再回头看看系统的介绍。
创建Spring MVC项目
- 1.在Eclipse JavaEE中新建工程,New - Other - Web - Dynamic Web Project,命名随便取个spring。
- 2.在WebContent - WEB-INF - lib目录下放入Spring相关的jar包。jar包是开发时需要引用的通用类,.jar就是一种封装(有点类似.zip,不过会多一个META-INF\MANIFEST.MF文件),用户并不需要知道jar包中有多少.class、不关心它们所属的package,只需要知道如何使用类的属性和方法。
3.在WebContent - WEB-INF目录下新建web.xml文件。web.xml是整个Web应用程序的部署描述文件,这是基于Servlet规范的Web应用程序都要有的。其中DispatcherServlet是Front Controller,负责接收所有的Web请求,并根据不同的处理逻辑委派到下一级控制器去实现。这里的servlet-mapping是一种映射,当Web请求到达了DispatcherServlet,它会寻找具体的HandlerMapping实例,最终获取当前Web请求应该用到的具体处理类。
1234567891011121314151617181920<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"><display-name>spring</display-name><!--configure the setting of springmvcDispatcherServlet and configure the mapping--><servlet><servlet-name>spring</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:springmvc-servlet.xml</param-value></init-param><!-- <load-on-startup>1</load-on-startup> --></servlet><servlet-mapping><servlet-name>spring</servlet-name><url-pattern>/</url-pattern></servlet-mapping></web-app>4.在项目的src目录下新建springmvc-servlet.xml文件。前面的web.xml中由DispatcherServlet分配给具体处理类处理好Web请求后,会返回一个ModelAndView实例。Spring提出了基于ViewResolver和View接口的Web视图处理抽象层,选择相应的视图名并显示。
1234567891011121314151617181920212223242526272829303132333435<?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:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsdhttp://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd"><!-- scan the package and the sub package --><context:component-scan base-package="testSpring"/><!-- don't handle the static resource --><mvc:default-servlet-handler /><!-- if you use annotation you must configure following setting --><mvc:annotation-driven /><!-- 为了指向WEB-INF中静态的js文件 --><mvc:resources mapping="/js/**" location="/WEB-INF/js/"/><!-- configure the InternalResourceViewResolver --><bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"id="internalResourceViewResolver"><!-- 前缀 --><property name="prefix" value="/WEB-INF/jsp/" /><!-- 后缀 --><property name="suffix" value=".jsp" /></bean><!-- upload settings --><bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><property name="maxUploadSize" value="102400000"></property></bean></beans>5.在WEB-INF目录下新建jsp文件夹,专门存放ViewResolver映射的jsp视图。例如新增hello.jsp。
6.为src添加类。例如创建包testSpring,新建MVCController.java,其中用到了Controller和RequestMapping注解。
123456789// 负责注册一个bean到spring上下文中"/mvc") // 注解为控制器指定可以处理哪些URL请求(public class MVCController {"/hello")(public String hello() {return "hello"; // 返回指定的视图文件名(不含后缀)}}7.此时在eclipse中启动Tomcat Server,访问localhost:8080/spring/mvc/hello即可访问到hello.jsp。
进一步体验Spring MVC
自动匹配参数: 可以根据变量名来匹配客户端传来的参数,获取各参数的值。在MVCController.java添加如下方法,在访问时加上
?name=bobo&age=21
即可在控制台看到对应的值。123456//match automatically"/person")(public String toPerson(String name, double age) {System.out.println(name+" "+age);return "hello";}自动装箱: 利用类中的属性名或方法名来匹配客户端传来的参数并对应赋值构造出一个实例。神奇的是不论在地址栏中附上的参数顺序如何、名字如何,总能正确映射。例如新建model包,创建Person类型。然后在MVCController.java中重载(但是前面有个RequestMapping注解,不确定算不算重载)toPerson方法:
1234567891011121314151617181920212223242526272829303132public class Person {private String sex;private int age;private String name;public String getSex() {return sex;}public void setSex(String sex) {this.sex = sex;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}}//boxing automatically"/person2")(public String toPerson(Person p){System.out.println(p.getName()+" "+p.getAge() + " " + p.getSex());return "hello";}使用InitBinder注解: 表示在初始化的时候就会调用的函数,这里将客户端传过来的参数(String类型)转化为Date类型。
123456789101112//the parameter was converted in initBinder"/date")(public String date(Date date){System.out.println(date);return "hello";}//At the time of initialization,convert the type "String" to type "date"public void initBinder(ServletRequestDataBinder binder){binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"),true));}使用Map向前台传递参数/对象: 除了从前台获取参数,还能通过方法传入的参数向前台传递参数。在前台的jsp文件中(其实也就是showPerson方法返回的叫做”show”的jsp文件)通过
request.getAttribute("p")
就可以获取Person实例了。在MVCController.java中继续添加如下方法:1234567891011121314//pass the parameters to front-end"/show")(public String showPerson(Map<String,Object> map){Person p = new Person();p.setAge(20);p.setName("Bobby");map.put("p", p);return "show";}// 在jsp中可以这样获取p实例(在jsp目录下添加show.jsp)// <% Person p = (Person)request.getAttribute("p"); %>// <p><%=p.getName() %></p>//使用Ajax获取前台Post传过来的参数: 前面用JQuery提供的Ajax已经玩过几遍了,现在使用Spring其实写出来也差不多。首先在jsp目录下添加ajax.jsp,需要指出的是这里要引用jQuery外部js,但是WEB-INF只对服务端开放,浏览器是不能直接访问到存在这个目录下的文件的,所以需要在springmvc-servlet.xml中配置静态资源访问路径
<mvc:resources mapping="/js/**" location="/WEB-INF/js/"/>
,否则对静态资源的请求会被拦截。然后在MVCController.java中继续添加方法,这里是返回一句问候语字符串,在前台由js弹窗显示。12345678910111213141516171819<body><input id="name" type="text" name="name" /><input id="btn" type="button" value="OK"/><!-- SpringMVC的jsp无法访问WEB-INF中静态的js文件,但通过添加servletxml文件可映射到特定文件夹 --><script type="text/javascript" src="../js/jquery-1.6.2.min.js"></script><script type="text/javascript">$(function() {$("#btn").click(function() {$.post("getPerson", {name: $("#name").val()},function(data) {alert(data);});});});</script></body>123456789//pass the parameters to front-end using ajax"/getPerson")(public void getPerson(String name, PrintWriter pw){pw.write("hello, " + name);}"/ajax")(public String sayHello(){return "ajax";}在Controller中使用redirect方式处理请求: 使用redirect将路径重定向到新的地址,再由该地址对应的Controller方法去处理并返回视图。继续在MVCController.java中添加:
12345//redirect"/redirect")(public String redirect(){return "redirect:hello";}文件上传: 首先要导入两个处理文件上传的jar包。
在springmvc-servlet.xml配置文件中加入如下bean引用类:1234<!-- upload settings --><bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><property name="maxUploadSize" value="102400000"></property></bean>在MVCController.java中加入处理文件上传的方法:
123456789101112131415"/upload",method=RequestMethod.POST)(value=public String upload(HttpServletRequest req) throws Exception{MultipartHttpServletRequest mreq = (MultipartHttpServletRequest)req;MultipartFile file = mreq.getFile("file");String fileName = file.getOriginalFilename();SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");FileOutputStream fos = new FileOutputStream(req.getSession().getServletContext().getRealPath("/")+"upload/"+sdf.format(new Date())+fileName.substring(fileName.lastIndexOf('.')));//System.out.println(req.getSession().getServletContext().getRealPath("/"));fos.write(file.getBytes());fos.flush();fos.close();return "hello";}在hello.jsp中也要相应地加上上传文件的控件:
1234<form action="upload" method="post" enctype="multipart/form-data"><input type="file" name="file"><br><input type="submit" value="submit"></form>使用@RequestParam注解参数的名称: 指定了请求中必须给出该名字的参数值,否则报错。在MVCController.java中加入方法:
1234567// 强制必须传入参数"/param")(value=public String testRequestParam(@RequestParam(value="id") Integer id,@RequestParam(value="name")String name){System.out.println(id+" "+name);return "hello";}RESTful风格的SpringMVC: REST(Representational State Transfer),指的是一组架构约束条件和原则,其中最重要的是客户端和服务器之间的交互在请求之间是无状态的,从客户端到服务器的每个请求都必须包含理解请求所必需的信息。在Spring MVC中,RESTful的查删改增的实现需要用RequestMapping和PathVariable注解来实现,同时需要注意form不支持PUT和DELETE方法,所以需要曲线实现。首先在hello.jsp中加入相关的form控件:
1234567891011121314151617<form action="/spring/rest/user/1" method="post"><input type="hidden" name="_method" value="PUT"><input type="submit" value="put"></form><form action="/spring/rest/user/1" method="post"><input type="submit" value="post"></form><form action="/spring/rest/user/1" method="get"><input type="submit" value="get"></form><form action="/spring/rest/user/1" method="post"><input type="hidden" name="_method" value="DELETE"><input type="submit" value="delete"></form>接着新建RestController类用于映射rest路径并且打印四种方法的字符串:
123456789101112131415161718192021222324252627"/rest")(public class RestController {"/user/{id}",method=RequestMethod.GET)(value=public String get(@PathVariable("id") Integer id){System.out.println("get" + id);return "/hello";}"/user/{id}",method=RequestMethod.POST)(value=public String post(@PathVariable("id") Integer id){System.out.println("post" + id);return "/hello";}"/user/{id}",method=RequestMethod.PUT)(value=public String put(@PathVariable("id") Integer id){System.out.println("put" + id);return "/hello";}"/user/{id}",method=RequestMethod.DELETE)(value=public String delete(@PathVariable("id") Integer id){System.out.println("delete" + id);return "/hello";}}返回json格式的数据: 还记得之前做bbs小项目的时候ajax+json的数据传递接触得很多了,现在换一批jar包看看。首先导入这些jar包(不过似乎没有什么类有引用到它们诶…)
然后新建JsonController类,直接返回Person对象12345678910111213141516171819"/json")(public class JsonController {"/json")(public String jsonJSP() {return "json";}"/personJson")(public Person get() {Person p = new Person();p.setName("Bobo");p.setAge(21);p.setSex("male");return p;}}对应地创建json.jsp页面,利用ajax发Get请求到的数据可以直接用点操作符访问值了。不过直接打印data的话并不是一个json字符串,而是
[object Object]
的形式,所以我得说做了这个json的小实验还是不太懂Spring怎么就通过json格式传输数据了1234567891011121314151617181920212223<input id="btn" type="button" value="OK"/><div id="container"></div><!-- SpringMVC的jsp无法访问WEB-INF中静态的js文件,但通过添加servletxml文件可映射到特定文件夹 --><script type="text/javascript" src="../js/jquery-1.6.2.min.js"></script><script type="text/javascript">$(function() {$("#btn").click(function() {$.ajax({url: "personJson",type: "get",success: function(data) {var c = $("#container");c.append("<div>Name:" + data.name + "</div>");c.append("<div>Age:" + data.age + "</div>");c.append("<div>Sex:" + data.sex + "</div>");}});});});</script>异常的处理(未验证): 分为在Controller内部处理局部异常、新建对所有Controller异常的处理类。
1234567891011121314151617181920212223242526272829303132333435// 1. 局部异常public ModelAndView exceptionHandler(Exception ex){ModelAndView mv = new ModelAndView("error");mv.addObject("exception", ex);System.out.println("in testExceptionHandler");return mv;}"/error")(public String error(){int i = 5/0;return "hello";}// 2.1 全局异常的处理类public class testControllerAdvice {public ModelAndView exceptionHandler(Exception ex){ModelAndView mv = new ModelAndView("error");mv.addObject("exception", ex);System.out.println("in testControllerAdvice");return mv;}}// 2.2 全局异常处理也可以在配置文件中设置,其中error是出错后指向的页面<!-- configure SimpleMappingExceptionResolver --><bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"><property name="exceptionMappings"><props><prop key="java.lang.ArithmeticException">error</prop></props></property></bean>自定义拦截器: Spring MVC中拦截器的主要作用是拦截用户的请求并进行相应的处理,有点类似于Servlet开发中的filter,但拦截器和过滤器还是有区别的。拦截器可以用来计算PageView、进行权限验证,或者是来判断用户是否登陆,或者是像12306 那样子判断当前时间是否是购票时间。创建自定义拦截器类,实现HandlerInterceptor接口:
12345678910111213141516171819202122public class MyInterceptor implements HandlerInterceptor {public void afterCompletion(HttpServletRequest arg0,HttpServletResponse arg1, Object arg2, Exception arg3)throws Exception {System.out.println("afterCompletion");}public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1,Object arg2, ModelAndView arg3) throws Exception {System.out.println("postHandle");}public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1,Object arg2) throws Exception {System.out.println("preHandle");return true;}}然后在springmvc-servlet.xml中加入
1234567<!-- interceptor setting --><mvc:interceptors><mvc:interceptor><mvc:mapping path="/mvc/**"/><bean class="testSpring.interceptor.MyInterceptor"></bean></mvc:interceptor></mvc:interceptors>此时访问/spring/mvc/下的页面就会在控制台看到打印的内容,说明拦截器起作用了,可以自定义其中各个过程的操作。
整合SpringIOC和SpringMVC
- 首先导入部分validation的jar包。包括
validation-api-1.1.0.Final.jar
,jboss-logging-3.1.3.ga.jar
,hibernate-validator-5.0.1.Final.jar
。 在testSpring下新建文件夹integrate。创建User类,其中用到了部分用于验证的注解,但奇怪的是似乎没起到作用,例如非空的name不传值也不会报错:
1234567891011121314151617181920212223242526272829303132333435363738public class User {private int id;private String name;"yyyy-MM-dd")(pattern=private Date birth;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Date getBirth() {return birth;}public void setBirth(Date birth) {this.birth = birth;}public String toString() {return "User [id=" + id + ", name=" + name + ", birth=" + birth + "]";}}创建UserController类,其中用到了IoC的Autowired注解,表示需要从外部注入依赖对象UserService,而在UserController内部就不用涉及这个属性的实例化了
12345678910111213"/integrate")(public class UserController {// 表示UserController依赖UserService。private UserService userService; // 用了IoC,可以看到这里并不涉及UserService的实例化。"/user")(public String saveUser(@RequestBody @ModelAttribute User u) {System.out.println(u);userService.save();return "hello";}}创建UserService类,注意用到了Component注解,表示这个类需要交给容器,然后配合配置文件扫描指定目录下的Component类,注入到需要它的对象中。
12345678910public class UserService {public UserService(){System.out.println("UserService Constructor...\n\n");}public void save(){System.out.println("save");}}修改Spring配置文件: 在src目录下新建applicationContext.xml,关键是用
context:component-scan base-package
指定到某个包下导入Component到IoC容器中123456789101112131415161718192021<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/utilhttp://www.springframework.org/schema/util/spring-util-4.0.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"xmlns:util="http://www.springframework.org/schema/util"xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"><context:component-scan base-package="testSpring.integrate"><context:exclude-filter type="annotation"expression="org.springframework.stereotype.Controller"/><context:exclude-filter type="annotation"expression="org.springframework.web.bind.annotation.ControllerAdvice"/></context:component-scan></beans>对应地把这个新的xml关联到原来的web.xml中:
12345678<!-- configure the springIOC --><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:applicationContext.xml</param-value></context-param>在原来的springmvc-servlet.xml中也需要对应修改扫描的目录,防止SpringMVC和SpringIOC对同一个对象的管理重合。这一点暂时还不太明白,是因为web.xml同时导入了这两个Xml所以要防止二者定义冲突吗?
1234567<!-- scan the package and the sub package --><context:component-scan base-package="testSpring.integrate"><context:include-filter type="annotation"expression="org.springframework.stereotype.Controller"/><context:include-filter type="annotation"expression="org.springframework.web.bind.annotation.ControllerAdvice"/></context:component-scan>在运行Tomcat的时候,可以看到在加载XML文件的时候,就调用了UserService的构造子,因此UserService的实例化和UserController的实例化解耦合了:
访问(http://localhost:8080/spring/integrate/user) ,由于此时没有传入任何User的属性,所以打印出来的都是空(为何不报错?)。附上参数?id=1&name=bobo&birth=2015-05-16后就会正常匹配到User的属性(但是为啥日期的格式和User中定的pattern不同?):
表单验证与国际化
沿用前面的User类,前面写的非空、日期格式等到这一步才会起作用。例如现在需要显示用户信息。首先新建FormController类:
1234567891011121314151617"/form")(public class FormController {"/add",method=RequestMethod.POST)(value=public String add(@Valid User u, BindingResult br){if(br.getErrorCount() > 0) { // 出错则不提交到showUserreturn "addUser";}return "showUser";}"/add",method=RequestMethod.GET)(value=public String add(Map<String,Object> map){map.put("user", new User());return "addUser";}}这里的逻辑是,第一次请求页面(GET)的时候new一个User对象,并返回addUser的jsp页面。而提交表单时,若发现了错误,则保持在addUser这个页面,若没有错误则跳转到showUser这个jsp页面。错误信息会直接显示在addUser原页面上,而且原来填入的信息不会被清空。
对应地在jsp目录下新建addUser和showUser的jsp。注意要想用
form:form
标签,需要在最前面添加一句引用<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
:12345678910111213<!--addUser.jsp的body部分--><form:form action="add" method="post" modelAttribute="user">id:<form:input path="id"/><form:errors path="id"/><br>name:<form:input path="name"/><form:errors path="name"/><br>birth:<form:input path="birth"/><form:errors path="birth"/><br><input type="submit" value="OK"></form:form><!--showUser.jsp的body部分--><% User u = (User)request.getAttribute("user"); %><p>ID:<%=u.getId() %></p><p>Name:<%=u.getName() %></p><p>Birth:<%=u.getBirth() %></p>可以自定义出错提示信息,在src目录下新建locale.properties:
12345NotEmpty.user.name=name cannot be emptyPast.user.birth=birth should be a past valueDateTimeFormat.user.birth=the format of input is wrongtypeMismatch.user.birth=the format of input is wrongtypeMismatch.user.id=the format of input is wrong效果如下:
格式正确后提交后就可以跳转到showUser国际化: (这个国际化实验暂时没有做成功不知为何…)在src下添加locale_zh_CN.properties
12username=帐号password=密码locale_en_US.properties
12username=user namepassword=password新建一个locale.jsp,同时在导入两个jar包
,并在jsp最前面加上<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
12<fmt:message key="username"></fmt:message><fmt:message key="password"></fmt:message>再稍微改一下MVCController,加个带有@RequestMapping的方法导向这个jsp,显示效果如下,但浏览器改成英文仍然是中文显示,不知为啥