002 第一季SpringBoot2核心技术-核心功能1:配置文件,Web开发:静态资源、请求参数、响应数据、Tymelef、拦截器、文件上传、异常处理、3大原生组件注入、切换tomact、定制化原理

三、核心技术之- ->核心功能

在这里插入图片描述

1. 配置文件

1.1 文件类型

1.1.1 properties

同以前的properties用法

优先级高于yml的方式。

1.1.2 yaml

1) 简介

YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。

非常适合用来做以数据为中心的配置文件

总结:properties和yml的区别
properties 和 yml 都是 Spring Boot 支持的两种配置文件,其中 yml 格式的配置文件可以看作是对 properties 配置文件的升级。它们的主要区别有 4 点:定义和定位不同、语法不同:yml 的语法更简单,且可读性更高、yml 可以更好的配置多种数据类型,比如对象和集合、yml 可以跨语言使用,通用性更好。

2) 基本语法
  • key: value;kv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格(实际上也没事,idea会自动把Tab转化为空格)
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • '#'表示注释,并且为单行注释(即:一个#只能注释一行)。
  • 字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义
  • 后缀可以是yaml或者yml
3) 数据类型
  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
  • 对象:键值对的集合。map、hash、object
行内写法:  k: {k1:v1,k2:v2,k3:v3}
#或
k: 
  k1: v1
  k2: v2
  k3: v3
  • 数组:一组按次序排列的值。array、list、set、queue
行内写法:  k: [v1,v2,v3]
#或者
k:
 - v1
 - v2
 - v3
4) 示例(读取yml文件中的内容)

在这里插入图片描述

//读取yml类型文件中的内容。
@ConfigurationProperties(prefix ="person")
@Component
@Data
public class Person {
    private String userName;
    private Boolean boss;
    private Date birth;
    private Integer age;
    private Pet pet;
    private String[] interests;
    private List<String> animal;
    private Map<String, Object> score;
    private Set<Double> salarys;
    private Map<String, List<Pet>> allPets;
}
-----------------------------------
@Data
public class Pet {
    private String name;
    private Double weight;
}

在这里插入图片描述

person:
  #  单引号会将 \n作为字符串输出   双引号会将\n 作为换行输出
  #  双引号不会转义,单引号会转义
  userName: zhangsan      #字面量
  boss: true
  birth: 2019/12/9
  age: 18
  #  interests: [篮球,足球]   #数组
  interests:
    - 篮球
    - 足球
    - 18
  animal: [阿猫,阿狗]  # list
  #  score:           # map
  #    english: 80
  #    math: 90
  score: {english:80,math:90}  
  salarys:           #set
    - 9999.98
    - 9999.99
  pet:              # 对象
    name: 阿狗
    weight: 99.99
  allPets:    # Map<String, List<Pet>> allPets
    sick:
      - {name: 阿狗,weight: 99.99}
      - name: 阿猫
        weight: 88.88
      - name: 阿虫
        weight: 77.77
    health:
      - {name: 阿花,weight: 199.99}
      - {name: 阿明,weight: 199.99}



#spring:
#  banner:
#    image:
#      bitdepth: 4
#  cache:
#    type: redis
#    redis:
#      time-to-live: 11000

控制层代码:

package com.atguigu.boot.controller;

import com.atguigu.boot.bean.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @Autowired
    Person person;

    @RequestMapping("/person")
    public Person person(){

        String userName = person.getUserName();
        System.out.println(userName);
        return person;
    }

}

在这里插入图片描述
启动主启动类进行访问测试:
在这里插入图片描述

1.2 配置提示

说明:在yml文件中,配置springboot提供的属性有提示,而自己写的属性没有提示,不好。如何让自己写的属性也有提示呢???

自定义的类和配置文件绑定一般没有提示。

解决

  1. 导入依赖。
  2. 因为这个配置处理器只是为了开发方便,与业务无关。所以建议在打包的插件中配置在项目打包时忽略掉配置处理器。

在这里插入图片描述

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>


 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

2. Web开发

2.1 SpringMVC自动配置概览

在这里插入图片描述
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)
The auto-configuration adds the following features on top of Spring’s defaults:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
    ○ 内容协商视图解析器和BeanName视图解析器
  • Support for serving static resources, including support for WebJars (covered later in this document).
    ○ 静态资源(包括webjars)
  • Automatic registration of Converter, GenericConverter, and Formatter beans.
    ○ 自动注册 Converter,GenericConverter,Formatter
  • Support for HttpMessageConverters (covered later in this document).
    ○ 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
  • Automatic registration of MessageCodesResolver (covered later in this document).
    ○ 自动注册 MessageCodesResolver (国际化用)
  • Static index.html support.
    ○ 静态index.html 页支持
  • Custom Favicon support (covered later in this document).
    ○ 自定义 Favicon
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
    ○ 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则

If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.
声明 WebMvcRegistrations 改变默认底层组件

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.
使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC

2.2 简单功能分析

2.2.1 使用插件创建Spring Boot(idea 2022版)

  1. 创建模块
    在这里插入图片描述

  2. 添加依赖,注意boot的版本。
    在这里插入图片描述

  3. 删除没用的结构·(可选)
    在这里插入图片描述

  4. 经常使用yml文件代替properties文件。
    在这里插入图片描述

2.2.2 静态资源访问

1) 静态资源目录

只要静态资源放在类路径(resources)下:

  • static
  • public
  • resources
  • META-INF/resources

访问当前项目根路径/ + 静态资源名

在这里插入图片描述
在这里插入图片描述

原理: 静态映射/**。

请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面 。

默认在以上这4个目录下,想要改变默认的静态资源路径需以下配置:这样现在的静态资源文件只能放在haha目录下,放在默认的4个目录下就访问不了了。
在这里插入图片描述

在这里插入图片描述

spring:
  web:
    resources:
      static-locations: [classpath:/haha/]
        #因为是数组,所以可以配置多个,用逗号隔开如: [classpath:/haha/,/aa/]
2) 静态资源访问前缀

说明:建议静态资源访问时加上前缀,比如:一个web应用有很多静态资源和动态资源,项目中有很多拦截器,只有登录后才能访问一些动态请求,拦截器拦截/**相当于是把静态资源也拦截了,为了拦截器配置方便,把静态资源访问都加上前缀,这样拦截器放行以指定前缀路径开始的所求请求,这样就非常方便过滤掉静态资源。

默认无前缀

spring:
  mvc:
    static-path-pattern: /res/**
   #static-path-pattern: /**   默认无前缀

当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
在这里插入图片描述
在这里插入图片描述

3) webjar(了解)

说明:它把js、css封装成了一个个的jar包,我们想用的时候只需要导入依赖即可。

自动映射 /webjars/**

官方地址:https://www.webjars.org/

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.5.1</version>
</dependency>

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径

2.2.3 欢迎页支持(首页)

  • 方式一:静态资源路径下 index.html
    ○ 可以配置静态资源路径
    ○ 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
    在这里插入图片描述
spring:
#  mvc:
#    static-path-pattern: /res/**   这个会导致welcome page功能失效
  web:
    resources:
      static-locations: [classpath:/haha/]

index.html页面:
在这里插入图片描述
说明只需要访问项目的根路径,连资源名都不用写了。
在这里插入图片描述

  • 方式二:controller能处理/index
    自己编写控制层方法,此控制层方法能够处理/index请求,之后返回一个页面。

2.2.4 自定义Favicon(网页小图标)

说明:访问网站时的小图标。
把一个图片放在静态资源目录下,名字为固定写法:favicon.ico
在这里插入图片描述

在这里插入图片描述

spring:
#  mvc:
#    static-path-pattern: /res/**   这个会导致 Favicon 功能失效

在这里插入图片描述

2.2.5 静态资源配置原理

  • SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
  • SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
  • 给容器中配了什么。
	@Configuration(proxyBeanMethods = false)
	@Import(EnableWebMvcConfiguration.class)
	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
	@Order(0)
	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
  • 配置文件的相关属性和xxx进行了绑定。WebMvcPropertiesspring.mvc、ResourcePropertiesspring.resources
1) 配置类只有一个有参构造器
	//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory Spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
//DispatcherServletPath  
//ServletRegistrationBean   给应用注册Servlet、Filter....
	public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
				ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
				ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
				ObjectProvider<DispatcherServletPath> dispatcherServletPath,
				ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
			this.resourceProperties = resourceProperties;
			this.mvcProperties = mvcProperties;
			this.beanFactory = beanFactory;
			this.messageConvertersProvider = messageConvertersProvider;
			this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
			this.dispatcherServletPath = dispatcherServletPath;
			this.servletRegistrations = servletRegistrations;
		}
2) 资源处理的默认规则(禁用静态资源)
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
	if (!this.resourceProperties.isAddMappings()) {
		logger.debug("Default resource handling disabled");
		return;
	}
	Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
	CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
	//webjars的规则
          if (!registry.hasMappingForPattern("/webjars/**")) {
		customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
				.addResourceLocations("classpath:/META-INF/resources/webjars/")
				.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
	}
          
          //
	String staticPathPattern = this.mvcProperties.getStaticPathPattern();
	if (!registry.hasMappingForPattern(staticPathPattern)) {
		customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
				.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
				.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
	}
}

在这里插入图片描述

spring:
  web:
    resources:
      add-mappings: false  #禁用所有静态资源规则,默认为true

在这里插入图片描述

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {

	private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
			"classpath:/resources/", "classpath:/static/", "classpath:/public/" };

	/**
	 * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
	 * /resources/, /static/, /public/].
	 */
	private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
3) 欢迎页的处理规则
	HandlerMapping:处理器映射。保存了每一个Handler能处理哪些请求。	

		@Bean
		public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
				FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
			WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
					new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
					this.mvcProperties.getStaticPathPattern());
			welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
			welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
			return welcomePageHandlerMapping;
		}

	WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
			ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
		if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
            //要用欢迎页功能,必须是/**
			logger.info("Adding welcome page: " + welcomePage.get());
			setRootViewName("forward:index.html");
		}
		else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
            // 调用Controller  /index
			logger.info("Adding welcome page template: index");
			setRootViewName("index");
		}
	}

4) favicon

2.3 请求参数处理

2.3.1 请求映射

概念:在做所有web开发之前需要编写controller,在每个方法编写@RequestMapping(xx)来声明这个方法能处理什么请求,这个过程叫作请求映射。

1) restful使用与原理

① 使用分析:

  • @xxxMapping;
  • Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
    • 核心Filter;HiddenHttpMethodFilter过滤器
      • 用法: 表单method=post,隐藏域 _method=put
      • 在SpringBoot中的yml配置文件中手动开启
        说明1:在学习SpringMvc时,想要使用restful通过表单发送put和delete请求,前提是在xml中配置HiddenHttpMethodFilter过滤器,之后在设置请求方式为post,传输请求参数_method
        说明2:在SpringBoot中默认配置的有过滤器(默认关闭),所以需要在yml/properties配置文件中手动开启,之后在设置请求方式,传输请求参数。(适用于前后端不分离)
  • 测试如下:

开启页面表单的Rest功能
在这里插入图片描述

# 说明:这一项配置是选择性开启,因为如果是前后端分离,我们不做前端开发,
#      只需要返回json数据就行了,也就不需要配置。
spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true   #开启页面表单的Rest功能,把post请求解析为put.delete

index.html页面:
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>atguigu,欢迎您</h1>
测试REST风格;
<form action="/user" method="get">
    <input value="REST-GET 提交" type="submit"/>
</form>
<form action="/user" method="post">
    <input value="REST-POST 提交" type="submit"/>
</form>
<form action="/user" method="post">
    <input name="_method" type="hidden" value="delete"/>
    <input name="_m" type="hidden" value="delete"/>
    <input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="post">
    <input name="_method" type="hidden" value="PUT"/>
    <input value="REST-PUT 提交" type="submit"/>
</form>
<hr/>
测试基本注解:
<ul>
    <a href="car/3/owner/lisi?age=18&inters=basketball&inters=game">car/{id}/owner/{username}</a>
    <li>@PathVariable(路径变量)</li>
    <li>@RequestHeader(获取请求头)</li>
    <li>@RequestParam(获取请求参数)</li>
    <li>@CookieValue(获取cookie值)</li>
    <li>@RequestBody(获取请求体[POST])</li>

    <li>@RequestAttribute(获取request域属性)</li>
    <li>@MatrixVariable(矩阵变量)</li>
</ul>

/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;<br/>
/cars/sell;low=34;brand=byd,audi,yd  ;矩阵变量 <br/>
页面开发,cookie禁用了,session里面的内容怎么使用;
session.set(a,b)---> jsessionid ---> cookie ----> 每次发请求携带。
url重写:/abc;jsesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递.

/boss/1/2

/boss/1;age=20/2;age=20

<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a>
<a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)</a>
<a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}</a>
<br/>
<form action="/save" method="post">
    测试@RequestBody获取数据 <br/>
    用户名:<input name="userName"/> <br>
    邮箱:<input name="email"/>
    <input type="submit" value="提交"/>
</form>
<ol>
    <li>矩阵变量需要在SpringBoot中手动开启</li>
    <li>根据RFC3986的规范,矩阵变量应当绑定在路径变量中!</li>
    <li>若是有多个矩阵变量,应当使用英文符号;进行分隔。</li>
    <li>若是一个矩阵变量有多个值,应当使用英文符号,进行分隔,或之命名多个重复的key即可。</li>
    <li>如:/cars/sell;low=34;brand=byd,audi,yd</li>
</ol>
<hr/>
测试原生API:
<a href="/testapi">测试原生API</a>
<hr/>
测试复杂类型:<hr/>
测试封装POJO;
<form action="/saveuser" method="post">
    姓名: <input name="userName" value="zhangsan"/> <br/>
    年龄: <input name="age" value="18"/> <br/>
    生日: <input name="birth" value="2019/12/10"/> <br/>
    <!--    宠物姓名:<input name="pet.name" value="阿猫"/><br/>-->
    <!--    宠物年龄:<input name="pet.age" value="5"/>-->
    宠物: <input name="pet" value="啊猫,3"/>
    <input type="submit" value="保存"/>
</form>

<br>
</body>
</html>

控制层方法:
在这里插入图片描述

   
package com.atguigu.boot.controller;


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

@RestController
public class HelloController {

    
    //    @RequestMapping(value = "/user",method = RequestMethod.GET)
    @GetMapping("/user")//简化写法
    public String getUser(){

        return "GET-张三";
    }

    //    @RequestMapping(value = "/user",method = RequestMethod.POST)
    @PostMapping("/user")
    public String saveUser(){
        return "POST-张三";
    }


    //    @RequestMapping(value = "/user",method = RequestMethod.PUT)
    @PutMapping("/user")
    public String putUser(){

        return "PUT-张三";
    }

    @DeleteMapping("/user")
//    @RequestMapping(value = "/user",method = RequestMethod.DELETE)
    public String deleteUser(){
        return "DELETE-张三";
    }

}

访问测试:
在这里插入图片描述
在这里插入图片描述

② Rest原理分析(表单提交要使用REST的时候):

  • 表单提交会带上_method=PUT
  • 请求过来被HiddenHttpMethodFilter拦截
    ○ 请求是否正常,并且是POST
    • 获取到_method的值。(value值不区分大小写,底层都会转化为大写的PUT,DELETE)
    • 兼容以下请求;PUT.DELETE.PATCH
    • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
    • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
----------------------------底层源码------------
	@Bean
	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
	@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
	public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
		return new OrderedHiddenHttpMethodFilter();
	}

③ Restful使用客户端工具发送请求:

  • 如PostMan直接发送Put、delete等方式请求,无需Filter。
  • 测试:
    首先注释掉filter
    在这里插入图片描述
    之后重启项目,使用postman发送put请求,发现仍然能正确返回。
    在这里插入图片描述

④ 此外还可以修改这个参数_method为其它自定义的参数。

  • 测试:
  • 添加配置类:
    在这里插入图片描述
package com.cn.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;

/**
 * ClassName: WebConfig
 * Package: com.cn.config
 * Description:
 *
 * @Author xxx
 * @Create 2023/5/5 14:35
 * @Version 1.0
 */
@Configuration(proxyBeanMethods = false)//组件没有依赖关系,快速放
public class WebConfig  {

    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        methodFilter.setMethodParam("_m");
        return methodFilter;
    }


}
  • 修改index.html页面,必须把参数值换为_m才能访问成功(可以直接添加一行,原先的不用删除)
    在这里插入图片描述
<input name="_m" type="hidden" value="delete"/>
  • 重启项目,进行访问
    在这里插入图片描述
2) 请求映射原理

在这里插入图片描述
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet-》doDispatch()

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// 找到当前请求使用哪个Handler(Controller的方法)处理
				mappedHandler = getHandler(processedRequest);
                
                //HandlerMapping:处理器映射。/xxx->>xxxx

在这里插入图片描述
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
在这里插入图片描述
所有的请求映射都在HandlerMapping中。

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 / 就能访问到index.html,只不过在浏览器输入地址时:http://localhost:8080/,这个/会被省略掉。
  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping
  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
    ○ 如果有就找到这个请求对应的handler
    ○ 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

2.3.2 普通参数与基本注解

1) 注解:

@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody

1.1) 测试页面:index.html

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>atguigu,欢迎您</h1>
测试REST风格;
<form action="/user" method="get">
    <input value="REST-GET 提交" type="submit"/>
</form>
<form action="/user" method="post">
    <input value="REST-POST 提交" type="submit"/>
</form>
<form action="/user" method="post">
    <input name="_method" type="hidden" value="delete"/>
    <input name="_m" type="hidden" value="delete"/>
    <input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="post">
    <input name="_method" type="hidden" value="PUT"/>
    <input value="REST-PUT 提交" type="submit"/>
</form>
<hr/>
测试基本注解:
<ul>
    <a href="car/3/owner/lisi?age=18&inters=basketball&inters=game">car/{id}/owner/{username}</a>
    <li>@PathVariable(路径变量)</li>
    <li>@RequestHeader(获取请求头)</li>
    <li>@RequestParam(获取请求参数)</li>
    <li>@CookieValue(获取cookie值)</li>
    <li>@RequestBody(获取请求体[POST])</li>

    <li>@RequestAttribute(获取request域属性)</li>
    <li>@MatrixVariable(矩阵变量)</li>
</ul>

/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符串。@RequestParam;<br/>
/cars/sell;low=34;brand=byd,audi,yd  ;矩阵变量 <br/>
页面开发,cookie禁用了,session里面的内容怎么使用;
session.set(a,b)---> jsessionid ---> cookie ----> 每次发请求携带。
url重写:/abc;jsesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递.

/boss/1/2

/boss/1;age=20/2;age=20

<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a>
<a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)</a>
<a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}</a>
<br/>
<form action="/save" method="post">
    测试@RequestBody获取数据 <br/>
    用户名:<input name="userName"/> <br>
    邮箱:<input name="email"/>
    <input type="submit" value="提交"/>
</form>
<ol>
    <li>矩阵变量需要在SpringBoot中手动开启</li>
    <li>根据RFC3986的规范,矩阵变量应当绑定在路径变量中!</li>
    <li>若是有多个矩阵变量,应当使用英文符号;进行分隔。</li>
    <li>若是一个矩阵变量有多个值,应当使用英文符号,进行分隔,或之命名多个重复的key即可。</li>
    <li>如:/cars/sell;low=34;brand=byd,audi,yd</li>
</ol>
<hr/>
测试原生API:
<a href="/testapi">测试原生API</a>
<hr/>
测试复杂类型:<hr/>
测试封装POJO;
<form action="/saveuser" method="post">
    姓名: <input name="userName" value="zhangsan"/> <br/>
    年龄: <input name="age" value="18"/> <br/>
    生日: <input name="birth" value="2019/12/10"/> <br/>
    <!--    宠物姓名:<input name="pet.name" value="阿猫"/><br/>-->
    <!--    宠物年龄:<input name="pet.age" value="5"/>-->
    宠物: <input name="pet" value="啊猫,3"/>
    <input type="submit" value="保存"/>
</form>

<br>
</body>
</html>
1.2) 测试:@PathVariable

index.html页面的超链接:
在这里插入图片描述

<a href="car01/3/owner/lisi">car01/{id}/owner/{username}--测试路径变量</a>

控制层代码:
在这里插入图片描述

package com.cn.controller;

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

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * ClassName: ParameterTestController
 * Package: com.cn.controller
 * Description:
 *
 * @Author xxx
 * @Create 2023/5/5 19:48
 * @Version 1.0
 */

@RestController
public class ParameterTestController {

    //  car01/2/owner/zhangsan
    @GetMapping("/car01/{id}/owner/{username}")
    public Map<String,Object> getCar01(
                                    //@PathVariable:获取restful风格的请求参数:
                                    //  1.可以单个的获取
                                    //  2.也可以直接获取一个Map集合
                                       @PathVariable("id") Integer id,
                                       @PathVariable("username") String name,
                                       @PathVariable Map<String,String> pv){

        Map<String,Object> map = new HashMap<>();
        map.put("id",id);
        map.put("name",name);
        map.put("pv",pv);

        return map;
    }
}


运行测试:
在这里插入图片描述
在这里插入图片描述

1.3) 测试:@RequestHeader

页面超链接:
在这里插入图片描述

<a href="car02/3/owner/lisi">car02/{id}/owner/{username}--测试获取请求头</a>

控制层代码:
在这里插入图片描述

@RestController
public class ParameterTestController {

    //  car02/2/owner/zhangsan
    @GetMapping("/car02/{id}/owner/{username}")
    public Map<String,Object> getCar02(
            //@RequestHeader:获取请求头信息
            //     1.可以单个的获取
            //     2.也可以一次性获取所有的请求头信息
            @RequestHeader("User-Agent") String userAgent,
            @RequestHeader Map<String,String> header){

        Map<String,Object> map = new HashMap<>();
        map.put("userAgent",userAgent);
        map.put("headers",header);

        return map;
    }
  }

获取的请求头:
在这里插入图片描述
运行:
在这里插入图片描述
在这里插入图片描述

1.4) 测试:@RequestParam

页面超链接:

<a href="car03/3/owner/lisi?age=18&inters=basketball&inters=game">car03/{id}/owner/{username}--测试获取请求参数</a>

控制层代码:

@RestController
public class ParameterTestController {



    //  car03/2/owner/zhangsan
    @GetMapping("/car03/{id}/owner/{username}")
    public Map<String,Object> getCar03(
            //@RequestParam:获取普通的请求参数,如果请求参数名和方法的形参名相同时可以省略。
            //     1.获取一个参数
            //     2.获取的同名的参数有多个值。通过List集合或可变形参或数组或字符串(只有list集合接收时才添加)
            //     3.获取所有的请求参数,包括参数只有一个值和参数有多个值
            @RequestParam("age") Integer age,
            @RequestParam("inters") List<String> inters,
            @RequestParam Map<String,String> params){

        Map<String,Object> map = new HashMap<>();
        map.put("age",age);
        map.put("inters",inters);
        map.put("params",params);

        return map;
    }
 }

效果:
在这里插入图片描述

1.5) 测试:@CookieValue

页面超链接:

 <a href="car04/3/owner/lisi?age=18&inters=basketball&inters=game">car04/{id}/owner/{username}--测试获取cookie值</a>

控制层代码:

@RestController
public class ParameterTestController {

 //  car04/2/owner/zhangsan
    @GetMapping("/car04/{id}/owner/{username}")
    public void getCar04(
            //@CookieValue:获取Cookie里面的值:
            //     1.获取Cookie里面key为Idea-9ccb666a的value值,根据key获取value
            //     2.获取整个Cookie信息,返回值值为Cookie对象
            @CookieValue("Idea-9ccb666a") String idea,
            @CookieValue("Idea-9ccb666a") Cookie cookie){

        System.out.println(idea);
        System.out.println(cookie.getName()+"===>"+cookie.getValue());
        
    }
 }

需要获取的Cookie:
在这里插入图片描述

效果:
在这里插入图片描述

1.6) 测试:@RequestBody

页面:
在这里插入图片描述

<form action="/save" method="post">
    测试@RequestBody获取数据 <br/>
    用户名:<input name="userName"/> <br>
    邮箱:<input name="email"/>
    <input type="submit" value="提交"/>
</form> <br/>

控制层代码:
在这里插入图片描述

@RestController
public class ParameterTestController {

    //@RequestBody:获取请求体的信息
    @PostMapping("/save")
    public Map postMethod(@RequestBody String content){
        Map<String,Object> map = new HashMap<>();
        map.put("content",content);
        return map;
    }
}

测试:
在页面输入值:
在这里插入图片描述
效果:
在这里插入图片描述

1.7) @RequestAttribute (获取域对象的值2种)

RequestController类:
在这里插入图片描述

package com.cn.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * ClassName: RequestController
 * Package: com.cn.controller
 * Description:
 *
 * @Author xxx
 * @Create 2023/5/6 14:56
 * @Version 1.0
 */
@Controller
public class RequestController {
    @GetMapping("/goto")
    public String goToPage(HttpServletRequest request){

        request.setAttribute("msg","成功了...");
        request.setAttribute("code",200);
        //现在还没有学习模板引擎,不能进行前端开发,所以模拟一个跳转页面
        return "forward:/success";  //转发到  /success请求
    }

    @ResponseBody
    @GetMapping("/success")
    //@RequestAttribute:获取request域属性的值:
    //    1.可以在方法的参数上,使用@RequestAttribute注解
    //    2.也可以直接通过原生的servletApi的方式获取。因为是请求转发用的是同一个域对象

    public Map success(@RequestAttribute(value = "msg",required = false) String msg,
                       @RequestAttribute(value = "code",required = false)Integer code,
                       HttpServletRequest request){
        Object msg1 = request.getAttribute("msg");

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

        map.put("reqMethod_msg",msg1);
        map.put("annotation_msg",msg);

        return map;

    }

}

在这里插入图片描述

1.8) @MatrixVariable (获取矩阵变量的值 )
  • 普通方式携带参数:/cars/{path}?xxx=xxx&aaa=ccc ,又叫作queryString(查询字符串),使用@RequestParam的方式获取请求参数

  • 矩阵变量方式携带参数:
    情况1:/cars/sell;low=34;brand=byd,audi,yd(一个key有多个值,使用逗号分隔)
    情况2:/cars/sell;low=34;brand=byd;brand=audi;brand=yd(一个key有多个值,命名多个重复的key)
    情况3:/boss/1;age=20/2;age=10(可以写在同一个url的多个路径中)

  • 矩阵变量的要求:

    • 矩阵变量需要在SpringBoot中手动开启
    • 根据RFC3986的规范,矩阵变量应当绑定在路径变量中!
    • 若是有多个矩阵变量,应当使用英文符号分号;进行分隔。
    • 若是一个矩阵变量有多个值,应当使用英文符号逗号,进行分隔,或者命名多个重复的key即可。
  • 矩阵变量使用场景:

    • 页面开发,cookie禁用了,session里面的内容怎么使用???
      因为session依赖于cookie,session.set(a,b)—> jsessionid —> cookie ----> 每次发请求携带jsessionid 。
    • 解决:使用矩阵变量的方式携带jsessionid,称为url重写
      url重写:/abc;jsesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递。

测试:
index.html的超链接:
在这里插入图片描述

<a href="/cars/sell;low=34;brand=byd,audi,yd">@MatrixVariable(矩阵变量)</a> <br/>
<a href="/cars/sell;low=34;brand=byd;brand=audi;brand=yd">@MatrixVariable(矩阵变量)</a> <br/>
<a href="/boss/1;age=20/2;age=10">@MatrixVariable(矩阵变量)/boss/{bossId}/{empId}</a>

控制层代码:
在这里插入图片描述

package com.cn.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * ClassName: TestJuZhen
 * Package: com.cn.controller
 * Description:
 *
 * @Author xxx
 * @Create 2023/5/6 19:49
 * @Version 1.0
 */
@RestController
public class TestJuZhen {

    //@MatrixVariable:获取矩阵变量的值
    //情况1:一个key有多个值,使用逗号分隔
    //情况2:一个key有多个值,命名多个重复的key  (2种情况获取值方式相同)
    //1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
    //2、SpringBoot默认是禁用了矩阵变量的功能,需要手动开启:2种
    //      手动开启的原理:对于路径的处理,底层都是使用UrlPathHelper进行解析,而这个UrlPathHelper里面
    //                   又有一个属性removeSemicolonContent用来支持矩阵变量的(即:默认为true,会自动移除
    //                    路径中分号后的内容,所以需要修改为false)。
    //3、矩阵变量必须有url路径变量才能被解析:
    //      即:路径不能直接写成 @GetMapping("/cars/sell") 的方式,因为矩阵变量是绑定在路径
    //         变量中的,所以不能直接写路径而是要写成路径的表示方式:@GetMapping("/cars/{path}")
    @GetMapping("/cars/{path}")
    public Map carsSell(@MatrixVariable("low") Integer low,
                        @MatrixVariable("brand") List<String> brand,
                        //使用restful风格获取查看这个路径值是什么
                        @PathVariable("path") String path){

        Map<String,Object> map = new HashMap<>();
        map.put("low",low);
        map.put("brand",brand);
        map.put("path",path);
        return map;
    }

    // 情况3:/boss/1;age=20/2;age=10,有2个路径变量,每个路径变量有绑定了相同的矩阵变量
    //  解决:使用pathVar属性指定是哪个路径变量,在使用value指定矩阵变量
    @GetMapping("/boss/{bossId}/{empId}")
    public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
                    @MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
        Map<String,Object> map = new HashMap<>();

        map.put("bossAge",bossAge);
        map.put("empAge",empAge);
        return map;

    }
}

配置类方式一:
在这里插入图片描述

package com.cn.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;

/**
 * ClassName: ParamConfig1
 * Package: com.cn.config
 * Description:
 *
 * @Author xxx
 * @Create 2023/5/6 20:12
 * @Version 1.0
 */

//方式一:实现WebMvcConfigurer接口,重写对应的方法。
@Configuration
public class ParamConfig1 implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        // 设置为falese:表示不移除分号(;)后面的内容。矩阵变量功能就可以生效
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
}

配置类方式二:
在这里插入图片描述

package com.cn.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.UrlPathHelper;

/**
 * ClassName: ParamConfig1
 * Package: com.cn.config
 * Description:
 *
 * @Author xxx
 * @Create 2023/5/6 20:12
 * @Version 1.0
 */

//方式二:使用@Bean的方式给容器中放一个 WebMvcConfigurer类型的组件
@Configuration
public class ParamConfig2  {

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                UrlPathHelper urlPathHelper = new UrlPathHelper();
                // 不移除;后面的内容。矩阵变量功能就可以生效urlPathHelper.setRemoveSemicolonContent(false);
                configurer.setUrlPathHelper(urlPathHelper);

            }
        };

    }
}

运行:
在这里插入图片描述

2) Servlet API:原生参数解析原理(+++++++++)

说明:控制器方法可以写Servlet API原生方式的参数有以下几种。

WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId

ServletRequestMethodArgumentResolver参数解析器 解析以上的部分参数

//源码:
@Override
public boolean supportsParameter(MethodParameter parameter) {
	Class<?> paramType = parameter.getParameterType();
	return (WebRequest.class.isAssignableFrom(paramType) ||
			ServletRequest.class.isAssignableFrom(paramType) ||
			MultipartRequest.class.isAssignableFrom(paramType) ||
			HttpSession.class.isAssignableFrom(paramType) ||
			(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
			Principal.class.isAssignableFrom(paramType) ||
			InputStream.class.isAssignableFrom(paramType) ||
			Reader.class.isAssignableFrom(paramType) ||
			HttpMethod.class == paramType ||
			Locale.class == paramType ||
			TimeZone.class == paramType ||
			ZoneId.class == paramType);
}
3) 复杂参数:域对象(Model)参数原理

Map、Model(map、model里面的数据会被放在request的请求域 相当于 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

Map<String,Object> map,  Model model, HttpServletRequest request 都是可以给request域中放数据,
request.getAttribute();

Map、Model类型的参数,会返回 mavContainer.getModel();—> BindingAwareModelMap 是Model 也是Map
mavContainer.getModel(); 获取到值的
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4) 自定义对象参数:绑定原理

可以自动类型转换与格式化,可以级联封装

/**  页面:
 *     姓名: <input name="userName"/> <br/>
 *     年龄: <input name="age"/> <br/>
 *     生日: <input name="birth"/> <br/>
 *     宠物姓名:<input name="pet.name"/><br/>
 *     宠物年龄:<input name="pet.age"/>
 */
@Data
public class Person {
    
    private String userName;
    private Integer age;
    private Date birth;
    private Pet pet;
    
}

@Data
public class Pet {

    private String name;
    private String age;

}

result

2.3.3 POJO封装过程

  • ServletModelAttributeMethodProcessor

2.3.4 参数处理原理

  • HandlerMapping中找到能处理请求的Handler(Controller.method())
  • 为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
  • 适配器执行目标方法并确定方法参数的每一个值
1) HandlerAdapter

在这里插入图片描述
0 - 支持方法上标注@RequestMapping
1 - 支持函数式编程的
xxxxxx

2) 执行目标方法
// Actually invoke the handler.
//DispatcherServlet -- doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法


//ServletInvocableHandlerMethod
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);

3 参数解析器-HandlerMethodArgumentResolver

确定将要执行的目标方法的每一个参数的值是什么;
SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
在这里插入图片描述
在这里插入图片描述

  • 当前解析器是否支持解析这种参数
  • 支持就调用 resolveArgument
4) 返回值处理器

在这里插入图片描述

5) 如何确定目标方法每一个参数的值
============InvocableHandlerMethod==========================
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

		MethodParameter[] parameters = getMethodParameters();
		if (ObjectUtils.isEmpty(parameters)) {
			return EMPTY_ARGS;
		}

		Object[] args = new Object[parameters.length];
		for (int i = 0; i < parameters.length; i++) {
			MethodParameter parameter = parameters[i];
			parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
			args[i] = findProvidedArgument(parameter, providedArgs);
			if (args[i] != null) {
				continue;
			}
			if (!this.resolvers.supportsParameter(parameter)) {
				throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
			}
			try {
				args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
			}
			catch (Exception ex) {
				// Leave stack trace for later, exception may actually be resolved and handled...
				if (logger.isDebugEnabled()) {
					String exMsg = ex.getMessage();
					if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
						logger.debug(formatArgumentError(parameter, exMsg));
					}
				}
				throw ex;
			}
		}
		return args;
	}
5.1) 挨个判断所有参数解析器那个支持解析这个参数
	@Nullable
	private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {
			for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
				if (resolver.supportsParameter(parameter)) {
					result = resolver;
					this.argumentResolverCache.put(parameter, result);
					break;
				}
			}
		}
		return result;
	}
5.2) 解析这个参数的值
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
5.3) 自定义类型参数 封装POJO

ServletModelAttributeMethodProcessor 这个参数处理器支持是否为简单类型。

public static boolean isSimpleValueType(Class<?> type) {
		return (Void.class != type && void.class != type &&
				(ClassUtils.isPrimitiveOrWrapper(type) ||
				Enum.class.isAssignableFrom(type) ||
				CharSequence.class.isAssignableFrom(type) ||
				Number.class.isAssignableFrom(type) ||
				Date.class.isAssignableFrom(type) ||
				Temporal.class.isAssignableFrom(type) ||
				URI.class == type ||
				URL.class == type ||
				Locale.class == type ||
				Class.class == type));
	}
@Override
	@Nullable
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
		Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");

		String name = ModelFactory.getNameForParameter(parameter);
		ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
		if (ann != null) {
			mavContainer.setBinding(name, ann.binding());
		}

		Object attribute = null;
		BindingResult bindingResult = null;

		if (mavContainer.containsAttribute(name)) {
			attribute = mavContainer.getModel().get(name);
		}
		else {
			// Create attribute instance
			try {
				attribute = createAttribute(name, parameter, binderFactory, webRequest);
			}
			catch (BindException ex) {
				if (isBindExceptionRequired(parameter)) {
					// No BindingResult parameter -> fail with BindException
					throw ex;
				}
				// Otherwise, expose null/empty value and associated BindingResult
				if (parameter.getParameterType() == Optional.class) {
					attribute = Optional.empty();
				}
				bindingResult = ex.getBindingResult();
			}
		}

		if (bindingResult == null) {
			// Bean property binding and validation;
			// skipped in case of binding failure on construction.
			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
			if (binder.getTarget() != null) {
				if (!mavContainer.isBindingDisabled(name)) {
					bindRequestParameters(binder, webRequest);
				}
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new BindException(binder.getBindingResult());
				}
			}
			// Value type adaptation, also covering java.util.Optional
			if (!parameter.getParameterType().isInstance(attribute)) {
				attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
			}
			bindingResult = binder.getBindingResult();
		}

		// Add resolved attribute and BindingResult at the end of the model
		Map<String, Object> bindingResultModel = bindingResult.getModel();
		mavContainer.removeAttributes(bindingResultModel);
		mavContainer.addAllAttributes(bindingResultModel);

		return attribute;
	}

WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中

GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
byte – > file

@FunctionalInterfacepublic interface Converter<S, T>
在这里插入图片描述
在这里插入图片描述
未来我们可以给WebDataBinder里面放自己的Converter;
private static final class StringToNumber<T extends Number> implements Converter<String, T>

自定义 Converter

    //1、WebMvcConfigurer定制化SpringMVC的功能
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                UrlPathHelper urlPathHelper = new UrlPathHelper();
                // 不移除;后面的内容。矩阵变量功能就可以生效
                urlPathHelper.setRemoveSemicolonContent(false);
                configurer.setUrlPathHelper(urlPathHelper);
            }

            @Override
            public void addFormatters(FormatterRegistry registry) {
                registry.addConverter(new Converter<String, Pet>() {

                    @Override
                    public Pet convert(String source) {
                        // 啊猫,3
                        if(!StringUtils.isEmpty(source)){
                            Pet pet = new Pet();
                            String[] split = source.split(",");
                            pet.setName(split[0]);
                            pet.setAge(Integer.parseInt(split[1]));
                            return pet;
                        }
                        return null;
                    }
                });
            }
        };
    }
6) 目标方法执行完成

将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据。
在这里插入图片描述

7) 处理派发结果

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);

InternalResourceView@Override
	protected void renderMergedOutputModel(
			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

		// Expose the model object as request attributes.
		exposeModelAsRequestAttributes(model, request);

		// Expose helpers as request attributes, if any.
		exposeHelpers(request);

		// Determine the path for the request dispatcher.
		String dispatcherPath = prepareForRendering(request, response);

		// Obtain a RequestDispatcher for the target resource (typically a JSP).
		RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
		if (rd == null) {
			throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
					"]: Check that the corresponding file exists within your web application archive!");
		}

		// If already included or response already committed, perform include, else forward.
		if (useInclude(request, response)) {
			response.setContentType(getContentType());
			if (logger.isDebugEnabled()) {
				logger.debug("Including [" + getUrl() + "]");
			}
			rd.include(request, response);
		}

		else {
			// Note: The forwarded resource is supposed to determine the content type itself.
			if (logger.isDebugEnabled()) {
				logger.debug("Forwarding to [" + getUrl() + "]");
			}
			rd.forward(request, response);
		}
	}
暴露模型作为请求域属性
// Expose the model object as request attributes.
		exposeModelAsRequestAttributes(model, request);
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request) throws Exception {

    //model中的所有数据遍历挨个放在请求域中
		model.forEach((name, value) -> {
			if (value != null) {
				request.setAttribute(name, value);
			}
			else {
				request.removeAttribute(name);
			}
		});
	}

2.4 数据响应与内容协商(响应数据)

说明

  • 响应页面:一般用于开发一个单体项目。
  • 响应数据:一般用来开发一些前后端分离的项目。
    在这里插入图片描述

2.4.1 响应JSON(响应json)

1) jackson.jar+@ResponseBody

说明:使用SpringBoot返回JSON数据的步骤:

  • 引入json依赖(引入web依赖会自动关联json依赖
  • 使用@ResponseBody注解
<!-- web场景依赖:-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
<!-- 进入web场景底层可以看到,自动引入了json场景:-->
    <dependency>
	      <groupId>org.springframework.boot</groupId>
	      <artifactId>spring-boot-starter-json</artifactId>
	      <version>2.3.4.RELEASE</version>
	      <scope>compile</scope>
    </dependency>

进入到web场景底层可以看到引入了json依赖:
在这里插入图片描述
给前端自动返回json数据;

1.1) 返回值解析器

在这里插入图片描述

try {
			this.returnValueHandlers.handleReturnValue(
					returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
		}
	@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

		HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
		if (handler == null) {
			throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
		}
		handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
	}
RequestResponseBodyMethodProcessor  	
@Override
	public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
			throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

		mavContainer.setRequestHandled(true);
		ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
		ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

		// Try even with null return value. ResponseBodyAdvice could get involved.
        // 使用消息转换器进行写出操作
		writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
	}
1.2) 返回值解析器原理

在这里插入图片描述

  • 1、返回值处理器判断是否支持这种类型返回值 supportsReturnType
  • 2、返回值处理器调用 handleReturnValue 进行处理
  • 3、RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
    ○ 1. 利用 MessageConverters 进行处理 将数据写为json
    • 1、内容协商浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型
      在这里插入图片描述
    • 2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
    • 3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
      • 1、得到MappingJackson2HttpMessageConverter可以将对象写为json
      • 2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
2) SpringMVC到底支持哪些返回值
ModelAndView
Model
View
ResponseEntity 
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask@ModelAttribute 且为对象类型的
返回值标注@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor
3) HTTPMessageConverter原理
3.1) MessageConverter规范

在这里插入图片描述
HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person

3.2) 默认的MessageConverter

在这里插入图片描述

0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式xml处理的

最终 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)

在这里插入图片描述

2.4.2 内容协商(响应xml和json)

根据客户端接收能力不同,返回不同媒体类型的数据。
eg:

  • 浏览器客户端返回json类型的数据。
  • 安卓客户端返回xml类型的数据。
1) 引入xml依赖

说明:响应xml需要引入依赖。

 <dependency>
     <groupId>com.fasterxml.jackson.dataformat</groupId>
     <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
2) postman分别测试返回json和xml

说明:可以使用 postman测试发送请求,返回json数据或者xml数据。

  • 返回json数据:需要改变请求头中Accept字段为:application/xml
  • 返回xml数据:需要改变请求头中Accept字段为:application/json
  • Accept:Http协议中规定的,告诉服务器本客户端可以接收的数据类型
    在这里插入图片描述
3) 开启浏览器参数方式内容协商功能

说明:为了方便内容协商,开启基于请求参数的内容协商功能。

  • 使用postman可以很方便的修改Accept请求头,接收响应类型的数据。
  • 如果是浏览器发送普通请求,浏览器中没办法设置Accept请求头信息(Ajax发送请求可以指定),那么如何改变接收的是xml还是json类型的数据呢???
  • 解决:浏览器发送普通请求,默认的优先级json高于xml(添加jackson依赖后xml优先级高于json),此时想要随时切换接收的类型数据(json、xml),可以在配置文件配置开启请求参数内容协商模式,之后在url上携带请求参数:format=json或者format=xml
spring:
    contentnegotiation:
      favor-parameter: true  #开启请求参数内容协商模式

发请求

  • http://localhost:8080/test/person?format=json
  • http://localhost:8080/test/person?format=xml

原理
在这里插入图片描述
确定客户端接收什么样的内容类型:

  1. Parameter策略优先确定是要返回json数据(通过获取请求头中的format的值来确定的)
    在这里插入图片描述
  2. 最终进行内容协商返回给客户端json即可。
4) 内容协商原理
  • 1、判断当前响应头中是否已经有确定的媒体类型。MediaType

  • 2、获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】
    contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
    在这里插入图片描述

    ○ HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
    在这里插入图片描述

  • 3、遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)

  • 4、找到支持操作Person的converter,把converter支持的媒体类型统计出来。

  • 5、客户端需要【application/xml】。服务端能力【10种、json、xml】
    在这里插入图片描述

  • 6、进行内容协商的最佳匹配媒体类型

  • 7、用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
    在这里插入图片描述
    导入了jackson处理xml的包,xml的converter就会自动进来

WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);

if (jackson2XmlPresent) {
			Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
			if (this.applicationContext != null) {
				builder.applicationContext(this.applicationContext);
			}
			messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
		}
5) 自定义 MessageConverter(重听)

实现多协议数据兼容。json、xml、x-guigu
0、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
1、Processor 处理方法返回值。通过 MessageConverter 处理
2、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
3、内容协商找到最终的 messageConverter

SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer

@Bean
public WebMvcConfigurer webMvcConfigurer(){
  return new WebMvcConfigurer() {

      @Override
      public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

      }
  }
}

在这里插入图片描述
在这里插入图片描述
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?【提示:参照SpringBoot官方文档web开发内容协商章节】

2.5 视图解析与模板引擎(响应页面)

2.5.1 概念

  • 视图解析:SpringBoot在处理完请求后跳转到某个页面的过程,叫作视图解析。

  • 模板引擎:模板引擎是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档

    • 就是将模板文件和数据通过模板引擎生成一个HTML代码
      在这里插入图片描述
  • 注意SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。

    • 原因:SpringBoot默认打包方式为jar包,jar包是个压缩包,jsp不支持在压缩包内编译的方式,所以SpringBoot是默认不支持jsp的。
    • 当然非要使用jsp,也可以进行配置(注意:jsp是放在webapp/WEB-INF目录下)。
  • 解决:想要进行页面渲染页面跳转,可以引入第三方的模板引擎进行实现。

  • SpringBoot支持的第三方模版引擎技术

    • FreeMarker
    • Thymeleaf (推荐)
    • groovy-templates
    • JSP

2.5.2 视图解析

在这里插入图片描述

1) 视图解析原理流程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2.5.3 模板引擎-Thymeleaf

1) thymeleaf简介

Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
现代化、服务端Java模板引擎

优点:语法简单,易上手。

缺点:thymeleaf模板引擎并不是一个高性能的引擎,对于开发高并发的应用进行页面跳转,应该是做一个前后端分离,让职业的前端人员来做。或者做一个高并发的后台管理系统,我们应该选择其它的模板引擎。但是对于一个简单的单体应用就可以使用它了。

总结:thymeleaf适用于做一个前后端不分离的简单的单体架构。

官网https://www.thymeleaf.org/
在这里插入图片描述
查看在线文档:
在这里插入图片描述
在这里插入图片描述

2) 基本语法
2.1) 表达式
表达式名字语法用途
变量取值${…}获取请求域、session域、对象等值
选择变量*{…}获取上下文对象值
消息#{…}获取国际化等值
链接@{…}生成链接
片段表达式~{…}jsp:include 作用,引入公共页面片段
2.2) 字面量
  • 文本值: ‘one text’ , ‘Another one!’ ,…
  • 数字: 0 , 34 , 3.0 , 12.3 ,…
  • 布尔值: true , false
  • 空值: null
  • 变量: one,two,… 变量不能有空格
2.3) 文本操作
  • 字符串拼接: +
  • 变量替换: |The name is ${name}|
2.4) 数学运算
  • 运算符: + , - , * , / , %
2.5) 布尔运算
  • 运算符: and , or
  • 一元运算: ! , not
2.6) 比较运算

比较: > , < , >= , <= ( gt , lt , ge , le )
等式: == , != ( eq , ne )

2.7) 条件运算
  • If-then: (if) ? (then)
    解释:前面成功了后面怎么办
  • If-then-else: (if) ? (then) : (else)
    解释:三元运算,前面成功了使用第一个,否则使用第二个
  • Default: (value) ?: (defaultvalue)
    解释:前面成功了使用默认值
2.8) 特殊操作
  • 无操作: _
3) 设置属性值 th:attr、th:value、th:action

设置单个值:th:attr="xx=xxx"

<form action="subscribe.html" th:attr="action=@{/subscribe}">
  <fieldset>
    <input type="text" name="email" />
    <!--设置默认值     使用thympleaf的写法
        执行流程:使用thympleaf表达式动态获取value值,然后覆盖默认的value值-->
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
  </fieldset>
</form>

设置多个值:th:attr="xx=xxx,xx=xxx"

<!--设置默认值     
    使用thympleaf的写法-->
<img src="../../images/gtvglogo.png"  
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

以上两个的简写 th:xxxx

<!--设置默认值     使用thympleaf的写法-->
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">

所有h5兼容的标签写法:
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes

4) 迭代 th:each

情况1:

<tr th:each="prod : ${prods}">
        <td th:text="${prod.name}">Onions</td>
        <td th:text="${prod.price}">2.41</td>
        <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

情况2:

<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
  <td th:text="${prod.name}">Onions</td>
  <td th:text="${prod.price}">2.41</td>
  <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
5) 条件运算 th:if、th:switch

情况1:th:if

<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>

情况2:th:switch

<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>
6) 属性优先级

在这里插入图片描述

2.5.3 thymeleaf使用

1) 引入Starter(依赖)
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
2) SpringBoot自动配置好了thymeleaf

底层源码:

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }

自动配好的策略:

  • 1、所有thymeleaf的配置值都在 ThymeleafProperties
  • 2、自动配置好了2个东西:
    • 配置好了 SpringTemplateEngine (thymeleaf的模板引擎)
    • 配置好了 ThymeleafViewResolver (视图解析器)
  • 4、我们只需要直接开发页面

页面默认放的位置:必须放在templates目录下,资源名必须是以.html后缀为结尾。

public static final String DEFAULT_PREFIX = "classpath:/templates/";

public static final String DEFAULT_SUFFIX = ".html";  //xxx.html
3) 页面开发
3.1) 编写html页面

在这里插入图片描述

<!DOCTYPE html>
<!--加入命名空间:xmlns:th="http://www.thymeleaf.org
    作用:导入之后使用thympleaf的标签会有提示效果,不导入也可以。-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--th:text 改变标签的文本值-->
<h1 th:text="${msg}">哈哈</h1>
<h2>
    <!--使用${}取地址值:真正把link代表的地址值取出来了-->
    <a href="www.atguigu.com" th:href="${link}">去百度</a>  <br/>
    <!--使用@{}取地址值:它会认为里面的都是字符串,之后会拼接成地址,
                      @{xxx}还会自动拼接项目访问路径-->
    <a href="www.atguigu.com" th:href="@{/link}">去百度2</a>
</h2>
</body>
</html>
3.2) 控制层代码

在这里插入图片描述

package com.cn.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;


@Controller
public class ViewTestController {
    @GetMapping("/atguigu")
    public String atguigu(Model model){

        //model中的数据会被放在请求域中 request.setAttribute("a",aa)
        model.addAttribute("msg","你好 guigu");
        model.addAttribute("link","http://www.baidu.com");
        return "success";
    }
}

运行测试:
在这里插入图片描述
在这里插入图片描述

3.3) 添加项目访问路径

在这里插入图片描述

# 添加项目的前置访问路径
server:
  servlet:
    context-path: /world

运行测试:
在这里插入图片描述

2.5.4 构建后台管理系统

1) 项目创建

在这里插入图片描述
在这里插入图片描述

2) 静态资源处理

复制笔记中初始源码,admin目录下的静态资源放到 static 文件夹下
在这里插入图片描述
复制登录页面:login.html

复制登录成功后跳转到的首页index.html,并改名为main.html
在这里插入图片描述

3) 实现后台管理系统基本功能
3.1) 修改login.html、main.html
  • login.html:

    • 注意:添加thymeleaf命名空间头<html lang="en" xmlns:th="http://www.thymeleaf.org">
      在这里插入图片描述
  • main.html:

    • 注意:添加thymeleaf命名空间头
      在这里插入图片描述
3.2) 实体类 User

作用:保存登录成功后输入的请求参数,用户名和密码。

在这里插入图片描述

package com.cn.bean;

import lombok.Data;


//模拟登录成功后才能访问,是为了保存login.html页面提交的请求参数 userName、passWord
@Data
public class User {
    private String userName;
    private String passWord;

}

3.3) 控制层代码 IndexController

在这里插入图片描述

package com.cn.controller;

import com.cn.bean.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.http.HttpSession;

/**
 * ClassName: IndexController
 * Package: com.cn.controller
 * Description:
 *
 * @Author xxx
 * @Create 2023/5/7 19:30
 * @Version 1.0
 */
@Controller
public class IndexController {

    /**
     * 1.浏览器发送请求,跳转到登录页面
     * @return
     */
    @GetMapping(value = {"/","/login"})
    public String loginPage(){

        return "login";
    }


    /**
     * 2.在登录页面输入用户名密码,点击登录通过表单发送请求: th:action="@{/login}"
     *  登录成功后跳转到后端管理的首页 main.html
     *  但是这样用请求转发跳转会有表单重复提交问题,所以再写一个方法,通过重定向跳转来解决。
     */
    @PostMapping("/login")
    public String main(User user, HttpSession session, Model model){
        // 模拟登录成功后才能访问
        if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassWord())){
            //把登陆成功的用户保存起来
            session.setAttribute("loginUser",user);

            //return "main"; 直接使用请求转发会有表单重复提交问题
            //登录成功重定向到main.html;  重定向防止表单重复提交
            return "redirect:/main.html";
        }else {
            //登陆失败一般会返回页面一个提示消息
            model.addAttribute("msg","账号密码错误");
            //回到登录页面
            return "login";
        }
    }

    /**
     * 3.跳转到main页面(首页)
     * 新问题:重定向一个新的方法跳转到main页面虽然解决了表单重复提交的问题,
     *       但是这样会导致在任何一个浏览器直接输入这个访问地址,就能访问到此方法进入到main页面,
     *       正确的业务流程是,登录成功后才能访问到main.html
     * 解决:需要在跳转main页面的方法中进行判断,登录之后才能进行访问。
     *
     * @return
     */
    @GetMapping("/main.html")
    public String mainPage(HttpSession session,Model model){

        //业务流程:在登录成功后,来到后台管理系统的任何页面都要进行判断是否登录
        //       (即:在每个返回后台管理系统页面的方法中进行判断)
        // 方式一:每个方法写一个判断,但是这样太麻烦。 (暂时以方式一为例进行测试)
        // 方式二:可以直接使用拦截器,过滤器统一处理。

        //方式一写法:
        Object loginUser = session.getAttribute("loginUser");
        if(loginUser != null){
            return "main";
        }else {
            //回到登录页面
            model.addAttribute("msg","请重新登录");
            return "login";
        }
    }


}

3.4) 运行测试

在这里插入图片描述
在这里插入图片描述

4) 模板抽取
4.1) 添加静态页面

在这里插入图片描述
对应的页面:
在这里插入图片描述
给每个页面都添加thymeleaf命名空间
在这里插入图片描述

4.2) 编写控制层方法返回页面

在这里插入图片描述

package com.cn.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * ClassName: TableController
 * Package: com.cn.controller
 * Description:
 *
 * @Author xxx
 * @Create 2023/5/8 22:53
 * @Version 1.0
 */
@Controller
public class TableController {

    @GetMapping("/basic_table")
    public String basic_table(){

        return "table/basic_table";
    }


    @GetMapping("/dynamic_table")
    public String dynamic_table(){

        return "table/dynamic_table";
    }


    @GetMapping("/responsive_table")
    public String responsive_table(){

        return "table/responsive_table";
    }

    @GetMapping("/editable_table")
    public String editable_table(){

        return "table/editable_table";
    }
}

4.3) 抽取公共的页面
  • 说明:这几个页面里面的左侧和上面的导航栏格式相同,所以可以进行抽取放到公共的页面。
    在这里插入图片描述

  • 创建公共页面:common.html
    在这里插入图片描述

  • 抽取步骤:略,参考官方文档-Template Layout(2种方式)
    在这里插入图片描述

5) 数据渲染

页面代码:
在这里插入图片描述

 <table class="display table table-bordered" id="hidden-table-info">
        <thead>
        <tr>
            <th>#</th>
            <th>用户名</th>
            <th>密码</th>
        </tr>
        </thead>
        <tbody>
        <tr class="gradeX" th:each="user,stats:${users}">
            <td th:text="${stats.count}">Trident</td>
            <td th:text="${user.userName}">Internet</td>
            <td >[[${user.passWord}]]</td>
        </tr>

        </tbody>
        </table>

控制层方法:
在这里插入图片描述

@GetMapping("/dynamic_table")
    public String dynamic_table(Model model){
        //表格内容的遍历
        List<User> users = Arrays.asList(new User("zhangsan", "123456"),
                new User("lisi", "123444"),
                new User("haha", "aaaaa"),
                new User("hehe ", "aaddd"));

        model.addAttribute("users",users);

        return "table/dynamic_table";
    }

测试:
在这里插入图片描述

2.6 拦截器

说明:后台管理项目只有登录成功后才能跳转到后台管理的任何页面,当前只有访问main.html才进行了判断,其它页面想要实现可以每个方法都进行判断,太过麻烦可以使用过滤器或者拦截器。

2.6.1 创建拦截器

在这里插入图片描述

package com.cn.interceptor;

/**
 * ClassName: LoginInterceptor
 * Package: com.cn.interceptor
 * Description:
 *
 * @Author xxx
 * @Create 2023/5/10 10:59
 * @Version 1.0
 */

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * 登录检查
 * 1、配置好拦截器要拦截哪些请求
 * 2、把这些配置放在容器中
 */
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {//创建拦截器

    /**
     * 目标方法执行之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();
        log.info("preHandle拦截的请求路径是{}",requestURI);


        /**
         * 登录检查逻辑: 先使用拦截器拦截请求进行判断,如果没有登录则跳转到
         *             登录页面,如果登录成功则放行执行目标方法
         */
        HttpSession session = request.getSession();
        Object loginUser = session.getAttribute("loginUser");
        if(loginUser != null){
            //放行
            return true;//true:代表放行
        }

        /**
         * 业务逻辑:拦截住未登录的请求,跳转到登录页面并显示提示信息
         *  只能使用原生的转发:因为这里使用了return false表示拦截,return已经用过了,
         *                    所以只能使用原生的请求转发进行页面跳转。
         *  只能是转发而不能是重定向: 因为想要把提示信息携带到登录页面,所以只能使用请求转发
         *                         不能使用重定向:response.sendRedirect("/")
         */
        request.setAttribute("msg","请先登录");
        //转发的路径  forward是固定写法
        request.getRequestDispatcher("/").forward(request,response);
        return false; //false代表拦截
    }

    /**
     * 目标方法执行完成以后
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle执行{}",modelAndView);
    }

    /**
     * 页面渲染以后
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion执行异常{}",ex);
    }
}

2.6.2 配置拦截器

在这里插入图片描述

package com.cn.config;


import com.cn.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 1、编写一个拦截器实现HandlerInterceptor接口
 * 2、编写配置类,将拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
 * 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
 */
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //添加拦截器:参数为创建的拦截器对象
        registry.addInterceptor(new LoginInterceptor())
                //拦截请求的方法:/**表示所有请求都被拦截包括静态资源
                .addPathPatterns("/**")
                //放行请求的方法:放行去登录页的请求、登录页点击登录的请求、放行静态资源的请求
                //去登录页和登录功能,即使你没有登陆成功这两个也是一定要所有人都能访问
                .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**");

        /**
         * 放行静态资源的方式:2种
         *   1.静态资源都是放在static目录下的对应目录css、fonts、images、js,
         *     访问css页面的的地址一定带/css前缀,访问js的只是一定带上/js前缀,所以可以写成
         *     .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**");
         *   2.如果静态资源下的文件多,使用第一种太麻烦,可以在配置文件中配置访问静态资源的访问前缀,
         *     之后在访问任何静态资源的页面都要加上static前缀,此时可以配置成
         *     .excludePathPatterns("/","/login","/static/**");
         */
    }
}

  • 第二种放行静态资源方式:
    • 在在配置文件中配置访问静态资源的访问前缀:
      在这里插入图片描述
    • 修改页面中访问静态资源的url地址,eg:
      在这里插入图片描述
    • 之后只需要在配置拦截器中配置从static开始放行即可
      在这里插入图片描述

2.6.3 运行测试

注释掉原先写在方法中判断是否登录的方式:
在这里插入图片描述
不登录直接在页面输入访问main.html页面的url,发现登录失败跳转到登录页面,说明请求被拦截器拦截了。
在这里插入图片描述
登录成功,查看控制台输出的登录成功后拦截的请求:
在这里插入图片描述

2.6.4 拦截器原理

  1. 根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】
  2. 先来顺序执行 所有拦截器的 preHandle方法
    2.1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
    2.2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
  3. 如果任何一个拦截器返回false。直接跳出不执行目标方法
  4. 所有拦截器都返回True。执行目标方法
  5. 倒序执行所有拦截器的postHandle方法。
  6. 前面的步骤有任何异常都会直接倒序触发 afterCompletion
  7. 页面成功渲染完成以后,也会倒序触发 afterCompletion

在这里插入图片描述
在这里插入图片描述

2.7 文件上传01(尚硅谷)

2.7.1 准备工作

复制笔记中的页面到项目中,并修改为引入公共模板的方式:
在这里插入图片描述
修改页面的访问路径:
在这里插入图片描述
添加控制层方法:
在这里插入图片描述

package com.cn.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * 文件上传测试
 */
@Controller
public class FormTestController {

    //跳转到文件提交页面
    @GetMapping("/form_layouts")
    public String form_layouts(){

        return "form/form_layouts";
    }
}

运行,发现登录后正确跳转到页面
在这里插入图片描述

2.7.2 页面表单

在这里插入图片描述

<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
    <div class="form-group">
        <label for="exampleInputEmail1">邮箱</label>
        <input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
    </div>
    <div class="form-group">
        <label for="exampleInputPassword1">名字</label>
        <input type="password" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
    </div>
    <!--单文件上传:设置type类型为file-->
    <div class="form-group">
        <label for="exampleInputFile">头像</label>
        <input type="file" name="headerImg" id="exampleInputFile">
    </div>
    <!--多文件上传:置type类型为file,并且添加multiple属性-->
    <div class="form-group">
        <label for="exampleInputFile">生活照</label>
        <input type="file" name="photos" multiple>
    </div>
    <div class="checkbox">
        <label>
            <input type="checkbox"> Check me out
        </label>
    </div>
    <button type="submit" class="btn btn-primary">提交</button>
</form>

2.7.3 文件上传代码

在这里插入图片描述

package com.cn.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

/**
 * 文件上传测试
 */
@Controller
@Slf4j
public class FormTestController {

    //跳转到文件提交页面
    @GetMapping("/form_layouts")
    public String form_layouts(){

        return "form/form_layouts";
    }
    /**
     *获取表单普通项:email、username
     *获取文件表单项:MultipartFile 自动封装上传过来的文件:
     *   1.通过MultipartFile, 获取单文件上传
     *   2.通过MultipartFile[]数组, 获取多文件上传(表单项一次性可以上传多个文件)
     *
     * @param email
     * @param username
     * @param headerImg
     * @param photos
     * @return
     */
    @PostMapping("/upload")
    public String upload(@RequestParam("email") String email,
                         @RequestParam("username") String username,
                         @RequestPart("headerImg") MultipartFile headerImg,
                         @RequestPart("photos") MultipartFile[] photos) throws IOException {

        //headerImg.getSize()获取上传文件的大小
        //photos.length获取有几个上传文件,即数组的长度。
        log.info("上传的信息:email={},username={},headerImg={},photos={}",
                email,username,headerImg.getSize(),photos.length);//日志目的:查看获取的参数情况

        //保存单个文件
        if(!headerImg.isEmpty()){ //文件不为空才能操作
            //保存到文件服务器,OSS服务器(阿里云对象存储服务器),这里方便测试保存到磁盘中
            //获取上传文件的文件名
            String originalFilename = headerImg.getOriginalFilename();
            /**
             * 文件输入一般用输入流:InputStream,但是用流需要读取字节信息
             *  还要开关流,比较麻烦,如何解决呢?
             *  SpringMVC 提供了工具API(transferTo,底层封装了InputStream的操作步骤)专门操作流文件.
             *  流会有异常,这里以抛出为例
             */
            //磁盘要有这个cache文件夹
            headerImg.transferTo(new File("D:\\cache\\"+originalFilename));
        }
        //保存多个文件,遍历获取的文件数组
        if(photos.length > 0){//大于0,数组有文件
            for (MultipartFile photo : photos) {
                if(!photo.isEmpty()){//每个文件不为空
                    String originalFilename = photo.getOriginalFilename();//获取上传的文件名
                    photo.transferTo(new File("D:\\cache\\"+originalFilename));//用的本地磁盘
                }
            }
        }


        return "main";
    }

}


2.7.4 配置文件:

在这里插入图片描述

properties文件写法:
#配置单个文件上传的最大值,默认为1MB
spring.servlet.multipart.max-file-size=10MB
#配置总请求上传的文件量的最大值,默认为10MB
spring.servlet.multipart.max-request-size=100MB


yml文件写法:
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 100MB

2.7.5 运行测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.7.6 自动配置原理

文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties

  • 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
  • 原理步骤
    ○ 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
    ○ 2、参数解析器来解析请求中的文件内容封装成MultipartFile
    ○ 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
    FileCopyUtils。实现文件流的拷贝
    @PostMapping("/upload")
    public String upload(@RequestParam("email") String email,
                         @RequestParam("username") String username,
                         @RequestPart("headerImg") MultipartFile headerImg,
                         @RequestPart("photos") MultipartFile[] photos)

在这里插入图片描述

2.8 文件上传02(黑马)

2.8.1 简介

概述

  • 文件上传,是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。
  • 文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
    在这里插入图片描述

要求

  • 前端程序:需要满足三要素

    • 在form表单中使用file文件上传标签
    • 表单提交方式必须为post,因为文件上传的数据都比较大,所以只能使用post提交。
    • 在form表单通过encType 属性指定表单的编码格式为multipart/form-data,因为普通默认的编码格式不适合传输大型的二进制数据的。
      在这里插入图片描述
      在这里插入图片描述
  • 后端程序

    • 普通表单项参数:和以前接收的方式相同
    • 文件表单项参数:直接使用Spring提供的Api,声明一个MultipartFile 类型的参数来接收,保证方法的形参名和表单项的name属性的值一致,如果不一致使用@requesrparam注解进行参数绑定。
      在这里插入图片描述

2.8.2 本地存储(很少有)

1) 概述
  • 在服务端,接收到上传上来的文件之后,将文件存储在本地服务器磁盘中。
  • 缺点
    • 文件直接存储在服务器的磁盘目录中,浏览器无法直接访问到文件。
    • 本地磁盘的空间有限制。
    • 本地磁盘有可能损坏。
      在这里插入图片描述
  • 解决
    • 自己买服务器自己搭建服务(FastDFS分布式文件系统、MinlO对象存储服务搭建集群):比较繁琐,成本高。
    • 别的公司提供好的云服务来存储上传的文件:阿里云,腾讯云,百度云,华为云等等:使用方便,安全可靠,需要支付一定费用。
2) 案例测试

页面:upload.html
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>上传文件</title>
</head>
<body>

    <form action="/upload" method="post" enctype="multipart/form-data">
        姓名: <input type="text" name="username"><br>
        年龄: <input type="text" name="age"><br>
        头像: <input type="file" name="image"><br>
        <input type="submit" value="提交">
    </form>

</body>
</html>

配置文件:
在这里插入图片描述

#配置单个文件上传的最大值,默认为1MB
spring.servlet.multipart.max-file-size=10MB
#配置总请求上传的文件量的最大值(一个请求中可以上传多个文件,设置上传多个文件最大值),默认为10MB
spring.servlet.multipart.max-request-size=100MB

控制层代码:
在这里插入图片描述

package com.cn.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.util.UUID;

@Slf4j
@RestController
public class UploadController {

    //本地存储文件
    @PostMapping("/upload")
    public String upload(String username , Integer age , MultipartFile image) throws Exception {
        log.info("文件上传: {}, {}, {}", username, age, image);

        //获取原始文件名 - 1.jpg  123.0.0.jpg,直接使用原始的文件名作为上传后的文件名,一旦
        //  上传的文件名相同则后面会覆盖前面的。----解决:使用uuid生成文件名+原始文件名的后缀
        String originalFilename = image.getOriginalFilename();

        //构造唯一的文件名 (不能重复) - uuid(通用唯一识别码) de49685b-61c0-4b11-80fa-c71e95924018
        //以最后一个点分割,获取最后一个点所处的位置。
        int index = originalFilename.lastIndexOf(".");
        //从最后一个点的位置开始向后截取,直到字符串的尾部,得到的结果就是后缀名。
        String extname = originalFilename.substring(index);
        String newFileName = UUID.randomUUID().toString() + extname;

        log.info("新的文件名: {}", newFileName);

        //将文件存储在服务器的磁盘目录中,参数为File对象。eg: E:\images
        //File对象的参数为:相对路径、绝对路径。即:路径目录+文件名,直接使用原始文件名
        //一旦前后上传的文件名相同会导致覆盖效果,所以一般使用uuid生成文件名前缀+原始文件名的后缀,生成
        // 一个新的文件名
        image.transferTo(new File("E:\\images\\"+newFileName));

        return "文件上传成功";
    }
}

在这里插入图片描述

在这里插入图片描述

2.8.3 阿里云OSS存储

1)概述

阿里云概述:

  • 阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商。
  • 云:云端,可以理解为互联网。
  • 云服务:通过互联网对外提供的各种各样的服务。
    • eg:语音服务、短信服务、邮件服务、视频直播服务、文字识别服务。对象存储服务…
  • 优点:项目开发时用到某一些服务就不需要自己来开发了,直接使用阿里云提供好的服务就行了。
    • eg:项目当中使用短信发送功能,我们自己开发需要和三大运营商对接,非常的繁琐。而现在是由阿里云和运营商对接,我们只需要调用阿里云的短信服务就可以很方便的发送短信了。这样大大降低了项目开发难度,同时也提高了开发效率。(收费)

阿里云OSS概述:

  • 阿里云对象存储OSS (0bject Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。
    在这里插入图片描述

  • 上传流程
    在这里插入图片描述

  • 使用第三方服务的通用思路:
    在这里插入图片描述

    • 准备工作:账号注册,实名认证,登录到后台的一些基本配置

    • 参照官方提供的SDK代码编写入门程序:通过入门程序把基本的功能和流程先测通。

      • SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
    • 集成使用:在项目中集成这个服务来完成特定的功能。

    • 总结:无论使用什么样的第三发的服务,比如当前使用的阿里云OSS,还是项目中使用到的短信服务,微信支付服务,基本的使用流程都是类似的。

  • 阿里云OSS(对象存储服务)----具体使用步骤:
    在这里插入图片描述

    • 充值:目前只是学习演示,不会在阿里云OSS存储大量的文件,所以不用充值。
    • Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
2)案例测试
2.1)准本工作1:注册,充值,开通服务
  1. 注册:打开https://www.aliyun.com/ ,申请阿里云账号并完成实名认证。
    在这里插入图片描述

  2. 充值 (可以不用做)

  3. 登录阿里云官网。 点击右上角的控制台。
    在这里插入图片描述
    将鼠标移至产品,找到并单击对象存储OSS,打开OSS产品详情页面。在OSS产品详情页中的单击立即开通。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    开通服务后,在OSS产品详情页面单击管理控制台直接进入OSS管理控制台界面。您也可以单击位于官网首页右上方菜单栏的控制台,进入阿里云管理控制台首页,然后单击左侧的对象存储OSS菜单进入OSS管理控制台界面。
    在这里插入图片描述

2.2)准本工作2:创建Bucket,获取秘钥
  1. 新建Bucket,命名为 web-wenjian-shangchuan ,读写权限为 公共读
    在这里插入图片描述
  2. 获取秘钥:秘钥是一个身份凭证,证明你是阿里云OSS这个云服务的合法用户。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
2.3)参照官方SDK编写入门程序
2.3.1)打开官方文档----文件上传代码示例
  • 打开OSS官方文档
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 添加依赖
    在这里插入图片描述
   <dependency>
       <groupId>com.aliyun.oss</groupId>
       <artifactId>aliyun-sdk-oss</artifactId>
       <version>3.15.1</version>
   </dependency>
  • 操作SDK示例代码
    在这里插入图片描述
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;

public class Demo {

    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "yourAccessKeyId";
        String accessKeySecret = "yourAccessKeySecret";
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "examplebucket";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "exampledir/exampleobject.txt";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        String filePath= "D:\\localpath\\examplefile.txt";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            InputStream inputStream = new FileInputStream(filePath);
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
            // 创建PutObject请求。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
} 
2.3.2)案例测试
  1. 添加依赖坐标到pom文件
    在这里插入图片描述

  2. 把官方文档的文件上传代码示例复制到控制层代码中,进行改造(只需要改造定义变量中赋的值,核心代码不用修改)
    在这里插入图片描述

    • OSS对象存储服务的地址
      在这里插入图片描述
    • 秘钥
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      • 如果秘钥泄露了,可以点击禁用在重新生成一个
        在这里插入图片描述
    • 存储空间的名字:
      在这里插入图片描述
    • 上传到存储空的文件名:自己起个名字。
    • 本地的哪一个文件上传到阿里云OSS:填写本地磁盘文件的绝对路径。
      在这里插入图片描述
  3. 改造后的代码
    在这里插入图片描述

package com.cn.controller;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;

public class Demo {

    public static void main(String[] args) throws Exception {
        //第一部分:定义一些变量
        // 1.指定OSS对象存储服务的地址:     Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 2.指定秘钥,证明你是合法的用户:    阿里云账号AccessKey拥有所有API的访问权限,风险很高。
        //                             强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "LTAI5tSHskmRrsoA2C1sikMm";
        String accessKeySecret = "d6WvJcYYhfsdq36Pl4jAt72wkxGO1N";
        // 3.指定文件上传所存储的工作空间Bucket: 填写Bucket名称,例如examplebucket。
        String bucketName = "web-wenjian-shangchuan";
        // 4.指定在工作空间Bucket所存储的文件名叫什么: 填写Object完整路径,完整路径中不能包含Bucket名称,
        //                                    例如exampledir/exampleobject.txt。
        String objectName = "1.jpg";
        // 5.指定将本地的哪一个文件上传到阿里云OSS:填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        //                                    如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        String filePath= "D:\\图片\\a1.jpeg";

        //第二部分:核心代码
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            InputStream inputStream = new FileInputStream(filePath);
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
            // 创建PutObject请求。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
        } catch (OSSException oe) {

            //第三部分:输出错误日志
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
} 
  1. 运行测试:
    在这里插入图片描述
  2. 打开阿里云OSS的后台,查看效果
    在这里插入图片描述
  3. 在浏览器输入地址进行回车,会从阿里云OSS下载上传后的图片
    在这里插入图片描述
    在这里插入图片描述
2.4)在项目中集成使用

详情查看:SpringBoot—CRUD测试案例

2.8 异常处理

2.8.1 错误处理

1) 默认规则
  • 默认情况下,Spring Boot提供/error处理所有错误的映射

    • 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据
      • 使用postman访问一个错误路径 :在这里插入图片描述
      • 使用浏览器访问一个错误路径 :
        在这里插入图片描述
  • 要对其进行自定义,添加View解析为error(即:可以自定义返回的错误页面)

    • 要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。
    • 自定义异常的页面需要放在:
      • static/error/xxx.html或者templates/error/xxx.html
    • error/下的4xx,5xx页面会被自动解析;
      • 即:此目录下以4开头或者以5开头的页面会被自动解析。在这里插入图片描述
  • 测试自定义错误页面:

    • 在控制层方法中添加代码造成500异常在这里插入图片描述
    • 添加自定义异常页面 在这里插入图片描述
    • 登录后台管理系统,输出错误的url地址,出现自定义的404页面 在这里插入图片描述
      在这里插入图片描述
    • 点击dynamic_table页面,因为对应的控制层方法中模拟了出现错误代码,所以会出现500页面
      在这里插入图片描述
      在这里插入图片描述
2) 定制错误处理逻辑(待定+++++++++++++)
  • 自定义错误页
    ○ error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页

  • @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的

  • @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error

  • Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
    ○ response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
    在这里插入图片描述

  • 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则
    在这里插入图片描述

  • ErrorViewResolver 实现自定义处理异常;
    ○ response.sendError 。error请求就会转给controller
    ○ 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
    basicErrorController 要去的页面地址是 ErrorViewResolver

3) 异常处理自动配置原理
  • ErrorMvcAutoConfiguration 自动配置异常处理规则
    容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes

    • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
    • DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
      在这里插入图片描述

    在这里插入图片描述

    容器中的组件:类型:BasicErrorController --> id:basicErrorController(json+白页 适配响应)

    • 处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);
    • 容器中有组件 View->id是error;(响应默认错误页)
    • 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
      ○ 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
    • 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
    • error/404、5xx.html

如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
在这里插入图片描述

4) 异常处理步骤流程
  • 1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException

  • 2、进入视图解析流程(页面渲染?)
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

  • 3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;

    • 3.1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
    • 3.2、系统默认的 异常解析器;
      在这里插入图片描述

    ○ 3.2.1、DefaultErrorAttributes先来处理异常。把异常信息保存到request域,并且返回null;
    ○ 3.2.2、默认没有任何人能处理异常,所以异常会被抛出

    • 3.2.2.1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理

    • 3.2.2.2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
      在这里插入图片描述

    • 3.2.2.3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html

    • 4、模板引擎最终响应这个页面 error/500.html

2.9 Web原生组件注入(Servlet、Filter、Listener)

2.9.1 使用Servlet API(方式一 推荐)

使用到的注解

  • @ServletComponentScan(basePackages = "com.atguigu.admin") :指定原生Servlet组件都放在哪里
  • @WebServlet(urlPatterns = "/my"):效果:直接响应,没有经过Spring的拦截器?
  • @WebFilter(urlPatterns={"/css/*","/images/*"})
  • @WebListener

扩展:DispatchServlet 如何注册进来

  • 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties对象;对应的配置文件配置项是 spring.mvc
  • 通过 ServletRegistrationBean<DispatcherServlet> 把 DispatcherServlet 配置进来。
  • 默认映射的是 / 路径,也可以在properties配置文件中修改spring.mvc.servlet.path=/mvc/,一般也不需要修改,没有啥意义。

Tomcat-Servlet:使用tomact做原生的servlet开发(java web阶段)

  • 多个Servlet都能处理到同一层路径,精确优选原则
    • A: /my/
    • B: /my/1

问题:为什么使用@WebServlet设置的路径访问,效果是直接响应,没有经过Spring的拦截器???

  • 当前项目有2个servlet:
    • 自己注册的servlet:MyServlet,路径是: /my
    • SpringMvc处理所有请求的前端控制器:DispatcherServlet,路径是: /
  • 同样此时也遵守精确优先原则:发送请求的路径为/my,此时直接走/my的路径而不是/
    image.png
1)注册servlet

说明:使用@ServletComponentScan()+@WebServlet()

步骤一:创建servlet,并使用servlet3.0的注解配置访问路径
在这里插入图片描述

package com.cn.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


//servlet3.0提供的注解,配置servlet的访问路径
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {

    private static final long serialVersionUID = -3250793476589229955L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("66666");
    }
}

步骤二:在主启动类上添加servlet的包扫描注解,注意即便默认值可以扫描但是注解本身不能省略。
在这里插入图片描述

package com.cn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
/**  @ServletComponentScan(basePackages = "com.cn") :
 *   spring提供的注解,可以把自己写的servlet扫描进来,默认值是主配置类所在的包以及它下面的子包
 *
 *   如果默认值可以扫描到,则注解中的值可以省略,但是注解本身不能省。如
 *    @ServletComponentScan()
 */
@ServletComponentScan(basePackages = "com.cn")
public class BootWeb05AdminApplication {

    public static void main(String[] args) {
        SpringApplication.run(BootWeb05AdminApplication.class, args);
    }

}

步骤三:访问测试,发现虽然之前配置了拦截器只有登录后才能访问,但是访问servlet不登录也能访问。
在这里插入图片描述

2)注册Filter

说明:使用@ServletComponentScan+@WebFilter

步骤一:创建过滤器
在这里插入图片描述

package com.cn.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;


@Slf4j
//设置拦截的路径,注意这里使用/*代表拦截所有
@WebFilter(urlPatterns={"/css/*","/images/*"})
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("MyFilter初始化完成");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("MyFilter拦截请求");
        //程序放行(用户访问啥,它就接着继续向下访问)
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {
        log.info("MyFilter销毁");
    }
}

步骤二:配置包扫描
在这里插入图片描述
步骤三:测试访问静态资源,发现拦截器已经工作
在这里插入图片描述
在这里插入图片描述

3)注册Listener

说明:@WebListener+@ServletComponentScan

步骤一:创建监听器
在这里插入图片描述

package com.cn.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@Slf4j
@WebListener //说明此类是个监听器
public class MySwervletContextListener implements ServletContextListener {

    //项目启动时调用
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("MySwervletContextListener监听到项目初始化完成");
    }

    //项目停止时调用
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("MySwervletContextListener监听到项目销毁");
    }
}

步骤二:配置包扫描
在这里插入图片描述

2.9.2 使用RegistrationBean(方式二)

ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean

1)注释掉第一种方式

说明:

  • 把使用Servlet API的方式注释掉,但是创建的这几个servet、Filter、Listener类要保留。
  • 至于主启动类配置的包扫描,注不注释都行,因为只要这几个类上的注解注释掉它就不生效了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2)创建配置类

在这里插入图片描述

package com.cn.servlet;


import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;


// (proxyBeanMethods = true):保证依赖的组件始终是单实例的,默认就是true单实例
@Configuration(proxyBeanMethods = true)
public class MyRegistConfig {

    //servlet
    @Bean
    public ServletRegistrationBean myServlet(){
        //创建自定义servlet的对象
        MyServlet myServlet = new MyServlet();
        //参数1:servlet对象   参数2:servlet的访问路径,一个servlet可以处理多个请求
        return new ServletRegistrationBean(myServlet,"/my","/my02");
    }

    //过滤器
    @Bean
    public FilterRegistrationBean myFilter(){

        MyFilter myFilter = new MyFilter();
        //写法一:拦截和servlet请求相同的路径 参数1:自定义的过滤器对象  参数2:上面配置servlet的方法名
//        return new FilterRegistrationBean(myFilter,myServlet());
        //方式二:自己设置拦截路径
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));//设置要拦截的路径
        return filterRegistrationBean;
    }

    //监听器
    @Bean
    public ServletListenerRegistrationBean myListener(){
        MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
        //参数:自定义的监听器对象
        return new ServletListenerRegistrationBean(mySwervletContextListener);
    }
}

运行测试:略

2.10 嵌入式Servlet容器

说明:springboot默认提供的是tomact服务器,可以进行切换。

2.10.1 切换嵌入式Servlet容器(切换tomact服务器)

  • 默认支持的webServer
    • Tomcat、Jetty、Netty、Undertow(默认不支持jsp)
    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
  • 切换服务器
    在这里插入图片描述
<!--导入web场景启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
     <!--因为web依赖会自动关联tomact依赖,所以首先要排除tomact场景依赖-->
     <exclusions>
         <exclusion>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-tomcat</artifactId>
         </exclusion>
     </exclusions>
</dependency>
   
<!--之后导入其它的web服务器依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

在这里插入图片描述

  • 原理
    • SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
    • web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
    • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂—> Servlet 的web服务器)
    • SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
    • 底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
    • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
    • ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
    • TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize—this.tomcat.start();
    • 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)

2.10.2 定制Servlet容器(修改相应的配置如 端口号)

说明:可以修改服务器的一些规则,如:端口号,字符编码等。不但是tomact服务器,其它的web服务器同样可以修改。

  • 方式一:实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> 接口
    • 把配置文件的值和ServletWebServerFactory 进行绑定
  • 方式二(推荐):修改配置文件 server.xxx
  • 方式三:直接自定义 ConfigurableServletWebServerFactory

xxxxxCustomizer:定制化器,可以改变xxxx的默认规则

import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

//方式一
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    @Override
    public void customize(ConfigurableServletWebServerFactory server) {
        server.setPort(9000);
    }

}

2.11 定制化原理(待定+++++++++++++)

2.11.1 定制化的常见方式

  • 修改配置文件;
  • 定制化器xxxxxCustomizer;
  • 编写自定义的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;视图解析器
  • Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
@Configuration
public class AdminWebConfig implements WebMvcConfigurer
  • @EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能
    • 原理
    • 1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页…
    • 2、一旦使用 @EnableWebMvc 会 @Import(DelegatingWebMvcConfiguration.class)
    • 3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用
      • 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
      • 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
      • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
    • 4、WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    • 5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。
  • … …

2.11.2 原理分析套路

SpringBoot配置的过程:

  • 场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项

  • 总结:SpringBoot基本上底层都已经配置好了,我们做web开发、数据访问、单元测试、场景整合等等,只需要做两步:引入场景依赖和修改配置文件内容。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值