JAVA入门基础_SpringBoot2入门

目录

SpringBoot2快速入门

配置maven仓库的配置文件

<mirrors>
      <mirror>
        <id>nexus-aliyun</id>
        <mirrorOf>central</mirrorOf>
        <name>Nexus aliyun</name>
        <url>http://maven.aliyun.com/nexus/content/groups/public</url>
      </mirror>
  </mirrors>
 
  <profiles>
         <profile>
              <id>jdk-1.8</id>
              <activation>
                <activeByDefault>true</activeByDefault>
                <jdk>1.8</jdk>
              </activation>
              <properties>
                <maven.compiler.source>1.8</maven.compiler.source>
                <maven.compiler.target>1.8</maven.compiler.target>
                <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
              </properties>
         </profile>
  </profiles>

创建一个Maven项目,修改pom文件添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.11</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.codestars</groupId>
    <artifactId>first_springboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>first_springboot</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- 引入web开发模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 简化POJO类的开发 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 整合junit配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 配置了该插件后,将项目打包成jar包后,可以通过 java -jar 直接运行-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

创建一个Controller控制器

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello world! 你好呀";
    }
}

运行项目即可,最终的项目目录如下

image

了解自动配置原理

依赖管理及其父项目(版本仲裁)

  • spring-boot-dependencies 中配置了几乎所有我们项目开发所需要用到依赖的版本

  • 因此我们项目中引入各种依赖时,基本无需关注版本的问题,会自动进行版本仲裁,如果需要修改,则自己在引入依赖时修改即可。

- 我们项目中引入的父工程是spring-boot-starter-parent
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
</parent>

- 而spring-boot-starter-parent的父工程是
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>

所有场景启动器spring-boot-starter中最底层的依赖

  • 只要见到spring-boot-starter-xxx就代表是spring官方开发的场景启动器

  • 只要引入了相对应场景的启动器starter,那么就会为我们引入大部分常规所需要的依赖

  • 一旦见到了xxx-spring-boot-starter的场景启动器,一般就是由第三方提供整合springboot的启动器

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>2.3.4.RELEASE</version>
  <scope>compile</scope>
</dependency>

我们的springboot的web项目为什么可以直接启动呢?

  • 我们引入的web场景启动器中,可以看到如下依赖
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <version>2.6.11</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.3.22</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.3.22</version>
      <scope>compile</scope>
    </dependency>

  • springboot在项目启动时,为我们加载了内置的tomcat帮我们运行项目

  • 并且会对web场景进行一些默认的自动配置,例如添加当初在web.xml文件中配置的编码过滤器、HiddenMethodFilter、在springmvc中配置的注解驱动、访问静态资源等等到一系列功能。

  • 而且其中的各种配置类,都会映射到类似于xxxxxProperties这样的类并加载到IOC容器中

容器功能

@Configuration注解

  • 标识在类上,指明一个类为一个spring的配置类

Full模式与Lite模式

  • 在springboot2之后,可以在@Configuration类中添加proxyBeanMethods属性

  • Full模式

- 配置为true(默认),代表Full模式,会为当前的配置类创建一个代理对象,每次调用其中带有@Bean注解的方法时,都会先去spring的ioc容器中寻找。 (影响效率)
  • Lise模式(效率高)
// 配置为false,代表Lite模式,如果通过调用该配置类中的方法,会直接生成一个新的对象,不会使用到spring的ioc容器中已经装载好的对象。

@Bean注解

  • 标识在方法上,需要在spring的配置类中使用

  • 该方法的方法名会作为组件的id方法的返回值会作为组件的类型

@Import注解

  • 标识上类上,通常与@Configuration注解配合使用,表示将一个类装载到IOC容器当中

@Conditional派生注解

  • 标识在类上,如果该类满足@Conditional所标识的规则时,就加载该组件(组件注入),否则就不加载该组件。

@ImportResource

  • 如果想要引入一些组件,而这些组件配置在xml文件中,则可以通过该注解指定xml文件的路径,将xml文件中配置的组件装载到spring的ioc容器当中

配置绑定的2种方式

@Component 配合 @ConfigurationProperties注解完成组件依赖注入

  • 在spring的配置文件中添加组件的值
user.name=张三
user.age=15
  • 在需要注入的Bean上添加上如下注解
@Component
@ConfigurationProperties(prefix = "user")
public class User {
    private String name;
    private Integer age;
}

@ConfigurationProperties配合@EnableConfigurationProperties完成组件依赖注入

  • @ConfigurationProperties标注在需要注册的组件上
@ConfigurationProperties(prefix = "user")
public class User {
    private String name;
    private Integer age;
}
  • @EnableConfigurationProperties配置在一个配置类上,指定需要注入的组件
@EnableConfigurationProperties(User.class)
@Configuration
public class MyConfigTest {
}

自动配置原理

@SpringBootApplication注解

  • 该注解点开之后,发现这是一个组合注解,其中有3个注解
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}

@SpringBootConfiguration

  • 该注解包装了@Configuration注解,实质上就是一个spring的配置类

@ComponentScan

  • 该注解仅是配置了包扫描

核心注解 @EnableAutoConfiguration 开启自动配置

  • 该组件仍然是一个组合组件,其中有2个注解
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration

@AutoConfigurationPackage 注解,自动配置包

  • 该注解中配置了一个@Import向spring的ioc容器中导入组件
@Import(AutoConfigurationPackages.Registrar.class)
  • AutoConfigurationPackages.Registrar这个类是一个静态内部类,其中导入了一堆组件,类的定义如下
	static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

		@Override
		public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
			register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
		}

		@Override
		public Set<Object> determineImports(AnnotationMetadata metadata) {
			return Collections.singleton(new PackageImports(metadata));
		}

	}
  • 其中的registerBeanDefinitions方法中调用了register方法,为我们容器中注册了一些组件,而我们现在需要知道它注册了什么组件

  • 因此打了一个断点在registerBeanDefinitions方法中调用register方法的地方进行了查看

  • 这也就是为什么springboot会扫描我们启动类所在的包及其子包中的所有组件的原因。

		// AnnotationMetadata当前注解的元数据,可以获取到当前注解所在的类在哪,也就是我们写的启动类
		public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
			// new PackageImports(metadata).getPackageNames()得到的就是我们当前注解所在类的包名,我这里是:com.codestars
			register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
		}

@Import(AutoConfigurationImportSelector.class)注解(重点)

通过该类的getAutoConfigurationEntry方法得到需要注册到Spring容器中的组件
	protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		// 获取到需要加载到spring容器中的所有组件的全限定类名,通过getCandidateConfigurations(annotationMetadata, attributes);
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		// 对全部需要加载到容器中的组件进行一系列的过滤,靠着各个组件中的@Conditional来判断是否需要加载
		configurations = removeDuplicates(configurations);
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
		configurations = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}
getCandidateConfigurations(annotationMetadata, attributes)获取获取到所有需要加载到Spring容器中的组件
  • 该方法中会调用SpringFactoriesLoader.loadFactoryNames()方法,该方法又会调用loadSpringFactories,这个方法中会获取到META-INF/spring.factories这个文件,这个文件的路径由FACTORIES_RESOURCE_LOCATION这个类变量声明,然后读取其中需要加载到Spring容器中的组件。

总结

  • SpringBoot在启动时,会扫描启动类所在的包及其子包,将组件添加到spring的ioc容器当中

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。会从xxxxProperties里面拿。注意组件上用到了@EnableConfigurationProperties这个注解去加载xxxProperties组件。

  • 同时会获取到spring-boot-autoconfigure-2.6.11.jar这个包下的/META-INF/spring.factories中所有需要注册到spirng容器中的组件的全类名,并通过这些组件上的@Conditional注解判断是否将其加载到容器。最终将这些组件都添加到spring容器当中

  • 定制化配置

    • 如果我们想要实现一些定制化功能,比如说不想使用spring为我们自动配置的编码过滤器,我们就可以自己创建一个spring的配置类装载一个编码过滤器。
    • 如果想要修改某个spring帮我们加载的功能的配置,可以通过application.properties修改(yaml也可以的)

最佳开发实践

  • 引入所需要的场景依赖

  • 查看自动配置为我们配置了哪些组件(提供了哪些功能)

  • 判断是否需要修改

    • 是否需要修改某些组件的配置
    • 是否需要自定义部分组件(不使用spring提供的某个组件时)

SpringBoot核心功能

配置文件

  • 配置文件支持2种语法,一种是properties、一种是yaml,yaml可以有2种后缀格式: 分别为:ymlyaml

  • 传统的properties的配置如何进行就不进行阐述了,主要是yml文件如何使用

YAML配置文件的格式

  • 定义一个实体类
@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;
}
  • 在yml中进行文件的配置
# yaml表示以上对象
person:
  userName: zhangsan
  boss: false
  birth: 2019/12/12 20:12:33
  age: 18
  # 对象
  pet:
    name: tomcat
    weight: 23.4
  # 数组
  interests: [篮球,游泳]
  # 集合
  animal:
    - jerry
    - mario
  # 配置Map集合,注意不要加 -
  score:
    english:
      first: 30
      second: 40
      third: 50
    math: [131,140,148]
    chinese: {first: 128,second: 136}
  # 配置set集合
  salarys: [3999,4999.98,5999.99]
  # 配置map集合,map中装的是List
  allPets:
    sick:
      - {name: tom}
      - {name: jerry,weight: 47}
    health: [{name: mario,weight: 47}]

如果想要自己编写的实体类在yml中有提示的话

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

Web开发

简单功能分析

静态资源访问

  • springboot在项目启动时为我们添加一个了一个资源映射:/**,该映射可以匹配到如下3个路径
{
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
}

image

  • 并且我们可以通过/webjars/资源路径的方式访问webjars中的静态资源
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
静态资源与Controller的优先级
  • 先找Controller看能不能处理,Controller不能处理再找静态资源处理器
静态资源的访问前缀、欢迎页、自定义Favicon
  • 修改静态资源的访问前缀
spring:
  mvc:
    # 静态资源访问前缀,默认为/**,注意:如果设置了静态资源的访问前缀,那么默认的index页面和favicon都仍然会通过/index ,/favicon找,所以会失效
    static-path-pattern: /res/**
  • 默认会在静态资源目录中找
    • index.html
    • favicon.ico

@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody 获取请求参数

    @GetMapping("/car/{id}/owner/{username}")
    public Map<String,Object> getCar(@PathVariable("id") Integer id,
                                     @PathVariable("username") String name,
                                     @PathVariable Map<String,String> pv,
                                     @RequestHeader("User-Agent") String userAgent,
                                     @RequestHeader Map<String,String> header,
                                     @RequestParam("age") Integer age,
                                     @RequestParam("inters") List<String> inters,
                                     @RequestParam Map<String,String> params,
                                     @CookieValue("_ga") String _ga,
                                     @CookieValue("_ga") Cookie cookie)
									 
									 
@PostMapping("/save")
    public Map postMethod(@RequestBody String content){
        Map<String,Object> map = new HashMap<>();
        map.put("content",content);
        return map;
    }
	
	
 //1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
    //2、SpringBoot默认是禁用了矩阵变量的功能
    //      手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
    //              removeSemicolonContent(移除分号内容)支持矩阵变量的
    //3、矩阵变量必须有url路径变量才能被解析
    @GetMapping("/cars/{path}")
    public Map carsSell(@MatrixVariable("low") Integer low,
                        @MatrixVariable("brand") List<String> brand,
                        @PathVariable("path") String path){
        Map<String,Object> map = new HashMap<>();

        map.put("low",low);
        map.put("brand",brand);
        map.put("path",path);
        return map;
    }
	
	
// /boss/1;age=20/2;age=10

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

    }

SpringMVC处理请求的流程

  • 执行DispatcherServlet中的doService方法()

  • 再执行到doDispatch(request, response)方法

  • 从处理器映射器集合中获取该路径能够执行的MappingHandle处理器

  • 通过MappingHandle来获取到HandleAdapter适配器

  • 执行所有该处理器的拦截器的preHandle方法(按照配置顺序执行)

  • 通过HandleAdapter适配器来执行MappingHandle处理器中的方法,处理后得到一个ModelAndView

    • 其中所有Map、Model、ModelMap等使用的都是同一个ModelAndView来存储请求域的数据
    • 放在ModelAndViewContainer中
  • 执行所有该处理器的拦截器的postHandle方法(按照配置逆序执行)

  • 执行派遣结果

  • 将ModelAndView中的Model中的存储到作用域中

  • 按照ModelAndView中的视图进行页面的渲染

  • 执行该处理器的拦截器的afterCompletion方法(按照配置逆序执行)

  • 最终响应给客户端。

处理器方法参数的解析原理

  • 通过HandleAdapter适配器来执行处理器的方法

  • 执行时会获取到所有的参数解析器返回值解析器

  • 然后遍历所有的参数列表,遍历时会将该参数与所有的参数解析器进行匹配

  • 如果匹配上了,则使用该参数解析器为参数进行赋值操作

  • 如果是自定义的参数,会使用到转换器

特殊参数:ModelMap、Model、Map等

  • 通过HandleAdapter适配器来执行处理器的方法

  • 执行时会获取到所有的参数解析器返回值解析器

  • 遍历参数列表拿到ModelMap、Model、Map等参数时,会分别匹配到不同的参数解析器

  • 但是匹配到的这几个参数解析器,最终都会返回一个BindModelMapContainer

  • 至于这3个参数存储到值为什么会保存到请求域中,是因为在进行最后的视图解析时,会将这些值存储到请求域中

自定义一个返回值解析器

//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;
                    }
                });
            }
        };
    }

处理器带@ResponseBody的处理流程

  • 获取到该处理器的HandlerMapping

  • 通过HandlerMapping获取到相对应的HandlerAdapter

  • 通过HandlerAdapter调用处理器的方法进行处理

  • 获取到处理器方法的参数解析器返回值处理器

  • 遍历处理器方法的参数列表获取到一个个参数,并都用参数解析器进行解析

  • 调用方法执行,执行后获取到方法的返回值

  • 通过返回值去匹配相对应的返回值处理器,如果是标注了@ResponseBody的方法,会由RequestResponseBodyMethodProcessor

  • 匹配到了对应的返回值解析器后,会调用该返回值处理器的handleReturnValue方法来完成返回值的处理

  • 通过NegotiationManager协商管理器获取到浏览器所需要的MediaType,使用ContentNegotiationStrategy内容协商策略来获取浏览器能够接收到Media类型,有基于请求头的内容协商策略,还有基于请求参数的(需要配置)。List<MediaType> acceptableTypes

  • 再获取到全部已经配置好的消息转换器,判断是否能够将返回值类型进行转换(调用canWrite()方法),如果能够将返回值类型进行转换,则将该转换器支持的Media存储到一个集合中。List<MediaType> producibleTypes

  • 根据浏览器能够接收到(有权重)List<MediaType> acceptableTypes集合与List<MediaType> producibleTypes进行匹配,获取到最终能够达成一致的MediaType集合List<MediaType> mediaTypesToUse,再对该集合一个排序MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

  • 然后获取List<MediaType> mediaTypesToUse中的第一个MediaType,selectedMediaType

  • 再次遍历所有的消息转换器MessageConverter、判断转换器不仅要支持转换对应的返回值类型,还要能支持选择的MediaType

  • 拿到最后匹配的消息转换器后,调用转换器的write方法,将结果响应给浏览器。

视图解析器原理分析

  • DispatcherServlet接收到一个请求

  • 获取到相对应的HandlerMapping

  • 通过HandlerMapping获取到对应的HandlerAdapter

  • HandlerAdapter执行目标方法获取到一个ModelAndView

  • 进行请求结果派发processDispatchResult(1087行),而后执行render渲染方法

  • 在执行渲染方法时,根据内容协商管理器中的协商策略最终获取到能够处理该视图的解析器

  • 最后调用视图解析器的render方法完成对页面视图的解析,最终返回结果

image
image

处理器方法异常时的处理流程分析

  • DispatchService在执行处理器方法时,一旦抛出异常mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

  • 那么方法本应该返回的ModelAndView就会为null

  • 捕获该异常后,将会把捕获到的异常赋值个另一个变量Exception dispatchException

  • 接下来依然执行派发结果,并且派发结果是会将该异常传入

  • 派发结果的执行会判断传入的异常是否会null,如果不为null,则调用执行获取异常的方法mv = processHandlerException(request, response, handler, exception);

  • 该方法中会循环当前所有的处理异常解析器,判断哪个异常解析器可以处理该异常

  • 如果有异常解析器可以处理该异常,则执行该异常处理器的resolveException方法,获取到一个ModelAndView,然后根据该ModelAndView进行处理

  • 如果没有异常解析器可以处理该异常,则最终将异常抛出

  • 然后根据Servlet机制会默认向服务器端转发一个/error的请求来进行处理。

image

SpringBoot为我们自动配置的异常处理规则

  • SpringBoot通过自动配置为我们添加了一个异常处理映射器`BasicErrorController

  • 可以通过错误代码返回到指定的页面,默认为静态资源路径或者模板路径

    • /static/error/4xx.html 、/templates/error/5xx.html

方法抛出异常又没有异常解析器处理的处理机制

  • 会默认重新向服务器发送一个/error请求

SpringBoot的常用功能

引入原生的Servlet、Filter、Listener等

引入原生的这些组件无法使用到spring的拦截器、编码过滤器等

通过@ServletComponentScan的方式

  • (1)正常的编写一个Servlet
@WebServlet("/my")
public class MyServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("你好呀");
    }
}

  • (2)在SpringBoot的启动类上加注解来扫描
@SpringBootApplication
@ServletComponentScan("com.codestars.config")

通过注册ServletRegistrationBean的方式

  • (1)写一个Servlet
public class MyServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("你好呀");
    }
}
  • (2)编写一个spring的配置类
@Configuration(proxyBeanMethods = true)
public class MyConfig implements WebMvcConfigurer {
    @Bean
    public ServletRegistrationBean myServletRegistration() {
        Servlet servlet = new MyServlet();
        return new ServletRegistrationBean<>(servlet, "/myhh");
    }
}

SpringBoot整合MyBatisPlus

MyBatisPlus简介

简介

在MyBatis的基础上进行封装,在不影响原有MyBatis功能的情况下实现功能的增强与扩展。

特性

  • 无侵入

  • 损耗小

  • 强大的CRUD操作,内置通用Mapper通用Service,通过少量配置完成大部分单表的CRUD操作,还有功能强大的条件构造器

  • 支持ActiveRecord模式,实体类只需要继承Model类即可进行强大的CRUD操作

  • 支持自定义全局通用操作

  • 内置代码生成器:2个依赖

  • 分页插件支持多种数据库

  • 内置性能分析插件

  • 内置全局拦截插件

框架结构

image

代码及文档地址

配置MyBatisPlus的开发环境

创建一张数据库表,添加点数据

	CREATE DATABASE `mybatis_plus` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
	use `mybatis_plus`;
	CREATE TABLE `user` (
	`id` bigint(20) NOT NULL COMMENT '主键ID',
	`name` varchar(30) DEFAULT NULL COMMENT '姓名',
	`age` int(11) DEFAULT NULL COMMENT '年龄',
	`email` varchar(50) DEFAULT NULL COMMENT '邮箱',
	PRIMARY KEY (`id`)
	) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

创建一个SpringBoot项目

修改pom文件引入需要的依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
    </dependencies>

修改application.yaml文件、开启别名、日志等

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: abc123
    type: com.zaxxer.hikari.HikariDataSource
# mybatis-plus配置文件
#mybatis-plus:
#  # 这个就是默认值,扫描xml文件所在的位置
#  mapper-locations: classpath*:/mapper/**/*.xml
#  global-config:
#    db-config:
#      # 设置表的前缀
#      table-prefix: t_
#  # 配置枚举扫描,3.5以后已弃用
#  type-enums-package:
mybatis-plus:
  type-aliases-package: com.codestars.pojo
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

修改启动类,添加Mapper接口扫描

@SpringBootApplication
// 如果进行该配置,则需要在每个Mapper上添加@Mapper注解
@MapperScan("com.codestars.mapper")
public class StudyMybatisPlusApplication {

添加一个Mapper接口

public interface UserMapper extends BaseMapper<User> {
    /**
     * 根据id查询用户
     * @return
     */
    User selectUserById();
}

添加一个Service和ServiceImpl(Service中调用的依然是BaseMapper中的方法)

public interface UserService extends IService<User> {
}

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

在resources下添加一个mapper文件夹(选做)

  • mybatisplus默认将类路径下的mapper文件夹作为Mapper映射文件
<mapper namespace="com.codestars.mapper.UserMapper">

    <select id="selectUserById" resultType="User">
        select * from user where id = #{id}
    </select>
</mapper>

image

测试简单的CRUD

IService中的新增方法

image

@SpringBootTest
class StudyMybatisPlusApplicationTests {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private UserService userService;

    @Test
    void testBaseMapperXml() {
        // 1、测试使用Mapper.xml映射的方式还是否可用
        User user = userMapper.selectUserById(1);
        System.out.println(user);
    }

    /**
     * 测试插入方法
     */
    @Test
    void testInsert(){
        // 插入单条记录
        User user = new User();
        user.setName("张三");
        user.setEmail("1@qq.com");
        user.setAge(55);
        boolean save = userService.save(user);
        System.out.println("是否新增成功:" + save);

        // 插入批量数据
        List<User> userList = Arrays.asList(
                new User(null,"李四1",11,"2@qq.com"),
                new User(null,"李四2",11,"2@qq.com"),
                new User(null,"李四3",11,"2@qq.com")
        );
        userService.saveBatch(userList);
    }
}
测试IService中的删除方法

image

    /**
     * 测试删除方法
     */
    @Test
    void testDelete(){
        // 根据id删除记录
        boolean b = userService.removeById(1);
        System.out.println("id为1的用户是否删除: " + b);

        // 根据id批量删除
        boolean b1 = userService.removeByIds(Arrays.asList(1, 2, 3));
        System.out.println("批量删除是否成功:" + b1);
    }
测试IService中的更新方法

image

    /**
     * 测试删除方法
     */
    @Test
    void testUpdate(){
        // 根据实体类中的id进行修改
        User user = new User(1L, "王六",111,"444@qq.com");
        userService.updateById(user);
    }
测试IService中的查询方法

image
image

    /**
     * 测试删除方法
     */
    @Test
    void testSelect(){
        // 1、获取表中所有的数据,转换为List集合
        List<User> list = userService.list();
        System.out.println(list);

        // 2、获取表中所有的数据,转换为Map集合
        List<Map<String, Object>> maps = userService.listMaps();
        System.out.println(maps);

        // 3、获取所有的记录id
        List<Object> objects = userService.listObjs();
        System.out.println("----------" + objects);

        // 4、通过id查询记录
        User byId = userService.getById(4);
        System.out.println(byId);
    }

对于实体类的常用注解

@TableName

当实体类的名称无法与数据表的名称对应时,需要使用该注解指定数据表的名称。

@TableName("t_user")
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

@TableId(主键字段及主键策略)

  • mybaitsplus默认会将数据库中的id字段作为主键

  • 如果数据库中的字段名和实体类中的属性名不为id,则mybatisplus无法确认id的字段,通过id获取记录等方法将会直接报错
    org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)

  • 这个时候可以通过@TableId来指定数据库主键的列

    // type = IdType.AUTO(自动递增,数据库中的主键必须是自增的),IdType.ASSIGN_ID雪花算法(默认)
    @TableId(value = "uid",type = IdType.ASSIGN_ID)

@TableFiled

  • 同样可以指定与数据库中匹配的字段

  • 可以指定是否为一个数据库字段

    // exist=true代表为一个数据库字段,否则不为一个数据库字段
    @TableField(exist = true)

@TableLogic

  • 当数据库中的记录不需要被真正的删除,只是进行逻辑删除时,可以给逻辑删除的字段添加该注解。

  • 0 为没有被逻辑删除的记录、1为被逻辑删除的字段。接下来进行的数据增删改查时,都会默认添加上一个where条件,该字段等于0

  • 将数据库中添加一个int类型的is-deleted字段

  • 为实体类添加上@TableLogic注解

    @TableLogic
    private Integer isDeleted;
  • 接下来一旦执行删除操作,只是将数据表中的is-deleted字段修改为1

@Version

  • 当需要使用到乐观锁时,需要用到该字段,作为版本控制

  • 添加数据表的字段,添加一个int类型的version,默认值可以为0

  • 为实体类中添加一个字段与其对应并添加@Version注解

  • 测试代码

    @Test
    public void testVersionLock() {
        // id为3的用户工资默认为3000元, 老总让甲为其增加1000,又让乙为其减少500
        // 甲和乙同时获取到了id为3的用户信息
        User zhangsan = userService.getById(3);
        User lisi = userService.getById(3);

        // 老总让甲修改了id为3的用户的工资,让其工资增加1000元
        zhangsan.setMoney(zhangsan.getMoney() + 1000);
        userService.updateById(zhangsan);

        // 老总觉得给的太多了,让乙将id为3的用户工资减少500元。
        lisi.setMoney(lisi.getMoney() - 500);
        boolean flag = userService.updateById(lisi);
        if (!flag) {
            // 如果失败说明没修改成功,重新修改,其实可以可以改成while
            lisi = userService.getById(3);
            lisi.setMoney(lisi.getMoney() - 500);
            userService.updateById(lisi);
        }

        // 总裁现在获取员工id为3的工资时
        User byId = userService.getById(3);
        System.out.println(byId);
    }

开启乐观锁功能和分页功能

@Configuration
public class MyConfig {

    @Bean
    public MybatisPlusInterceptor myMybatisPlusInterceptor() {
        MybatisPlusInterceptor myMybatisPlusInterceptor = new MybatisPlusInterceptor();
        // 添加分页拦截器
        myMybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        // 添加乐观锁机制
        myMybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        
        return myMybatisPlusInterceptor;
    }
}

条件构造器

条件构造器的体系结构

image

QueryWrapper

and条件拼接(默认)

    @Test
    public void testQueryWrapper() {
        QueryWrapper<User> queryWrapper = new QueryWrapper();
        // 1、普通and条件拼接(默认),获取name=张三,age大于10的用户
        queryWrapper.eq("name", "张三")
                .gt("age", 10);
        // 2、根据条件构造器查询所有数据
        List list = userService.list(queryWrapper);
        list.forEach(System.out::println);
    }

组装排序条件

    @Test
    public void testQueryWrapper() {
        QueryWrapper<User> queryWrapper = new QueryWrapper();
        // 1、按照年龄降序排序,再按照uid升序排序
        queryWrapper.orderByDesc("name")
                .orderByAsc("uid");
        
        // 2、根据条件构造器查询所有数据
        List list = userService.list(queryWrapper);
        list.forEach(System.out::println);
    }

条件的优先级(and或or中写lambda表达式)

    @Test
    public void testQueryWrapper() {
        QueryWrapper<User> queryWrapper = new QueryWrapper();
        // 1、普通and条件拼接(默认),获取(年龄大于20并且用户名中包含有a)或邮箱为null的用户信息
        queryWrapper.and(query -> query.gt("age", 20)
                                       .like("name", "a"))
                    .or()
                    .isNull("email");

        // 2、根据条件构造器查询所有数据
        List list = userService.list(queryWrapper);
        list.forEach(System.out::println);
    }

组装select子句(查询指定字段)

    @Test
    public void testQueryWrapperSelect() {
        QueryWrapper<User> queryWrapper = new QueryWrapper();
        // 1、指定需要查询的字段
        queryWrapper.select("name", "age","email");

        // 2、根据条件构造器查询所有数据
        List list = userService.list(queryWrapper);
        list.forEach(System.out::println);
    }

子查询的运用(了解)

    @Test
    public void testQueryWrapperSelect() {
        QueryWrapper<User> queryWrapper = new QueryWrapper();
        // 1、查询uid小于3的用户信息
        queryWrapper.inSql("uid", "select uid from t_user where uid = 3");

        // 2、根据条件构造器查询所有数据
        List list = userService.list(queryWrapper);
        list.forEach(System.out::println);
    }

实现条件匹配,例如xml文件中的if等

    @Test
    public void testQueryWrapperIf() {
        String uid = null;
        String name = "张三";
        Integer age = 10;
        
        QueryWrapper<User> queryWrapper = new QueryWrapper();
        // 1、根据条件判断是否进行sql拼接
        queryWrapper.eq(StringUtils.isNotBlank(uid),"uid", uid)
                    .like(StringUtils.isNotBlank(name),"name", name)
                    .eq(age != null, "age", age);

        // 2、根据条件构造器查询所有数据
        List list = userService.list(queryWrapper);
        list.forEach(System.out::println);
    }

LambdaQueryWrapper(防止数据库字段写错)

    @Test
    public void testQueryWrapperLambda() {

        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper();
        // 1、获取name为张三并且年龄等于20的用户信息
        queryWrapper.eq(User::getName, "张三")
                    .gt(User::getAge, 20);

        // 2、根据条件构造器查询所有数据
        List list = userService.list(queryWrapper);
        list.forEach(System.out::println);
    }

UpdateQueryWrapper

    @Test
    // @Transactional
    public void testUpdateWrapper() {

        UpdateWrapper<User> updateWrapper = new UpdateWrapper();
        // 将uid = 1的用户的姓名修改为“李四“
        updateWrapper.set("name","李四")
                     .eq("uid",1);

        userService.update(updateWrapper);
    }

LambdaQueryWrapper

    @Test
    // @Transactional
    public void testUpdateWrapper() {

        LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper();
        // 将uid = 1的用户的姓名修改为“李四“
        updateWrapper.set(User::getName,"李四")
                     .eq(User::getUid,1);

        userService.update(updateWrapper);
    }

分页插件

注意,多租户插件与分页插件配合使用的坑

切记: 一定要把多租户插件配置在前面,分页插件配置在后面,不然分页插件查询的total不正确的。

配置分页插件拦截器

@Configuration
public class MyConfig {

    @Bean
    public MybatisPlusInterceptor myMybatisPlusInterceptor() {
        MybatisPlusInterceptor myMybatisPlusInterceptor = new MybatisPlusInterceptor();
        // 添加分页拦截器
        myMybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        // 添加乐观锁机制
        myMybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

        return myMybatisPlusInterceptor;
    }
}

使用Service中已经提供的分页方法

    @Test
    public void testPage() {
        // 第一页、一页2条记录
        Page<User> page = new Page<User>(1,2);
        userService.page(page);

        System.out.println("数据的List集合:" + page.getRecords());
        System.out.println("总页数:long类型:" + page.getPages());
        System.out.println("总记录数:long类型:" + page.getTotal());
        System.out.println("当前页数:long类型:" + page.getCurrent());
        System.out.println("是否有上一页:boolean类型:" + page.hasPrevious());
        System.out.println("是否有下一页:boolean类型:" + page.hasNext());
    }

通过自己编写的Mapper.xml的方式

  • 编写Mapper接口
    /**
     * 根据关键字分页
     * @param page
     * @param keyword
     * @return
     */
    List<User> selectMyPage(@Param("page") Page<User> page, @Param("keyword") String keyword);
  • 编写XML文件
    <select id="selectMyPage" resultType="com.codestars.pojo.User">
        select * from t_user
        <where>
            <if test="keyword != null and keyword != ''">
                name like ""#{keyword}""
            </if>
        </where>
    </select>
  • 测试类
    @Test
    void testMapperPage() {
        Page<User> page = new Page<>(1, 3);
        List<User> userList = userMapper.selectMyPage(page, null);
        // 无法自动将数据封装到page当中
        page.setRecords(userList);

        System.out.println("数据的List集合:" + page.getRecords());
        System.out.println("总页数:long类型:" + page.getPages());
        System.out.println("总记录数:long类型:" + page.getTotal());
        System.out.println("当前页数:long类型:" + page.getCurrent());
        System.out.println("是否有上一页:boolean类型:" + page.hasPrevious());
        System.out.println("是否有下一页:boolean类型:" + page.hasNext());
    }

通用枚举

  • 如果实体类中的某个变量是枚举,例如性别属性

  • 那么可以在定义的枚举中,把映射到数据库字段的值添加@EnumValue注解

public enum GenderEnum {
    MAN(0,"男"),
    WOMAN(1,"女");


    @EnumValue
    private Integer num;

    private String gender;

    GenderEnum(Integer num, String gender) {
        this.num = num;
        this.gender = gender;
    }
}
  • 实体类的gender变量
    private GenderEnum gender;
  • 扫描通用枚举(mybatisplus3.5之后不需要)
# 配置扫描通用枚举
type-enums-package: com.codestars.config

多数据源

加入动态数据源场景启动器

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.0</version>
        </dependency>

在application.yml中配置多个数据源

spring:
  datasource:
    dynamic:
      primary: master
      strict: false # 是否为严格模式,如果是严格模式,未匹配到数据源直接报错
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
          username: root
          password: abc123
          type: com.zaxxer.hikari.HikariDataSource
        slave_1:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/mybatis_plus_2?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
          username: root
          password: abc123
          type: com.zaxxer.hikari.HikariDataSource

在用于访问数据库的Mapper或者ServiceImpl上添加@DS注解指定需要访问的数据源

@DS("slave_1")
public interface AccountMapper extends BaseMapper<Account> {
}

@Service
@DS("master")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

MyBatisPlus代码生成器

  • 添加依赖
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>
  • 执行如下代码
package com.codestars.study_mybatis_plus;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.Collections;

/**
 * @author codeStars
 * @date 2022/9/3 15:53
 */
public class TestCode {
    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/mybatis_plus?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai", "root", "abc123")
                        .globalConfig(builder -> {
                            builder.author("codestars") // 设置作者 
                                    // .enableSwagger() // 开启 swagger 模式
                                    .fileOverride() // 覆盖已生成文件
                                    .outputDir("D://mybatis_plus"); // 指定输出目录
                        })
                        .packageConfig(builder -> {
                            builder.parent("com.codestars") // 设置父包名
                                    .moduleName("mybatisplus") // 设置父包模块名
                                                                // 设置mapperXml生成路径
                                    .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://mybatis_plus"));
                    
                        })
                        .strategyConfig(builder -> {
                            builder.addInclude("t_user") // 设置需要生成的表名
                                    .addTablePrefix("t_", "c_"); // 设置过滤表前缀
                        })
                        .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                        .execute();
    }
}

SpringBoot整合Knife4j

引入依赖

    <properties>
        <knife4j.version>2.0.9</knife4j.version>
    </properties>

    <dependency>
        <groupId>com.github.xiaoy	min</groupId>
        <artifactId>knife4j-spring-boot-starter</artifactId>
        <version>${knife4j.version}</version>
    </dependency>

添加配置类

@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {

    @Bean(value = "dockerBean")
    public Docket dockerBean() {
        //指定使用Swagger2规范
        Docket docket=new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(new ApiInfoBuilder()
                //描述字段支持Markdown语法
                .description("# Knife4j RESTful APIs")
                // .termsOfServiceUrl("https://doc.xiaominfo.com/")
                .contact("xiaoymin@foxmail.com")
                .version("1.0")
                .build())
                //分组名称
                .groupName("一个小项目")
                .select()
                //这里指定Controller扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.woniu.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
}

项目访问地址

http://localhost:8080/myContext/doc.html

  • 别忘了修改成自己的端口号,以及上下文

  • 之所以能访问,是以为该启动器为我们配置了一个Filter
    image

posted @ 2022-09-03 16:49  CodeStars  阅读(74)  评论(0编辑  收藏  举报