之前看的那本《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请求应该用到的具体处理类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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 > <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 > </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视图处理抽象层,选择相应的视图名并显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd" > <context:component-scan base-package ="testSpring" /> <mvc:default-servlet-handler /> <mvc:annotation-driven /> <mvc:resources mapping ="/js/**" location ="/WEB-INF/js/" /> <bean class ="org.springframework.web.servlet.view.InternalResourceViewResolver" id ="internalResourceViewResolver" > <property name ="prefix" value ="/WEB-INF/jsp/" /> <property name ="suffix" value =".jsp" /> </bean > <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注解。
1 2 3 4 5 6 7 8 9 @Controller @RequestMapping("/mvc") public class MVCController { @RequestMapping("/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
即可在控制台看到对应的值。
1 2 3 4 5 6 @RequestMapping("/person") public String toPerson (String name, double age) { System.out.println(name+" " +age); return "hello" ; }
自动装箱: 利用类中的属性名或方法名来匹配客户端传来的参数并对应赋值构造出一个实例。神奇的是不论在地址栏中附上的参数顺序如何、名字如何,总能正确映射。例如新建model包,创建Person类型。然后在MVCController.java中重载(但是前面有个RequestMapping注解,不确定算不算重载)toPerson方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public 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; } } @RequestMapping("/person2") public String toPerson (Person p) { System.out.println(p.getName()+" " +p.getAge() + " " + p.getSex()); return "hello" ; }
使用InitBinder注解: 表示在初始化的时候就会调用的函数,这里将客户端传过来的参数(String类型)转化为Date类型。
1 2 3 4 5 6 7 8 9 10 11 12 @RequestMapping("/date") public String date (Date date) { System.out.println(date); return "hello" ; } @InitBinder 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中继续添加如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RequestMapping("/show") public String showPerson (Map<String,Object> map) { Person p = new Person (); p.setAge(20 ); p.setName("Bobby" ); map.put("p" , p); return "show" ; }
使用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弹窗显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <body > <input id ="name" type ="text" name ="name" /> <input id ="btn" type ="button" value ="OK" /> <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 >
1 2 3 4 5 6 7 8 9 @RequestMapping("/getPerson") public void getPerson (String name, PrintWriter pw) { pw.write("hello, " + name); } @RequestMapping("/ajax") public String sayHello () { return "ajax" ; }
在Controller中使用redirect方式处理请求: 使用redirect将路径重定向到新的地址,再由该地址对应的Controller方法去处理并返回视图。继续在MVCController.java中添加:
1 2 3 4 5 @RequestMapping("/redirect") public String redirect () { return "redirect:hello" ; }
文件上传: 首先要导入两个处理文件上传的jar包。 在springmvc-servlet.xml配置文件中加入如下bean引用类:
1 2 3 4 <bean id ="multipartResolver" class ="org.springframework.web.multipart.commons.CommonsMultipartResolver" > <property name ="maxUploadSize" value ="102400000" > </property > </bean >
在MVCController.java中加入处理文件上传的方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RequestMapping(value="/upload",method=RequestMethod.POST) 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('.' ))); fos.write(file.getBytes()); fos.flush(); fos.close(); return "hello" ; }
在hello.jsp中也要相应地加上上传文件的控件: 1 2 3 4 <form action ="upload" method ="post" enctype ="multipart/form-data" > <input type ="file" name ="file" > <br > <input type ="submit" value ="submit" > </form >
使用@RequestParam注解参数的名称: 指定了请求中必须给出该名字的参数值,否则报错。在MVCController.java中加入方法:
1 2 3 4 5 6 7 @RequestMapping(value="/param") 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控件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <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路径并且打印四种方法的字符串: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Controller @RequestMapping("/rest") public class RestController { @RequestMapping(value="/user/{id}",method=RequestMethod.GET) public String get (@PathVariable("id") Integer id) { System.out.println("get" + id); return "/hello" ; } @RequestMapping(value="/user/{id}",method=RequestMethod.POST) public String post (@PathVariable("id") Integer id) { System.out.println("post" + id); return "/hello" ; } @RequestMapping(value="/user/{id}",method=RequestMethod.PUT) public String put (@PathVariable("id") Integer id) { System.out.println("put" + id); return "/hello" ; } @RequestMapping(value="/user/{id}",method=RequestMethod.DELETE) public String delete (@PathVariable("id") Integer id) { System.out.println("delete" + id); return "/hello" ; } }
返回json格式的数据: 还记得之前做bbs小项目的时候ajax+json的数据传递接触得很多了,现在换一批jar包看看。首先导入这些jar包(不过似乎没有什么类有引用到它们诶…) 然后新建JsonController类,直接返回Person对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Controller @RequestMapping("/json") public class JsonController { @RequestMapping("/json") public String jsonJSP () { return "json" ; } @ResponseBody @RequestMapping("/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格式传输数据了 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <input id ="btn" type ="button" value ="OK" /> <div id ="container" > </div > <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异常的处理类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @ExceptionHandler public ModelAndView exceptionHandler (Exception ex) { ModelAndView mv = new ModelAndView ("error" ); mv.addObject("exception" , ex); System.out.println("in testExceptionHandler" ); return mv; } @RequestMapping("/error") public String error () { int i = 5 /0 ; return "hello" ; } @ControllerAdvice public class testControllerAdvice { @ExceptionHandler public ModelAndView exceptionHandler (Exception ex) { ModelAndView mv = new ModelAndView ("error" ); mv.addObject("exception" , ex); System.out.println("in testControllerAdvice" ); return mv; } } <!-- 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接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class MyInterceptor implements HandlerInterceptor { @Override public void afterCompletion (HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception { System.out.println("afterCompletion" ); } @Override public void postHandle (HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) throws Exception { System.out.println("postHandle" ); } @Override public boolean preHandle (HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception { System.out.println("preHandle" ); return true ; } }
然后在springmvc-servlet.xml中加入 1 2 3 4 5 6 7 <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不传值也不会报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public class User { private int id; @NotEmpty private String name; @Past @DateTimeFormat(pattern="yyyy-MM-dd") 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; } @Override public String toString () { return "User [id=" + id + ", name=" + name + ", birth=" + birth + "]" ; } }
创建UserController类,其中用到了IoC的Autowired注解,表示需要从外部注入依赖对象UserService,而在UserController内部就不用涉及这个属性的实例化了
1 2 3 4 5 6 7 8 9 10 11 12 13 @Controller @RequestMapping("/integrate") public class UserController { @Autowired private UserService userService; @RequestMapping("/user") public String saveUser (@RequestBody @ModelAttribute User u) { System.out.println(u); userService.save(); return "hello" ; } }
创建UserService类,注意用到了Component注解,表示这个类需要交给容器,然后配合配置文件扫描指定目录下的Component类,注入到需要它的对象中。
1 2 3 4 5 6 7 8 9 10 @Component public 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容器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd http://www.springframework.org/schema/context http://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中: 1 2 3 4 5 6 7 8 <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所以要防止二者定义冲突吗? 1 2 3 4 5 6 7 <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类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Controller @RequestMapping("/form") public class FormController { @RequestMapping(value="/add",method=RequestMethod.POST) public String add (@Valid User u, BindingResult br) { if (br.getErrorCount() > 0 ) { return "addUser" ; } return "showUser" ; } @RequestMapping(value="/add",method=RequestMethod.GET) 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" %>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 <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 > <% User u = (User)request.getAttribute("user"); %> <p > ID:<%=u.getId() %></p > <p > Name:<%=u.getName() %></p > <p > Birth:<%=u.getBirth() %></p >
可以自定义出错提示信息,在src目录下新建locale.properties:
1 2 3 4 5 NotEmpty.user.name=name cannot be empty Past.user.birth=birth should be a past value DateTimeFormat.user.birth=the format of input is wrong typeMismatch.user.birth=the format of input is wrong typeMismatch.user.id=the format of input is wrong
效果如下: 格式正确后提交后就可以跳转到showUser
国际化: (这个国际化实验暂时没有做成功不知为何…)在src下添加locale_zh_CN.properties
locale_en_US.properties 1 2 username=user name password=password
新建一个locale.jsp,同时在导入两个jar包 ,并在jsp最前面加上<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
1 2 <fmt:message key ="username" > </fmt:message > <fmt:message key ="password" > </fmt:message >
再稍微改一下MVCController,加个带有@RequestMapping的方法导向这个jsp,显示效果如下,但浏览器改成英文仍然是中文显示,不知为啥