SpringMVC

    1. 简介

MVC模式简介

M-Model模型

模型(Model)的职责是负责业务数据。广义的模型包括实体类、DAO、service。

V-View视图

视图(View)的职责是负责显示界面和用户交互(收集用户信息)。属于视图的组件一般是不包含业务逻辑和控制逻辑的JSP。

C-Controller控制器

控制器(Controller)是模型层M和视图层V之间的桥梁,用于控制流程。

什么是SpringMVC

SpringMVC是一个mvc框架,用来简化基于mvc架构的web应用程序的开发。

SpringMVC是Spring框架的一个模块,SpringMVC和Spring无需通过中间整合层进行整合。

SpringMVC的核心组件

名称

中文名

作用

DispatcherServlet

前端控制器

框架核心,接收请求,响应结果,负责调度

HanlderMapping

处理器映射器

根据请求的URL查找Handler(Controller)

HandlerAdapter

处理器适配器

按照特定规则(HandlerAdapter要求的规则)去执行Handler

Controller(Handler)

处理器

负责请求处理流程。注意:按照HandlerAdapter要求的规则编写Handler,这样才能被HandlerAdapter正确执行

ModelAndView

模型和视图

封装业务处理结果和逻辑视图名

ViewResolver

视图解析器

进行视图解析,根据逻辑视图名解析成真正的视图

View

视图

View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf...)

SpringMVC的处理流程

1):用户发起请求到DispatcherServlet

2):DispatcherServlet请求HandlerMapping查找 Handler(可以根据xml配置、注解进行查找)

3):DispatcherServlet调用HandlerAdapter去执行Handler(Controller),Handler返回ModelAndView

4):DispatcherServlet请求ViewResolver去解析ModelAndView(根据逻辑视图名解析成真正的视图(jsp)),ViewResolver返回View

5):DispatcherServlet进行视图渲染,将Model填充到request域

6):DispatcherServlet向用户响应结果

前后端分离项目,从输入URL地址,到页面完成展示之间发生了什么

  1. 浏览器查找强缓存,如果命中则直接返回资源文件,否则进入下一步
  2. DNS解析,根据域名查到ip后向目标ip发送请求。DNS解析顺序:

-》浏览器DNS缓存

-》本地操作系统DNS缓存

-》路由器/交换机DNS缓存

-》互联网DNS缓存服务器

  1. 服务器对应端口侦听的web服务(以Tomcat为例)连接器(Connector)获得请求
  2. web服务连接器将请求交给服务引擎(Engine)
  3. 服务引擎根据域名查找主机(Host)
  4. 主机根据URI定位Web应用程序(Context)
  5. Web应用程序(以SpringMVC为例)根据URI定位资源:

-》DispatcherServlet接收请求,通过HandlerMapping查找Handler

-》DispatcherServlet调用HandlerAdapter去执行Handler(Controller)

-》Handler调用其他Spring组件处理业务,查询数据库,返回数据

-》DispatcherServlet请求HttpMessageConverter将数据转换为JSON

-》DispatcherServlet把JSON数据写入HttpServletResponse

  1. web服务(以Tomcat为例)原路返回HttpServletResponse:

Context-》Host-》Engine-》Connector-》浏览器

  1. 客户端浏览器接收响应,渲染页面

详见:一次搞定前端“核心主线”——从输入URL到页面展示发生了什么 - 知乎

    • XML配置的SpringMVC编程步骤

搭建环境

1)导包

需要Spring的所有jar包和Commons Logging日志包。此处示例版本为spring-framework-3.2.8.RELEASE 和commons-logging-1.2

Spring包下载地址: http://repo.springsource.org/libs-release-local/org/springframework/spring/ 选择需要的版本

Commons Logging包下载地址: Apache Commons Logging - Download Apache Commons Logging 选择需要的版本

2)在src下添加Spring的xml配置文件,名称可自定,例如spring-mvc.xml

代码略,初始内容同applicationContext.xml

配置DispatcherServlet组件

3)在web.xml中配置前端控制器组件DispatcherServlet并指定上一步添加的Spring配置文件的路径

web-app根元素

<!-- ========== 配置SpringMVC的前端控制器 ========== -->

<servlet>

<!-- DispatcherServlet的初始化方法会启动spring容器 -->

<servlet-name>springmvc</servlet-name>

<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

   <!-- contextConfigLocation用于指定SpringMVC的配置文件路径,不配置则默认为/WEB-INF/servlet名称-serlvet.xml -->

<init-param>

<param-name>contextConfigLocation</param-name>

<param-value>classpath:spring-mvc.xml</param-value>

</init-param>

     <!-- 在应用启动时立即加载该servlet -->

<load-on-startup>1</load-on-startup>

</servlet>

<servlet-mapping>

<servlet-name>springmvc</servlet-name>

<!--

   第一种(*.后缀):访问以 .后缀 结尾,由DispatcherServlet进行解析

   第二种(/):所以访问的地址都由DispatcherServlet进行解析,对于静态文件的解析需要配置不让DispatcherServlet进行解析

   使用此种方式可以实现 RESTful风格的url

   第三种(/*):这样配置不对,使用这种配置,最终要转发到一个jsp页面时,仍然会由DispatcherServlet解析jsp地址,不能根据jsp页面找到handler,会报错。

    -->

<url-pattern>*.do</url-pattern>

</servlet-mapping>

web-app根元素

步骤2)3)的原理:DispatcherServlet会根据配置文件信息创建一个WebApplicationContext容器对象,也称为上下文环境。WebApplicationContext继承自ApplicationContext容器,其初始化方式和BeanFactory、ApplicationContext有所区别,因为WebApplicationContext需要ServletContext实例,也就是说,它必须在拥有Web容器的前提下才能完成启动Spring Web应用上下文的工作。有了WebApplicationContext,就可以使用Spring的IOC、AOP等其他功能了。

编写Controller组件和ModelAndView组件

4)写Controller(处理器)的实现类,实现handleRequest方法,该方法返回ModelAndView对象,该对象可封装模型数据和逻辑视图名信息

package com.tongwx.controller;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;

import org.springframework.web.servlet.mvc.Controller;

public class HelloController implements Controller {

public ModelAndView handleRequest(HttpServletRequest arg0, HttpServletResponse arg1) throws Exception {

/*

 * ModelAndView有两个构造器:

 * (1)ModelAndView(String viewName), viewName是视图名。

 * (2)ModelAndView(String viewName, Map modelData), modelData是处理结果数据,将存储到request的attribute中。

 */

return new ModelAndView("hello");

}

}

5)写jsp

/WEB-INF/hello.jsp:

<h1>Hello,SpringMVC!</h1>

配置HandlerMapping组件和ViewResolver组件

6)在Spring配置文件中配置HandlerMappingViewResolver

HandlerMapping

通过HandlerMapping组件,前端控制器DispatcherServlet可以将客户HTTP请求映射到Controller组件上。

Spring提供了多种基于XML配置的HandlerMapping实现类,具体如下:

SimpleUrlHandlerMapping:维护一个HTTP请求和Controller映射关系列表(map),根据列表对应关系调用Controller。

ViewResolver

视图解析器ViewResolver通过Controller组件返回的ModelAndView实例中的逻辑视图名来解析视图。

Spring提供了多种视图解析器,具体如下:

UrlBasedViewResolver:将视图名直接解析成对应的URL,不需要显示地映射定义。 若视图名与视图资源名一致,就可以使用该解析器,而无需映射。

InternalResourceViewResolver:UrlBasedViewResolver的子类。它支持InternalResourceView(对Servlet和JSP的包装),以及其子类JstlView和TilesView响应类型。

VelocityViewResolver/FreeMarkerViewResolver:UrlBasedViewResolver的子类。它能支持Velocity和FreeMarker等视图技术。

beans根元素

<!-- ========== 配置处理器映射器HandlerMapping ========== -->

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">

<property name="mappings">

<props>

<!-- 第一步:指定url对应哪个handler名 -->

<prop key="/hello.do">helloController</prop>

</props>

</property>

</bean>

<!-- 第二步:指定handler名对应哪个handler类 -->

<bean id="helloController" class="com.tongwx.controller.HelloController" />

<!-- ========== 配置视图解析器ViewResolver ========== -->

<bean id="jspViewResolver"

class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix" value="/WEB-INF/jsp/" />

<property name="suffix" value=".jsp" />

</bean>

beans根元素

7)测试:

浏览器访问:http://localhost:8080/mySpringMvcDemo/hello.do 

    • SpringMVC编程步骤

使用注解声明Controller组件,可使Controller组件不用实现Controller接口,处理请求的方法因此可以灵活定义。

搭建环境

1)导包。方法同前。

2)添加一个Spring的配置文件。方法同前。

配置DispatcherServlet组件

3)配置DispatcherServlet并指定Spring配置文件路径方法同前。

编写Controller组件和ModelAndView组件

4)写Controller(处理器)。在类上添加@Controller注解,在类或方法上添加@RequestMapping注解并指定请求URI参数。这样就不需实现Controller接口方法可以自定义返回值可以是ModelAndView, 也可以是String

package com.tongwx.controller;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

@Controller

public class HiController {

@RequestMapping("/hi.do")

public String hello() {

//返回一个视图名

return "hi";

}

}

5)写jsp

/WEB-INF/hi.jsp:

<h1>Hi,SpringMVC!</h1>

配置HandlerMapping组件和ViewResolver组件

6)在Spring配置文件中配置以下三项内容:

开启组件扫描,指定Controller组件所在包,使@Controller注解生效;

开启MVC注解扫描,以扫描MVC的@RequestMapping等注解;

配置视图解析器,方法同前。

context:component-scan会扫描指定包的@Component、@Controller,@Service,@Respository注解。

context:component-scan还会扫描@Required、@Autowired、@PostConstruct、@PersistenceContext、@Resource、@PreDestroy等注解,因此

context:component-scan配了就不用配<context:annotation-config/>了。

Spring3.1之前的版本需作如下配置

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/><!-- 类前注解 -->

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/><!-- 方法前注解 -->

Spring3.2及之后的版本除了可用简化的<mvc:annotation-driven />配置,和Spring3.1及之前的配置外,还可用如下配置:

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/><!-- 类前注解 -->

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/><!-- 方法前注解 -->

beans根元素

<!-- 配置Spring组件扫描 -->

<context:component-scan base-package="com.tongwx.controller" />

<!-- 配置MVC注解扫描 -->

<mvc:annotation-driven />

<!-- 配置视图解析器 -->

<bean id="jspViewResolver"

class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix" value="/WEB-INF/jsp/" /><!-- 勿忘最后的斜杠 -->

<property name="suffix" value=".jsp" />

</bean>

beans根元素

7)测试:

浏览器访问:http://localhost:8080/mySpringMvcDemo/hi.do 

接收请求参数值前端为地址传参或表单传参时

方式 提供HttpServletRequest类型的方法参数

将request对象作为方法的入参即可,Spring会自动向入参注入请求参数值。

优点直接,缺点需要自己处理数据类型转换。

@RequestMapping("/checkLogin.do")

public String checkLogin(HttpServletRequest request) {

//从入参request中获取请求参数值

String username = request.getParameter("username");

String password = request.getParameter("password");

//向页面传值

request.setAttribute("username", username);

if (null != username && null != password && "tongwx".equals(username) && "123456".equals(password)) {

return "welcome";

}

return "error";

}

方式 提供与请求参数对应的方法参数

参数名一致时,Spring会自动将请求参数注入到方法参数;参数名不一致时,使用 @RequestParam("页面参数名") 注解映射不一致的名称。

优点参数类型自动转换,但可能出现类型转换异常。

@RequestMapping("/checkLogin2.do")

public ModelAndView checkLogin2(String username, @RequestParam("password") String pwd) {

//参数名一致时,Spring会自动将表单参数注入到方法参数

System.out.println(username);

//参数名不一致时,使用 @RequestParam("页面参数名") 注解映射不一致的名称

System.out.println(pwd);

//封装向页面传的值

Map<String, String> map = new HashMap<String, String>();

map.put("username", username);

if (null != username && null != pwd && "tongwx".equals(username) && "123456".equals(pwd)) {

//向页面传值

return new ModelAndView("welcome", map);

}

return new ModelAndView("error");

}

方式 提供封装请求参数的JavaBean对象作为方法参数

step1. 写一个JavaBean类,封装各个请求参数,属性名与请求参数的name相同。

step2. 将JavaBean对象作为处理器的方法参数。String将自动将请求参数注入对象。

@RequestMapping("/checkLogin3.do")

public ModelAndView checkLogin3(User user) {

System.out.println(user.getUsername());

System.out.println(user.getPassword());

//封装向页面传的值

Map<String, String> map = new HashMap<String, String>();

map.put("username", user.getUsername());

if (null != user.getUsername() && null != user.getPassword() && "tongwx".equals(user.getUsername())

&& "123456".equals(user.getPassword())) {

//向页面传值

return new ModelAndView("welcome", map);

}

return new ModelAndView("error");

}

特殊方式:路径参数

参数名一致时,可省略value属性

@Controller
@GlobalResponseBody
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;

    @PostMapping("/{idCard}")
    public User findByIdCard(@PathVariable(value = "idCard", required = true)String idCard){
        return userService.findByIdCard(idCard);
    }

}

前端请求url:http://localhost:8080/user/0013

向页面传值

(1)第一种方式 使用request对象

将数据绑订到request对象,然后转发给jsp。

(2)第二种方式 使用session对象

将数据绑订到session对象。

(3)第三种方式 使用ModelAndView对象

将数据封装到ModelAndView对象,然后作为方法的返回值。Model数据会通过HttpServletRequest的Attribute传到JSP页面中。

(4)第四种方式 使用ModelMap对象

将该对象作为方法的形参,将数据添加到该对象里面即可。ModelMap数据会通过HttpServletRequest的Attribute传到JSP页面中。

(4)第五种方式 使用@ModelAttribute注解

在处理器方法的参数部分或JavaBean属性方法上使用@ModelAttribute("JSP页面提取的名字")注解,@ModelAttribute数据就会通过HttpServletRequest的Attribute传到JSP页面中。

如果JSP的参数名和处理器方法的参数名一致,可省略该注解。

@RequestMapping("/checkLogin4.do")

//在处理器方法的参数部分或JavaBean属性方法上使用@ModelAttribute("JSP页面提取的名字")注解,

//@ModelAttribute数据就会通过HttpServletRequest的Attribute传到JSP页面中。

//如果JSP的参数名和处理器方法的参数名一致,可省略@ModelAttribute注解。

public String checkLogin4(@ModelAttribute("u") User user) {//JSP代码:${requestScope.u.username }

System.out.println(user.getUsername());

System.out.println(user.getPassword());

if (null != user.getUsername() && null != user.getPassword() && "tongwx".equals(user.getUsername())

&& "123456".equals(user.getPassword())) {

return "welcome";

}

return "error";

}

@ModelAttribute("username")

public String getUsername() {

return username;

}

重定向

SpringMVC默认采用转发方式定位视图,如果需要重定向方式可采用下面几种方法:

(1)方法的返回值是String时加上redirect:前缀,例如:

return "redirect:toIndex.do";

(2)方法的返回值是ModelAndView时以RedirectView为其构造参数,例如:

return new ModelAndView(new RedirectView("toIndex.do"));

处理表单编码问题

在web.xml中添加SpringMVC提供的一个过滤器(CharacterEncodingFilter)来解决:

web-app根元素

<!-- ========== 配置中文乱码过滤器 ========== -->

<filter>

<filter-name>encodingFilter</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>encodingFilter</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

web-app根元素

注意:

a. 表单数据要以post方式提交。
b. 页面编码要与过滤器指定的编码保持一致。

SpringMVC中获取HttpServletRequest的几种方法

  1. 方法参数:在Controller的方法参数上写上HttpServletRequest
  2. 从RequestContextHolder上下文获取:((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
  3. 依赖注入:@Autowired HttpServletRequest request;或@Resource HttpServletRequest request;注意不要使用@ModelAttribute

Spring MVC 中获取session的几种方法

  1. 先获取HttpServletRequest(方法见上面),再request.getSession()
  2. 方法参数:在Controller的方法参数上写上HttpSession session
  3. 使用@SessionAttributes:这种方式并不是直接获取到session,但是我们可以通过这种方式将我们想要值放入到session中去。

什么是拦截器

Spring提供的一个特殊的组件,前端控制器( DispatcherServlet)在收到请求之后,会先调用 拦截器,再调用处理器(Controller)。
注:
过滤器属于Servlet规范当中定义的特殊的组件。 而拦截器属于Spring框架。

如何写一个拦截器

step1. 写一个java类,实现HandlerInterceptor接口,在接口方法中实现具体的拦截处理逻辑。

拦截流程(两种):

preHandle(...)返回true --> 处理器方法 --> postHandle(...) --> 视图处理 -->afterCompletion(...);

preHandle(...)返回false --> 中断流程,不执行后续拦截器和处理器。

如果只需要实现某个方法,可以继承HandlerInterceptorAdapter。

package com.tongwx.interceptor;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpSession;

import org.springframework.web.servlet.HandlerInterceptor;

import org.springframework.web.servlet.ModelAndView;

/**

 * 登录验证拦截器

 */

public class LoginHandlerInterceptor implements HandlerInterceptor {

/**

 * 拦截除了登录之外的所有请求,并判断session中是否存在登录信息,若不存在则重定向到登录页面。

 */

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

throws Exception {

System.out.println(request.getRequestURI());

HttpSession session = request.getSession();

if (null == session.getAttribute("username")) {

response.sendRedirect("toLogin.do");

return false;

}

return true;

}

public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,

ModelAndView modelAndView) throws Exception {

}

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)

throws Exception {

}

}

step2. 在Spring配置文件中配置拦截器。

beans根元素

<!-- 配置登录验证拦截器 -->

<mvc:interceptors>

<mvc:interceptor>

<!-- 拦截路径 -->

<mvc:mapping path="/**"/>

<!-- 要排除的路径 -->

<mvc:exclude-mapping path="/toLogin.do"/>

<mvc:exclude-mapping path="/login*.do"/>

<!-- 拦截器类 -->

<bean class="com.tongwx.interceptor.LoginHandlerInterceptor"></bean>

</mvc:interceptor>

</mvc:interceptors>

beans根元素

将异常抛给Spring框架,让Spring框架来处理异常。

方式一:全局处理:配置简单异常处理器

step1.在Spring配置文件中,配置简单异常处理器(SimpleMappingExceptionResovler)。

<!-- 配置简单异常处理器 -->

<bean

class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">

<property name="exceptionMappings">

<props>

<!-- 指定异常类型和对应视图名 -->

<prop key="java.lang.NumberFormatException">error1</prop>

<prop key="java.lang.StringIndexOutOfBoundsException">error2</prop>

</props>

</property>

</bean>

step2.添加异常处理页面。获取异常对象名:${exception }

方式二:全局处理:实现HandlerExceptionResolver接口自定义异常处理器

适合全局处理有处理过程的异常。

step1.自定义一个异常处理器类实现HandlerExceptionResolver接口

import java.util.HashMap;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.HandlerExceptionResolver;

import org.springframework.web.servlet.ModelAndView;

public class MyMappingExceptionResolver implements HandlerExceptionResolver {

public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,

Exception ex) {

HashMap<String, Object> model = new HashMap<String, Object>();

model.put("ex", ex);

return new ModelAndView("exception", model);

}

}

step2.在Spring配置文件中配置自定义的异常处理器

<!-- 配置自定义异常处理器 -->

<bean id="exceptionHandler" class="com.tongwx.exception.MyMappingExceptionResolver"></bean>

step3.添加异常处理页面。

方式三:局部处理:使用@ExceptionHandler注解

适合局部处理有处理过程的异常。

step1. 在处理器类当中,添加一个异常处理方法,处理当前处理器的其它方法所抛出的异常。

/**

 * 异常处理方法(处理当前处理器的其它方法所抛出的异常)

 */

@ExceptionHandler

public String execute(Exception e, HttpServletRequest request) {

//依据异常类型,分别做不同的处理

if (e instanceof NumberFormatException) {

//异常的处理...

request.setAttribute("errorMsg", "数字转换异常");

return "exception2";

} else if (e instanceof StringIndexOutOfBoundsException) {

request.setAttribute("errorMsg", "下标越界");

return "exception2";

}

request.setAttribute("errorMsg", e);

return "system_error";

}

step2.添加异常处理页面。

注:如要批量处理局部异常,可定义一个局部异常处理类,添加上述方法,然后让需要使用该异常处理方法的处理器继承该类。

  1. 定义一个统一返回对象包装类:
  2. 定义一个统一返回注解,用于注解要使用统一返回的Controller类或其方法:
  3. 定义一个实现ResponseBodyAdvice接口的统一返回通知类:
  4. 返回String类型的包装需要特殊处理:
  5. 定义一个统一异常类:
  6. 在需要统一返回的类或方法上添加@GlobalResponseBody注解:
  7. 测试POST请求:http://localhost:8080/user/testReturnString,将返回:

package com.tongwx.demo.config;

import lombok.Data;

@Data
public class GlobalResponse {

    private Object obj;
    private boolean success;
    private String failureCode;
    private String failureMessage;

    public static GlobalResponse buildSuccess(Object obj) {
        GlobalResponse response = new GlobalResponse();
        response.setSuccess(true);
        response.setObj(obj);
        return response;
    }
    public static GlobalResponse buildFail(String message) {
        GlobalResponse response = new GlobalResponse();
        response.setSuccess(false);
        response.setFailureMessage(message);
        return response;
    }
}

package com.tongwx.demo.config;

import org.springframework.web.bind.annotation.ResponseBody;

import java.lang.annotation.*;

import static java.lang.annotation.RetentionPolicy.RUNTIME;


/**
 * Controller使用统一返回标记
 * 注意:接口请求参数中若含有HttpServletResponse,则该注解不起作用
 * {@link GlobalResponseAdvice}
 */
@Retention(RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ResponseBody
public @interface GlobalResponseBody {

}

package com.tongwx.demo.config;

import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.io.File;
import java.lang.annotation.Annotation;

/**
 * Controller使用统一返回
 */
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {

    private static final Class<? extends Annotation> ANNOTATION_TYPE = GlobalResponseBody.class;

    /**
     * 控制是否包装返回结果
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return AnnotatedElementUtils.hasAnnotation(methodParameter.getContainingClass(), ANNOTATION_TYPE)
                || methodParameter.hasMethodAnnotation(ANNOTATION_TYPE);
    }

    /**
     * 包装返回结果
     * 注:返回String的情况已特殊处理:{@link GlobalWebMvcConfigurer#configureMessageConverters(java.util.List)}
     */
    @Override
    public Object beforeBodyWrite(Object obj, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //防止重复包装和不需包装的情况
        if(obj instanceof GlobalResponse || obj instanceof File){
            return obj;
        }
        return GlobalResponse.buildSuccess(obj);
    }
}

package com.tongwx.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class GlobalWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        /*
        问题:
            当Controller返回String时,GlobalResponseAdvice#beforeBodyWrite方法将String处理成GlobalResponse
            在AbstractHttpMessageConverter#addDefaultHeaders调用实现类方法StringHttpMessageConverter#getContentLength,
            将GlobalResponse传给getContentLength方法第一个参数时发生ClassCastException
        解决方法:
            将json转换器放到转换器列表首位,使得先让json转换器处理返回值,这样String转换器就没机会处理了。
         */
        //
        converters.add(0, new MappingJackson2HttpMessageConverter());
        // 以下解决方式(删除String转换器)经测试无效:
//        converters.removeIf(httpMessageConverter -> httpMessageConverter.getClass() == StringHttpMessageConverter.class);
    }
}

package com.tongwx.demo.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
@Slf4j
public class GlobalExceptionAdvice {

    /**
     * 全局异常捕捉处理
     */
    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public GlobalResponse errorHandler(Exception ex) {
        if(ex.getMessage() != null && (
                ex.getMessage().startsWith("JSON parse error: parseDecimal error") ||
                        ex.getMessage().startsWith("JSON parse error: parseInt error"))){
            return GlobalResponse.buildFail("数字转换异常,请确保提交的数字类型参数中没有非数字");
        }

        log.error(ex.getMessage(), ex);
        return GlobalResponse.buildFail(ex.getMessage());
    }

    @ResponseBody
    @ExceptionHandler(value = RuntimeException.class)
    public GlobalResponse errorHandlerRuntimeException(RuntimeException ex) {
            return GlobalResponse.buildFail(ex.getMessage());
    }
}

package com.tongwx.demo.controller;

import com.tongwx.demo.config.GlobalResponseBody;
import com.tongwx.demo.entity.User;
import com.tongwx.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@GlobalResponseBody
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;

    @PostMapping("/{idCard}")
    public User findByIdCard(@PathVariable(value = "idCard", required = true)String idCard){
        return userService.findByIdCard(idCard);
    }
    @PostMapping("/testReturnString")
    public String findByIdCard(){
        return "好";
    }
}

{

    "obj""好",

    "success"true,

    "failureCode"null,

    "failureMessage"null

}

Spring MVC默认是单例模式,Controller、Service、Dao都是单例,减少了对象创建和垃圾收集的时间。

由于只有一个Controller的实例,当多个线程同时调用它的时候,它的成员变量就不是线程安全的,因此尽量避免在Controller中使用成员变量。如果一定要使用成员变量,则可以改用以下方式: 
1. Controller中声明 scope=”prototype”,即设置为多例模式 
2.在Controller中使用ThreadLocal变量,如:private ThreadLocal<Integer> count = new ThreadLocal<Integer>();

    1. + SpringMVC框架中的四种容器

容器

Web容器

Servlet容器

Spring容器

SpringMVC容器

英文

如Tomcat

ServletContext

Root WebApplicationContext

Servlet WebApplicationContext

创建者

服务器

服务器

ContextLoaderListener

DispatcherServlet

默认配置文件

server.xml

web.xml

applicationContext.xml

servlet名称-serlvet.xml

SpringMVC的启动过程:

Servlet容器

项目启动时,Tomcat会扫描每个WebApp的web.xml配置文件,找到Filter、Listener和Servlet等配置,并为每个WebApp创建各自的全局上下文servlet容器(ServletContext),每个WebApp下的所有Filter、Listener和Servlet共享一个servlet容器。

Spring容器(父上下文)

web.xml中配置的ContextLoaderListener在监听到ServletContext初始化时,会加载context-param标签内指定的配置文件(默认名称applicationContext.xml)并在contextInitialized方法中创建Spring的Ioc容器(Root WebApplicationContext接口,实际实现类是XmlWebApplicationContext),管理service层、dao层的Bean。

ContextLoaderListener会将Spring容器的引用存放到ServletContext中一个属性中,key是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE的值,可以使用Spring提供的工具类取出上下文对象:WebApplicationContextUtils.getWebApplicationContext(ServletContext);

SpringMVC容器(子上下文)

Spring容器初始化完毕后,第一个请求到达时,web.xml中配置的DispatcherServlet开始初始化,读取自己init-param标签内的配置文件(默认名称servlet名称-serlvet.xml)并在initStrategies方法中创建SpringMVC容器(Servlet WebApplicationContext接口,实际实现类也是XmlWebApplicationContext),管理Controller层的Bean。每个 DispatcherServlet有自己的SpringMVC容器。

DispatcherServlet会将SpringMVC容器的引用存放到ServletContext中一个属性中,key是"org.springframework.web.servlet.FrameworkServlet.CONTEXT"+Servlet名称,同时也存放到Request对象中,key是DispatcherServlet.class.getName() + ".CONTEXT"。可以使用工具类取出上下文对象:RequestContextUtils.getWebApplicationContext(request);

父子容器关系

DispatcherServlet通过ServletContext获取Spring容器,并将Spring容器设置为SpringMVC容器的父级容器。

子容器可以访问父容器,父容器不能访问子容器,从而在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象。

父子容器的选用

说明 :Spring 并没有限制我们,必须使用父上下文。我们可以自己决定如何使用。

方案一,传统型:

父上下文容器中保存数据源、服务层、DAO层、事务的Bean。

子上下文容器中保存Mvc相关的Action的Bean.

事务控制在服务层。

由于父上下文容器不能访问子上下文容器中内容,事务的Bean在父上下文容器中,无法访问子上下文容器中内容,就无法对子上下文容器中Action进行AOP(事务)。aop要想切入Controller,需要在父容器中配置扫描@Controller,但可能导致子容器无法生成前端控制器,导致页面请求404。

当然,做为“传统型”方案,也没有必要这要做。

方案二,激进型:

Java世界的“面向接口编程”的思想是正确的,但在增删改查为主业务的系统里,Dao层接口,Dao层实现类,Service层接口,Service层实现类,Action父类,Action。再加上众多的O(vo\po\bo)和jsp页面。写一个小功能 7、8个类就写出来了。 开发者说我就是想接点私活儿,和PHP,ASP抢抢饭碗,但我又是Java程序员。最好的结果是大项目能做好,小项目能做快。所以“激进型”方案就出现了-----没有接口、没有Service层、还可以没有众多的O(vo\po\bo)。那没有Service层事务控制在哪一层?只好上升到Action层。

本文不想说这是不是正确的思想,我想说的是Spring不会限制你这样做。

由于有了父子上下文,你将无法实现这一目标。解决方案是只使用子上下文容器,不要父上下文容器。所以数据源、服务层、DAO层、事务的Bean、Action的Bean都放在子上下文容器中。就可以实现了,事务(注解事务)就正常工作了。这样才够激进。

总结:不使用listener监听器来加载spring的配置文件,只使用DispatcherServlet来加载spring的配置,不要父子上下文,只使用一个DispatcherServlet,事情就简单了,什么麻烦事儿也没有了。

Java--大项目能做好--按传统方式做,规规矩矩的做,好扩展,好维护。

Java--小项目能做快--按激进方式做,一周时间就可以出一个版本,先上线接受市场(用户)的反馈,再改进,再反馈,时间就是生命(成本)。

测试Spring父子容器配置

1)SpringMVC的配置文件一定要放在DispatcherServlet中。

2)SpringAOP若想切入MVC,那么就把Spring配置文件也放在DispatcherServlet中。

测试结果:

Spring配置文件

SpringMVC配置文件

<aop:after-throwing>

统一异常切入记日志

HandlerExceptionResolver

统一异常拦截记日志并跳转错误页面

事务

全部配置文件放到DispatcherServlet中

扫描所有注解

不扫描

切入MVC切入Spring切入dao

拦截MVC拦截Spring拦截dao

事务正常,Spring异常回滚

不扫描

扫描所有注解

切入MVC切入Spring切入dao

拦截MVC拦截Spring拦截dao

事务正常,Spring异常回滚

扫描除Controller以外所有注解

只扫描Controller注解

切入MVC切入Spring切入dao

拦截MVC拦截Spring拦截dao

事务正常,Spring异常回滚

扫描所有注解

扫描所有注解

切入MVC切入Spring切入dao

拦截MVC拦截Spring拦截dao

事务正常,Spring异常回滚

全部配置文件放到ContextLoaderListener

随你怎么配

随你怎么配

启动报错

DispatcherServlet导ApplicationContext-mvc,ContextLoaderListener导applicationContext-*

扫描所有注解

扫描所有注解

未切入MVC未切入Spring切入dao

拦截MVC拦截Spring拦截dao

事务失效,Spring异常未回滚

扫描除Controller以外所有注解

只扫描Controller注解

未切入MVC切入Spring切入dao

拦截MVC拦截Spring拦截dao

事务正常,Spring异常回滚

扫描所有注解

只扫描Controller注解

未切入MVC切入Spring切入dao

拦截MVC拦截Spring拦截dao

事务正常,Spring异常回滚

扫描所有注解

不扫描

前端404

前端404

不扫描

扫描所有注解

未切入MVC未切入SpringC切入dao

拦截MVC拦截Spring拦截dao

事务失效,Spring异常未回滚

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值