spring mvc需要spring-webmvc-xxx.jar。
spring mvc中的几个组件:
DispatcherServlet:前端控制器,是org.springframework.web.servlet.DispatcherServlet
HandlerMapping:处理器映射器,是HandlerMapping接口,仅有一个 HandlerExecutionChain getHandler(HttpServletRequest request)方法,用来获得处理器链,常用的实现类是RequestMappingHandlerMapping(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping)
Handler:处理器,处理用户请求的,需要我们自己开发
HandlerAdapter:处理器适配器,是HandlerAdapter接口,常用的实现类是RequestMappingHandlerAdapter(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter)
ViewResolver:视图解析器,是ViewResolver接口,常用的实现类是InternalResourceViewResolver(org.springframework.web.servlet.view.InternalResourceViewResolver)
View:视图页面,需要我们自己开发
用了spring mvc后请求-响应的基本流程是:
1.用户发起request请求,请求至DispatcherServlet前端控制器
2.DispatcherServlet前端控制器请求HandlerMapping处理器映射器查找Handler处理器
3.HanlderMapping处理器映射器找到Handler处理器后将该Handler处理器返回给DispatcherServlet前端控制器
4.DispatcherServlet前端控制器调用处理器适配器去执行Handler处理器
5.Handler执行完成返回ModelAndView(org.springframework.web.servlet.ModelAndView类,封装了Object view和ModelMap model属性)
6.HandlerAdapter处理器适配器将ModelAndView返回给DispatcherServlet前端控制器
7.DispatcherServlet前端控制器调用ViewResolver视图解析器进行视图解析,解析后生成view
8.ViewResolver视图解析器将view返回给DispatcherServlet前端控制器
9.DispatcherServlet调用view的渲染视图的方法,将模型数据填充到request域中
10.DispatcherServlet向用户响应结果
首先,需要在web.xml文件中配置DispatcherServlet前端控制器,通过contextConfigLocation属性(DispatcherServlet父类FrameworkServlet的一个属性 private String contextConfigLocation;)来指定spring mvc配置文件的路径,也可以配置此Servlet的<load-on-startup />,从而让tomcat一启动就创建DispatcherServlet实例(非必须,可选),配置如下:
<servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>DispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
我们在spring mvc配置文件需要配置三大组件:HandlerMapping处理器映射器、HandlerAdapter处理器适配器、ViewResolver视图解析器,配置示例如下:
<context:component-scan base-package="com.kou.controller"></context:component-scan> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"></bean> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"></bean> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"></property> <property name="suffix" value=".jsp"></property> </bean>
在上面的配置文件中,配置了RequestMappingHandlerMapping处理器映射器、RequestMappingHandlerAdapter处理器适配器、InternalResourceViewResolver视图解析器,还用<context:component-scan />元素指定了自动扫描的包。
用RequestMappingHandlerMapping处理器映射器、RequestMappingHandlerAdapter处理器适配器开发Handler处理器时,不要求处理器类实现任何接口。
常用注解的用法:
@controller
标注某个类,表示该类是Handler处理器类。
@RequestMapping
既可以标注Handler处理器类中的方法,也可以标注Handler处理器类。标注Handler处理器类的话,该类中的所有方法都会用到在@RequestMapping中配置的属性。@RequestMapping有很多属性很常用:
method = { RequestMethod.POST }表示该方法只能匹配POST请求,如果不配的话,表示可以匹配任意方式的请求;
value = { "/test1" }表示该方法匹配请求路径是/test1的请求,value属性必配。此时如果同时用@RequestMapping来标注Handler处理器类的话,假如配置value属性为value = "/delete",则刚刚配置的方法就匹配路为/delete/test1的请求。
value属性支持Ant风格的url配置,?匹配任意一个字符,*匹配任意个字符,**匹配任意层路径,如/delete/**/test1可以匹配/delete/aaa/test1、/delete/aaa/bbb/test1。
params = { "userName=kou", "age" }表示该方法匹配的请求的请求参数必须包含userName和age,且userName参数的值必须为"kou"。
如果请求url在处理器中没有方法可以匹配,假如url是/getAll,此时服务器会报警告:No mapping found for HTTP request with URI [/springmvc/getAll] in DispatcherServlet with name 'dispatcherServlet',浏览器会展示404页面。
@PathVariable
与@RequestMapping配合使用,修饰处理器方法的形参,将请求url中占位符的值绑定到形参中,相当于给形参设值,用例:
@Controller @RequestMapping(value = "/hello") public class HelloController { @Resource private HelloService helloService; @RequestMapping(value = { "/dd/{Id}" }) public void getAll(HttpServletRequest request, HttpServletResponse response, @PathVariable(value = "Id") String id) { System.out.println(id); } }
如果请求的url是/hello/dd/10,则此时将匹配到getAll()方法,@PathVariable从url中截取出ID对应的字符串,并赋值给参数id,此时可以在方法体拿到id的值,为10。
这里要注意两点:
@PathVariable的value属性的值要和RequestMapping的value属性的花括号的值保持一致,否则页面会报500,提示Missing URI template variable
最好让@PathVariable修饰的参数的类型为String。假如请求的url是/hello/dd/100a,则此时也能匹配到getAll()方法,如果@PathVariable修饰的参数的类型为Integer或是其他非String类型的话,可能报转换异常MethodArgumentTypeMismatchException: Failed to convert value of type [java.lang.String] to required type [java.lang.Integer]; nested exception is java.lang.NumberFormatException: For input string: "100a"
@RequestParam
修饰处理器方法的形参,会先得到前端请求参数的值然后再赋给指定的形参:
@Controller @RequestMapping(value = "/hello") public class HelloController { @Resource private HelloService helloService; @RequestMapping(value = { "/dd" }) public void getAll(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "username") String userName) { System.out.println(userName); } }
如果请求url是/hello/dd,则会匹配到getAll()方法,@RequestParam会先执行request.getParameter("username")得到username参数的值,然后赋值给形参userName,总的来说就相当于执行userName=request.getParameter("username"),此时我们就可以在方法体中获得userName的值。如果username对应的参数值为null,则userName也会为null。如果设置了@RequestParam的话,则此请求必须有username参数,否则页面会报400,提示Required String parameter 'username' is not present,后台程序不报错。如果我们不想前台页面报错的话,只需设置@RequestParam的required属性值为false,默认值是true。
值得注意的是,@RequestParam不仅可以从get请求、一般post请求中获取请求参数的值,也可以从multipart/form-data的post请求中得到请求参数的值,见下文介绍的文件上传。
@RequestHeader
获取请求报头中的指定的属性值并绑定到指定形参中
@CookieValue
获取请求中指定的Cookie值并绑定到指定形参中
一般情况下,控制器方法返回字符串类型的值会被当成逻辑视图名处理,如返回字符串"result",则配置的InternalResourceViewResolver视图解析器会解析此字符串,返回/WEB-INF/jsp/result.jsp。但是,如果返回的字符串以forward:或者redirect:开始,则spring mvc会对这种字符串特殊处理,不会再交给试图解析器去解析,而是将forward:或者redirect:当成指示符、其后的字符串作为url来处理,也就是说会直接转发或者重定向到后半部分字符串指定的url。比如说返回字符串是"forward:/index.jsp",则会直接转发到index.jsp页面。
如果要符合RESTful风格,我们需将DispatcherServlet的请求映射由*.action变为/,但这时spring mvc将捕获所有请求,包括静态资源的请求,spring mvc会将他们看成一个普通请求处理,因找不到对应处理器将导致错误。比如说,在WEB-INF目录下,新建一个image目录,专门存放图片,这个时候访问/image/111.png,服务器会报警告: No mapping found for HTTP request with URI [/springmvc/image/111.png] in DispatcherServlet with name 'dispatcherServlet'。有3种解决办法:
1.在web.xml中配置默认Servlet(如Tomcat的org.apache.catalina.servlets.DefaultServlet)的映射,用来匹配所有可能的、各种类型的静态资源,示例:
<!-- 静态资源映射,指定静态资源请求交由web容器的默认Servlet处理 -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.jpg</url-pattern>
<url-pattern>*.png</url-pattern>
<url-pattern>*.gif</url-pattern>
<url-pattern>*.bmp</url-pattern>
<url-pattern>*.ico</url-pattern>
<url-pattern>*.js</url-pattern>
<url-pattern>*.css</url-pattern>
</servlet-mapping>
这种解决办法要求servlet或者说应用服务器必须是Tomcat、Jetty、JBoss、GlassFish中的一种,对Resin、WebLogic、WebSphere不适用,因为Resin的默认servlet的名字是resin-file,WebLogic的默认servlet的名字是FileServlet,WebSphere的默认servlet的名字是resin-file。
2.用<mvc:resources />元素,在spring mvc的配置文件中配置,配置示例:
<mvc:resources location="/image/" mapping="/image/**" />
上面配置表示如果请求是/image开头的,将去/image目录下去找对应的资源。例如,如果请求url是/image/small/111.png,将去image/small目录下找111.png。那么如果要请求css文件,只需再配置css的路径和映射即可。这种解决办法需要提前给各种静态文件分好文件夹,如果文件夹没有指定或是指定错了,就会报找不到文件错误。
3.用<mvc:default-servlet-handler />元素,示例如下:
<mvc:default-servlet-handler />
只需在spring mvc配置文件配置这么一行即可。这将启用spring-mvc-xxx.jar包中的DefaultServletHttpRequestHandler处理器,它会根据当前服务器环境去找对应的默认的servlet处理静态资源的请求,相比于方法1的好处就是,默认的servlet名不是写死的,而是交给spring动态去找的,这样在切换服务器时不用改这里,而且非常简单,就一行。
<mvc:annotation-driven />用法:
这个元素可以自动注册RequestMappingHandlerMapping和RequestMappingHandlerAdapter,也就是说,如果在spring mvc配置文件中配置了<mvc:annotation-driven />,就无需在<bean />元素中配置这两个组件了。
此外,在spring mvc配置文件中配置了<mvc:annotation-driven />的话,就可以支持以下注解的使用了:
@RequestBody,用于修饰处理器方法形参,解析请求体参数并赋给方法形参,用于contentType值为application/json且请求参数是json字符串的post请求,需要配合HttpMessageConverter使用。
@ResponseBody,用来修饰处理器类或是处理器方法,需要HttpMessageConverter接口来对处理器方法的返回值进行转换,处理器方法返回值类型不同,使用到的具体实现类也不一样,这也是策略模式了。
如果处理器方法返回值是String类型,则要用StringHttpMessageConverter实现类,该类默认的charSet是ISO-8859-1,需要配置其supportedMediaTypes属性值为text/html;charset=UTF-8,这样才能转为UTF-8编码。
如果处理器方法返回的是集合类型或者实体类类型,则具体使用哪个实现类要看应用中引用的处理json的jar包是什么。如果没有引用任何处理json的jar包,这个时候会报错的No converter found for return value of type: class java.util.ArrayList。如果引用了Jackson包,则会用MappingJackson2HttpMessageConverter实现类,该类默认的charSet是UTF-8,最终将向前端返回json。
文件上传:
spring处理文件上传需要使用spring-web-xxx.jar包中的两个接口:MultipartFile接口和MultipartResolver接口。通常我们在spring mvc 配置文件中注册CommonsMultipartResolver实现类,这个实现类是Spring在commons-fileupload基础上开发的,所以此时应用需要引入commons-fileupload-xxx.jar。注意,在注册CommonsMultipartResolver时,必须指定id为multipartResolver,否则处理器方法参数拿不到file数据。
这里要特别注意一点,前端请求的路径不能是/upload,也就说是action属性值不能是upload,否则处理器方法参数也拿不到file数据,这点不能解释。
<form action="uploadFile" method="post" enctype="multipart/form-data"> <input name="file" type="file"> <input type="submit" value="提交"> </form>
则要在spring mvc配置文件中配置如下:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>
此外,还可以在配置文件中配置multipartResolver的maxUploadSize属性来限制每次上传文件的总大小(允许一次上传几个文件)或是配置maxUploadSizePerFile属性来限制单个上传文件的大小,但是实测如果上传文件总大小超过限制或是单个文件超过限制的话,后台会报异常,提示The field file exceeds its maximum permitted size,而前端页面直接显示连接被重置,后台又报错,前端提示又不友好,所以我们一般不在这里限制文件上传大小,而是写在处理器方法中。在处理器方法中我们用MultipartFile接口类型参数接收前端传过来的数据,这里的形参名要与前端上传框架的name属性值保持一致,否则接收不到数据,如果不想一致或者不能一致的话,用@RequestParam先接收再赋值即可。
主要用到MultipartFile接口的以下几个方法:
String getOriginalFilename();得到上传的文件的名称,如3月报表.xlsx、姚明.jpg,通过名称我们可以截取出后缀名,可以用后缀名来限制前端传文件的类型
long getSize();得到上传文件的大小,以byte为单位,可以用来限制前端上传文件的大小
transferTo(File dest);将接收到的文件数据写到指定的目标文件中,也就是保存操作
处理器方法示例:
private static final String DEFALTUPLOADPATH = "d:/upload/"; @RequestMapping(value = "/uploadFile") public void uploadFile(MultipartFile file, String pathName, HttpServletRequest request) { File tmpFile = null; try { String fileName = file.getOriginalFilename(); System.out.println("fileName:" + fileName); if (StringUtils.isEmptyOrWhitespaceOnly(fileName)) { System.out.println("必须选择文件"); return; } // 获取上传文件扩展名 String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()); fileExt = fileExt.toLowerCase(); System.out.println("文件类型:" + fileExt); if (("jpg").equals(fileExt) || ("jpeg").equals(fileExt) || ("png").equals(fileExt) || ("bmp").equals(fileExt) || ("gif").equals(fileExt)) { BufferedImage sourceImg = ImageIO.read(file.getInputStream()); int imgWidth = sourceImg.getWidth(); int imgHeight = sourceImg.getHeight(); System.out.println("上传的图片宽:" + imgWidth); System.out.println("上传的图片高:" + imgHeight); } // 对扩展名进行小写转换 long fileSize = file.getSize(); System.out.println("fileSize=" + fileSize); // 判断文件是否为空 File uploadDir = new File(DEFALTUPLOADPATH); // 指定的上传目录如果没有的话,就新建此目录 if (!uploadDir.exists()) { uploadDir.mkdir(); } Date now = new Date(); // 上传文件重命名。因为如果不重命名的话,如果上传的文件名称和目录中某个文件相同,则会覆盖原来的文件,哪怕只是文件名相同,而内容不同,这不行 String tmpFileName = String.valueOf(now.getTime()) + Math.round(Math.random() * 1000) + "." + fileExt; // 文件路径 String tmpFilePath = DEFALTUPLOADPATH + tmpFileName; tmpFile = new File(tmpFilePath); file.transferTo(tmpFile); // 如果上传的是图片的话,以下代码可以过滤图片尺寸 } catch (Exception e) { e.printStackTrace(); } finally { if (tmpFile.exists()) { System.out.println("上传成功,文件路径为:" + tmpFile.getAbsolutePath()); } else { System.out.println("上传失败"); } } }
以上在控制台打印的提示信息,在实际开发时是向前端返回的提示信息,如果想以json返回,只需用@ResponseBody标注此方法,然后返回实体类型、集合类型、Map类型的数据就行。
上面还针对如果上传文件类型是图片的话做了一些特殊处理,如可以获取上传图片的高和宽,进而可以做一些限制。