SpringMVC
核心流程
1.客户端发送请求
2.DispatcherServlet统一接收请求,需要维护一个Spring容器(ApplicationContext),使用init方法完成ApplicationContext的初始化,首先需要看servletContext中是否已有容器,如果有就直接用,如果没有就初始化(有可能会在listener中维护),容器维护Controller组件,这些组件中包含Handler方法
3.访问到doGet和doPost方法,经过doService然后再合并到doDispatch中
4.由doDispatch来获得url,通过HandlerMapping建立映射关系
5.经过HandlerAdapter,最终执行到HandlerMethod(容器中的组件的方法)
init方法
public final void init() throws ServletException { this.initServletBean(); } protected final void initServletBean() throws ServletException { try { this.webApplicationContext = this.initWebApplicationContext(); } }
WebApplicationContext也是容器,是ApplicationContext的子接口;还增加对Web应用的支持,在WebApplicationContext中可以使用ServletContext
doDispatch
分发请求
protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.processRequest(request, response); } protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.processRequest(request, response); }
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doService(request, response); }
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { this.doDispatch(request, response); }
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HandlerExecutionChain mappedHandler = null; try { try { ModelAndView mv = null; Object dispatchException = null; try { // HandlerMapping通过url获得对应的Handler方法 mappedHandler = this.getHandler(processedRequest); if (mappedHandler == null) { this.noHandlerFound(processedRequest, response); return; } // 适配器HandlerAdapter HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 执行Handler方法 → 根据HandlerMapping的执行结果去执行的 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } this.applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception var20) { dispatchException = var20; } catch (Throwable var21) { dispatchException = new NestedServletException("Handler dispatch failed", var21); } // 处理执行后的结果 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); } catch (Exception var22) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22); } catch (Throwable var23) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23)); } } finally { if (asyncManager.isConcurrentHandlingStarted()) { if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else if (multipartRequestParsed) { this.cleanupMultipart(processedRequest); } } }
创建一个完整的SpringMVC应用
引入依赖
spring-web、spring-webmvc、spring相关依赖(5+1)
servlet-api(provided)
jackson-annotations、core、databind(json)
只需要引入标红的即可
<dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>3.0-alpha-1</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.15.RELEASE</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.4</version> </dependency>
配置DispatcherServlet
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <!--全局(根路径下的jsp → 类似于html 👉 会将jsp编译为Servlet)--> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
还需要提供Spring的配置文件所在的位置
public void setContextConfigLocation(@Nullable String contextConfigLocation) { this.contextConfigLocation = contextConfigLocation; }
在servlet标签中使用init-param标签 👉 set方法
<init-param> <!--set方法是谁 👉 setContextConfigLocation--> <param-name>contextConfigLocation</param-name> <!--set方法的形参是什么--> <param-value>classpath:application.xml</param-value> </init-param>
配置文件
<context:component-scan base-package="com.cskaoyan"/> <!--springmvc的注解驱动 👉 类型转换、参数校验、Json--> <mvc:annotation-driven/>
运行流程
访问hello请求进入了helloController里的hello方法
我们打断点查看doDispatch方法的流程,断点位置如下
搭建应用的步骤
1.pom.xml→3行依赖
2.web.xml→创建模板
3.application.xml→创建模板
常见的依赖:
<properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <packaging>war</packaging> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>3.0-alpha-1</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.15.RELEASE</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.4</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.15.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> </dependencies>
@RequestMapping
建立URL和Handler方法之间的映射关系
与EE中的Servlet类似,可以处理分发过来的请求
// 可以窄化请求,可以写在类上或方法上 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface RequestMapping { String name() default ""; // value属性是字符串数组,可以映射多个URL,也可以使用通配符 @AliasFor("path") String[] value() default {}; @AliasFor("value") String[] path() default {}; // 请求方法限定 → 关系是or,引申@GetMapping、@PostMapping RequestMethod[] method() default {}; // 请求参数限定 → 关系是and String[] params() default {}; // 请求头限定 → 关系是and String[] headers() default {}; // Content-Type请求头的值 → 关系是and String[] consumes() default {}; // Accept请求头的值 → 关系是and String[] produces() default {}; }
属性值中有s的代表的就是and的关系
***URL路径映射 value属性 → String[]
value属性是字符串数组
多个URL映射到同一个Handler方法上
/index 、/home
/hello、/helloworld
//注解的属性值为数组,如果数组中只有一个值,可以省略掉{} @RequestMapping({"hello", "helloworld"}) @ResponseBody public String hello() { return "hello world 2.0"; }
窄化请求
将请求中共有的部分提取到类上,达到窄化的效果
/user/login
/user/register
/user/modify
最终方法中映射的url就是:类上的@RequestMapping的value属性+方法上的@RequestMapping的value属性
分隔符会自动补充
@Controller @RequestMapping("user") public class UserController { //@RequestMapping("user/login") @RequestMapping("login") // → user/login @ResponseBody public String login() { return "login"; } }
使用通配符*
/goodbye/songge
/goodbye/ligenli
/goodbye/xueqie
/goodbye/nanfeng
/goodbye/*
@RequestMapping("goodbye/*") @ResponseBody public String goodbye() { return "byebye"; }
请求方法限定 method属性 → RequestMethod[]
限定了Handler方法只能处理特定的请求方法的请求
两个方法之间是or的关系
@Controller//("method") @RequestMapping("method") public class MethodController { @RequestMapping(value = "get",method = RequestMethod.GET) //method/get @ResponseBody public String methodGet() { return "Method GET"; } @RequestMapping(value = "post",method = RequestMethod.POST) @ResponseBody public String methodPost() { return "Method POST"; } //增加两个值,这两个值之间的关系 → or → 也就是满足其中一项即可 @RequestMapping(value = "double",method = {RequestMethod.GET,RequestMethod.POST}) @ResponseBody public String methodDouble() { return "Method double"; } }
引申注解
@GetMapping、@PostMapping
增强了方法限定的功能
@RequestMapping( method = {RequestMethod.GET} ) public @interface GetMapping {} @RequestMapping( method = {RequestMethod.POST} ) public @interface PostMapping {}
使用注解改造
//@RequestMapping(value = "get",method = RequestMethod.GET) //method/get @GetMapping("get") @ResponseBody public String methodGet() { return "Method GET"; } //@RequestMapping(value = "post",method = RequestMethod.POST) @PostMapping("post") @ResponseBody public String methodPost() { return "Method POST"; }
请求参数限定 params属性 → String[]
限定的是请求参数要有哪一些,只能多,不能少
//params → 多个值是and的关系,全都要 @RequestMapping(value = "register",params = {"username","password"}) @ResponseBody public String register() { return "register"; }
如果状态码出现400,那么一定要检查请求参数的封装有没有出问题
请求头限定 headers属性 → String[]
限定的是请求头要有哪一些
//多个值之间的关系是and,全都要 @RequestMapping(value = "limit",headers = {"aaa","bbb"}) @ResponseBody public String headerLimit() { return "header limit"; }
如果不符合要求,这里报的是404
consumes → Content-Type
限定的值Content-Type这个请求头对应的值,值的格式是xxx/xxx
@RequestMapping(value = "consumes", consumes = "aaa/bbb") @ResponseBody public String contentTypeLimit() { return "ContentType limit"; }
produces → Accept
限定的Accept这个请求头对应的值,值的格式是xxx/xxx
@RequestMapping(value = "produces", produces = "ccc/ddd") @ResponseBody public String acceptLimit() { return "Accept limit"; }
Handler方法的返回值
ModelAndView
单体应用中会去使用的结果:里面包含了视图信息,以及数据信息
@RequestMapping("hello") //没有写@ResponseBody public ModelAndView hello() { //想要访问webapp路径下hello.jsp 并且给里面的username赋值 ModelAndView modelAndView = new ModelAndView(); // 通过ModelAndView要设定访问视图名为hello.jsp modelAndView.setViewName("/hello.jsp"); // 通过ModelAndView要设定username对应的值 modelAndView.addObject("username", "songge"); return modelAndView; }
String
没有@ResponseBody:把返回值的字符串作为ModelAndView的viewName
有@ResponseBody:直接响应字符串
//返回值字符串作为视图名 @RequestMapping("hello2") public String hello2(Model model) { //相当于 //ModelAndView modelAndView = new ModelAndView(); //modelAndView.setViewName("/hello.jsp"); model.addAttribute("username", "ligenli"); //和上面的hello方法做的事情是一样的 return "/hello.jsp"; }
***响应Json
- jackson依赖
- mvc:annotation-driven
- Handler方法返回值写为需要转换为Json字符串的实例,即提供一个对象,@ResponseBody
这里要注意:返回的实例要提供无参构造方法和get\set方法
@RequestMapping("hello/json") @ResponseBody public User helloJson() { User user = new User(); user.setUsername("松哥"); user.setPassword("李艮隶"); return user; }
引申注解:@RestController = @ResponseBody + @Controller
意味着该类下所有的方法响应的都是Json(该类不响应视图)
//@Controller //@ResponseBody @RestController public class JsonController { }
这里会由Handler自动将实例转换为json数据
Handler方法的形参
主要做的是请求参数的封装,请求参数使用Handler方法的形参来封装
***请求参数接收
直接接收
在形参中直接接收:请求的参数名必须和Handler方法的形参名一致
字符串
任何参数都可以直接用字符串来接收
// 直接接收请求参数 @RequestMapping("register") public BaseRespVo register(String username,String password,String age,String gender) { return BaseRespVo.ok(username + ":" + password); }
基本类型和对应的包装类
直接写在形参中即可,MVC提供了类型转换器
// 直接接收请求参数 // 在形参中可以直接使用基本类型,或者其包装类来接收 String → 你接收的类型 // SpringMVC给我们提供了类型转换器 @RequestMapping("register2") //public BaseRespVo register2(String username,String password,int age,String gender) { public BaseRespVo register2(String username,String password,Integer age,String gender) { // return BaseRespVo.ok(username + ":" + password + ";age = " + age); }
接收这些值建议使用包装类来接收,避免接收的过程中出现null值的转换错误
数组
即多个同名的参数,在接收的时候可以用一个数组来接收
localhost:8080/user/register3?username=songge&password=ligenli&age=28&gender=male
&hobbys=sing&hobbys=dance&hobbys=rap&hobbys=basketball
// 接收数组 → 请求参数名和Handler方法的形参名一致,数组的类型根据你的参数的类型选择 @RequestMapping("register3") public BaseRespVo register3(String username,String password, Integer age,String gender, String[] hobbys) { // return BaseRespVo.ok(hobbys); }
Date日期
&birthday=1991-06-13
// 接收日期 → 请求参数名和Handler方法的形参名一致 @RequestMapping("register4") public BaseRespVo register4(String username, String password, Integer age, String gender, String[] hobbys, Date birthday) { return BaseRespVo.ok(hobbys); }
自动提供的日期类型转换器只支持yyyy/MM/dd的格式
&birthday=1991/06/13
@RequestMapping("reg3") public BaseRespVo reg3(Date date){ return BaseRespVo.ok(date); }
SpringMVC提供的类型转换器
// 接收日期 → 请求参数名和Handler方法的形参名一致 //指定接收的日期格式 //birthday=1991-06-13 @RequestMapping("register5") public BaseRespVo register5(String username, String password, Integer age, String gender, String[] hobbys, @DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday) { return BaseRespVo.ok(hobbys); }
自定义的类型转换器
1.写一个自定义的类型转换器
提供了接口,直接实现即可
//第一个泛型:作为convert方法的形参类型;第二个泛型作为convert方法的返回值类型 //其实做的就是一个由S类型转换为T类型的转换器 //在convert方法中就要写自定义的转换业务 @FunctionalInterface public interface Converter<S, T> { @Nullable T convert(S var1); } // 接收日期 //自定义的类型转换器 → 请求参数名和Handler方法的形参名一致,来看类型转换器列表里是否包含形参类型的转换器 //birthday=1991-06-13
@Component public class String2DateConvert implements Converter<String, Date> { @Override public Date convert(String s) { SimpleDateFormat simpleDateFormat = null; Date date = null; if (s.contains("-")&&s.length()==10){ simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); }else if (s.length()==19){ simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); }else { simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd"); } try { date = simpleDateFormat.parse(s); } catch (ParseException e) { e.printStackTrace(); } return date; } }
2.类型转换器的配置
<!--conversionService还要提供给SpringMVC--> <mvc:annotation-driven conversion-service="conversionService"/> <!--可以使用注解来注册这个组件--> <!--<bean id="string2DateConverter" class="com.cskaoyan.converter.String2DateConverter"/>--> <!--注册一个组件conversionService → 提供新增的converter--> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <!--注册局部组件--> <!--<bean class="com.cskaoyan.converter.String2DateConverter"/>--> <!--引用全局组件 bean属性引用组件id--> <ref bean="string2DateConverter"/> </set> </property> </bean>
3.@JsonFormat
将返回值标记为需要的日期格式
@JsonFormat(pattern = "yyyy-MM-dd") T data;
写需要作为返回数据的属性上,同时也需要使用pattern属性。
要注意这里是返回参数,要和接收进行区别。这里转换也只会转换date类型,不会影响其他类型的数据的转换
文件 MultipartFile
文件上传
MultipartFile提供了一个方法 → 保存文件到指定位置
1.引入依赖commons-io、commons-fileupload
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency>
2.注册MultipartResolver组件
<!--组件id为固定值,不能写为其他值--> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="512000000"/> //这里是限制文件上传的大小 </bean>
3.构造一个文件上传的请求,并且接收文件
<form action="upload/file" method="post" enctype="multipart/form-data"> <%--name属性值就是请求参数名--%> <input type="file" name="file"><br> <input type="submit"> </form>
4.提供一个Handler方法,接收上传的文件参数
// 类型使用MultipartFile,形参名要和请求参数名一致 @RequestMapping("file") public BaseRespVo uploadFile(MultipartFile file) throws IOException { //保存在指定位置 // transferTo方法的形参提供的就是保存的位置以及名字 //如果这个destFile已经存在,会覆盖 → 解决这个问题,就是new一个不存在的file // → 使用一个不会重复的文件名 UUID //也可以做这样一件事:上传的时候是啥名字,就以啥名字来接收 String originalFilename = file.getOriginalFilename();//获得原始文件名 String name = file.getName(); //获得是请求参数名 → 这里是file,也就是形参名 String contentType = file.getContentType(); //正文类型 → 文件类型 long size = file.getSize(); //文件大小 //InputStream inputStream = file.getInputStream(); //输入流 File destFile = new File("D:\\tmp", originalFilename); file.transferTo(destFile); return BaseRespVo.ok(); } //上传多个文件 @RequestMapping("files") public BaseRespVo uploadFile(MultipartFile[] files) throws IOException { //可以通过遍历的方式保存起来 for (MultipartFile file : files) { String originalFilename = file.getOriginalFilename(); File destFile = new File("D:\\tmp", originalFilename); file.transferTo(destFile); } return BaseRespVo.ok(); }
注意
不管接收到的是什么类型的参数,请求参数名必须和Handler方法的形参名一致
JavaBean
将请求参数封装到JavaBean的实例中,相当于是将形参名写成了JavaBean中的属性名,这时候也要注意属性名与请求参数名必须一致
并且需要提供无参构造和set方法,因为转换工具是通过set方法来赋值的
@Data public class User { // 原先Handler方法的形参变更为了成员变量 // 成员变量的类型和原先在Handler方法的形参中的写法是一样的 String username; String password; Integer age; String gender; String[] hobbys; //@DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday; }
// 以JavaBean来接收请求参数 → 请求参数名和JavaBean的成员变量名(set方法)一致 @RequestMapping("register7") public BaseRespVo register7(User user) { //以JavaBean来进行接收 → 实际上使用的JavaBean的无参构造方法和set方法来封装的 //User user1 = new User(); //user1.setUsername(); //user1.setPassword(); return BaseRespVo.ok();//做baseRespVo实例转换为字符串的过程中 }
接收方式的选择
有些情况下直接用形参来接收,有些情况下用JavaBean来接收,也有可能两者都用
混用
一部分参数是经常使用的,一部分参数使用不频繁
@RequestMapping("user/query") public BaseRespVo queryUsers(Integer age, String gender, PageInfo pageInfo){ return BaseRespVo.ok(); } @Data class PageInfo{ Integer page; Integer limit; }
继承JavaBean
如果需要增加请求参数,且这部分也是常有的,在不修改原来的代码的情况下,可以使用一个新的JavaBean来继承原来的对象
@RequestMapping("user/query") public BaseRespVo queryUsers( UserPageInfo pageInfo){ return BaseRespVo.ok(); } // 改编方式1 @Data class UserPageInfo{ Integer age; String gender; Integer page; Integer limit; } // 改编方式2 @Data class UserPageInfo extends PageInfo{ Integer age; String gender; }