目录
一. 搭建 SpringMVC
1.使用 SpringMVC 需要引入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<!-- servlet api (3.0及以上版本,由于tomcat中已经存在了这个包,所以设置为provided权限)-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- 添加json 依赖jar包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.7.0</version>
</dependency>
传统方式
- 需要在 WEB-INF 包的 web.xml 中,或创建配置类的方式配置 DispatcherServlet , ContextLoaderListener,扫描文件,文件映射等
web.xml 配置示例
<?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"
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>ego-manager-web</display-name>
<!-- post方式提交乱码解决 begin -->
<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>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- */ -->
<!-- spring后端配置 begin(通过一个监听器,扫描Spring的配置文件,启动Spring) -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext-*.xml</param-value>
</context-param>
<!-- 配置 DispatcherServlet 指定 SpringMvc配置文件的位置,启动SpringMVC-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/springmvc.xml</param-value>
</init-param>
</servlet>
<!--配置映射-->
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
- resources 文件夹下创建创建配置文件,配置
<?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.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 启用spring mvc 功能-->
<mvc:annotation-driven />
<!-- 配置访问根路径对应资源操作 -->
<!-- 其中 path 表示为请求的路径,view-name 表示为你需要做的资源操作 -->
<mvc:view-controller path="/" view-name="index" />
<!-- 设置使用注解的类所在的package(基本包)-->
<context:component-scan base-package="com.ssm.controller"/>
<!-- 静态资源映射 location是本地静态资源路径 mapping是映射的url地址,访问时就使用该地址 -->
<mvc:resources location="/WEB-INF/static/" mapping="/**" />
<!-- 配置视图解析器(使用不同的视图,解析器不同,此处使用jsp) -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- 配置前缀 -->
<property name="prefix" value="/WEB-INF/pages/"/>
<!-- 配置后缀 -->
<property name="suffix" value=".jsp"/>
</bean>
<!-- 文件上传解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 默认编码试 -->
<property name="defaultEncoding" value="UTF-8"/>
<!-- 最大文件上传大小 -->
<property name="maxUploadSize" value="10485760"/>
</bean>
<!-- 配置拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<!--注意拦截的什么,放行的什么, 拦截所有,然后指定放行
放行项目中的所有静态资源,
放行登入页面,
放行登入后台
根据需求定义, 如果是商城,新闻等,
拦截的 url(必须登录才可以访问的资源)
/**包括路径及其子路径
如果是/admin/*-拦截的是/admin/add,/admin/list...但是
/admin/add/user 不被拦截
如果是/admin/**-拦截的是/admin 路径及其子路径
*/-->
<mvc:mapping path="/**"/>
<!-- 不拦截的 url(不需要登录就可以访问的资源) -->
<mvc:exclude-mapping path="/Public/**"/>
<mvc:exclude-mapping path="/Template/**"/>
<mvc:exclude-mapping path="/login/**"/>
<mvc:exclude-mapping path="/user/login/**"/>
<mvc:exclude-mapping path="/user/logout/**"/>
<!--放行图像验证码请求-->
<mvc:exclude-mapping path="/image/**"/>
<!--对应java中自定义的拦截器,此处使用ref->
<ref bean="managerLoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
</beans>
配置类方式(注意servlet 3.0及以上版本)
- 创建关于SpringMVC 的配置类,配置视图解析器,拦截器等等
import com.ssm.interceptor.ManagerInterceptor;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
//该配置类只扫描mvc相关的controller
@ComponentScan(value = "com.ssm",includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
}, useDefaultFilters = false)
//开启SpringMVC(相当于xml中的<mvc:annotation-driven />)
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter {
//为什么要继承 WebMvcConfigurerAdapter(实现WebMvcConfigurer接口也可以,是该类的父接口)
//通过继承该类,重写该类中的定制方法,配置视图解析器,拦截器,些类型转换器,格式化器等功能
//设置视图解析器
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
//指定jsp页面视图解析器( WEB-INF/pages下的.jsp结尾的)
//当请求页面是,默认会给放回的页面名字加上前缀,后缀寻找返回
registry.jsp("/WEB-INF/pages/",".jap");
}
//设置静态资源,例如访问项目痛的图片等
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
//设置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//ManagerInterceptor 自定义拦截器
InterceptorRegistration registration = registry.addInterceptor(new ManagerInterceptor());
//设置拦截的请求
registration.addPathPatterns("/**");
//设置放行的请求
registration.excludePathPatterns("/Public/**");
}
}
- 创建关于Spring 的配置类
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.mvc.Controller;
//该配置类是 Spring 相关配置类,只扫描Spring相关的,排除mvc相关的
@ComponentScan(value = "com.ssm", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})})
public class RootConfig {
}
- 创建 SpringMVC 启动时自动加载执行的类,该类继承 AbstractAnnotationConfigDispatcherServletInitializer 抽象类,重写抽象类中的接口,获取上面配置的两个配置类,与被 DispatcherServlet 监管的请求路径
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
//当启动web容器时,创建该类对象,调用方法初始化容器,前端控制器等
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
//获取根容器方法,需要创建配置类
//(相当于前面的获取Spring 的配置文件)
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
//获取web容器配置类(相当于前面的加载 mvc相关文件的配置)
//需要创建配置类
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{AppConfig.class};
}
//获取 DispatcherServlet 的映射信息
//"/" 代表拦截所有请求(包括静态资源css,html等,但是不包括页面)
//"/*" 会拦截页面
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
SpringMVC 接收请求执行流程
1. 源码分析
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//1.获取 HandlerExecutionChain
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//2.调用getHandlerAdapter()方法传递获取到的 HandlerExecutionChain
//获取HandlerAdapter 处理器映射器(通过处理器映射器,执行处理器的方法)
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (this.logger.isDebugEnabled()) {
this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
//3.执行拦截器中的方法,获取当前请求匹配到的所有的interceptor拦截器
//遍历执行每一个拦截器的拦截preHandle()方法,返回放行或拦截的布尔值
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//4.首先执行通过获取到的HandlerExecutionChain 调用 getHandler() 获取到
//当前请求的 Handler处理器,
//然后执行此处的调用 handle() 方法,传递处理器对象,通过处理器对象调用执行处理方法,
//也就是controller,执行完毕后返回ModelAndView
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
//5.获取当前请求对应的拦截器,执行拦截器中的postHandle()方法(注意此处时反向执行的)
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
//6.处理 ModelAndView,该方法中会调用一个 render()方法,在render()方法中,会通过
//获取到的 ModelAndView 调用getView()方法,获取一个 View 对象
//然后通过获取到的 View 对象调用了另一个 render() 方法
//在该方法中调用了renderMergedOutputModel() 方法
//renderMergedOutputModel()方法中会获取 RequestDispatcher 转发处理器
//通过转发处理器调用 forward(request, response) 最终实现转发,相当于渲染视图
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
//7.此处是在finally块中,最终逆向执行interceptor中的afterCompletion()方法
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
2. 步骤总结
- 首先 DispatcherServlet 监控拦截到客户端发送的请求,如果匹配不成功404页面报错,如果匹配成功
- 调用getHandler() 获取 HandlerExecutionChain
- 传递获取到的 HandlerExecutionChain 调用getHandlerAdapter()方法获取HandlerAdapter 处理器映射器
- 通过获取到的 HandlerExecutionChain 调用 applyPreHandle()方法,获取当前请求匹配到的所有的interceptor拦截器,遍历执行每一个拦截器的preHandle()方法,返回放行或拦截的布尔值
- 通过获取到的HandlerExecutionChain 调用 getHandler() 获取到当前请求的 Handler处理器,也就是对应当前请求的Controller
- 通过获取到的HandlerAdapter 调用 handle() 方法,传递处理器对象,通过处理器对象调用执行处理方法controller,执行完毕后返回ModelAndView
- 通过HandlerExecutionChain 调用 applyPostHandle() 方法,获取当前请求对应的拦截器,执行拦截器中的postHandle()方法(注意此处时反向执行的)
- 调用 processDispatchResult()方法, 处理 ModelAndView,该方法中会调用一个 render()方法,通过 ModelAndView 调用getView(),获取一个 View 对象,调用 View对象的 render() 方法,在View对象的 render() 方法中调用了renderMergedOutputModel() 方法,获取 RequestDispatcher 转发处理器,通过转发处理器执行 forward® 最终实现转发,相当于渲染视图
- 最终HandlerExecutionChain 调用 applyAfterConcurrentHandlingStarted() 逆向执行interceptor中的afterCompletion()方法
二. 异步请求
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
@Controller
public class AsyncController {
//方式一: 当前端控制器返回的是 Callable 类型的数据时
//Spring会将Callable提交到一个TaskExecutor任务执行器中
//使用一个隔离线程进行执行,DispatcherServlet和所有的Filter退出
//response还保持打开状态,
//当Callable产生结果状态时,SpringMVC会将这个结果重新派发给Servlet容器
//恢复处理过程
@RequestMapping("/asyncTest")
@ResponseBody
public Callable<String> asyncTest(){
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
return "异步方法执行返回的数据";
}
};
//这个数据是上面call方法执行完毕后返回的
return callable;
}
//方式二: 另一种处理方式,给前端返回 DeferredResult 类型对象
//这个接收请求的方法是在方法中创建的 DeferredResult对象
//调用了setResult() 方法后执行的
//可以创建这个对象放入一个容器中,指定这个对象的id,要进行异步处理
//的方法在容器中根据id去获取这个对象,调用setResult()方法处理
@RequestMapping("/deferredResult")
@ResponseBody
public DeferredResult<String> deferredResultTest(){
//构造器中第一个参数为设置超时时间,与超时后的报错提示信息
DeferredResult<String> deferredResult = new DeferredResult((long)3000,"失败提示信息");
//当这个 deferredResult 对象调用了 setResult() 方法后,才会给前端真正的响应
//deferredResult.setResult("异步执行完毕");
Map<String,DeferredResult> resultMap = new HashMap<>();
resultMap.put("aaa",deferredResult);
//调用处理方法处理
this.dispose(resultMap);
return deferredResult;
}
//处理方法
public void dispose(Map<String,DeferredResult> resultMap){
for(Map.Entry<String,DeferredResult> entry : resultMap.entrySet()){
if(entry.getKey().equals("aaa")){
entry.getValue().setResult("处理完毕");
}
}
}
}
三. 请求转发与重定向
1. 请求转发,只发送一次请求,地址栏不会变,数据不会丢失,是服务器行为
2. 重定向,做了两次请求,地址栏会变,数据会丢失,是客户端行为
3. 重定向域名不同的网站,例如重定向到其它服务,重定向到第三方服务是,域名可能不同,会触发浏览器的安全策略问题
请求转发
//请求转发到通过request域对象携带User对象到WEB-INF文件夹下的page页面,
// 需要先转发到后台test07中,test07后台获取域对象中的名为user的参数
//返回个page.jsp页面
@RequestMapping("test05")
public String test05(HttpServletRequest request){
//创建User对象
User user =new User();
user.setName("小明");
//将User对象存入request域对象中
request.setAttribute("user",user);
//请求转发到后台test07
return "forward:test07";
}
//请求转发到webapp根目录下的v1.jsp页面,并携带参数,
//使用Model对象存储数据
@RequestMapping("test10")
public String test10(Model model){
//创建User对象
User user=new User();
user.setName("小黑");
//将user对象添加到model对象中
model.addAttribute("user",user);
//返回v1.jsp页面
return"forward:v1.jsp";
}
重定向示例代码
//直接重定向到webapp根目录下的.jsp结尾的页面
@RequestMapping("test03")
public String test03(){
System.out.println("test03");
//直接重定向到webapp根目录下.jsp结尾的页面,
//由于直接重定向到页面不会经过视图解析器,
//需要书写"页面名.jsp页面类型"
return "redirect:v1.jsp";
}
//如果想要重定向到WEB-INF下视图解析器解析的文件夹下的页面
//需要重定向请求后台,通过后台返回请求页面
@RequestMapping("test04")
public String test04(){
System.out.println("test04");
//重定向到后台test07请求,
//test07后台请求返回给一个 WEB-INF文件夹下 page.jsp页面
return "redirect:test07";
}
前端 jsonp 重定向跨域
$("#btn").click(function(){
$.ajax({
async : true,
url : "https://api.douban.com/v2/book/search",
type : "GET",
dataType : "jsonp", // 返回的数据类型,设置为JSONP方式
jsonp : 'callback', //指定一个查询参数名称来覆盖默认的 jsonp 回调参数名 callback
jsonpCallback: 'handleResponse', //设置回调函数名
data : {
q : "javascript",
count : 1
},
success: function(response, status, xhr){
console.log('状态为:' + status + ',状态是:' + xhr.statusText);
console.log(response);
}
});
后端可以利用nginx反向代理解决
user nginx;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name upload.myshop.com;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
location / {
proxy_pass http://192.168.0.104:8888;
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,PATCH,OPTIONS;
# 解决假请求问题,如果是简单请求则没有这个问题,但这里是上传文件,首次请求为 OPTIONS 方式,实际请求为 POST 方式
# Provisional headers are shown.
# Request header field Cache-Control is not allowed by Access-Control-Allow-Headers in preflight response.
add_header Access-Control-Allow-Headers DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range;
return 200;
}
}
}
}