spring揭秘26-springmvc06-springmvc注解驱动的web应用

【README】

本文部分内容总结自《spring揭秘》,作者王福强,非常棒的一本书,墙裂推荐;

此外,本文还参考了springmvc官方文档: Web on Servlet Stack

1)本文代码参见: github: springmvcDiscoverAnnotationDemo

2) 本文依赖xml配置初始化springmvc容器;(如需要通过java初始化springmvc容器,也很简单,自行实现,可以参考:Enable MVC Configuration



【1】springmvc注解驱动web应用

1)springmvc提供了2种类型注解,包括 @Controller 与 @RequestMapping, 用于标注二级控制器及请求处理方法,以便HandlerMapping查找被标注的二级控制器并把请求转给该控制器做业务逻辑处理;

  • 此外,依赖注解驱动springmvc应用, 需要在springmvc容器的xml配置文件中新增 component-scan元素,用于扫描注解标注的类,实例化bean并注册到springmvc容器;

2)通过springmvc注解与component-scan可以把注解标注的处理器类实例化后注册到springmvc容器; 但有2个问题:这些处理器如何被springmvc识别用于处理web请求呢? 第二个问题,如果不继承AbstractController,springmvc框架调用处理器的哪个方法处理web请求呢(具体的,是一级控制器调用HandlerAdapter,HandlerAdapter再调用处理器方法执行业务逻辑)

spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理) 可知,一级控制器DispatcherServlet通过web请求查找二级控制器并执行业务逻辑的步骤如下

  • 首先从HandlerMapping中查找是否存在与请求标志(如请求路径)匹配的处理器,若有则返回处理器包装对象HandlerExecutionChain;
  • 接着根据HandlerExecutionChain中封装的实际处理器查找HandlerAdapter处理器适配器;
  • 调用处理器适配器HandlerAdapter的handle方法, 处理器适配器再调用实际处理器的handle方法执行具体业务逻辑;

3)综上: 要实现通过注解驱动springmvc应用,springmvc需要提供3个组件,包括HandlerMaping, HandlerAdapter,以及Handler(注解标注的处理器,如Controller);

  • HandlerMapping: 从HandlerMapping中找出与请求标志(如URL)匹配的二级处理器(如Controller),并把二级处理器封装到HandlerExecutionChain再返回;
  • HandlerAdapter: 处理器适配器,用于上游一级控制器(如Dispatcher)调用统一的处理器接口执行业务逻辑; 适配器再调用具体二级处理器的业务逻辑方法;
  • Handler:二级处理器,封装了具体业务逻辑方法,如Controler(也就是被注解标注的目标类与方法 );


【1.1】springmvc注解驱动web应用的3个组件

1)spingmvc通过注解实现web请求处理的2个问题回顾:

  • 问题1:springmvc通过什么条件找出二级处理器处理web请求?
  • 问题2:springmvc找到二级处理器后,调用该处理器的哪个方法处理web请求?

2)springmvc提供的解决方案:

  • RequestMappingHandlerMapping: 封装请求标志(如路径)到@Controller与@RequestMapping标注的处理器的映射(具体说是处理器方法对象,类型为HandlerMethod; 这也再一次佐证处理器不止Controller一种,可以是其他类型,如HandlerMethod;当然了HandlerMethod封装了@Controller标注的处理器 );
    • 处理器方法对象HandlerMethod用beanType(类型为Class)封装 @Controller标注的类class, 用method(类型为反射的Method)封装@RequestMapping标注的方法;
    • 这在理论上解决了问题1与问题2; 为什么是理论上 ? 因为只是找到了具体的处理器方法(HandlerMethod),但如何调用处理器的处理方法,需要依赖于HandlerAdapter;
  • RequestMappingHandlerAdapter:DispatcherServlet传入request与处理器方法HandlerMethod对象到适配器handle方法,适配器handle方法调用HandlerMethod的handleInternal处理方法,底层通过反射调用到具体处理器的处理方法(@Controller标注类中@RequestMapping标注的方法)

在这里插入图片描述

【补充】invocableMethod封装了HandlerMethod对象;

3)springmvc注解驱动web应用的3个组件:

  • RequestMappingHandlerMapping: 封装请求标志(如路径)到@Controller与@RequestMapping标注的处理器(具体是HandlerMethod处理器方法对象)的映射
  • RequestMappingHandlerAdapter:DispatcherServlet传入request与处理器方法HandlerMethod对象到适配器handle方法,进而通过反射调用HandlerMethod的具体处理方法;
  • 这里只有2个组件,还有1个组件呢? 最后一个组件就是我们的二级处理器Handler,即被@Controller与@RequestMapping标注的处理器;


【1.2】springmvc注解驱动web应用代码实践

1)说明: 本文依赖xml配置初始化springmvc容器;(如需要通过java编码初始化springmvc容器,也很简单,自行实现,可以参考:Enable MVC Configuration

2)xml配置初始化springmvc容器的注意点:springmvc容器的xml配置文件【applicationContext.xml】,必须新增元素 <mvc:annotation-driven/> , <context:component-scan base-package=“com.tom.springmvc” />

  • mvc:annotation-driven作用: 启用mvc配置功能,并自动注册DispatcherServlet处理请求的基础设施bean ; 如下(参考 Special Bean Types):
    • HandlerMapping(RequestMappingHandlerMapping, SimpleUrlHandlerMapping )
    • HandlerAdapter (包括但不限于 RequestMappingHandlerAdapter
    • HandlerExceptionResolver
    • ViewResolver
    • LocaleResolver, LocaleContextResolver
    • ThemeResolver
    • MultipartResolver
    • FlashMapManager
  • context:component-scan作用(回顾):自动检测被注解标注的class并注册到spring容器; 注解列表如下。参见 Classpath Scanning and Managed Components
    • @Component, @Repository, @Service,@Controller, @RestController, @ControllerAdvice, @Configuration
    • 此外,context:component-scan还默认起到 <annotation-config/>标签的作用, 还可以激活这些注解: @Required,
      @Autowired, @PostConstruct, @PreDestroy, @Resource, @PersistenceContext and @PersistenceUnit (即配置了context:component-scan,则无需配置annotation-config标签)

【web.xml】

<?xml version="1.0" encoding="UTF-8"?>
<web-app
        xmlns = "https://jakarta.ee/xml/ns/jakartaee"
        xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation = "https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
        version = "5.0"
        metadata-complete = "false"
>
  <display-name>springmvcDiscover</display-name>

  <!-- 指定ContextLoaderListener加载web容器时使用的多个xml配置文件(默认使用/WEB-INF/applicationContext.xml) -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
  </context-param>

  <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>

  <!-- 注册过滤器代理 -->
  <filter>
    <filter-name>customFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>customFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!-- 配置监听器ContextLoaderListener,其加载顶层WebApplicationContext web容器-->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <!-- 注册一级控制器 DispatcherServlet,用于拦截所有请求(匹配url-pattern) -->
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- DispatcherServlet启动读取xml配置文件加载组件,构建web容器(子),通过contextConfigLocation为其配置多个xml文件-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
    </init-param>
    <load-on-startup>2</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>

</web-app>

【applicationContext.xml】springmvc顶级web容器加载的xml配置

<?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:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/enable.html -->
    <!--启用mvc配置, 该元素可以注册DispatcherServlet处理请求的基础设施bean,
    参见 https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/special-bean-types.html -->
    <mvc:annotation-driven/>

    <!-- https://docs.spring.io/spring-framework/reference/core/beans/classpath-scanning.html -->
    <!-- spring可以自动检测被注解标注的class并注册到spring容器 -->
    <!--context:component-scan 启用了 <context:annotation-config>的功能 -->
    <!-- 此外, context:component-scan 也包含了 AutowiredAnnotationBeanPostProcessor,CommonAnnotationBeanPostProcessor 功能 -->
    <context:component-scan base-package="com.tom.springmvc" />

    <!-- 注册SimpleMappingExceptionResolver-处理器异常解析器 -->
    <bean name="simpleMappingExceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="defaultErrorView" value="/error/defaultErrorPage" />
        <property name="exceptionAttribute" value="exceptionInfo" />
        <property name="exceptionMappings">
            <props>
                <prop key="com.tom.springmvc.exception.TomWebException">/error/tomWebErrorPage</prop>
                <prop key="java.lang.Exception">/error/exceptionBaseErrorPage</prop>
            </props>
        </property>
    </bean>

</beans>

【dispatcher-servlet.xml】DispatcherServlet初始化次顶级web容器的xml配置

<?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:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 注册自定义处理器拦截器 -->
    <bean id="timeCostHandlerInterceptor"  class="com.tom.springmvc.handlerinterceptor.TimeCostHandlerInterceptor"/>

    <!-- 注册HandllerMapping bean到springweb容器, BeanNameUrlHandlerMapping使用URL与Controller的bean名称进行匹配 -->
    <bean id="beanNameUrlHandlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
        <property name="interceptors">
            <list>
                <ref bean="timeCostHandlerInterceptor" />
            </list>
        </property>
    </bean>

    <!-- 注册视图解析器bean到springweb容器 -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
        <property name="order" value="1" />
    </bean>

    <!-- 注册BeanNameViewResolver视图解析器到springweb容器(一级控制器的web容器WebApplicationContext) -->
    <bean id="beanNameViewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver">
        <property name="order" value="2" />
    </bean>

</beans>

【BankCardAnnotationController】具体的处理器方法类及处理器方法

@Controller
@RequestMapping("/annotation")
public class BankCardAnnotationController {

    private BankCardAppService bankCardAppService;

    public BankCardAnnotationController(BankCardAppService bankCardAppService) {
        System.out.println("BankCardAnnotationController created");
        this.bankCardAppService = bankCardAppService;
    }

    @RequestMapping(value = "/annotationQryCardList", method = RequestMethod.GET)
    public ModelAndView qryCardList(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("qryCardList accessed");
        ModelAndView modelAndView = new ModelAndView("bankCardListPage");
        modelAndView.addObject("bankCardList", bankCardAppService.listCard());
        return modelAndView;
    }
}

【bankCardListPage.jsp】前端展示页面jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"  import="java.util.List" import="java.util.ArrayList"  isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户列表</title>
</head>
<body>
    <table border="1" cellpadding="0" cellspacing="0" bordercolor="#000000">
        <tr>
            <td>编号</td>
            <td>银行卡号</td>
            <td>备注</td>
        </tr>
        <c:forEach items="${bankCardList}" var="bankCard">
            <tr>
                <td>${bankCard.id}</td>
                <td>${bankCard.cardNo}</td>
                <td>${bankCard.remark}</td>
            </tr>
        </c:forEach>
    </table>
</body>
</html>

【访问效果】

在这里插入图片描述



【2】springmvc常用注解

springmvc常用注解清单参见(包括标注类,方法及方法参数的注解): springmvc Annotated Controllers



【2.1】@Controller注解(标注处理器类)

1)Controller注解定义:Controller是元注解: 因为它被其他注解Component标注;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";
}

2)@Controller注解: component-scan元素可以扫描被@Controller标注的类,并注册到spring容器; RequestMappingHandlerMapping 从springmvc容器中查找被Controller标注的处理器,作为处理请求的候选处理器,以便handlerMapping在请求映射时查找;



【2.2】@RequestMapping注解(标注处理器类或处理器方法)

1)@RequestMapping详细介绍,参见 : RequestMapping API

2)定义:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
@Reflective({ControllerMappingReflectiveProcessor.class})
public @interface RequestMapping {
    String name() default "";

    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};

    String[] params() default {};

    String[] headers() default {};

    String[] consumes() default {};

    String[] produces() default {};
}

3)@RequestMapping注解:显然,它可以标注类或方法;标注类, 标识该类是处理器所在类;标注方法,标识该方法是处理请求的方法;

  • 参数:默认是path路径属性值; 若@RequestMapping同时标注类或方法,则其处理的请求路径是标注类的路径拼接上标注方法的路径;如BankCardAnnotationController的qryCardList处理方法映射的请求路径是 /annotation/annotationQryCardList


【2.2.1】@RequestMapping注解的params元素

【BankCardAnnotationController2】

@RequestMapping标注方法,且params = {“k1=v1”, “k2=v2”}, 表示请求必须带参数k1与k2,否则报请求错误404;

@Controller
@RequestMapping("/annotation2")
public class BankCardAnnotationController2 {

    private final BankCardAppService bankCardAppService;

    public BankCardAnnotationController2(BankCardAppService bankCardAppService) {
        this.bankCardAppService = bankCardAppService;
    }

    @RequestMapping(value = "/annotationQryCardList", params = {"k1=v1", "k2=v2"}, method = {RequestMethod.GET, RequestMethod.POST})
    public ModelAndView qryCardList(String k1, String k2) throws Exception {
        System.out.println("qryCardList accessed");
        System.out.println("k1=" + k1 + ", k2=" + k2);
        ModelAndView modelAndView = new ModelAndView("bankCardListPage");
        modelAndView.addObject("bankCardList", bankCardAppService.listCard());
        return modelAndView;
    }
}

【访问效果】

在这里插入图片描述



【2.2.2】@RequestMapping注解的consumes与produces元素

【注意】若@RequestMapping注解同时标注了类与方法,且带了consumes与produces元素,则方法级别的注解元素值覆盖类级别;

1)consumes元素:可以配置1个或多个mime类型,表示被标注方法可以处理的请求的MIME类型,用于匹配请求的Content-Type实体头;

  • Content-Type实体头: 用于指出http实体内容的MIME类型, 客户端发送的请求报文与服务器返回的响应报文都可以包含Content-Type;
  • 简单理解:被标注方法的consumes元素值与客户端请求实体头Content-Type进行匹配(包含逻辑运算),若匹配成功,则被标注方法可以处理该请求,否则查找下一个被标注方法
  • MIME:多用途互联网邮件扩展类型,描述报文实体的数据格式,如 application/json (json格式), text/plain (普通文本), text/html (html格式);

2)produces元素 :可以配置1个或多个mime类型,表示被标注方法可以处理的请求愿意接受的MIME类型, 用于匹配请求的Accept请求头;

  • Accept请求头: 用于指出客户端能够处理的MIME类型;可以包含多个,如 Accept: text/html,image/*
  • 简单理解:即被标注方法的produces元素值与客户端请求头Accept进行匹配,若匹配成功,则被标注方法可以处理该请求,否则查找下一个被标注方法; (若客户端没有带Accept请求头,则服务器认为客户端接收任何MIME类型的响应报文)

【BankCardAnnotationController3】

@Controller
@RequestMapping("/annotation3")
public class BankCardAnnotationController3 {

    private final BankCardAppService bankCardAppService;

    public BankCardAnnotationController3(BankCardAppService bankCardAppService) {
        this.bankCardAppService = bankCardAppService;
    }

    @RequestMapping(value = "/annotationQryCardList", consumes = {"text/plain;charset=UTF-8"}, produces = {"text/html;charset=UTF-8"}, method = {RequestMethod.GET, RequestMethod.POST})
    public ModelAndView qryCardList(String k1, String k2) throws Exception {
        System.out.println("qryCardList accessed");
        System.out.println("k1=" + k1 + ", k2=" + k2);
        ModelAndView modelAndView = new ModelAndView("bankCardListPage");
        modelAndView.addObject("bankCardList", bankCardAppService.listCard());
        return modelAndView;
    }
}

在这里插入图片描述


【补充】如果通过浏览器访问,没有指定Content-Type实体头部,则报415,HTTP状态 415 - 不支持的媒体类型 ;



【2.3】@RequestParam注解(标注方法参数)

1)@RequestParam注解定义:只能标注方法参数,使得把请求参数绑定到方法参数;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true; // 默认被标注的方法参数一定有值

    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

【BankCardAnnotationControllerWithRequestParam】 使用@RequestParam标注userName入参;把请求参数k1与userName绑定;

@Controller
@RequestMapping("/bankCardAnnotationControllerWithRequestParam")
public class BankCardAnnotationControllerWithRequestParam {

    private final BankCardAppService bankCardAppService;

    public BankCardAnnotationControllerWithRequestParam(BankCardAppService bankCardAppService) {
        this.bankCardAppService = bankCardAppService;
    }

    @RequestMapping(value = "/annotationQryCardList", consumes = {"text/plain;charset=UTF-8"}, produces = {"text/html;charset=UTF-8"}, method = {RequestMethod.GET, RequestMethod.POST})
    public ModelAndView qryCardList(@RequestParam("k1") String userName) throws Exception {
        System.out.println("userName=" + userName);
        ModelAndView modelAndView = new ModelAndView("bankCardListPage");
        modelAndView.addObject("bankCardList", bankCardAppService.listCard());
        return modelAndView;
    }
}

【postman访问效果】

userName=tom

在这里插入图片描述



【2.4】@ModelAttribute注解(标注方法或方法参数)

1)@ModelAttribute注解定义: 标注方法或方法参数;本章节部分内容总结自 Spring MVC and the @ModelAttribute Annotation

  • 作用:把方法参数或方法返回值作为属性绑定到ModelAndView;


【2.4.1】@ModelAttribute注解标注处理器方法(HandlerMethod)

1)@ModelAttribute注解标注方法时:它接收的参数类型与 @RequestMapping相同;但是 @ModelAttribute标注的方法不能匹配请求;

2)同一个处理器中,@ModelAttribute注解标注的方法先于@RequestMapping注解标注的方法执行;因为在执行任何处理器方法前(@RequestMapping标注),模型对象已经被创建了;(即@ModelAttribute标注的方法执行时添加到model的属性,所有的处理器方法在执行时都可以使用)

【HelloWorldAnnotationControllerWithModelAttribute】@ModelAttribute标注方法

@Controller
@RequestMapping("/helloWorld")
public class HelloWorldAnnotationControllerWithModelAttribute {

    private final BankCardAppService bankCardAppService;

    public HelloWorldAnnotationControllerWithModelAttribute(BankCardAppService bankCardAppService) {
        this.bankCardAppService = bankCardAppService;
    }

    @ModelAttribute("tips")
    public String helloWorld(@RequestParam("k1") String userName) throws Exception {
        System.out.println("@ModelAttribute annotates helloWorld() userName=" + userName);
        return "hello world";
    }

    @ModelAttribute
    public void helloWorld2(ModelMap modelMap) throws Exception {
        System.out.println("@ModelAttribute annotates helloWorld2()");
        modelMap.addAttribute("tips2", "hello world 2");
    }

    @RequestMapping(value = "/helloWorld3", consumes = {"text/plain;charset=UTF-8", "text/html;charset=UTF-8"}, produces = {"application/json;charset=UTF-8"}
            , method = {RequestMethod.GET, RequestMethod.POST})
    public String helloWorld3(@RequestParam("k1") String userName, ModelMap modelMap) throws Exception {
        System.out.println("@RequestMapping helloWorld(): userName=" + userName);
        System.out.println("@RequestMapping helloWorld():  tips = " + modelMap.getAttribute("tips"));
        System.out.println("@RequestMapping helloWorld():  tips2 = " + modelMap.getAttribute("tips2"));
        return "helloWorld";
    }
}

【运行效果】

@ModelAttribute annotates helloWorld2()
@ModelAttribute annotates helloWorld() userName=tom
@RequestMapping helloWorld3(): userName=tom
@RequestMapping helloWorld3():  tips = hello world
@RequestMapping helloWorld3():  tips2 = hello world 2 

【代码解说】

  • 1)@ModelAttribute(“tips”)标注方法helloWorld, 该方法返回值hello world作为ModelAndView的属性值,属性名=tips(ModelAttribute注解的value元素值); 所以我们在helloWorld3()方法中可以通过ModelMap获取到tips属性值;
  • 2)ModelMap是什么: ModelMap类型为LinkedHashMap,是ModelAndView中的一个属性; ModelAndView新增对象方法addObject(),实际是把attributeName与attributeValue新增到ModelMap中; (简单理解:ModelAndView把ModelMap作为属性进行封装,操作ModelAndView就是操作ModelMap,两者可以等价 );
  • 3)@ModelAttribute(“tips”)标注方法helloWorld2(),其方法参数可以引用ModelMap,方法体内部显式操作ModelMap;

【ModelAndView】

public class ModelMap extends LinkedHashMap<String, Object> {
    //...
}

public class ModelAndView {

	/** View instance or view name String. */
	@Nullable
	private Object view;

	/** Model Map. */
	@Nullable
	private ModelMap model; //  ModelMap model 作为 ModelAndView的一个属性  

	/** Optional HTTP status for the response. */
	@Nullable
	private HttpStatusCode status;

	/** Indicates whether this instance has been cleared with a call to {@link #clear()}. */
	private boolean cleared = false;
    
    // ModelAndView新增对象
    public ModelAndView addObject(String attributeName, @Nullable Object attributeValue) {
		getModelMap().addAttribute(attributeName, attributeValue);
		return this;
	}
    // ... 
}


【2.4.2】@ModelAttribute注解标注处理器方法参数(HandlerMethod Parameter)

1)@ModelAttribute注解标注处理器方法参数: 它表明springmvc会把注解的value元素值作为属性键从ModelAndView中查找属性值,并作为与属性键同名的方法参数值调用处理器方法

  • 若ModelAndView中不存在该属性,则初始化一个属性键-属性值对,并添加到ModelAndView;

【HelloWorldAnnotationControllerWithModelAttribute】

@RequestMapping(value = "/helloWorld4", consumes = {"text/plain;charset=UTF-8", "text/html;charset=UTF-8"}, produces = {"application/json;charset=UTF-8"}
            , method = {RequestMethod.GET, RequestMethod.POST})
    public String helloWorld4(@ModelAttribute("bankCardDto") BankCardDto bankCardDto, ModelMap modelMap) throws Exception {
        System.out.println("@RequestMapping helloWorld4()");
        modelMap.addAttribute("bankCardDto", bankCardDto);
        bankCardDto.setId(1L);
        bankCardDto.setRemark(String.valueOf(modelMap.getAttribute("tips")));
        return "helloWorld";
    }

【helloWorld.jsp】

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"  import="java.util.List" import="java.util.ArrayList"  isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>helloWorld</title>
</head>
<body>
    tips:  ${tips} <br>
    tips2:  ${tips2} <br>
    bankCardDto:  id = ${bankCardDto.id};cardNo = ${bankCardDto.cardNo}; remark = ${bankCardDto.remark};
</body>
</html>

【执行效果】

@ModelAttribute annotates helloWorld() userName=tom
@ModelAttribute annotates helloWorld2()
BankCardDto Constructor without arguments // 显然ModelAndView没有属性=bankCardDto的值,所以ModelAndView初始化一个(调用构造器方法)
@RequestMapping helloWorld4()

在这里插入图片描述



【2.5】@RequestBody注解标注处理器方法参数

1)@RequestBody注解标注处理器方法参数: 表示springmvc读取请求体并通过HttpMessageConverter反序列化为Object对象,作为处理器方法参数调用方法;

2) 使用@RequestBody或@ResponseBody,需要注册MappingJackson2HttpMessageConverter 消息转换器; 只需要引入jackson-core与jackson-databind制品库即可;springmvc在classpath下检测到jackson制品库存在,则springmvc启动时默认注册MappingJackson2HttpMessageConverter (无需我们手动注册);

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestBody {

	/**
	 * Whether body content is required.
	 * <p>Default is {@code true}, leading to an exception thrown in case
	 * there is no body content. Switch this to {@code false} if you prefer
	 * {@code null} to be passed when the body content is {@code null}.
	 * @since 3.2
	 */
	boolean required() default true;

}

@RequestBody只能标注参数

3)@RequestBody的作用是把请求体反序列化为Object对象; 请求体通常采用json格式,所以处理器方法@RequestMapping的consumes应该设置为application/json;


【pom.xml】引入jackson-core与jackson-databind依赖(制品库)

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-core</artifactId>
  <version>2.18.0</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.18.0</version>
</dependency>


【2.5.1】@RequestBody注解标注处理器方法参数代码实践

【HelloWorldControllerRequestBody】

@Controller
@RequestMapping("/helloWorldControllerRequestBody")
public class HelloWorldControllerRequestBody {

    public HelloWorldControllerRequestBody() {
        // do nothing.
    }

    @RequestMapping(value = "/helloWorldRequestBody", consumes = {"application/json;charset=UTF-8"}
            , produces = {"text/html;charset=UTF-8", "application/json;charset=UTF-8"}
            , method = {RequestMethod.POST})
    public String helloWorldRequestBody(@RequestBody BankCardDto bankCardDto, ModelMap modelMap) throws Exception {
        System.out.println("@RequestMapping @RequestBody helloWorldRequestBody()");
        modelMap.addAttribute("bankCardDto", bankCardDto);
        bankCardDto.setRemark(bankCardDto.getCardNo() + "-@RequestBody备注");
        return "helloWorld";
    }
}

【访问效果】

在这里插入图片描述


3)默认情况下,以下HttpMessageConverters实例被springmvc自动注册,参见 springmvc Http Message Converters

  • ByteArrayHttpMessageConverter – converts byte arrays (自动注册,无需条件
  • StringHttpMessageConverter – converts Strings (自动注册,无需条件
  • ResourceHttpMessageConverter – converts org.springframework.core.io.Resource for any type of octet stream (自动注册,无需条件
  • SourceHttpMessageConverter – converts javax.xml.transform.Source (自动注册,无需条件
  • FormHttpMessageConverter – converts form data to/from a MultiValueMap<String, String> (自动注册,无需条件
  • Jaxb2RootElementHttpMessageConverter – converts Java objects to/from XML (added only if JAXB2 is present on the classpath)
  • MappingJackson2HttpMessageConverter – converts JSON (added only if Jackson 2 is present on the classpath) 只要Jackson2类在classpath路径下存在
  • MappingJacksonHttpMessageConverter – converts JSON (added only if Jackson is present on the classpath) 只要Jackson类在classpath路径下存在
  • AtomFeedHttpMessageConverter – converts Atom feeds (added only if Rome is present on the classpath)
  • RssChannelHttpMessageConverter – converts RSS feeds (added only if Rome is present on the classpath)


【2.6】@ResponseBody注解标注处理器方法

部分内容总结自: springmvc @ResponseBody

1)@ResponseBody注解标注处理器方法: 表示springmvc通过HttpMessageConverter把返回结果序列化为响应报文体;

2) 与@RequestBody类似,使用@ResponseBody,需要注册MappingJackson2HttpMessageConverter 消息转换器; 只需要引入jackson-core与jackson-databind制品库即可; 注册方法参见2.5章节

【@ResponseBody】@ResponseBody可以标注类与方法(具体的,标注处理器类与处理器方法)

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {

}

3)@ResponseBody也支持标注处理器类, 所有的处理器方法都继承处理器类的标注,即所有处理器方法的返回结果都会被HttpMessageConverter序列化为响应报文体;

  • 这实际上是@RestController 起的作用,@RestController是包含@Controller与@ResponseBody的元注解

【@RestController】

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller // 控制器类注解
@ResponseBody  // 返回结果序列化为响应报文体注解 
public @interface RestController {

    /**
     * The value may indicate a suggestion for a logical component name,
     * to be turned into a Spring bean in case of an autodetected component.
     * @return the suggested component name, if any (or empty String otherwise)
     * @since 4.0.1
     */
    @AliasFor(annotation = Controller.class)
    String value() default "";

}


【2.6.1】@ResponseBody注解标注处理器方法代码实践

【HelloWorldControllerResponseBody】

@Controller
@RequestMapping("/helloWorldControllerResponseBody")
public class HelloWorldControllerResponseBody {

    private final BankCardAppService bankCardAppService;

    public HelloWorldControllerResponseBody(BankCardAppService bankCardAppService) {
        this.bankCardAppService = bankCardAppService;
    }

    @RequestMapping(value = "/helloWorldResponseBody", consumes = {"application/json;charset=UTF-8"}
            , produces = {"application/json;charset=UTF-8"}
            , method = {RequestMethod.POST})
    @ResponseBody
    public List<BankCardDto> helloWorldResponseBody() throws Exception {
        System.out.println("@RequestMapping @ResponseBody helloWorldResponseBody()");
        return bankCardAppService.listCard();
    }
}

【访问效果】 处理器方法helloWorldResponseBody返回结果是List<BankCardDto>, 因为处理器方法被@ResponseBody标注,所以springmvc会通过HttpMessageConverter(具体是MappingJackson2HttpMessageConverter 消息转换器)序列化为json格式的响应报文体;

在这里插入图片描述



【2.6.2】@ResponseBody注解标注处理器方法调试

在这里插入图片描述



【2.7】@ControllerAdvice注解(标注类),又叫Controller增强注解

1)@ControllerAdvice注解定义的背景:

  • 问题:@ExceptionHandler、@InitBinder 和 @ModelAttribute 方法仅适用于被 @Controller 标注的类或类层次结构,无法应用于springmvc应用中所有被 @Controller标注的类; 即每个处理器(Controller)都需要写一套@ExceptionHandler、@InitBinder 和 @ModelAttribute , 导致代码冗余
  • 解决方法:引入@ControllerAdvice与@RestControllerAdvice注解; 在@ControllerAdvice或@RestControllerAdvice注解标注的类中使用@ExceptionHandler、@InitBinder 和 @ModelAttribute 标注的方法可以应用于任何控制器;最大的优点之一是可以把整个springmvc应用的异常处理逻辑集中在一个类来处理,而不是散落在各个处理器类中


【2.7.1】@ControllerAdvice注解声明统一异常处理代码实践

总结自: Understanding Spring’s @ControllerAdvice

1)@ControllerAdvice注解声明统一异常处理的关注点:

  • 创建自身的异常类;
  • 一个应用只有一个@ControllerAdvice标注的类;
  • 在ControllerAdvice类中编写处理异常方法,并使用@ExceptionHandler标注;
  • 对于每一个异常都提供给一个异常处理器;

【GlobalExceptionHandler】全局异常处理器

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Log LOGGER = LogFactory.getLog(GlobalExceptionHandler.class);

    public GlobalExceptionHandler() {
        // do nothing.
    }

    @ExceptionHandler({TomWebException.class, Exception.class})
    public ResponseEntity<TomWebModel> handleException(Exception ex, WebRequest request) {
        // 打印异常栈
        LOGGER.error("GlobalExceptionHandler#handleException() method handle exception. ", ex);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        if (ex instanceof TomWebException tomWebException) {
            return new ResponseEntity<>(new TomWebModel(tomWebException), headers, HttpStatus.OK);
        }
        return new ResponseEntity<>(new TomWebModel("000000", ex.getMessage()), headers, HttpStatus.OK);
    }
}

【TomWebException】异常类

public class TomWebException extends RuntimeException {

    private String code;

    private String message;

    public TomWebException() {
        super();
    }

    public TomWebException(String message) {
        super("TomWebException-" + message);
    }

    public TomWebException(String code, String message) {
        super(code + "-" + message);
        this.code = code;
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

【TomWebModel】异常类对应的异常信息模型

public class TomWebModel {

    private String code;

    private String message;

    public TomWebModel() {
        // do nothing
    }

    public TomWebModel(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public TomWebModel(TomWebException tomWebException) {
        this.code = tomWebException.getCode();
        this.message = tomWebException.getMessage();
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

【TomWebThrowExceptionController】抛出异常的处理器

@Controller
public class TomWebThrowExceptionController {

    @RequestMapping("/throwException")
    protected ModelAndView throwException(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (Objects.isNull(request.getParameter("testParamKey"))) {
            throw new TomWebException(BusiDateUtils.getNowTextYearToSecond() + " testParamKey查无记录");
        }
        return new ModelAndView("index");
    }

    @RequestMapping("/throwExceptionWithCode")
    protected ModelAndView throwExceptionWithCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (Objects.isNull(request.getParameter("testParamKey"))) {
            throw new TomWebException("TOM001", BusiDateUtils.getNowTextYearToSecond() + " testParamKey查无记录");
        }
        return new ModelAndView("index");
    }
}

【异常捕获效果】

在这里插入图片描述



【2.8】@PathVariable注解(标注处理器方法参数)

总结自 Spring @PathVariable Annotation

1) @PathVariable注解(标注处理器方法参数): @PathVariable注解被用于解析请求路径节点,并把路径节点绑定到方法参数;



【2.8.1】@PathVariable注解(标注处理器方法参数)代码实践

【HelloWorldControllerPathVariable】

@Controller
@RequestMapping("/sichuan")
public class HelloWorldControllerPathVariable {

    private final BankCardAppService bankCardAppService;

    public HelloWorldControllerPathVariable(BankCardAppService bankCardAppService) {
        this.bankCardAppService = bankCardAppService;
    }

    @RequestMapping(value = "/{city}/{user}", consumes = {"application/json;charset=UTF-8"}
            , produces = {"application/json;charset=UTF-8"}
            , method = {RequestMethod.POST})
    @ResponseBody
    public UserDto findUser(@PathVariable("city") String city, @PathVariable("user") String userName) throws Exception {
        return UserDto.build(System.currentTimeMillis(), userName, city);
    }
}

【访问效果】

在这里插入图片描述



【3】处理器方法入参与出参类型(仅了解)

【3.1】处理器方法常用输入参数类型

参见: springmvc handler method arguments

1)处理器方法常用输入参数类型:

  • WebRequest
    jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse, jakarta.servlet.http.HttpSession

  • HttpMethod

  • java.util.Locale

  • java.util.TimeZone + java.time.ZoneId

  • java.io.InputStream, java.io.Reader

  • java.io.OutputStream, java.io.Writer

  • @PathVariable

  • @RequestParam

  • @RequestHeader

  • @CookieValue

  • @RequestBody

  • HttpEntity

  • @RequestPart

  • java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap

  • RedirectAttributes

  • @ModelAttribute

  • Errors, BindingResult

  • SessionStatus + class-level @SessionAttributes

  • UriComponentsBuilder

  • @SessionAttribute

  • @RequestAttribute



【3.2】处理器方法常用返回结果参数类型

参见: springmvc return typs

1)处理器方法常用返回结果参数类型:

  • @ResponseBody

  • HttpEntity, ResponseEntity

  • HttpHeaders

  • ErrorResponse

  • String

  • View

  • java.util.Map, org.springframework.ui.Model

  • @ModelAttribute

  • ModelAndView object

  • void

  • Callable

  • ListenableFuture, java.util.concurrent.CompletionStage, java.util.concurrent.CompletableFuture
    StreamingResponseBody



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值