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 复制代码
<?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 复制代码
<?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 复制代码
<?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】具体的处理器方法类及处理器方法

java 复制代码
@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

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标注;

java 复制代码
@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)定义:

java 复制代码
@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;

java 复制代码
@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】

java 复制代码
@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注解定义:只能标注方法参数,使得把请求参数绑定到方法参数;

java 复制代码
@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绑定;

java 复制代码
@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访问效果】

c++ 复制代码
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标注方法

java 复制代码
@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";
    }
}

【运行效果】

c++ 复制代码
@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】

java 复制代码
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】

java 复制代码
@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】

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>

【执行效果】

c++ 复制代码
@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 (无需我们手动注册);

java 复制代码
@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依赖(制品库)

xml 复制代码
<!-- 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】

java 复制代码
@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可以标注类与方法(具体的,标注处理器类与处理器方法)

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {

}

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

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

【@RestController】

java 复制代码
@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】

java 复制代码
@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】全局异常处理器

java 复制代码
@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】异常类

java 复制代码
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】异常类对应的异常信息模型

java 复制代码
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】抛出异常的处理器

java 复制代码
@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】

java 复制代码
@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

相关推荐
李慕婉学姐6 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆8 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin8 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20058 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉8 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国9 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882489 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈9 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_999 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹9 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理