三层架构是 web 程序的纵向结构,是 web 程序的身体,而MVC 模式只存在于三层架构的展现层,是 web 程序展示时相互沟通的一种好的方式。
Spring MVC 使我们可以简单地,开发灵活且松耦合的Web项目。
在Servlet 2.5及以下,servlet类需要在web.xml文件中,配置<servlet>元素,才能访问这个Servlet。
但在Spring MVC + Servlet 3.0下,通过实现WebApplicationInitializer 接口,便可实现等同web.xml的配置,做到无web.xml。然后可以在WebApplicationInitializer实现类中,通过Spring MVC的 DispatcherServlet配置Servlet。
下面用Maven构建一个,基于注解和Java配置的,无web.xml配置的Spring MVC工程:
1、构建Maven项目,pom.xml文件如下:
4.0.0 com.qfedu MySpringMVC 1.0 war 1.8 UTF-8 UTF-8 3.6.1 3.1.0 2.2 1.2 3.1.0 4.3.9.RELEASE 1.2.3 1.7.25 org.apache.maven.plugins maven-compiler-plugin ${compiler.version} org.apache.maven.plugins maven-war-plugin ${war.version} false javax javaee-web-api 7.0 provided org.springframework spring-webmvc ${spring-framework.version} javax.servlet jstl ${jstl.version} javax.servlet javax.servlet-api ${servlet.version} provided javax.servlet.jsp jsp-api ${jsp.version} provided org.springframework spring-tx ${spring-framework.version} org.slf4j slf4j-api ${slf4j.version} log4j log4j 1.2.17 org.slf4j jcl-over-slf4j ${slf4j.version} ch.qos.logback logback-classic ${logback.version} test ch.qos.logback logback-core ${logback.version} ch.qos.logback logback-access ${logback.version}
2、在Maven的src / main / resources 目录下,新建logback.xml文件,用来配置日志:
true logbak: %d{HH:mm:ss.SSS} %logger{36} - %msg%n
3、依然在 src / main / resources 下,建立 views 目录,然后在 views 目录下,新建 index.jsp 页面(内容随意) —— 页面没有放在Maven标准的 src /main / webapp / WEB-INF 下,这里的目的是,让大家熟悉 Spring Boot 的页面放置习惯,即放在 src / main / resources 目录下。(在Eclipse,用模板创建 jsp 页面时,不知道为什么,文件自动被移到了 webapp 目录下,因此如果是这样,需要再移回来)。
4、Spring MVC 的配置类:
package com.qfedu.MySpringMVC.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.view.InternalResourceViewResolver;import org.springframework.web.servlet.view.JstlView;@Configuration@EnableWebMvc@ComponentScan("com.qfedu.MySpringMVC")public class MyMvcConfig { @Bean public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver viewResolver = new InternalResourceViewResolver(); viewResolver.setPrefix("/WEB-INF/classes/views/"); viewResolver.setSuffix(".jsp"); viewResolver.setViewClass(JstlView.class); return viewResolver; }}
ViewResolver 用于映射开发时的路径,与视图的真实位置。因此,对于开发路径 src / main / resource / views,ViewResolver 需要将路径前缀配置为 / WEB-INF / classes / views / ,因为运行时页面会自动编译到该目录下。如图:
5、Web —— Servlet配置:
package com.qfedu.MySpringMVC.config;import javax.servlet.ServletContext;import javax.servlet.ServletException;import javax.servlet.ServletRegistration.Dynamic;import org.springframework.web.WebApplicationInitializer;import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;import org.springframework.web.servlet.DispatcherServlet;//实现了WebApplicationInitializer接口, 将会被启动容器的类SpringServletContainerInitializer获取到并完成配置, 就和web.xml一样public class WebInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { //新建WebApplicationContext,并注册配置类(注册配置类中返回的ViewResolver bean),关联当前的ServletContext AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(MyMvcConfig.class); context.setServletContext(servletContext); //DispatcherServlet:前端控制器 //用WebApplicationContext新建Spring MVC的 DispatcherServlet,然后注册并返回Servlet注册对象 Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(context)); //设置DispatcherServlet的Mapping和LoadOnStartUp次序 servlet.addMapping("/"); servlet.setLoadOnStartup(1); }}
6、一个基本的控制器:
package com.qfedu.MySpringMVC.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@Controllerpublic class HomeContoller { @RequestMapping("/index") public String index() { return "index"; }}
最后部署到服务器上,即可访问 。
以上步骤即可完成一个比较 基本的Spring MVC 项目。
Spring MVC 的常用注解:
1、@Controller:将类声明为 Bean,并作为 Spring MVC 的 Controller,Dispatcher Servlet 会根据配置类自动扫描 Controller,并将 Web 请求映射到注解了@RequestMapping 的类、方法上。值得注意的是,在声明普通 Bean 时,使用@Component、@Service、@Repository、@Controller 都是一样的(这一点可以查看 Spring 的源码);但是在声明 Spring MVC 的 Controller 时,只能使用 @Controller。
2、@RequestMapping:可注解在类和方法上,用来将 Web 请求(访问路径和参数)和处理类,以及方法进行映射。注解在方法上的路径会继承类上的路径;可选择 Servlet 的 request 和 response 作为方法的参数;通过 produces 参数,对 request 和 response 的 contentType(包括字符集 charset)进行配置。
3、@ResponseBody:支持将返回值放在 response 内,而不是返回一个页面(即通过 response 返回一些数据,这一点对基于 Ajax 的程序很友好)。可注解在返回值之前,或方法之上。
4、@RequestBody:允许请求的参数在 request 内,通过 POST 方式请求。注解在参数前,则参数为请求体内对应的数据。
5、@PathVariable:用来接收路径参数,如 / news / 001 的号数 001 可作为参数,而不是请求参数 /new ? id = 001。
6、@RestController:是一个组合注解,组合了@Controller 和@ResponseBody,即这是一个只用于数据交互的 Controller(所有的方法都返回数据到 response,而不是页面)。
测试用例:
添加 Jackson Dataformat XML 的依赖,以便对象和 json 或 xml 之间的转换(目前可能由于阿里云镜像的原因,添加了这个依赖的比较新的版本之后,程序就卵了,如果这样,就换个旧点的版本):
com.fasterxml.jackson.dataformat jackson-dataformat-xml ${jackson.version}
或者只使用 Jackson 对 json 的支持(即上面的依赖包含下面的依赖):
com.fasterxml.jackson.core jackson-databind ${jackson.version}
实体类 Entity —— POJO:
package com.qfedu.MySpringMVC.domain;public class DemoEntity { private Long id; private String name; public DemoEntity() { //必须有一个空构造函数,作为 default,不然 Spring MVC 无法自动将对象转换成 json 格式 } public DemoEntity(Long id, String name) { this.id = id; this.name = name; } // 省略getter and setter}
普通控制器 Controller:
package com.qfedu.MySpringMVC.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@Controller@RequestMapping("/foo")public class FooController { // 具体的方法,下面分别介绍}
上面的控制器的方法:
演示@RequestMapping 和@ResponseBody:
@RequestMapping(produces = "text/plain;charset=UTF-8")public @ResponseBody String index(HttpServletRequest request) { return "url:" + request.getRequestURL() + " can access";}
@RequestMapping 未标注路径,则继承类路径 /foo,而返回值有@ResponseBody,方法根据 produces 的设置直接返回 contentType="text/plain; charset=UTF-8" 的数据。直接访问 ,将浏览一个 plain 的 html 页面,如下:
演示指定方法路径的@RequestMapping,以及获取 request 参数(访问结果如:):
@RequestMapping(value = "/requestParam", produces = "text/plain;charset=UTF-8")public @ResponseBody String DemoRequestParam(Long id) { return "id:" + id;}
Spring 很爽的地方是,会自动拆装,而且要什么给什么(包括实体类和Spring内置的类,这个功能使得处理表单提交变得更简单):
结果如:。
@RequestMapping(value = "/obj", produces = "text/plain;charsetF-8")public @ResponseBody String DemoRequestObj(DemoEntity obj) { return "id:" + obj.getId() + ",name:" + obj.getName();}
也可以将不同的路径,映射到相同的方法(即 value 为 String 的数组,结果如:,也可以访问 name1):
@RequestMapping(value = {"name1", "name2"}, produces = "text/plain;charset=UTF-8")public @ResponseBody String DemoMutilplePath(HttpServletRequest request) { return "url:" + request.getRequestURL() + " can access";}
演示@PathVariable(注意与上面的地址的区别,结果如:):
@RequestMapping(value = "/pathvar/{param}", produces = "text/plain;charset=UTF-8")public @ResponseBody String demoPathVar(@PathVariable String param) { return "param:" + param;}
组合了@ResponseBody(不用为每个返回数据的方法注解) 的控制器 RestController:
package com.qfedu.MySpringMVC.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/rest")public class DemoRestController { // 具体的方法,下面分别介绍}
上面控制器的方法:
演示返回 json 数据:
@RequestMapping(value = "/getjson", produces = "application/json;charset=UTF-8") //MediaType.APPLICATION_JSON_UTF8_VALUEpublic DemoEntity getJson(DemoEntity obj) { return obj;}
contentType 为 json。访问如::
演示返回 xml 数据:
@RequestMapping(value = "getxml", produces = "application/xml;charset=UTF-8")public DemoEntity getXml(DemoEntity obj) { return new DemoEntity(obj.getId(), obj.getName());}
访问如:,结果:
Spring MVC 基本配置:
将之前的 Spring MVC 配置类升 一下级,继承 WebMvcConfigurerAdapter 类(或者实现 WebMvcConfigurer 接口:Adapter 类是这个接口的实现类),通过重写这个类的一些方法,可以定制一些基本的配置(依然要@EnableWebMvc,开启对 Spring MVC 的支持,否则以下的配置将无效)。
静态资源映射:
若程序的静态文件(如图片等)需要直接访问,或者 html 页面需要访问,可以为配置类重写 addResourceHandlers 方法来实现。
演示用例:
添加静态资源:
在 src / main / resource 下建立 assets 目录,该目录下存放所有的静态资源。再如下图新建一个资源:
配置类:
package com.qfedu.MySpringMVC.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;import org.springframework.web.servlet.view.InternalResourceViewResolver;import org.springframework.web.servlet.view.JstlView;@Configuration@EnableWebMvc@ComponentScan("com.qfedu.MySpringMVC")public class MyMvcConfig extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 映射:addResourceHandler方法的参数是对外的访问路径(URI),addResourceLocations方法的参数是文件存放的位置 // URI如果是 '/assets/**',则可以访问映射的assets目录下的多级资源 // 如果是 '/assets/*',则只能访问assets目录,如果有多级目录,也不能访问 registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/assets/"); } // 其他配置省略(下同)}
访问如 :
拦截器设置:
拦截器(Interceptor)会对每一个请求处理的前后,进行相关的业务处理,有点类似于 Servlet 的 Filter。
演示用例:
自定义 Interceptor 由一个实现了 HandlerInterceptor 接口,或者继承了 HandlerInterceptorAdapter 类的、普通的 Bean 实现:
package com.qfedu.MySpringMVC.interceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.springframework.web.servlet.ModelAndView;import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;public class DemoInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { long startTime = System.currentTimeMillis(); request.setAttribute("startTime", startTime); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { long startTime = (long) request.getAttribute("startTime"); request.removeAttribute("startTime"); long endTime = System.currentTimeMillis(); System.out.println("本次请求处理时间为:" + new Long(endTime - startTime) + "ms"); request.setAttribute("handleTime", endTime - startTime); }}
为配置类重写 addInterceptors 方法,在方法中注册自定义的拦截器:
package com.qfedu.MySpringMVC.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;import org.springframework.web.servlet.view.InternalResourceViewResolver;import org.springframework.web.servlet.view.JstlView;import com.qfedu.MySpringMVC.interceptor.DemoInterceptor;@Configuration@EnableWebMvc@ComponentScan("com.qfedu.MySpringMVC")public class MyMvcConfig extends WebMvcConfigurerAdapter { @Bean public DemoInterceptor demoInterceptor() { return new DemoInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(demoInterceptor()); }}
访问任意路径,如 ,查看控制台:
@ControllerAdvice:
通过@ControllerAdvice,可以将对控制器的全局配置放在同一个位置,注解了@ControllerAdvice 的类的方法可使用注解@ExcpetionHandler、@InitBinder、@ModelAttribute,这些配置对所有的控制器内注解了@RequestMapping 的方法有效,即全局控制控制器。
1、@ExceptionHandler:用于全局处理控制器里发生的异常。
2、@ModelAttribute:绑定键值对到 Model 里。
3、@InitBinder:用来设置 WebDataBinder —— WebDataBinder 用于选择性的,自动绑定前台请求的参数到 Model 中。
演示用例:
定制 ControllerAdvice:
package com.qfedu.MySpringMVC.advice;import org.springframework.ui.Model;import org.springframework.web.bind.WebDataBinder;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.InitBinder;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.context.request.WebRequest;import org.springframework.web.servlet.ModelAndView;// 控制器建言,属于Spring的Bean,下面的建言都是全局的// Advice for all controllers@ControllerAdvicepublic class HandlerAdvice { // 其value属性指定捕获的异常类型 // 下面的异常处理,会将异常信息以键值对的形式保存到Model中,并跳转到对应的View @ExceptionHandler(Exception.class) public ModelAndView exception(Exception exception, WebRequest request) { ModelAndView modelAndView = new ModelAndView("error"); modelAndView.addObject("errorMessage", exception.getMessage()); return modelAndView; } @ModelAttribute public void addAttribute(Model model) { // 由于是在@ControllerAdvice的类中,因此@RequestMapping都能获得下面添加的键值对 // 在@RequestMapping的方法中,注解@ModelAttribute("key")在参数上,即可注入 model.addAttribute("msg", "额外信息"); } @InitBinder public void initBinder(WebDataBinder webDataBinder) { webDataBinder.setDisallowedFields("id"); // 过滤掉request的id属性,然后WebDataBinder自动将request绑定到Model中 } }
演示控制器(直接抛异常):
package com.qfedu.MySpringMVC.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.RequestMapping;import com.qfedu.MySpringMVC.domain.DemoEntity;@Controllerpublic class AdviceController { @RequestMapping("/advice") public String getSomething(@ModelAttribute("msg") String msg, DemoEntity obj) { throw new IllegalArgumentException("非常抱歉,参数有误 ,来自@ModelAttribute:" + msg + "(实际上和报错没啥关系)"); } }
在 src / main / resource / views 目录下,新建 error.jsp:
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%><%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>@ControllerAdvice Demo ${ errorMessage }
访问 :
在对应的@RequestMapping 的方法中,打断点调试启动再访问(可以看到 request 给的 obj 对象,已经过滤掉了 id 属性):
其他配置:
1、快捷的 ViewController:
前面使用@RequestMapping 配置页面跳转的时候,存在着无任何业务处理,只是简单的页面跳转,如:
@Controllerpublic class HomeController { @RequestMapping("/index") public String index() { return "index"; // viewName } }
通常在实际开发中,会涉及到大量这样的情况,如果都这样写则会很麻烦。简化配置:可以在配置类中重写 WebMvcConfigurerAdapter 的 addViewControllers 方法,直接统一注册 viewController:
@Overridepublic void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/index").setViewName("index");}
这样一来,代码将会更简洁,更加容易管理,而且程序因此效率更高:
2、路径匹配参数配置:
在 Spring MVC 中,默认情况下,如果路径末尾的参数带 "." 的话,如使用之前的@RequestMapping:localhost:8080/MySpringMVC/foo/pathvar/xx.yy,那么 "." 以及后缀都会被忽略掉:
重写之前配置类的 configurePathMatch 方法:
@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) { // 当匹配到后缀时,是否使用后缀。如果是enabled,例如匹配为 /users.* ,则会映射为 /users configurer.setUseSuffixPatternMatch(true); // 默认为true:enabled}
现在后缀将不会被忽略:
在开发中,更多基本配置请查看 WebMvcConfigurerAdapter 或 WebMvcConfigurer 的 API 文档,或者直接查看其源码。
Spring MVC 高级配置:
文件上传配置:
演示用例:
添加文件上传依赖(为了简化文件操作,添加 Commons IO 的依赖):
commons-fileupload commons-fileupload 1.3.1 org.apache.commons commons-io 1.3.2
在 src / main / resources / views 目录下,新建 upload.jsp(其中表单必须有 enctype 属性,method 必须为 post):
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>upload page
注册对应的 ViewController:
@Overridepublic void addViewControllers(ViewControllerRegistry registry) { // 其他省略 registry.addViewController("/toUpload").setViewName("upload");}
添加文件上传解析器 MutilpartResolver 的 Bean:
@Beanpublic MultipartResolver multipartResolver() { // 这是Spring的Resolver,不是Apache的 CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); multipartResolver.setMaxUploadSize(1000000); multipartResolver.setDefaultEncoding("UTF-8"); //解决文件名中文乱码问题 return multipartResolver;}
文件上传业务,控制器:
package com.qfedu.MySpringMVC.controller;import java.io.File;import java.io.IOException;import org.apache.commons.io.FileUtils;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.multipart.MultipartFile;@RestControllerpublic class UploadController { @RequestMapping(value = "/upload", method = RequestMethod.POST) public String upload(MultipartFile file) { try { // 本来应该上传到文件服务器上,为了演示更直接简单,直接将上传文件写到本机上 FileUtils.writeByteArrayToFile(new File("E:/upload/" + file.getOriginalFilename()), file.getBytes()); //快速写文件到磁盘。好用而且效率高 return "ok"; } catch (IOException e) { e.printStackTrace(); return "wrong"; } } }
部署并启动服务器,访问 ,进行文件上传,如下:
点击上传,如果返回 ok,则上传成功,查看本地文件,如:
自定义 HttpMessageConverter:
HttpMessageConverter 是用来处理 request 和 response 内的数据的。Spring 内置了大量 HttpMessageConverter,例如 MappingJackson2HttpMessageConverter、StringHttpMessageConverter 等,也可以根据自己的业务需求,自定义 HttpMessageConverter(需要在配置类中注册为 Bean,并通过重写 WebMvcConfigurerAdapter 的方法注册到 MVC 环境中)。
Spring 的 HttpMessageConverter 都是直接或间接继承抽象类 AbstractHttpMessageConverter<T>:
package com.qfedu.MySpringMVC.messageconvert;import java.io.IOException;import java.nio.charset.Charset;import org.springframework.http.HttpInputMessage;import org.springframework.http.HttpOutputMessage;import org.springframework.http.MediaType;import org.springframework.http.converter.AbstractHttpMessageConverter;import org.springframework.http.converter.HttpMessageNotReadableException;import org.springframework.http.converter.HttpMessageNotWritableException;import org.springframework.util.StreamUtils;import com.qfedu.MySpringMVC.domain.DemoEntity;public class MyMessageConverter extends AbstractHttpMessageConverter{ public MyMessageConverter() { // 新建一个自定义的媒体类型 contentType="application/x-qfedu"; super(new MediaType("application", "x-qfedu", Charset.forName("UTF-8"))); } @Override protected boolean supports(Class clazz) { // 返回值表明,当前MessageConverter只支持处理DemoEntity类 return DemoEntity.class.isAssignableFrom(clazz); } @Override protected DemoEntity readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { // 处理媒体类型为x-qfedu的@RequestMapping的请求数据,@RequestBody拿到的是处理后的数据 String temp = StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("UTF-8")); String[] tempArr = temp.split("-"); return new DemoEntity(new Long(tempArr[0]), tempArr[1]); } @Override protected void writeInternal(DemoEntity obj, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { // 处理@RequestMapping的方法的返回值,@ResponseBody返回的也是处理后的数据 String out = "hello:" + obj.getId() + "-" + obj.getName(); outputMessage.getBody().write(out.getBytes()); }}
注册为 Bean,然后在注册为 HttpMessageConverter。在 Spring MVC 中,注册 HttpMessageConverter 有两个方法:
- configurerMessageConverters:会覆盖掉 Spring MVC 默认注册的多个内置 HttpMessageConverter。
- extendMessageConverters:仅添加一个自定义的 HttpMessageConverter,不会有覆盖。
@Beanpublic MyMessageConverter myConverter() { return new MyMessageConverter();}@Overridepublic void extendMessageConverters(List> converters) { converters.add(myConverter());}
添加请求页面 converter.jsp:
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>HttpMessageConverter Demo
相应的 ViewController:
@Overridepublic void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/converter").setViewName("converter");}
要请求的 Controller:
package com.qfedu.MySpringMVC.controller;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import com.qfedu.MySpringMVC.domain.DemoEntity;@RestControllerpublic class ConverterController { @RequestMapping(value = "/convert", produces = "application/x-qfedu") public DemoEntity convert(@RequestBody DemoEntity obj) { return obj; } }
访问 ,请求后的结果:
服务器端推送技术:
在日常开发中,很多类型的网站需要服务器端推送技术,最容易理解和实现的解决方案是:客户端使用 Ajax 向服务器轮询消息,但是由于客户端不知道服务端什么时候有数据,因此不知道什么时候该请求消息,轮询的频率不好控制,因此服务器的压力会很大。
为了,服务器端主动推送信息,客户端则随时监听推送事件,准备接受消息。下面将演示基于 SSE(Server Send Event 服务端发送事件)的推送。该方法需要新式浏览器的支持,是半双工通信。除此之外,还有一种全双工的新技术 —— WebSocket。
演示 SSE:
控制器(SSE 服务端,输出的媒体类型为 text/event-stream 是对服务端的支持):
package com.qfedu.MySpringMVC.controller;import java.util.Random;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class SseController { @RequestMapping(value = "/push", produces = "text/event-stream") public String push() { Random r = new Random(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } // 消息传输格式:data:消息体\n\n。前缀:data:。后缀:\n\n。 return "data:Testing 1, 2, 3... " + r.nextInt() + "\n\n"; } }
新建 sse.jsp —— SSE 客户端(使用浏览器内置的 EventSource 对象,建立对服务端的监听,并绑定一系列监听处理事件):
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>SSE Demo
配置 see.jsp 的访问映射:
@Overridepublic void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/sse").setViewName("sse");}
然后运行,访问 :
Spring MVC 的测试:
MVC 相关的测试主要针对控制器。测试 Web 项目通常不需要发布、部署、启动,为此需要一些 Servlet 相关的模拟对象(Mock 对象),比如:MockMvc、MockHttpServletRequest、MockHttpServletResponse、MockHttpSession 等。
测试是非常有用的,让测试用例通过测试,才能保证软件的质量和可控性。在现实开发中,会事先确定项目需求(即先要知道产品是什么样子的,然后按照需求去开发),因此有一个概念叫测试驱动开发(Test Driven Development,TDD):设计人员先按照需求,写一个预期结果的测试用例(此时还没有实现代码,测试肯定是无法通过的),为了让测试用例通过,不断编码和重构,最终通过,一个项目模型也驱动而生。
演示用例(使用 Spring TestContext Framework 对普通控制器和 RestController 的页面跳转进行单元测试):
添加 Spring Test 依赖(基于JUnit):
junit junit 4.12 test org.springframework spring-test ${spring-framework.version} test
单例 Bean:
package com.qfedu.MySpringMVC.service;import org.springframework.stereotype.Service;@Servicepublic class DemoService { public String sayHello() { return "hello"; } }
测试用例(没有编译错误,但此时测试无法通过):
package com.qfedu.MySpringMVC;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.mock.web.MockHttpServletRequest;import org.springframework.mock.web.MockHttpSession;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import org.springframework.test.context.web.WebAppConfiguration;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.setup.MockMvcBuilders;import org.springframework.web.context.WebApplicationContext;import com.qfedu.MySpringMVC.config.MyMvcConfig;import com.qfedu.MySpringMVC.service.DemoService;@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(classes = MyMvcConfig.class)// 为WebApplicationContext容器指定 web 程序资源的根目录:不能包含Spring资源前缀,如classpath:、file:(即解释为文件系统资源);不能以slash(斜线/)结尾// 默认为文件系统 src / main / webapp 。注:在发布为WAR的标准Maven工程中,这是作为web程序根的标准目录@WebAppConfiguration("src/main/resources")public class TestControllerIntergrationTests { private MockMvc mockMvc; @Autowired private DemoService demoService; // 获得容器(已经被@RunWith、@ContextConfiguration配置好了) // WebApplicationContext容器需要额外配置 @WebAppConfiguration @Autowired private WebApplicationContext context; // 此处仅作演示,并未使用 @Autowired MockHttpSession session; @Autowired MockHttpServletRequest request; @Before public void setup() { mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); } @Test public void testNormalController() throws Exception { // forwardedUrl:页面转向的真正路径;status-OK:200 mockMvc.perform(get("/testNormal")) .andExpect(status().isOk()) .andExpect(view().name("page")) .andExpect(forwardedUrl("/WEB-INF/classes/views/page.jsp")) .andExpect(model().attribute("msg", demoService.sayHello())); } @Test public void testRestController() throws Exception { mockMvc.perform(get("/testRest")) .andExpect(status().isOk()) .andExpect(content().contentType("text/plain;charset=UTF-8")) .andExpect(content().string(demoService.sayHello())); }}
运行:
因此,增加两个测试控制器:
普通控制器:
package com.qfedu.MySpringMVC.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.RequestMapping;import com.qfedu.MySpringMVC.service.DemoService;@Controllerpublic class NormalController { @Autowired DemoService demoService; @RequestMapping("/testNormal") public String testPage(Model model) { model.addAttribute("msg", demoService.sayHello()); return "page"; } }
对应视图 page.jsp 内容随意。
RestController:
package com.qfedu.MySpringMVC.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import com.qfedu.MySpringMVC.service.DemoService;@RestControllerpublic class MyRestController { @Autowired DemoService demoService; @RequestMapping(value = "/testRest", produces = "text/plain;charset=UTF-8") public String testRest() { return demoService.sayHello(); }}
再次测试控制器能否成功页面跳转,以及相应请求信息是否正确: