01 springBoot2入门

官网有对应的示例

02 了解自动配置原理

1、SpringBoot特点

1.1 依赖管理

父项目做依赖管理

<!-- 依赖管理 -->
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.1</version><relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- 依赖管理的父依赖 -->
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.5.1</version>
</parent>
<!-- 几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制,只能使用我设定好的版本 -->

starter场景启动器

只要引入starter,这个场景的所有常规需要的依赖我们都自动引入

比如我们引入web的场景启动器可以看到常用需要的依赖也一起导入了,我们就可以直接使用了

官方的starter都是以spring-boot-starter开头的,然后接着是他们的场景名

另外还有第三方提供的starter,*-spring-boot-starter

版本控制

我们不需要去指定版本,springboot会帮我们控制版本

如果我们想要指定的版本,也可以通过pom.xml文件去配置我们特定的版本

<!-- 在当前项目配置文件下添加 -->
<properties><mysql.version>5.1.43</mysql.version>
</properties>

1.2 自动配置

我们只要引入我们对应的场景,springboot就会帮我们自动配好配置文件

比如自动配置tomcat的dispatcherServlet、还有字符编码过滤等等。。。

默认的包结构:主程序所在的包及其下面的所有子包里面的组件都会默认被扫描进来,不用我们以前的包扫描配置,一切spiingBoot帮我们配好了

修改默认包扫描

如果想要修改默认的包扫描路径我们可以修改默认配置

​ ①@SpringBootApplication(scanBasePackages = "com.xiaozhi")

​ ②也可以使用ComponentScan指定扫描路径

这样的话我们就可以扫描com.xiaozhi下的所有包,而不只是主程序所在包下的所有包

自动配置项

我们在配置文件中进行赋值,最终它会映射到对应的配置类上,然后在容器中创建对象

我们的自动配置类并不是所有都能使用的,只有引入对应的场景才会开启对应的自动配置

2、容器功能

2.1 组件添加

1、@Configuration

Fuill模式与Lite模式

这两个模式的开启对应@Configuration属性proxyBeanMethods值为true和false

true为fuill模式,springBoot每次都会检查容器中是否存在这个bean,有的话就返回现有的,没有的话就创建,确保每个@Bean方法被调用多少次返回的都是单例的

false为lite模式,每个@Bean方法被调多少次返回的组件都是新创建的

注意:组件依赖必须使用Fill模式

设置为true,那调用的就是为单例的

public static void main(String[] args) {ConfigurableApplicationContext run = SpringApplication.run(Springboot1Application.class, args);Person person = run.getBean("person", Person.class);Animal cat = run.getBean("animal", Animal.class);// 判断person中的animal对象和System.out.println(person.getAnimal());System.out.println("是否单例" + (person.getAnimal() == cat));
}

设置为false,每次调用都是新的实例

2、@Component衍生的注解

@Controller、@Service、@Repository

3、组件扫描

①@ComponentScan

②Import

@Import({Person.class, Animal.class})   // 在容器中自动创建两个类型的组件,组件名是全类名
public class Config {

4、@Conditional

条件装配:满足Conditional指定的条件,则进行组件注入

它可以作用在配置类上,条件不满足那么这个配置类下的所有bean都不能注册

@Configuration(proxyBeanMethods = true)
public class Config {@Beanpublic Animal animal() {Animal animal = new Animal("cat");return animal;}// 存在animal这个名字的baen我才创建这个对象@ConditionalOnBean(name = "animal")@Beanpublic Person person() {Person person = new Person("xiaozhi",18);person.setAnimal(animal());return person;}
}

2.2 原生配置文件引入

① @ImportResource

注意:使用这个注解一定要在配置类中

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"><bean id="person" class="com.xiaozhi.pojo.Person"><property name="name" value="xiaozhi"/><property name="age" value="18"/><property name="animal" ref="animal"/></bean><bean id="animal" class="com.xiaozhi.pojo.Animal"><property name="name" value="cat"/></bean>
</beans>
@Configuration(proxyBeanMethods = true)
@ImportResource("classpath:bean.xml")
public class Config {//    @Bean
//    public Animal animal() {//        Animal animal = new Animal("cat");
//        return animal;
//    }
//    // 匹配的类型要在它之前创建它才会创建,不然就不会创建
//    @ConditionalOnBean(name = "animal")
//    @Bean
//    public Person person() {//        Person person = new Person("xiaozhi",18);
//        person.setAnimal(animal());
//        return person;
//    }
}

2.3 配置绑定

读取properties文件中的内容,并封装到javaBean中

原生的java代码我们需要创建一个Properties对象加载我们的properties文件,然后遍历得到配置文件中的每个名字,然后判断得到我们需要的内容

现在我们可以通过注解来完成读取配置文件的内容

① @ConfigurationProperties

只有在容器中的组件,才会有springBoot的功能

@ConfigurationProperties(prefix = "person")      // 配置文件的前缀
public class Person {
person.name=major

② @EnableConfigurationProperties + @ConfigurationProperties

@EnableConfigurationProperties(person.class)的两个功能

  1. 开启person类的配置功能
  2. 把这个person注册到容器中
@EnableConfigurationProperties(Person.class)
public class Springboot1Application {

注意:它的组件名是类名-全类名

3、自动装配原理入门

我们来看看springBoot帮我们做了什么事

3.1 引导自动加载配置类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

我们可以看到除了一些元注解外,有三个注解是@springBoot的主要功能注解

① @SpringBootConfiguration


表明它是一个配置类

② @ComponentScan

自定扫描那些

③ @EnableAutoConfiguration

主要功能是导入组件

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

1、@AutoConfigurationPackage

@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

我们点进去可以看到它进行了一个批量导入

启动springBoot它会执行这个方法将我们定义的bean注册到容器中,我们可以debug计算一下


可以看到我们计算出来的结果就是我们主启动类所在包,也就是说@AutoConfigurationPackage的功能是将主启动类所在包下的所有组件进行注册。

2、@Import(AutoConfigurationImportSelector.class)

导入一系列组件

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {public interface DeferredImportSelector extends ImportSelector {

可以看出来AutoConfigurationImportSelector是一个导入选择器,它可以有自己的逻辑实现

首先它会执行上面这个方法,然后执行getAutoConfigurationEntry(annotationMetadata)方法

这个方法中主要是getCandidateConfigurations(annotationMetadata, attributes)方法得到我们需要加载组件的名字,其他的操作就是排除一些我们不要的,下面的按需自动开启配置项会讲到




所以springBoot它会去加载类路径下的META-INF/spring.factories文件,然后得到需要导入的组件进行注册

可以看到确实是有的,我们打开这个文件进行查看

一共127个官方的自动配置包

springBoot它是会默认找org.springframework.boot.autoconfigure.EnableAutoConfiguration属性的


对应前面的getCandidateConfigurations(annotationMetadata, attributes)方法返回结果是一致的

测试

我们导入一个第三方的starter,查看结果

导入mybatis-spring-boot-starter

上面这个是它的spring.factories文件,我们可以看到需要导入的自动配置类有两个,我们debug查看一下是否有变化

可以看到数量编程了129了

3.2 按需开启配置项

springBoot并不是将127个全部都导入,它是按需开启的,只有导入对应的starter它才会有效,我们可以看一个案例

我们可以有@ConditionalOnClass这个注解,这个注解的意思就是,有Advice.class这个类那么AspectJAutoProxyingConfiguration类下的配置才会生效,其他的自动配置类也是类似的,需要达到条件才会开启。

3.3 修改默认配置

比如我们不需要springBoot默认帮我们配置好的,那么我们可以使用@Bean来配置一个我们自己的,以我们自己的优先

我们这边以字符编码拦截器为例,也就是我们的 HttpEncodingAutoConfiguration

@Configuration
public class Config {@Bean@ConditionalOnMissingBeanpublic CharacterEncodingFilter characterEncodingFilter() {CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();filter.setEncoding("gbk");return filter;}
}
@SpringBootApplication
@EnableConfigurationProperties(Person.class)
public class Springboot1Application {public static void main(String[] args) {ConfigurableApplicationContext run = SpringApplication.run(Springboot1Application.class, args);CharacterEncodingFilter filter = run.getBean("characterEncodingFilter", CharacterEncodingFilter.class);System.out.println("编码类型为:" + filter.getEncoding());   // 得到编码值}
}

@ConditionalOnProperty

设置为false就会报错


自我理解:我查看了源码,没有发现和配置文件绑定的属性,那么它就是根据配置文件的的值来判断是否开启这个功能

03 配置文件

1 文件类型

1.1 properties

这个没什么好说的

2.2 yaml

①简介

YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。 非常适合用来做以数据为中心的配置文件

②基本用法

  • key: value;kv之间有空格

  • 大小写敏感

  • 使用缩进表示层级关系

  • 缩进的空格数不重要,只要相同层级的元素左对齐即可

  • '#'表示注释

  • 字符串无需加引号,如果要加,’'与""表示字符串内容 会被 转义/不转义

    • 单引号会将转义符以字符串的形式输出
    • 双引号就是转义之后输出

③数据类型

  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
  • 对象:键值对的集合。map、hash、set、object
行内写法:  k: {k1:v1,k2:v2,k3:v3}
#或
k: k1: v1k2: v2k3: v3
  • 数组:一组按次序排列的值。array、list、queue
行内写法:  k: [v1,v2,v3]
#或者
k:- v1- v2- v3

④示例

@Data
@Component
@ConfigurationProperties(prefix = "user")
public class User {private String name;private Integer age;private Date birth;private Pet pet;private String[] animal;private List<String> hobby;private Map<String, Pet> map;private Set<Double> salary;private Map<String, List<Pet>> allPets;
}
user:name: 小智age: 18birth: 2001/12/12pet: {name: 阿猫, age: 28}animal:- 阿猫- 阿狗hobby: [敲代码, 健身]map:- 怀狗: {name: 黑鬼, age: 19}salary: 99.99all-pets:sick:- {name: heigui, age: 20}- {name: tom, age: 19}health: [{name: mario, age: 29}]
@RestController
public class TestController {@Autowiredprivate User user;@RequestMapping("/user")public User user () {return user;}
}

结果如下

{"name":"猛男","age":18,"birth":"2001-12-11T16:00:00.000+00:00","pet":{"name":"阿猫","age":28},"animal":["阿猫","阿狗"],"hobby":["敲代码","健身"],"map":{"0":{"name":"黑鬼","age":19}},"salary":[99.99],"allPets":{"sick":[{"name":"heigui","age":20},{"name":"tom","age":19}],"health":[{"name":"mario","age":29}]}}

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>

04 Web开发

devtools热部署

导入对应的依赖

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

使用:点击锤子或者ctrl+F9

一、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**

二、简单功能分析

1.静态资源访问

①静态资源目录

静态资源默认放在/static、/public、/resources、/META-INF/resources目录下都可以访问到

访问路径:当前项目路径 + 资源名

注意:如果controller中有和资源名一样的访问路径,先给controller进行处理,如果controller不能处理,才到静态资源管理器来进行处理

在static目录下放入名为static.jpg的资源

@RestController
public class TestController {@RequestMapping("/static.jpg")public String str () {return "hello";}
}

可以看到我们访问的是controller

②静态资源访问前缀

默认无前缀

从官方文档中我们知道默认路径是/**,我们可以通过spring.mvc.static-path-pattern属性来修改默认的映射路径

spring:mvc:static-path-pattern: /res/**

测试

注意:我们改变默认映射路径后会引发欢迎页支持和自定义Favicon使用不了,在原理讲解的时候会知道为什么用不了

我们还可以修改默认的目录

  web:resources:static-locations: [classpath:/xiaozhi/]

设置这个之后我们的默认目录就不能使用了,只能使用我们自定义的

③webjar

自动映射到/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.欢迎页支持

在资源目录下创建index.html文件,springBoot会自动映射

注意:如果自定义了资源映射路径,那么就不起作用了,修改默认资源路径不影响欢迎页支持

3.自定义Favicon

favicon.ico 放在静态资源目录下即可。

4.静态资源配置原理

我们的自动配置都在xxxAutoConfiguration 类 (自动配置类)中

这个是跟web功能相关的,我们去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 {

@ConditionalOnMissingBean(WebMvcConfigurationSupport.class):没有WebMvcConfigurationSupport类型的bean的时候才生效,也就是说我们可以完全自定义。

给容器配置了什么

@Configuration(proxyBeanMethods = false)   // 配置类
@Import(EnableWebMvcConfiguration.class)   // 导入对应的组件
@EnableConfigurationProperties({ WebMvcProperties.class,   org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {

配置文件的相关属性和配置文件进行了绑定:


①配置类中只有一个有参构造

// 有参构造所有参数的值都会从容器中找

// resourceProperties    获取配置文件中绑定的值
// webProperties        获取配置文件中绑定的值
// mvcProperties        获取配置文件中绑定的值
// beanFactory          spring的beanFactory
// messageConvertersProvider    找到所有的HttpMessageConverters
// resourceHandlerRegistrationCustomizerProvider    找到资源处理器的自定义器
// dispatcherServletPath
// servletRegistrations     给应用注册Servlet、Filter....
public WebMvcAutoConfigurationAdapter(org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,ObjectProvider<HttpMessageConverters> messageConvertersProvider,ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,ObjectProvider<DispatcherServletPath> dispatcherServletPath,ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties: webProperties.getResources();this.mvcProperties = mvcProperties;this.beanFactory = beanFactory;this.messageConvertersProvider = messageConvertersProvider;this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();this.dispatcherServletPath = dispatcherServletPath;this.servletRegistrations = servletRegistrations;this.mvcProperties.checkConfiguration();
}

②资源处理的默认规则

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {if (!this.resourceProperties.isAddMappings()) {logger.debug("Default resource handling disabled");return;}addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {registration.addResourceLocations(this.resourceProperties.getStaticLocations());if (this.servletContext != null) {ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);registration.addResourceLocations(resource);}});
}

这个就是处理我们静态资源的方法

也就是说我们把这个值调为true,这个方法就不会执行,也就是禁用了默认的规则

spring:
#  mvc:#    static-path-pattern: /res/**web:resources:add-mappings: false   # 禁用所有静态资源规则

dubug进行调试,结果和我们想的一样,它直接结束了方法

我们看一下默认的静态资源规则的运行原理

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {if (!this.resourceProperties.isAddMappings()) {logger.debug("Default resource handling disabled");return;}addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {registration.addResourceLocations(this.resourceProperties.getStaticLocations());if (this.servletContext != null) {ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);registration.addResourceLocations(resource);}});
}

执行第一个addResourceHandler方法就是设置webjars的访问路径和映射路径

第二个addResourceHandler方法就是设置我们其他的静态资源路径,可以看到有两个属性是绑定了我们的配置文件,我们可以debug来查看一下对应的值



我们现在知道了我们默认的访问路径是在WebProperties类下的内部类Resources中的属性

我们再回到我们的配置文件

拓展一下

private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,Consumer<ResourceHandlerRegistration> customizer) {if (registry.hasMappingForPattern(pattern)) {return;}ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);customizer.accept(registration);registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());customizeResourceHandlerRegistration(registration);
}

在这个方法中有三个set方法,这个就是可以设置静态资源的信息,也都是绑定了我们的配置文件,我们可以通过配置文件来修改对应的值

// 缓存时间,以s为单位
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
// 缓存控制器
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
// 是否设置文件(也就是静态资源文件)最后修改的日期和时间
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());

我们来验证我们的假设,在配置文件中看看是否存在

我们到浏览器查看一下是否有修改

可以看到确实是可以的!

③欢迎页处理规则

@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, Resource welcomePage, String staticPathPattern) {if (welcomePage != null && "/**".equals(staticPathPattern)) {// 满足条件,使用欢迎页logger.info("Adding welcome page: " + welcomePage);setRootViewName("forward:index.html");}else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {// 调用controller     /indexlogger.info("Adding welcome page template: index");setRootViewName("index");}
}

判断是不是在/**路径下的,如果你设置了static-locations属性它也是相当于映射到根目录下的,所以当我们设置路径前缀的时候,欢迎页就会使用不了。只有index.html在 /**下才能使用欢迎页

④Favicon

服务器默认是将/**路径下的图标发送给浏览器

三、请求参数处理

1.请求映射

①rest使用

  • 格式:@xxxMapping;
  • rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
    • 以前:**/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user *GET-*获取用户 *DELETE-*删除用户 *PUT-*修改用户 *POST-*保存用户
  • 使用:表单使用post请求提交,hidden隐藏域中_method=put
  • 需要我们手动开启rest风格

代码实现

html页面

<form action="/test" method="post"><input type="submit" value="Post">
</form>
<form action="/test" method="get"><input type="submit" value="get">
</form>
<form action="/test" method="post"><input type="hidden" name="_method" value="put"><input type="submit" value="put">
</form>
<form action="/test" method="post"><input type="hidden" name="_method" value="delete"><input type="submit" value="delete">
</form>

java代码

@RestController
public class TestController {@PostMapping("test")public String post() {return "post";}@GetMapping("test")public String get() {return "get";}@PutMapping("test")public String put() {return "put";}@DeleteMapping("test")public String delete() {return "delete";}
}

我们还要手动开启rest风格

spring:mvc:hiddenmethod:filter:enabled: true

②rest原理解析

在我们的WebMvcAutoConfiguration类下

@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {return new OrderedHiddenHttpMethodFilter();
}

我们看到它绑定了配置文件,官方默认是关闭的,所以我们需要手动开启

创建了一个OrderedHiddenHttpMethodFilter对象,它又实现了OrderedFilter,而OrderedFilter又是继承了Filter接口,所以我们它就是一个拦截器,我们通过源码来看一下它的功能是什么

HiddenHttpMethodFilter类下有一个方法,这个方法就是它的核心方法

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {HttpServletRequest requestToUse = request;// 判断是否是post请求类型,没有发生异常if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {// 默认是得到我们隐藏域_method中的参数String paramValue = request.getParameter(this.methodParam);if (StringUtils.hasLength(paramValue)) {// 将得到的值转换成大写的String method = paramValue.toUpperCase(Locale.ENGLISH);// 判断是否有这个类型的方法if (ALLOWED_METHODS.contains(method)) {// 将得到的方法值进行包装,返回一个HttpServletRequestWrapper对象requestToUse = new HttpMethodRequestWrapper(request, method);}}}// 过滤器放行,我们用的request对象是我们修改过的,getMethod方法得到的就是我们传入的值filterChain.doFilter(requestToUse, response);
}

详细展开说明

我们调用this.methodParam就是DEFAULT_METHOD_PARAM常量

public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = DEFAULT_METHOD_PARAM;

ALLOWED_METHODS.contains(method)的意思是判断是否包含下面其中的一个

private static final List<String> ALLOWED_METHODS =Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));

创建HttpMethodRequestWrapper对象进行返回

private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {private final String method;public HttpMethodRequestWrapper(HttpServletRequest request, String method) {super(request);this.method = method;}@Overridepublic String getMethod() {return this.method;}
}

它的作用就是将method设为我们参数值,然后交给我们的过滤器链放行,我们之后使用getMethod()方法得到的就是我们传入的值

③拓展:修改默认的隐藏域名

我们可以自己注册一个HiddenHttpMethodFilter 组件来设置我们想要的名字

@Configuration
public class MyConfig {@Beanpublic HiddenHttpMethodFilter hiddenHttpMethodFilter(){HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();hiddenHttpMethodFilter.setMethodParam("_h");return hiddenHttpMethodFilter;}
}

2.请求映射原理

DispatcherServlet是处理我们请求映射的,下面是它的结构树

它是一个servlet程序,那么就会有doGet()doPost()方法,我们最终的请求会执行这两个方法

HttpServletBean中并没有这两个方法,我们接着往下找,在FrameworkServlet找到了这两个方法

执行这个方法,我们点进去查看一下

processRequest(request, response);方法中有一个核心方法

doService(request, response);

这个方法的具体实现是在DispatcherServlet类中

@Overrideprotected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {

在这个方法中还有一个核心方法,这个方法才是我们的具体实现

try {doDispatch(request, response);
}

最终调用这个核心方法

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

我们可以debug计算一下得到的值

这个方法的作用就是得到处理这个请求的是哪个方法

我们再点进去看一下getHandler(processedRequest)方法的具体实现

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {if (this.handlerMappings != null) {// 将映射信息进行遍历for (HandlerMapping mapping : this.handlerMappings) {// 判断映射处理器是否可以处理当前请求,可以就返回HandlerExecutionChain对象,不能就返回nullHandlerExecutionChain handler = mapping.getHandler(request);if (handler != null) {return handler;}}}return null;
}

这些映射信息是springBoot在启动的时候就已经帮我们封装到HandlerMapping中了

RequestMappingHandlerMapping是标注了@RequestMapping 注解的方法的映射

WelcomePageHandlerMapping是首页的映射

继续往下探究,我们看一下mapping.getHandler(request);方法做了那些事

// 调用了下面这个方法
getHandlerInternal(request);
// 方法返回下面执行的方法
return super.getHandlerInternal(request);
// 调用下面的方法得到返回的对象值
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);

@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {List<Match> matches = new ArrayList<>();// 得到匹配的路径List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);if (directPathMatches != null) {// 得到最匹配的那个,然后放入到集合中addMatchingMappings(directPathMatches, matches, request);}if (matches.isEmpty()) {addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);}if (!matches.isEmpty()) {// 得到集合中的第一个值Match bestMatch = matches.get(0);// 如果有多个请求匹配,它会报异常if (matches.size() > 1) {Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));

3.参数与基本注解

3.1 注解

requile属性是判断这个参数是不是必须的,false表示这个参数不是必须的

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

①@PathVariable

得到请求路径变量的参数值

@GetMapping("/test/{name}/{age}")
public Map<String, Object> pathVariable(@PathVariable String name,@PathVariable Integer age,@PathVariable Map<String, String> paramMap) {// 还可以用map<String, String>接收值Map<String, Object> map = new HashMap<>();map.put("name", name);map.put("age", age);map.put("paramMap", paramMap);return map;
}

②@RequestHeader

得到请求头参数的值

@GetMapping("/test/requestHeader")
public Map<String, Object> requestHeader(@RequestHeader("Accept") String accept,@RequestHeader Map<String, String> paramMap ) {// 使用map集合接收就是全部的请求头信息Map<String, Object> map = new HashMap<>();map.put("paramMap", paramMap);map.put("accept", accept);return map;
}
③@ModelAttribute

获取请求域中的值

    @GetMapping("/test/request")public String request(HttpServletRequest request) {request.setAttribute("name","小智");request.setAttribute("age", 18);return "forward:/test/modelAttribute";}@ResponseBody@GetMapping("/test/modelAttribute")public Map<String, Object> modelAttribute(@ModelAttribute("name") String name,@ModelAttribute("age") String age) {// 不能使用map集合去接收参数,参数接收的类型一定是String类型的,不然会报错Map<String, Object> map = new HashMap<>();map.put("name", name);map.put("age", age);return map;}
④@RequestParam

得到请求参数值

@GetMapping("/test/requestParam")
public Map<String, Object> requestParam(@RequestParam("name") String name,@RequestParam("age") String age,@RequestParam Map<String, String> paramMap) {// 可以用map<String, String>接收值Map<String, Object> map = new HashMap<>();map.put("name", name);map.put("age", age);map.put("paramMap", paramMap);return map;
}

⑤@CookieValue

得到cookie的值

@GetMapping("/test/cookie")
public Map<String, Object> requestParam(@CookieValue("Idea-4a5d61e4") String value) {// 可以用map<String, String>接收值Map<String, Object> map = new HashMap<>();map.put("value", value);return map;     // {"value":"b6df2f01-c4d7-4866-a9bf-536b81d7af1f"}
}
⑥@RequestBody

获得请求体的参数,Post请求才有请求体

@PostMapping("/test/responseBody")
public Map<String, Object> responseBody(@RequestBody String user) {// 可以用map<String, String>接收值Map<String, Object> map = new HashMap<>();map.put("user", user);return map;
}
<form action="/test/responseBody" method="post">账号<input type="text" name="name">密码<input type="text" name="psw"><input type="submit" value="Post">
</form>

⑦@MatrixVariable

得到路径中矩阵变量

语法:";"后面的参数

eg:/cars/sell;low=34;brand=byd,audi,yd ";"后面的内容就是我们得到的参数值

  • springBoot默认是关闭的,我们需要手动开启,我们源码探究它做了什么

    • 第一种开启方式:实现WebMvcConfigurer,因为处理矩形变量的方法是在这个类下的
    • 使用@Bean注解直接将WebMvcConfigurer注入到容器中
@Override
// 配置路径匹配
public void configurePathMatch(PathMatchConfigurer configurer) {if (this.mvcProperties.getPathmatch().getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) {configurer.setPatternParser(new PathPatternParser());}configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());configurer.setUseRegisteredSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());this.dispatcherServletPath.ifAvailable((dispatcherPath) -> {String servletUrlMapping = dispatcherPath.getServletUrlMapping();if (servletUrlMapping.equals("/") && singleDispatcherServlet()) {// UrlPathHelper类就是对针对URL进行处理UrlPathHelper urlPathHelper = new UrlPathHelper();urlPathHelper.setAlwaysUseFullPath(true);configurer.setUrlPathHelper(urlPathHelper);}});
}
  • UrlPathHelper类下removeSemicolonContent属性就是判断是否移除路径中分号(;)后面的的内容

    • true:就是移除
    • false:不移除,开启矩形变量功能
    /*** Whether configured to remove ";" (semicolon) content from the request URI.是否配置为删除“;”(请求URI中的内容。*/
    public boolean shouldRemoveSemicolonContent() {checkReadOnly();return this.removeSemicolonContent;
    }
    

代码实现

开启

@Configuration
public class MyConfig implements WebMvcConfigurer {// 第一种方式@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {UrlPathHelper urlPathHelper = new UrlPathHelper();urlPathHelper.setRemoveSemicolonContent(false);configurer.setUrlPathHelper(urlPathHelper);}// 第二种方式@Beanpublic WebMvcConfigurer webMvcConfigurer() {return new WebMvcConfigurer() {@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {UrlPathHelper urlPathHelper = new UrlPathHelper();urlPathHelper.setRemoveSemicolonContent(false);configurer.setUrlPathHelper(urlPathHelper);}};}
}
@GetMapping("/test/{path}")
// 必须是在路径变量的后面接上
public Map<String, Object> matrixVariable(@MatrixVariable("cat") String cat,@MatrixVariable("dog") String dog,@PathVariable("path") String path) {Map<String, Object> map = new HashMap<>();map.put("cat", cat);map.put("dog", dog);map.put("path", path);return map;
}// 获取两个不同的路径变量后面相同的矩形变量值// 使用pathVar属性区分是获取哪一个路径变量后面的值@GetMapping("/boss/{bossId}/{empId}")public Map<String, Object> matrixVariable2(@MatrixVariable(value = "age",pathVar = "bossId") String bossAge,@MatrixVariable(value = "age",pathVar = "empId") String empAge,@PathVariable("bossId") String bossId,@PathVariable("empId") String empId) {Map<String, Object> map = new HashMap<>();map.put("bossAge", bossAge);map.put("empAge", empAge);map.put("bossId", bossId);map.put("empId", empId);return map;}


发现一个坑@GetMapping("/test/{bossId}/{empId}")@GetMapping("/test/{path}")两个同时存在会报错,我们修改一下第一个路径为/boss/{bossId}/{empId}就又可以正常访问了,如果非要使用/test前缀的话也可以在两个路径变量中间再加一个值,例如:/test/{bossId}/test/{empId}即可

3.2 注解原理

@GetMapping("/test/RequestHeader")
public Map<String, Object> RequestHeader(@RequestHeader("Accept") String accept,@RequestHeader Map<String, String> paramMap ) {// 使用map集合接收就是全部的请求头信息Map<String, Object> map = new HashMap<>();map.put("paramMap", paramMap);map.put("accept", accept);return map;
}

我们之前说过mappedHandler是映射处理器,找到对应处理请求的方法,将方法的信息进行封装

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  • HandlerAdapter(处理程序适配器)是一个接口

    • supports方法判断有没有分解器可以处理这个参数,有就true
    • handle方法处理

调用HandlerAdapter中的handle()方法来执行方法,debug看它做了什么

public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {return handleInternal(request, response, (HandlerMethod) handler);
}

调用方法得到返回值

mav = invokeHandlerMethod(request, response, handlerMethod);    // 核心方法
// invokeHandlerMethod中的核心方法
invocableMethod.invokeAndHandle(webRequest, mavContainer);
// 最终来到真正执行方法的方法
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// invokeForRequest方法中的核心方法,得到参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
// 这个方法它是在一个循环里面的,循环的次数就是我们参数的个数,它会匹配能解析这个参数的参数解析器,然后进行解析得到匹配的值
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

往下执行这个方法

@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;
}


每个参数解析器都有对应的规则,这个可以debug进行查看它的执行规则

得到参数后就可以执行方法了

4.ServletApi

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

@Overridepublic 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);}

5.复杂参数

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();

6.自定义类型参数 封装POJO

将数据封装到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));}

我们来看看它是怎么将我们的数据封装到我们自定义的对象中的

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 instancetry {attribute = createAttribute(name, parameter, binderFactory, webRequest);}catch (BindException ex) {if (isBindExceptionRequired(parameter)) {// No BindingResult parameter -> fail with BindExceptionthrow ex;}// Otherwise, expose null/empty value and associated BindingResultif (parameter.getParameterType() == Optional.class) {attribute = Optional.empty();}else {attribute = ex.getTarget();}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.Optionalif (!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 modelMap<String, Object> bindingResultModel = bindingResult.getModel();mavContainer.removeAttributes(bindingResultModel);mavContainer.addAllAttributes(bindingResultModel);return attribute;
}

这个就是它的核心处理方法了!

WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);

WebDataBinder 数据绑定器,将请求参数的值绑定到指定的javaBean中

bindRequestParameters(binder, webRequest);这个才是真正的将数据设置给javaBean

我们也可以自定义自己的converters

@FunctionalInterfacepublic interface Converter<S, T> 函数式接口,String类型转换成我们对象类型

7.自定义converters

    @Beanpublic WebMvcConfigurer webMvcConfigurer() {return new WebMvcConfigurer() {@Overridepublic void addFormatters(FormatterRegistry registry) {registry.addConverter(new Converter<String, Person>() {@Overridepublic Person convert(String source) {Person person = new Person();// 定义自己的规则String[] str = source.split(",");person.setName(str[0]);person.setAge(Integer.parseInt(str[1]));return person;}});}};}

四、数据响应和内容协商

1.响应json

1、jackson + @Response注解

引入对应的依赖

web场景已经帮我们引入了json场景

上面我们说了我们参数处理器的原理,那么接下来就是返回值处理器了,看一下我们底层做了什么转换

①返回值处理器
// doDispatch中的核心方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 往下调方法
// 处理返回值的核心方法
// public void invokeAndHandle
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);
}
@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);// 使用消息转换器进行写出,最终写到response中writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
②返回值处理器原理

1.获取对应的处理器

// 这个方法是得到我们能够处理返回值的返回值处理器
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {boolean isAsyncValue = isAsyncReturnValue(value, returnType);// 挨个遍历所有的返回值处理器for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {// 处理异步的if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {continue;}// 判断是否可以处理返回值if (handler.supportsReturnType(returnType)) {// 可以就就行返回return handler;}}return null;
}

eg:RequestResponseBodyMethodProcessor

@Override
public boolean supportsReturnType(MethodParameter returnType) {// 判断有没有标准件@ResponseBody注解,有的话就使用这个返回值处理器return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||returnType.hasMethodAnnotation(ResponseBody.class));
}

其他的处理器有它自己的判断,判断是否符合条件

2.处理器执行处理方法

  • writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);

    • ①内容协商(浏览器默认会以请求头的方式告诉服务器它可以接收的数据类型)

      • acceptableTypes = getAcceptableMediaTypes(request); 得到浏览器可以接收的类型
    • ②服务器根据自己自身的能力,决定服务器端可以生产出什么类型的数据
      • List producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
    • ③springMvc挨个遍历所有容器的HttpMessageConverter,然后将数据写出去
      • genericConverter.write(body, targetType, selectedMediaType, outputMessage);

2.springMVC支持的返回值

这个是根据返回值处理器得出的

ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;

3.HTTPMessageConverter消息转换器原理

1、MessageConverter规范

HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。

eg:Person对象转为JSON。或者 JSON转为Person

2、得到支持的媒体类型

protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {Set<MediaType> mediaTypes =(Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);if (!CollectionUtils.isEmpty(mediaTypes)) {return new ArrayList<>(mediaTypes);}List<MediaType> result = new ArrayList<>();// 挨个遍历消息转换器,看谁能处理我们的转换for (HttpMessageConverter<?> converter : this.messageConverters) {if (converter instanceof GenericHttpMessageConverter && targetType != null) {// 进行判断,是否可以写出if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {// 将支持的媒体类型放入到集合中result.addAll(converter.getSupportedMediaTypes(valueClass));}}else if (converter.canWrite(valueClass, null)) {result.addAll(converter.getSupportedMediaTypes(valueClass));}}return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
}

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处理的。

2.内容协商

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

①引入xml依赖

 <dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId>
</dependency>

我们引入解析xml的包后,浏览器默认返回的就是我们xml的类型,因为xml的权重比json的高

②postman分别测试返回json和xml

③开启浏览器参数方式内容协商

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

spring:contentnegotiation:favor-parameter: true  #开启请求参数内容协商模式

开启了这个模式就会多出一个参数内容协商策略器来得到我们需要返回的媒体类型

通过传递format参数来得到我们想要返回的数据类型


④内容协商原理

  1. 判断当前响应头中是否已经有确定的媒体类型

    writeWithMessageConverters方法

    MediaType contentType = outputMessage.getHeaders().getContentType();

  2. 获取请求头中可以接收的媒体类型

    acceptableTypes = getAcceptableMediaTypes(request);

    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {// 遍历得到内容协商策略器,获取支持的媒体类型for (ContentNegotiationStrategy strategy : this.strategies) {List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {continue;}return mediaTypes;}return MEDIA_TYPE_ALL_LIST;
    }
    

    123

  3. 遍历得到系统中所有的messageConverter,看谁支持操作这个对象(Person)

导入了jackson处理xml的包,xml的converter就会自动进来```java
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);}// 导入了这个类它就会帮我们添加到所有的messageConvertermessageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
```
  1. 统计支持操作Person的Converter,把converter支持的媒体类型统计出来

  2. 客户端需要xml。服务端支持的(10种,json,xml)

  1. 进行内容协商的最佳匹配

    for (MediaType requestedType : acceptableTypes) {for (MediaType producibleType : producibleTypes) {if (requestedType.isCompatibleWith(producibleType)) {mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));}}
    }for (MediaType mediaType : mediaTypesToUse) {if (mediaType.isConcrete()) {selectedMediaType = mediaType;break;}else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;break;}
    }
    
  2. 最后进行转换,然后调用方法进行输出

    if (selectedMediaType != null) {selectedMediaType = selectedMediaType.removeQualityValue();// 挨个遍历消息转换器for (HttpMessageConverter<?> converter : this.messageConverters) {GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter<?>) converter : null);// 多从判断,判断是否支持操作这个数据,判断是否可以将数据转为我们想要的媒体类型if (genericConverter != null ?((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :converter.canWrite(valueType, selectedMediaType)) {// 得到要输出的内容body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,(Class<? extends HttpMessageConverter<?>>) converter.getClass(),inputMessage, outputMessage);if (body != null) {Object theBody = body;LogFormatUtils.traceDebug(logger, traceOn ->"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");addContentDispositionHeader(inputMessage, outputMessage);if (genericConverter != null) {// 调用方法进行写出操作genericConverter.write(body, targetType, selectedMediaType, outputMessage);}else {((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);}}else {if (logger.isDebugEnabled()) {logger.debug("Nothing to write: null body");}}return;}}
    }
    

⑤自定义MessageConverter

实现多数据兼容。json、xml、zhi

1、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理

2、Processor 处理方法返回值。通过 MessageConverter 处理

3、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)

4、内容协商找到最终的 messageConverter

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

@Configuration
public class MyConfig implements WebMvcConfigurer {// 将默认的覆盖掉@Overridepublic void configureMessageConverters(List<HttpMessageConverter<?>> converters) {}// 扩展功能@Overridepublic void extendMessageConverters(List<HttpMessageConverter<?>> converters) {}
}

我们使用扩展的来添加我们自己的MessageConverter

首先自定义我们MessageConverter

public class ZhiMessageConverter implements HttpMessageConverter<Person> {@Overridepublic boolean canRead(Class<?> clazz, MediaType mediaType) {return false;}@Overridepublic boolean canWrite(Class<?> clazz, MediaType mediaType) {return clazz.isAssignableFrom(Person.class);}/*** 服务器要统计所有的messageConverter都能写出那些内容* @return*/@Overridepublic List<MediaType> getSupportedMediaTypes() {// 转为我们自定的类型return MediaType.parseMediaTypes("application/zhi");}@Overridepublic Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {return null;}@Overridepublic void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {// 自定义协议的写出String data = person.getName() + ";" + person.getAge();// 写出去OutputStream body = outputMessage.getBody();body.write(data.getBytes());}
}

添加到系统中

@Configuration
public class MyConfig implements WebMvcConfigurer {@Overridepublic void extendMessageConverters(List<HttpMessageConverter<?>> converters) {converters.add(new ZhiMessageConverter());}
}

debug分析

  1. 得到客户端需要的到的内容

  1. 得到服务器可以生产的媒体类型

  1. 进行内容协商匹配

  2. 进行输出

⑥浏览器请求自定义数据

我们知道了参数内容协商策略只支持json和xml

所以我们要自己自定义一个我们自己的参数内容协商策略

/*
*  自定义内容策略
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {Map<String, MediaType> mediaTypes = new HashMap<>();mediaTypes.put("json",MediaType.APPLICATION_JSON);mediaTypes.put("xml",MediaType.APPLICATION_XML);// 我们自定义的类型mediaTypes.put("zhi",MediaType.parseMediaType("application/zhi"));// 自定义的内容协商策略,指定那些参数对应那些媒体类型ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(mediaTypes);configurer.strategies(Arrays.asList(strategy));
}

但是,这会造成一个问题,我们再次使用postman使用请求头的方式请求数据的时候返回的都是返回json类型的数据

因为我们自定义了我们自己的,所以默认请求头的就被我们覆盖了,我们在自定义的策略中添加上就可以了

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {Map<String, MediaType> mediaTypes = new HashMap<>();mediaTypes.put("json",MediaType.APPLICATION_JSON);mediaTypes.put("xml",MediaType.APPLICATION_XML);// 我们自定义的类型mediaTypes.put("zhi",MediaType.parseMediaType("application/zhi"));// 自定义的内容协商策略,指定那些参数对应那些媒体类型ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(mediaTypes);// 自定义参数名,默认是formatstrategy.setParameterName("ss");// 默认的请求头策略HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();configurer.strategies(Arrays.asList(strategy, headerStrategy));
}

我们还可以自定义我们想要的接收参数

有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。

SpringBoot有为我们提供基于配置文件的快速修改媒体类型功能

五、模板引擎与视图解析

视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。

1 模板引擎-Thymeleaf

官方文档:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

1.基本语法

①表达式
表达式名字 语法 用途
变量取值 ${…} 获取请求域、session域、对象等值
选择变量 *{…} 获取上下文对象值
消息 #{…} 获取国际化等值
链接 @{…} 生成链接
片段表达式 ~{…} jsp:include 作用,引入公共页面片段
②字面量

文本值: ‘one text’ , ‘Another one!’ **,…**数字: 0 , 34 , 3.0 , 12.3 **,…**布尔值: true , false

空值: null

变量: one,two,… 变量不能有空格

③文本操作

字符串拼接: +

变量替换: |The name is ${name}|

④数学运算

运算符: + , - , * , / , %

⑤布尔运算

运算符: and , or

一元运算: ! , not

⑥比较运算

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

⑦条件运算

If-then: (if) ? (then)

If-then-else: (if) ? (then) : (else)

Default: (value) ?: (defaultvalue)

⑧特殊操作

无操作: _

2设置属性值-th:attr

设置单个值

<form action="subscribe.html" th:attr="action=@{/subscribe}"><fieldset><input type="text" name="email" /><input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/></fieldset>
</form>

设置多个值

<img src="../../images/gtvglogo.png"  th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

以上两个的代替写法 th:xxxx

<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

3.迭代

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

4.条件运算

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

5.属性优先级

2 thymeleaf使用

1.引入stater

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2.自动配置好了thymeleaf

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

自动配置策略

  1. 绑定了ThymeleafProperties配置类
  2. 配置好了SpringTemplateEngine模板引擎
  3. 配好了thymeleafViewResolver(模板视图解析器)

3 构建后台管理系统

1、项目创建

thymeleaf、web-starter、devtools、lombok

2、静态资源处理

自动配置好,我们只需要把所有静态资源放到 static 文件夹下

3、路径构建

th:action="@{/login}"

4、模板抽取

th:insert/replace/include

5、页面跳转

@PostMapping("/login")
public String main(User user, HttpSession session, Model model){if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){//把登陆成功的用户保存起来session.setAttribute("loginUser",user);//登录成功重定向到main.html;  重定向防止表单重复提交return "redirect:/main.html";}else {model.addAttribute("msg","账号密码错误");//回到登录页面return "login";}}

6、数据渲染

@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";
}

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

4 视图解析原理

1、重定向原理

  1. 得到它的返回值处理器ViewNameMethodReturnValueHandler

  1. 执行方法

```java
protected boolean isRedirectViewName(String viewName) {// 判断名字是否匹配这个正则表达式或者是不是以redirect:开头的return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"));
}
```
  1. 处理完成之后,返回一个ModelAndView对象,里面存放我们的数据和视图地址

![在这里插入图片描述](https://img-blog.csdnimg.cn/2021071715593716.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NjkyMzAxNA==,size_16,color_FFFFFF,t_70)
  1. 得到数据和视图之后就要将他们进行输出给客户端进行展示

  1. 处理派发结果

  1. 得到View视图对象

**render -->  resolveViewName**```java
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,Locale locale, HttpServletRequest request) throws Exception {if (this.viewResolvers != null) {// 遍历系统中所有的视图解析器,看谁能解析这个视图for (ViewResolver viewResolver : this.viewResolvers) {View view = viewResolver.resolveViewName(viewName, locale);if (view != null) {return view;}}}return null;
}
```![在这里插入图片描述](https://img-blog.csdnimg.cn/20210717155950255.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NjkyMzAxNA==,size_16,color_FFFFFF,t_70)
  1. 最后重定向

    view.render(mv.getModelInternal(), request, response);

2、请求转发原理

和重定向原理基本一致,最后也是调用了原生的请求转发forward方法

3、视图渲染

过程和重定向的差不多,重定向调用的是ContentNegotiatingViewResolver内容协商视图解析器,我们的视图是有我们导入进来的ThymeleafViewResolver视图解析器进行解析

我们来看看它是怎么进行解析的

  1. 来到DispatcherServlet中的render方法

    view = resolveViewName(viewName, mv.getModelInternal(), locale, request)

  1. view调用render方法

    view.render(mv.getModelInternal(), request, response);

    ThymeleafView: renderFragment()

    设置一些列东西后,得到我们的视图内容,视图模板引擎调viewTemplateEngineprocess方法进行输出

因为我们会使用thymeleaf中的语法获取值,处理和分析就是为了将我们的值渲染到视图上

最终调用flush 方法进行输出,到此我们就完成了我们的页面渲染

六、拦截器

1、HandlerInterceptor接口

public class MyInterceptor implements HandlerInterceptor {/*** 拦截之前执行* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断是否登录,登录了才能进行操作HttpSession session = request.getSession();User user = (User) session.getAttribute("user");if (user == null) {// 跳转到登录页面request.setAttribute("msg","请先登录");request.getRequestDispatcher("/").forward(request, response);return false;}return true;}/*** 目标方法完成之后执行* @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}/*** 页面渲染之后执行* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}

2、配置拦截器

将我么自定义的拦截器放入到容器中

@Configuration
public class AdminConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**")// 一定要注意不要将静态资源也拦截了.excludePathPatterns("/","/login","/css/**","/js/**","/fonts/**","/images/**");}
}

3、拦截器原理

  1. 首先还是在DispatcherServlet下的doDispatch()方法中

映射处理器中有我们所有的拦截器
  1. 在执行方法之前,它会去调用拦截器进行判断,如果全部拦截器都为true,那么就执行目标方法

    if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;
    }
    
    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {// 遍历循环得到每个拦截器for (int i = 0; i < this.interceptorList.size(); i++) {HandlerInterceptor interceptor = this.interceptorList.get(i);// 调用拦截器的preHandle方法,返回true就是放行if (!interceptor.preHandle(request, response, this.handler)) {triggerAfterCompletion(request, response, null);return false;}// 执行了拦截器的索引this.interceptorIndex = i;}return true;
    }
    
  2. 执行完目标方法返回modelAndView对象之后,mappedHandler执行applyPostHandle方法

    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)throws Exception {// 反序遍历我们执行成功的拦截器for (int i = this.interceptorList.size() - 1; i >= 0; i--) {HandlerInterceptor interceptor = this.interceptorList.get(i);// 执行postHandle方法interceptor.postHandle(request, response, this.handler, mv);}
    }
    
  3. 我们上面的所有操作如果有发生异常的都会触发triggerAfterCompletion方法,这个方法就是执行我们的每个拦截器的afterCompletion方法

    void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {for (int i = this.interceptorIndex; i >= 0; i--) {HandlerInterceptor interceptor = this.interceptorList.get(i);try {interceptor.afterCompletion(request, response, this.handler, ex);}catch (Throwable ex2) {logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);}}
    }
    
  4. 如果开启了异步并发,那么它会执行异步拦截器中的afterConcurrentHandlingStarted方法

七、文件上传

1.页面代码

<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="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password"></div><div class="form-group"><label for="exampleInputFile">头像</label><input type="file" name="headerImg" id="exampleInputFile"></div><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>

两个要注意的点:

  1. 上传文件form元素中一定要有编码类型:enctype="multipart/form-data"
  2. <input type="file" name="photos" multiple>添加multiple属性可以同时上传多个文件

2.文件上传的controller

@Slf4j
@Controller
public class FormController {@GetMapping("/form_layouts")public String form_layouts() {return "/form/form_layouts";}@PostMapping("/upload")public String upload(@RequestParam String email,@RequestParam String username,// 可以不要注解,建议加上@RequestPart("headerImg") MultipartFile headerImg,@RequestPart("photos") MultipartFile[] photos) throws IOException {log.info("上传的信息:email={},username={},headerImg={},photos={}",email,username,headerImg.getSize(),photos.length);if (!headerImg.isEmpty()) {// 保存到文件服务器,oss对象存储服务器,在这里我们保存到本地硬盘String filename = headerImg.getOriginalFilename();headerImg.transferTo(new File("F:\\testUpload\\" + filename));}if (photos.length > 0) {// 遍历进行保存for (MultipartFile photo : photos) {if (!photo.isEmpty()) {String filename = photo.getOriginalFilename();photo.transferTo(new File("F:\\testUpload\\" + filename));}}}return "main";}
}

3.文件上传原理

原理和之前不一样的就是得到的解析器不同

  1. 在得到解析器之前,它会进行检查,判断这个请求是否有文件上传

  1. 有的话就将当前的request对象进行封装,封装成MultipartHttpServletRequest,它是继承了HttpServletRequest的

  1. 接下来就是执行目标方法,处理参数环节

    getMethodArgumentValues方法
    // 确定参数值
    args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
    

遍历参数处理器,看那个可以处理当前这个参数

  1. 调用方法参数解析器中的resolveArgument来解析参数

    RequestPartMethodArgumentResolver --> resolveArgument

    Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
    


    因为我们debug的是多个文件的那个参数,所以执行完上面的就直接返回了

**最后调用最终的方法进行返回**```java
return adaptArgumentIfNecessary(arg, parameter);protected Object adaptArgumentIfNecessary(@Nullable Object arg, MethodParameter parameter) {if (parameter.getParameterType() == Optional.class) {if (arg == null || (arg instanceof Collection && ((Collection<?>) arg).isEmpty()) ||(arg instanceof Object[] && ((Object[]) arg).length == 0)) {return Optional.empty();}else {return Optional.of(arg);}}return arg;
}
```
  1. 最后返回结果

八、异常处理

1、默认规则

我们服务器出现异常后,springBoot默认会找我们templates/error下的错误页面,命名是4xx、5xx或者可以具体到404错误

客户端和浏览器是有区别的:

  • 浏览器返回的是页面

  • 客户端返回的是json数据

他们返回的信息都是一样的,只是展示出来的不一样而已,信息可以通过取出来,eg:{}取出来,eg:取出来,eg:{status}取出状态码

2、定义错误页面处理逻辑

  • 自定义错误页

    1. error/404.html error/5xx.html; 有精确的错误状态码页面就匹配精确, 没有的话就找4xx.html,如果都没有就返回系统默认的错误页面

    2. @ControllerAdvice + @ExceptionHandler处理全局异常 底层是ExceptionHandlerExceptionResolver专门处理标有 @ExceptionHandler注解的异常

        /*处理整个web controller的异常*/@Slf4j@ControllerAdvicepublic class GlobalExceptionHandler  {@ExceptionHandler({ArithmeticException.class,NullPointerException.class})   // 处理异常// 返回值也可以是ModelAndView对象public String handlerArithException(Exception e) {log.error("异常:{}" ,e);return "login"; // 视图地址}}
  1. @ResponseStatus + 自定义异常 ResponseStatusExceptionResolver处理标注@ResponseStatus注解的异常类


调用底层的response.sendError(statusCode); tomcat发/error请求*

        @ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户数量太多")    // reason为原因public class UserTooManyException extends RuntimeException {public UserTooManyException(String message) {super(message);}}
  1. spring底层的异常,如参数转换异常

    **DefaultHandlerExceptionResolver 处理框架底层的异常**
    

    它的底层调用底层的response.sendError(HttpServletResponse.SC BAD_ REQUEST, ex.getMessage(); tomcat发/error请求

  2. 自定义实现HandlerExceptionResolver异常,可以作为默认的全局异常处理规则

    // 自定义处理异常解析器
    @Order(Ordered.HIGHEST_PRECEDENCE)  // 值越小,优先级越高
    @Component
    public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {@Overridepublic ModelAndView resolveException(HttpServletRequest request,HttpServletResponse response,Object handler, Exception ex) {try {response.sendError(511, "自定义的解析器");} catch (IOException e) {e.printStackTrace();}return new ModelAndView();}
    }
    

  1. ErrorViewResolver实现自定义处理异常
  • 没有能处理这个异常的解析器,tomcat就会发送**/error请求,然后被BasicErrorControler**处理
  • 底层response.sendError也是发送**/errorBasicErrorControler**处理
  • BasicErrorControler要跳转到那个错误页是由ErrorViewResolver来解析的,所以我们也可以自定义ErrorViewResolver的逻辑来处理跳转的错误页

这些页面处理逻辑都有对应的一个处理异常错误解析器

3、异常处理自动配置原理

  • ErrorMvcAutoConfiguration 自动配置异常处理规则

    1. 组件:DefaultErrorAttributes id:errorAttributes

      • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver

      • 定义错误页面可以包含哪些数据

    2. 组件:BasicErrorController id:basicErrorController 它是controller,我们没有自定义错误页面逻辑,那么就它会发送/error请求,这个就是负责跳转到默认"/error"的controller

      • 返回页面的处理方法

        返回json字符串

        • 容器中有组件: View id:error

          它返回系统默认的视图,调用render方法进行渲染

        • 容器中放组件:BeanNameViewResolver id:beanNameViewResolver

          视图解析器:按照返回的视图名作为组件id去容器中找View对象

    3. 组件:DefaultErrorViewResolver id:conventionErrorViewResolver

      • 发生错误,它会根据出现错误的状态码找到对应的视图

4、异常处理原理解析

我们给它制造一个算数异常 int i = 1/0;

1.拿到适配器,然后执行目标方法,一旦发生错误,异常就会被捕捉

2.处理派发结果 processDispatchResult()

3.processDispatchResult --> processHandlerException 遍历所有的处理异常解析器,看谁能处理这个异常,如果不能,它就会继续往上抛异常

  1. DefaultErrorAttributes:它就是将错误信息保存到请求域中,返回为null

  1. 还有一个是组合的处理异常解析器,它里面还会进行遍历每个解析器,他们都有自己的规则

4.如果这个异常没有任何一个能处理,那么底层就会发送/error请求,会被底层的BasicErrorController处理

  1. 经过内容协商,找到适合的处理方法

  2. 将保存在请求域中的数据拿出来

  3. 来遍历所有的错误视图解析器,看谁能解析这个错误页面,默认的就是springBoot帮我们配好的那个

    默认的解析器的作用就是将我们的错误状态码作为错误页的名字

  4. 最终就会响应erroe/5xx这个页面

九、web原生组件注入(Servlet、Filter、Listener)

1、使用Servlet ApI

推荐使用这种方式

@ServletComponentScan(basePackages = “com.atguigu.admin”) :指定原生Servlet组件都放在那里

@WebServlet(urlPatterns = “/my”):效果:直接响应,没有经过Spring的拦截器?

​ 因为spring的拦截器拦截的是它的DispatcherServlet,我们并没有定义拦截器拦截我们自定义的Serlvet

​ 它是一个优先匹配原则,比如同样都是处理/my下的请求,那么如果有确定的就是映射到确定的

eg: ①/my ②/my/aa 那么就会精准匹配②

@WebFilter(urlPatterns={"/css/*","/images/*"})

@WebListene

代码实现

@WebServlet(urlPatterns = "/my") // 处理请求路径
public class MyServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.getWriter().write("666");}
}
@Slf4j
@WebFilter(urlPatterns = {"/css/**","/js/**"})    // 拦截路径
public class MyFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {log.info("过滤器初始化");  }@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {chain.doFilter(request, response);log.info("过滤器正在工作");}@Overridepublic void destroy() {log.info("过滤器销毁");}
}
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {@Overridepublic void contextInitialized(ServletContextEvent sce) {log.info("监听初始化完成");}@Overridepublic void contextDestroyed(ServletContextEvent sce) {log.info("监听到销毁");}
}

还要扫描原生的组件,不然是不会生效的

@ServletComponentScan(basePackages = "com.xiaozhi.admin")
@Configuration
public class AdminConfig implements WebMvcConfigurer {



2、使用RegistrationBean

ServletRegistrationBean,FilterRegistrationBean, andServletListenerRegistrationBean

代码实现

@Configuration
public class AdminConfig implements WebMvcConfigurer {@Beanpublic ServletRegistrationBean myServlet() {// 对象后面就是路径映射return new ServletRegistrationBean(new MyServlet(), "/my", "you");}@Beanpublic FilterRegistrationBean myFilter() {FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new MyFilter());// 设置拦截的路径filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));return filterRegistrationBean;}@Beanpublic ServletListenerRegistrationBean myServletListener() {MyServletContextListener myServletContextListener = new MyServletContextListener();return new ServletListenerRegistrationBean(myServletContextListener);}}

十、嵌入式Servlet容器

1.切换嵌入式Servlet容器

springBoot不用配置服务器是因为它已经将服务器内嵌进来了

默认支持的webServer

  • Tomcat, Jetty, Undertow
  • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
  • 默认是tomcat服务器,我们也可以手动切换服务器

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><!--移除tomcat--><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><!--替换tomcat--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-undertow</artifactId></dependency>

原理

  • 我们先找到自动配置类,我们看它给我们做了什么

  • ServletWebServerFactoryAutoConfiguration(web服务工厂 --> 生产web服务的)

  • ServletWebServerFactoryConfiguration服务器的配置类,动态使用不同的服务器


    只有导入对应的包才能使用对应的服务器,默认是导入tomcat的包

  • 导入了tomcat的包,那么它就会有 TomcatServletWebServerFactory(tomcat的服务器工厂)

  • TomcatServletWebServerFactory创建出Tomcat服务器并启动;

  • TomcatWebServer 的构造器拥有初始化方法initialize—this.tomcat.start();

  • 内嵌服务器就是手动来执行它的代码,设置它的一些配置

2.定制Servlet容器

  • 修改配置文件server.xxx
  • 实现WebServerFactoryCustomizer
    • 将配置文件的值和ServletWebServerFactory进行绑定
  • 自定义ConfigurableServletWebServerFactory

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

@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {@Overridepublic void customize(ConfigurableServletWebServerFactory server) {server.setPort(9000);}}

十一、定制化原理

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、原理分析套路

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

总结

请求处理完整流程

拦截器流程

05 数据访问

一、SQL

1、数据源的自动配置

1.导入JDBC场景

  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

数据库驱动

官方没有导入驱动,因为官方不知道我们要操作什么数据库,所以需要我们自己手动导入

<!--官方给我们的数据库驱动做了版本仲裁,默认是8版本的-->
<mysql.version>8.0.25</mysql.version><!--需要我们自己来修改版本-->
<!--第一种方式:直接引入-->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.49</version>
</dependency><!--第二种方式:重新声明版本-->
<properties><java.version>1.8</java.version><mysql.version>5.1.49</mysql.version>
</properties>

2.分析自动配置

DataSourceAutoConfiguration:数据源的自动配置

  • 修改数据源相关的配置:spring.datasource

    @ConfigurationProperties(prefix = "spring.datasource")
    public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
  • 数据库连接池的配置,容器中没有数据源它才会配置

  • 因此我们默认使用的就是springBoot给我们配置好的。HikariDataSource,因为引入jdbc场景的时候就引入了它的包

DataSourceTransactionManagerAutoConfiguration:事物管理器的自动配置

JdbcTemplateAutoConfiguration:JDBC模板的自动配置,它可以对数据库crud

JndiDataSourceAutoConfiguration:Jndi的自动配置

XADataSourceAutoConfiguration:分布式事物相关的自动配置

3、修改配置项

spring:datasource:username: rootpassword: rooturl: jdbc:mysql://localhost:3306/db4?studyserverTimezone=UTC&useUnicode=true&characterEncoding=utf-8driver-class-name: com.mysql.jdbc.Driver

4、测试

@SpringBootTest
class AdminApplicationTests {// springBoot帮我们自动引入了@AutowiredJdbcTemplate jdbcTemplate;@Testvoid contextLoads() {String sql = "select count(*) from stu";System.out.println(jdbcTemplate.queryForObject(sql, Integer.class));}
}

2、使用Druid数据源

druid官方github地址

https://github.com/alibaba/druid

整合第三方技术的两种方式

  • 自定义
  • 官方starter

1.自定义方式

中文文档:https://github.com/alibaba/druid/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98

①引入依赖
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.17</version>
</dependency>
②创建数据源注入到容器中

可以使用xml文件的方式去配置,在这里使用配置类的方式配置

@Configuration
public class DataSourceConfig {// 绑定配置文件之后,我们只需要修改配置文件中的就可以了@ConfigurationProperties(prefix = "spring.datasource")@Beanpublic DataSource dataSource() {DruidDataSource dataSource = new DruidDataSource();
//        dataSource.setUsername("root");     // 设置用户名
//        dataSource.setPassword("root");     // 设置密码return dataSource;}
}

测试

@Autowired
DataSource dataSource;@Test
void test1() {System.out.println(dataSource.getClass());
}

③StatViewServlet

StatViewServlet的用途包括:

  • 提供监控信息展示的html页面
  • 提供监控信息的JSON API
/*** 配置druid监控页功能* @return*/
@Bean
public ServletRegistrationBean statViewServlet() {StatViewServlet statViewServlet = new StatViewServlet();ServletRegistrationBean<Servlet> servletServletRegistrationBean =new ServletRegistrationBean<>(statViewServlet,"/druid/*");return servletServletRegistrationBean;
}// 输入网址访问http://localhost:8080/druid/datasource.html
④监控统计功能
// 绑定配置文件之后,我们只需要修改配置文件中的就可以了
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource dataSource() throws SQLException {DruidDataSource dataSource = new DruidDataSource();//        dataSource.setUsername("root");     // 设置用户名//        dataSource.setPassword("root");     // 设置密码dataSource.setFilters("stat");  // 开启监控功能return dataSource;
}

也可以使用配置文件

spring:datasource:filters: 'stat'

结果显示

⑤web和spring关联监控
/*** web关联监控* @return*/
@Bean
public FilterRegistrationBean webStatFilter() {WebStatFilter webStatFilter = new WebStatFilter();FilterRegistrationBean<Filter> filterFilterRegistrationBean =new FilterRegistrationBean(webStatFilter);filterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));filterFilterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");return filterFilterRegistrationBean;
}
⑥设置登录账号和密码


⑦不懂看官网,有教程

2.官方starter

官方文档:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

①引入官方的stater
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.17</version>
</dependency>
②Druid自动配置
@Configuration
@ConditionalOnClass(DruidDataSource.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)    // 它要在spring默认的数据源启动前启动
// 和配合文件进行绑定
// DruidStatProperties  spring.datasource.druid
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidSpringAopConfiguration.class,DruidStatViewServletConfiguration.class,DruidWebStatFilterConfiguration.class,DruidFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {
  • DruidSpringAopConfiguration aop相关的
  • DruidStatViewServletConfiguration 监控页的配置,默认开启
  • DruidWebStatFilterConfiguration 监控配置,默认开启
  • DruidFilterConfiguration Druid自己的Filter配置

代码实现

spring:datasource:username: rootpassword: rooturl: jdbc:mysql://localhost:3306/db4?studyserverTimezone=UTC&useUnicode=true&characterEncoding=utf-8driver-class-name: com.mysql.jdbc.Driverdruid:initial-size: 5   # 初始数max-active: 10    # 最大活动数aop-patterns: com.xiaozhi.admin.*  # 监控SpringBeanfilters: stat,wall     # 底层开启功能,stat(sql监控),wall(防火墙)stat-view-servlet:  # 配置监控页功能login-username: adminlogin-password: adminurl-pattern: /druid/*enabled: trueweb-stat-filter:  # 监控webenabled: trueexclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"url-pattern: /*filter:stat:   # 对上面filters里面的stat的详细配置slow-sql-millis: 1000enabled: truewall:enabled: trueconfig:drop-table-allow: false

3、整合mybatis操作

https://github.com/mybatis

<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version>
</dependency>

1.配置模式

  • 全局配置文件

  • SqlSessionFactory 自动配好了

  • SqlSessionTemplate 自动配好了

  • @EnableConfigurationProperties(MybatisProperties.class) 绑定了配置文件 mybatis.xxxx

  • @org.springframework.context.annotation.Configuration
    // 扫描标注有@Mapper注解的接口
    @Import(AutoConfiguredMapperScannerRegistrar.class)
    @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

注意:一定要给mapper接口标注@Mapper注解,要不然就使用@MapperScan()注解扫描进去,不然会识别不了

配置方式
mybatis:config-location: classpath:mybatis/mybatis-config.xml  # 全局配置文件位置mapper-locations: classpath:mybatis/mapper/*.xml  #sql映射文件位置type-aliases-package: com.xiaozhi.admin.pojo  # 开启类别名

也可以使用全部都是配置文件的方式

mybatis:# 如果使用configuration配置项,那么就不需要全局配置文件了,两个只能存在一个,同时存在会报错#  config-location: classpath:mybatis/mybatis-config.xml  # 全局配置文件位置mapper-locations: classpath:mybatis/mapper/*.xml  #sql映射文件位置type-aliases-package: com.xiaozhi.admin.pojo  # 开启类别名configuration:  # 相当于全局配置文件的配置项map-underscore-to-camel-case: true  # 开启驼峰命名
测试

sql语句

CREATE TABLE city
(id      INT(11) PRIMARY KEY AUTO_INCREMENT,NAME    VARCHAR(30) ,state   VARCHAR(30),country VARCHAR(30)
);

实体类

@Data
public class City {private Long id;private String name;private String state;private String country;
}

Mapper接口

@Mapper
public interface CityMapper {void insert(City city);
}

也可以

@MapperScan(basePackages = "com.xaiozhi.admin.mapper")
@Configuration
public class MybatisConfig {}

编写映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaozhi.admin.mapper.CityMapper"><insert id="insert">INSERT INTO city (name, state, country) VALUES(#{name}, #{state}, #{country})</insert></mapper>

测试

@Test
public void Test2(){cityMapper.insert(new City(null, "111", "222", "333"));
}@Test
public void Test3(){System.out.println(cityMapper.findById(1L));
}

2.注解模式

@Mapper
public interface CityMapper {@Select("select * from city where id = #{id}")void insert(City city);@Insert("INSERT INTO city (name, state, country) VALUES(#{name}, #{state}, #{country})")City findById(long id);
}
扩展

注解也可以进行一些较复杂的动作

编写一个完整的开发流程

service

@Service
public class CityServiceImpl implements CityService {@AutowiredCityMapper cityMapper;@Overridepublic void saveCity(City city) {cityMapper.insert(city);}
}

controller

@Controller
public class CityController {@AutowiredCityService cityService;@ResponseBody@PostMapping("/city")public City city(City city) {cityService.saveCity(city);return city;}
}

使用postman进行测试

返回的id是为null的,我们想要插入的同时也要设置我们的id值,我们可以这样做

配置文件的形式

   <insert id="insert" useGeneratedKeys="true" keyProperty="id">INSERT INTO city (name, state, country) VALUES(#{name}, #{state}, #{country})</insert>

使用注解

使用@Options注解放我们其他的功能

@Insert("INSERT INTO city (name, state, country) VALUES(#{name}, #{state}, #{country})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insert(City city);

结果显示

3.混合模式

混合模式就是注解 + xml映射文件

简单的sql语句我们可以使用注解

复杂的语句我们可以使用映射文件来处理

4、整合Mybatis-Plus完成CRUD

https://mp.baomidou.com/

1.引入stater
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.1</version>
</dependency>

它引入了mybatis的整合包,所以引入它就可以有mybatis的全部功能了

注意:它和配置文件绑定的前缀是mybatis-plus

2.按照官方的文档来学习

二、NoSQL

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

1、reids自动配置

①引入starter

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

默认导入的是kettuce客户端,需要jedis的需要导入jedis的包,还需要手动开启

②自动配置

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
  • 绑定了配置文件 spring.redis.xxxx

  • @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) 支持的两个客户端

    如果想要使用jedis就需要导入jedis的包,还需要手动修改客户端类型

  • RedisTemplate<Object, Object> 自动配好了

  • StringRedisTemplate 自动配好了

  • 我们使用RedisTemplate和 StringRedisTemplate 操作redis数据库就可以了

2、RedisTemplate与Lettuce

# redis配置
springredis:host: 192.168.6.156port: 6379# 有密码的要加上密码
@Autowired
StringRedisTemplate redisTemplate;@Test
public void Test4(){ValueOperations<String, String> operations = redisTemplate.opsForValue();operations.set("hello", "world");String hello = operations.get("hello");System.out.println(hello);
}

3、切换到jedis

<!-- 导入jedis -->
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>
  # redis配置
springredis:host: 192.168.6.156port: 6379# 有密码的要加上密码client-type: jedis    # 修改客户端为jedis# 修改jedis客户端的配置jedis:pool:max-active: 10max-wait: 1s

4、使用redis来记录访问次数

使用拦截器来处理

@Component
public class RedisInterceptor implements HandlerInterceptor {@AutowiredStringRedisTemplate redisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String uri = request.getRequestURI();System.out.println(uri);redisTemplate.opsForValue().increment(uri); // 每次访问 + 1return true;}
}
@Configuration
public class MyConfig implements WebMvcConfigurer {@AutowiredRedisInterceptor redisInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(redisInterceptor).addPathPatterns("/*")// 放行所有的静态资源.excludePathPatterns("/","/login","/css/**","/js/**","/fonts/**","/images/**");}
}

06 单元测试

1.JUnit5变化

springBoot2.2版本开始引入JUnit5作为单元测试默认库

作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。

JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。

JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。

注意

springBoot2.4版本开始移除了对Vintage的依赖,如果需要使用JUnit4需要自行引入

我们只需要引入单元测试的starter就可以使用对应的功能了

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>

SpringBoot整合Junit以后

  • 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
  • Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚

2.JUnit5常用注解

JUnit5的注解与JUnit4的注解有所变化

https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations