文章目录
- 【README】
- 【1】springmvc注解驱动web应用
- 【2】springmvc常用注解
-
- 【2.1】@Controller注解(标注处理器类)
- 【2.2】@RequestMapping注解(标注处理器类或处理器方法)
- 【2.3】@RequestParam注解(标注方法参数)
- 【2.4】@ModelAttribute注解(标注方法或方法参数)
-
- 【2.4.1】@ModelAttribute注解标注处理器方法(HandlerMethod)
- [【2.4.2】@ModelAttribute注解标注处理器方法参数(HandlerMethod Parameter)](#【2.4.2】@ModelAttribute注解标注处理器方法参数(HandlerMethod Parameter))
- 【2.5】@RequestBody注解标注处理器方法参数
- 【2.6】@ResponseBody注解标注处理器方法
- 【2.7】@ControllerAdvice注解(标注类),又叫Controller增强注解
- 【2.8】@PathVariable注解(标注处理器方法参数)
- 【3】处理器方法入参与出参类型(仅了解)
【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】处理器方法常用返回结果参数类型
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