springboot笔记

springboot笔记

基本信息

什么是Spring

Spring是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson 。

Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。

Spring是如何简化Java开发的

为了降低Java开发的复杂性,Spring采用了以下4种关键策略:

1、基于POJO的轻量级和最小侵入性编程,所有东西都是bean;

2、通过IOC,依赖注入(DI)和面向接口实现松耦合;

3、基于切面(AOP)和惯例进行声明式编程;

4、通过切面和模版减少样式代码,RedisTemplate,xxxTemplate;

什么是SpringBoot

学过javaweb的同学就知道,开发一个web应用,从最初开始接触Servlet结合Tomcat, 跑出一个Hello Wolrld程序,是要经历特别多的步骤;后来就用了框架Struts,再后来是SpringMVC,到了现在的SpringBoot,过一两年又会有其他web框架出现;你们有经历过框架不断的演进,然后自己开发项目所有的技术也在不断的变化、改造吗?建议都可以去经历一遍;

言归正传,什么是SpringBoot呢,就是一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置, you can “just run”,能迅速的开发web应用,几行代码开发一个http接口。

所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”,进而衍生出一些一站式的解决方案。

是的这就是Java企业级应用->J2EE->spring->springboot的过程。

随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;

Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。

简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。

Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。

Spring Boot的主要优点:

  • 为所有Spring开发者更快的入门
  • 开箱即用,提供各种默认配置来简化项目配置
  • 内嵌式容器简化Web项目
  • 没有冗余代码生成和XML配置的要求

HelloWorld

准备工作

我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。

我的环境准备:

  • java version “1.8.0_181”
  • Maven-3.6.1
  • SpringBoot 2.x 最新版

开发工具:

  • IDEA

创建基础项目说明

Spring官方提供了非常方便的工具让我们快速构建应用

Spring Initializr:https://start.spring.io/

**项目创建方式一:**使用Spring Initializr 的 Web页面创建项目

1、打开 https://start.spring.io/

2、填写项目信息

3、点击”Generate Project“按钮生成项目;下载此项目

4、解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。

5、如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。

**项目创建方式二:**使用 IDEA 直接创建项目

1、创建一个新项目

2、选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现

3、填写项目信息

4、选择初始化的组件(初学勾选 Web 即可)

5、填写项目路径

6、等待项目构建成功

项目结构分析:

通过上面步骤完成了基础项目的创建。就会自动生成以下文件。

1、程序的主启动类

2、一个 application.properties 配置文件

3、一个 测试类

4、一个 pom.xml

pom.xml 分析

打开pom.xml,看看Spring Boot项目的依赖:

<!-- 父依赖 --><parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>2.2.5.RELEASE</version>    <relativePath/></parent>
<dependencies>    <!-- web场景启动器 -->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>    <!-- springboot单元测试 -->    <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-test</artifactId>        <scope>test</scope>        <!-- 剔除依赖 -->        <exclusions>            <exclusion>                <groupId>org.junit.vintage</groupId>                <artifactId>junit-vintage-engine</artifactId>            </exclusion>        </exclusions>    </dependency></dependencies>
<build>    <plugins>        <!-- 打包插件 -->        <plugin>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-maven-plugin</artifactId>        </plugin>    </plugins></build>

编写一个http接口

1、在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到

2、在包中新建一个HelloController类

@RestControllerpublic class HelloController {
    @RequestMapping("/hello")    public String hello() {        return "Hello World";    }    }

3、编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;控制台输出了 Tomcat 访问的端口号!

image-20211027204601571

简单几步,就完成了一个web接口的开发,SpringBoot就是这么简单。所以我们常用它来建立我们的微服务项目!

将项目打成jar包,点击 maven的 package

image-20211027204624674

如果遇到以上错误,可以配置打包时 跳过项目运行测试用例

<!--    在工作中,很多情况下我们打包是不想执行测试用例的    可能是测试用例不完事,或是测试用例会影响数据库数据    跳过测试用例执    --><plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-surefire-plugin</artifactId>    <configuration>        <!--跳过项目运行测试用例-->        <skipTests>true</skipTests>    </configuration></plugin>

如果打包成功,则会在target目录下生成一个 jar 包

image-20211027204714614

打成了jar包后,就可以在任何地方运行了!

运行原理探究

我们之前写的HelloSpringBoot,到底是怎么运行的呢,Maven项目,我们一般从pom.xml文件探究起;

父依赖

其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件!

<parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-parent</artifactId>    <version>2.2.5.RELEASE</version>    <relativePath/> <!-- lookup parent from repository --></parent>

点进去,发现还有一个父依赖

<parent>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-dependencies</artifactId>    <version>2.2.5.RELEASE</version>    <relativePath>../../spring-boot-dependencies</relativePath></parent>

这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;

以后我们导入依赖默认是不需要写版本;但是如果导入的包没有在依赖中管理着就需要手动配置版本了;

启动器 spring-boot-starter

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

springboot-boot-starter-xxx:就是spring-boot的场景启动器

spring-boot-starter-web:帮我们导入了web模块正常运行所依赖的组件;

SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器),只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来 , 我们要用什么功能就导入什么样的场景启动器即可 ;我们未来也可以自己自定义 starter;

主启动类

默认的主启动类

//@SpringBootApplication 来标注一个主程序类//说明这是一个Spring Boot应用@SpringBootApplicationpublic class SpringbootApplication {
   public static void main(String[] args) {     //以为是启动了一个方法,没想到启动了一个服务      SpringApplication.run(SpringbootApplication.class, args);   }
}

但是**一个简单的启动类并不简单!**我们可以分析一下这些注解都干了什么

@SpringBootApplication

作用:标注在某个类上说明这个类是SpringBoot的主配置类 , SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;

进入这个注解:可以看到上面还有很多其他注解!

@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(    excludeFilters = {@Filter(    type = FilterType.CUSTOM,    classes = {TypeExcludeFilter.class}), @Filter(    type = FilterType.CUSTOM,    classes = {AutoConfigurationExcludeFilter.class})})public @interface SpringBootApplication {    // ......}
@ComponentScan

这个注解在Spring中很重要 ,它对应XML配置中的元素。

作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中

@SpringBootConfiguration

作用:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类;

我们继续进去这个注解查看

// 点进去得到下面的 @Component@Configurationpublic @interface SpringBootConfiguration {}
@Componentpublic @interface Configuration {}

这里的 @Configuration,说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件;

里面的 @Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!

我们回到 SpringBootApplication 注解中继续看。

@EnableAutoConfiguration

@EnableAutoConfiguration :开启自动配置功能

以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我们配置 ;@EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效;

点进注解接续查看:

@AutoConfigurationPackage :自动配置包

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

@import :Spring底层注解@import , 给容器中导入一个组件

Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;

这个分析完了,退到上一步,继续看

@Import({AutoConfigurationImportSelector.class}) :给容器导入组件 ;

AutoConfigurationImportSelector :自动配置导入选择器,那么它会导入哪些组件的选择器呢?我们点击去这个类看源码:

1、这个类中有一个这样的方法

// 获得候选的配置protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {    //这里的getSpringFactoriesLoaderFactoryClass()方法    //返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");    return configurations;}

2、这个方法又调用了 SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {    String factoryClassName = factoryClass.getName();    //这里它又调用了 loadSpringFactories 方法    return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());}

3、我们继续点击查看 loadSpringFactories 方法

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {    //获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身    MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);    if (result != null) {        return result;    } else {        try {            //去获取一个资源 "META-INF/spring.factories"            Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");            LinkedMultiValueMap result = new LinkedMultiValueMap();
            //将读取到的资源遍历,封装成为一个Properties            while(urls.hasMoreElements()) {                URL url = (URL)urls.nextElement();                UrlResource resource = new UrlResource(url);                Properties properties = PropertiesLoaderUtils.loadProperties(resource);                Iterator var6 = properties.entrySet().iterator();
                while(var6.hasNext()) {                    Entry<?, ?> entry = (Entry)var6.next();                    String factoryClassName = ((String)entry.getKey()).trim();                    String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());                    int var10 = var9.length;
                    for(int var11 = 0; var11 < var10; ++var11) {                        String factoryName = var9[var11];                        result.add(factoryClassName, factoryName.trim());                    }                }            }
            cache.put(classLoader, result);            return result;        } catch (IOException var13) {            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);        }    }}

4、发现一个多次出现的文件:spring.factories,全局搜索它

spring.factories

我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!

image-20211027205846289

WebMvcAutoConfiguration

我们在上面的自动配置类随便找一个打开看看,比如 :WebMvcAutoConfiguration

image-20211027205937552

可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!

所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。

结论:

  1. SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
  2. 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
  3. 整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
  4. 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件 ;
  5. 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;

现在大家应该大概的了解了下,SpringBoot的运行原理,后面我们还会深化一次!

SpringApplication

不简单的方法

我最初以为就是运行了一个main方法,没想到却开启了一个服务;

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

SpringApplication.run分析

分析该方法主要分两部分,一部分是SpringApplication的实例化,二是run方法的执行;

SpringApplication

这个类主要做了以下四件事情:

1、推断应用的类型是普通的项目还是Web项目

2、查找并加载所有可用初始化器 , 设置到initializers属性中

3、找出所有的应用程序监听器,设置到listeners属性中

4、推断并设置main方法的定义类,找到运行的主类

查看构造器:

public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {    // ......    this.webApplicationType = WebApplicationType.deduceFromClasspath();    this.setInitializers(this.getSpringFactoriesInstances();    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));    this.mainApplicationClass = this.deduceMainApplicationClass();}

yaml配置注入

配置文件

SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的

  • application.properties

    • 语法结构 :key=value
  • application.yml

    • 语法结构 :key:空格 value

**配置文件的作用 :**修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了;

比如我们可以在配置文件中修改Tomcat 默认启动的端口号!测试一下!

server.port=8081

yaml概述

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

以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml

传统xml配置:

<server>    <port>8081<port></server>

yaml配置:

 server:  prot: 8080

yaml基础语法

说明:语法要求严格!

1、空格不能省略

2、以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。

3、属性和值的大小写都是十分敏感的。

字面量:普通的值 [ 数字,布尔值,字符串 ]

字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;

k: v

注意:

  • “ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;

    比如 :name: “kuang \n shen” 输出 :kuang 换行 shen

  • ‘’ 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出

    比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen

对象、Map(键值对)

#对象、Map格式k:     v1:    v2:

在下一行来写对象的属性和值得关系,注意缩进;比如:

student:    name: qinjiang    age: 3

行内写法

student: {name: qinjiang,age: 3}

数组( List、set )

用 - 值表示数组中的一个元素,比如:

pets: - cat - dog - pig

行内写法

pets: [cat,dog,pig]

修改SpringBoot的默认端口号

配置文件中添加,端口号的参数,就可以切换端口;

server:  port: 8082

yaml注入配置文件

1、在springboot项目中的resources目录下新建一个文件 application.yml

2、编写一个实体类 Dog;

package com.kuang.springboot.pojo;
@Component  //注册bean到容器中public class Dog {    private String name;    private Integer age;        //有参无参构造、get、set方法、toString()方法  }

3、思考,我们原来是如何给bean注入属性值的!@Value,给狗狗类测试一下:

@Component //注册beanpublic class Dog {    @Value("阿黄")    private String name;    @Value("18")    private Integer age;}

4、在SpringBoot的测试类下注入狗狗输出一下;

@SpringBootTestclass DemoApplicationTests {
    @Autowired //将狗狗自动注入进来    Dog dog;
    @Test    public void contextLoads() {        System.out.println(dog); //打印看下狗狗对象    }
}

结果成功输出,@Value注入成功,这是我们原来的办法对吧。

image-20211027211546383

5、我们在编写一个复杂一点的实体类:Person 类

@Component //注册bean到容器中public class Person {    private String name;    private Integer age;    private Boolean happy;    private Date birth;    private Map<String,Object> maps;    private List<Object> lists;    private Dog dog;        //有参无参构造、get、set方法、toString()方法  }

6、我们来使用yaml配置的方式进行注入,大家写的时候注意区别和优势,我们编写一个yaml配置!

person:  name: qinjiang  age: 3  happy: false  birth: 2000/01/01  maps: {k1: v1,k2: v2}  lists:   - code   - girl   - music  dog:    name: 旺财    age: 1

7、我们刚才已经把person这个对象的所有值都写好了,我们现在来注入到我们的类中!

/*@ConfigurationProperties作用:将配置文件中配置的每一个属性的值,映射到这个组件中;告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应*/@Component //注册bean@ConfigurationProperties(prefix = "person")public class Person {    private String name;    private Integer age;    private Boolean happy;    private Date birth;    private Map<String,Object> maps;    private List<Object> lists;    private Dog dog;}

8、IDEA 提示,springboot配置注解处理器没有找到,让我们看文档,我们可以查看文档,找到一个依赖!

image-20211027211730158

<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 --><dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-configuration-processor</artifactId>  <optional>true</optional></dependency>

9、确认以上配置都OK之后,我们去测试类中测试一下:

@SpringBootTestclass DemoApplicationTests {
    @Autowired    Person person; //将person自动注入进来
    @Test    public void contextLoads() {        System.out.println(person); //打印person信息    }
}

结果:所有值全部注入成功!

image-20211027211908136

yaml配置注入到实体类完全OK!

课堂测试:

1、将配置文件的key 值 和 属性的值设置为不一样,则结果输出为null,注入失败

2、在配置一个person2,然后将 @ConfigurationProperties(prefix = “person2”) 指向我们的person2;

加载指定的配置文件

**@PropertySource :**加载指定的配置文件;

@configurationProperties:默认从全局配置文件中获取值;

1、我们去在resources目录下新建一个person.properties文件

name=kuangshen

2、然后在我们的代码中指定加载person.properties文件

@PropertySource(value = "classpath:person.properties")@Component //注册beanpublic class Person {
    @Value("${name}")    private String name;
    ......  }

3、再次输出测试一下:指定配置文件绑定成功!

image-20211027212114170

配置文件占位符

配置文件还可以编写占位符生成随机数

person:    name: qinjiang${random.uuid} # 随机uuid    age: ${random.int}  # 随机int    happy: false    birth: 2000/01/01    maps: {k1: v1,k2: v2}    lists:      - code      - girl      - music    dog:      name: ${person.hello:other}_旺财      age: 1

回顾properties配置

我们上面采用的yaml方法都是最简单的方式,开发中最常用的;也是springboot所推荐的!那我们来唠唠其他的实现方式,道理都是相同的;写还是那样写;配置文件除了yml还有我们之前常用的properties , 我们没有讲,我们来唠唠!

【注意】properties配置文件在写中文的时候,会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8;

settings–>FileEncodings 中配置;

image-20211027212157715

测试步骤:

1、新建一个实体类User

@Component //注册beanpublic class User {    private String name;    private int age;    private String sex;}

2、编辑配置文件 user.properties

user1.name=kuangshenuser1.age=18user1.sex=

3、我们在User类上使用@Value来进行注入!

@Component //注册bean@PropertySource(value = "classpath:user.properties")public class User {    //直接使用@value    @Value("${user.name}") //从配置文件中取值    private String name;    @Value("#{9*2}")  // #{SPEL} Spring表达式    private int age;    @Value("男")  // 字面量    private String sex;}

4、Springboot测试

@SpringBootTestclass DemoApplicationTests {
    @Autowired    User user;
    @Test    public void contextLoads() {        System.out.println(user);    }
}

结果正常输出:

image-20211027212318053

JSR303数据校验

先看看如何使用

Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的name只能支持Email格式;

@Component //注册bean@ConfigurationProperties(prefix = "person")@Validated  //数据校验public class Person {
    @Email(message="邮箱格式错误") //name必须是邮箱格式    private String name;}

运行结果 :default message [不是一个合法的电子邮件地址];

image-20211027212507284

使用数据校验,可以保证数据的正确性;

常见参数

@NotNull(message="名字不能为空")private String userName;@Max(value=120,message="年龄最大不能查过120")private int age;@Email(message="邮箱格式错误")private String email;
空检查@Null       验证对象是否为null@NotNull    验证对象是否不为null, 无法查检长度为0的字符串@NotBlank   检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.@NotEmpty   检查约束元素是否为NULL或者是EMPTY.    Booelan检查@AssertTrue     验证 Boolean 对象是否为 true  @AssertFalse    验证 Boolean 对象是否为 false      长度检查@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  @Length(min=, max=) string is between min and max included.
日期检查@Past       验证 DateCalendar 对象是否在当前时间之前  @Future     验证 DateCalendar 对象是否在当前时间之后  @Pattern    验证 String 对象是否符合正则表达式的规则
.......等等除此以外,我们还可以自定义一些数据校验规则

多环境切换

profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;

多配置文件

我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本;

例如:

application-test.properties 代表测试环境配置

application-dev.properties 代表开发环境配置

但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件

我们需要通过一个配置来选择需要激活的环境:

#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;#我们启动SpringBoot,就可以看到已经切换到dev下的配置了;spring.profiles.active=dev

yaml的多文档块

和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件,更加方便了 !

server:  port: 8081#选择要激活那个环境块spring:  profiles:    active: prod
---server:  port: 8083spring:  profiles: dev #配置环境的名称

---
server:  port: 8084spring:  profiles: prod  #配置环境的名称

注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的!

配置文件加载位置

外部加载配置文件的方式十分多,我们选择最常用的即可,在开发的资源文件中进行配置!

官方外部配置文件说明参考文档

image-20211027212704967

springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:

优先级1:项目路径下的config文件夹配置文件优先级2:项目路径下配置文件优先级3:资源路径下的config文件夹配置文件优先级4:资源路径下配置文件

优先级由高到底,高优先级的配置会覆盖低优先级的配置;

SpringBoot会从这四个位置全部加载主配置文件;互补配置;

我们在最低级的配置文件中设置一个项目访问路径的配置来测试互补问题;

#配置项目的访问路径server.servlet.context-path=/kuang

自动配置原理

配置文件到底能写什么?怎么写?

SpringBoot官方文档中有大量的配置,我们无法全部记住

image-20211027212918813

分析自动配置原理

我们以**HttpEncodingAutoConfiguration(Http编码自动配置)**为例解释自动配置原理;

//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;@Configuration 
//启动指定类的ConfigurationProperties功能;  //进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;  //并把HttpProperties加入到ioc容器中@EnableConfigurationProperties({HttpProperties.class}) 
//Spring底层@Conditional注解  //根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;  //这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效@ConditionalOnWebApplication(    type = Type.SERVLET)
//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;@ConditionalOnClass({CharacterEncodingFilter.class})
//判断配置文件中是否存在某个配置:spring.http.encoding.enabled;  //如果不存在,判断也是成立的  //即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;@ConditionalOnProperty(    prefix = "spring.http.encoding",    value = {"enabled"},    matchIfMissing = true)
public class HttpEncodingAutoConfiguration {    //他已经和SpringBoot的配置文件映射了    private final Encoding properties;    //只有一个有参构造器的情况下,参数的值就会从容器中拿    public HttpEncodingAutoConfiguration(HttpProperties properties) {        this.properties = properties.getEncoding();    }        //给容器中添加一个组件,这个组件的某些值需要从properties中获取    @Bean    @ConditionalOnMissingBean //判断容器没有这个组件?    public CharacterEncodingFilter characterEncodingFilter() {        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();        filter.setEncoding(this.properties.getCharset().name());        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));        return filter;    }    //。。。。。。。}

一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!

  • 一但这个配置类生效;这个配置类就会给容器中添加各种组件;
  • 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
  • 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;
  • 配置文件能配置什么就可以参照某个功能对应的这个属性类
//从配置文件中获取指定的值和bean的属性进行绑定@ConfigurationProperties(prefix = "spring.http") public class HttpProperties {    // .....}

我们去配置文件里面试试前缀,看提示!

image-20211027213018545

这就是自动装配的原理!

精髓

1、SpringBoot启动会加载大量的自动配置类

2、我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;

3、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)

4、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;

**xxxxAutoConfigurartion:自动配置类;**给容器中添加组件

xxxxProperties:封装配置文件中相关属性;

自定义Starter

我们分析完毕了源码以及自动装配的过程,我们可以尝试自定义一个启动器来玩玩!

说明

启动器模块是一个 空 jar 文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库;

命名归约:

官方命名:

  • 前缀:spring-boot-starter-xxx
  • 比如:spring-boot-starter-web…

自定义命名:

  • xxx-spring-boot-starter
  • 比如:mybatis-spring-boot-starter

编写启动器

1、在IDEA中新建一个空项目 spring-boot-starter-diy

2、新建一个普通Maven模块:kuang-spring-boot-starter

image-20211027213155953

3、新建一个Springboot模块:kuang-spring-boot-starter-autoconfigure

image-20211027213213654

4、点击apply即可,基本结构

image-20211027213229870

5、在我们的 starter 中 导入 autoconfigure 的依赖!

<!-- 启动器 --><dependencies>    <!--  引入自动配置模块 -->    <dependency>        <groupId>com.kuang</groupId>        <artifactId>kuang-spring-boot-starter-autoconfigure</artifactId>        <version>0.0.1-SNAPSHOT</version>    </dependency></dependencies>

6、将 autoconfigure 项目下多余的文件都删掉,Pom中只留下一个 starter,这是所有的启动器基本配置!

image-20211027213321112

7、我们编写一个自己的服务

package com.kuang;
public class HelloService {
    HelloProperties helloProperties;
    public HelloProperties getHelloProperties() {        return helloProperties;    }
    public void setHelloProperties(HelloProperties helloProperties) {        this.helloProperties = helloProperties;    }
    public String sayHello(String name){        return helloProperties.getPrefix() + name + helloProperties.getSuffix();    }
}

8、编写HelloProperties 配置类

package com.kuang;
import org.springframework.boot.context.properties.ConfigurationProperties;
// 前缀 kuang.hello@ConfigurationProperties(prefix = "kuang.hello")public class HelloProperties {
    private String prefix;    private String suffix;
    public String getPrefix() {        return prefix;    }
    public void setPrefix(String prefix) {        this.prefix = prefix;    }
    public String getSuffix() {        return suffix;    }
    public void setSuffix(String suffix) {        this.suffix = suffix;    }}

9、编写我们的自动配置类并注入bean,测试!

package com.kuang;
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
@Configuration@ConditionalOnWebApplication //web应用生效@EnableConfigurationProperties(HelloProperties.class)public class HelloServiceAutoConfiguration {
    @Autowired    HelloProperties helloProperties;
    @Bean    public HelloService helloService(){        HelloService service = new HelloService();        service.setHelloProperties(helloProperties);        return service;    }
}

10、在resources编写一个自己的 META-INF\spring.factories

# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.kuang.HelloServiceAutoConfiguration

11、编写完成后,可以安装到maven仓库中!

image-20211027213457680

新建项目测试我们自己写的启动器

1、新建一个SpringBoot 项目

2、导入我们自己写的启动器

 <dependency>    <groupId>com.kuang</groupId>    <artifactId>kuang-spring-boot-starter</artifactId>    <version>1.0-SNAPSHOT</version></dependency>

3、编写一个 HelloController 进行测试我们自己的写的接口!

package com.kuang.controller;
@RestControllerpublic class HelloController {
    @Autowired    HelloService helloService;
    @RequestMapping("/hello")    public String hello(){        return helloService.sayHello("zxc");    }
}

4、编写配置文件 application.properties

kuang.hello.prefix="ppp"kuang.hello.suffix="sss"

5、启动项目进行测试,结果成功 !

image-20211027213623070

整合Druid

Druid简介

Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。

Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。

Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。

Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。

Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。

Github地址:https://github.com/alibaba/druid/

com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:

image-20211028151356681

image-20211028151423558

image-20211028151439059

配置数据源

1、添加上 Druid 数据源依赖。

<!-- https://mvnrepository.com/artifact/com.alibaba/druid --><dependency>    <groupId>com.alibaba</groupId>    <artifactId>druid</artifactId>    <version>1.1.21</version></dependency>

2、切换数据源;之前已经说过 Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以 通过 spring.datasource.type 指定数据源。

spring:  datasource:    username: root    password: 123456    url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8    driver-class-name: com.mysql.cj.jdbc.Driver    type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源

3、数据源切换之后,在测试类中注入 DataSource,然后获取到它,输出一看便知是否成功切换;

image-20211028151548213

4、切换成功!既然切换成功,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等设置项;可以查看源码

spring:  datasource:    username: root    password: 123456    #?serverTimezone=UTC解决时区的报错    url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8    driver-class-name: com.mysql.cj.jdbc.Driver    type: com.alibaba.druid.pool.DruidDataSource
    #Spring Boot 默认是不注入这些属性值的,需要自己绑定    #druid 数据源专有配置    initialSize: 5    minIdle: 5    maxActive: 20    maxWait: 60000    timeBetweenEvictionRunsMillis: 60000    minEvictableIdleTimeMillis: 300000    validationQuery: SELECT 1 FROM DUAL    testWhileIdle: true    testOnBorrow: false    testOnReturn: false    poolPreparedStatements: true
    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j    filters: stat,wall,log4j    maxPoolPreparedStatementPerConnectionSize: 20    useGlobalDataSourceStat: true    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

5、导入Log4j 的依赖

 <!-- https://mvnrepository.com/artifact/log4j/log4j --><dependency>    <groupId>log4j</groupId>    <artifactId>log4j</artifactId>    <version>1.2.17</version></dependency>

6、现在需要程序员自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;我们需要 自己添加 DruidDataSource 组件到容器中,并绑定属性;

package com.kuang.config;
import com.alibaba.druid.pool.DruidDataSource;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configurationpublic class DruidConfig {
    /*       将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建       绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效       @ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中       前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中     */    @ConfigurationProperties(prefix = "spring.datasource")    @Bean    public DataSource druidDataSource() {        return new DruidDataSource();    }
}

7、去测试类中测试一下;看是否成功!

@SpringBootTestclass SpringbootDataJdbcApplicationTests {
    //DI注入数据源    @Autowired    DataSource dataSource;
    @Test    public void contextLoads() throws SQLException {        //看一下默认数据源        System.out.println(dataSource.getClass());        //获得连接        Connection connection =   dataSource.getConnection();        System.out.println(connection);
        DruidDataSource druidDataSource = (DruidDataSource) dataSource;        System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());        System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
        //关闭连接        connection.close();    }}

输出结果 :可见配置参数已经生效!

image-20211028151750476

配置Druid数据源监控

Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。

所以第一步需要设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理;

//配置 Druid 监控管理后台的Servlet;//内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式@Beanpublic ServletRegistrationBean statViewServlet() {    ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
    // 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet     // 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到    Map<String, String> initParams = new HashMap<>();    initParams.put("loginUsername", "admin"); //后台管理界面的登录账号    initParams.put("loginPassword", "123456"); //后台管理界面的登录密码
    //后台允许谁可以访问    //initParams.put("allow", "localhost"):表示只有本机可以访问    //initParams.put("allow", ""):为空或者为null时,表示允许所有访问    initParams.put("allow", "");    //deny:Druid 后台拒绝谁访问    //initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问
    //设置初始化参数    bean.setInitParameters(initParams);    return bean;}

配置完毕后,我们可以选择访问 :http://localhost:8080/druid/login.html

image-20211028151831798

进入之后

配置 Druid web 监控 filter 过滤器

//配置 Druid 监控 之  web 监控的 filter//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计@Beanpublic FilterRegistrationBean webStatFilter() {    FilterRegistrationBean bean = new FilterRegistrationBean();    bean.setFilter(new WebStatFilter());
    //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计    Map<String, String> initParams = new HashMap<>();    initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");    bean.setInitParameters(initParams);
    //"/*" 表示过滤所有请求    bean.setUrlPatterns(Arrays.asList("/*"));    return bean;}

整合MyBatis

官方文档:http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/

Maven仓库地址:https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/2.1.1

image-20211028152017041

整合测试

1、导入 MyBatis 所需要的依赖

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

2、配置数据库连接信息(不变)

spring:  datasource:    username: root    password: 123456    #?serverTimezone=UTC解决时区的报错    url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8    driver-class-name: com.mysql.cj.jdbc.Driver    type: com.alibaba.druid.pool.DruidDataSource
    #Spring Boot 默认是不注入这些属性值的,需要自己绑定    #druid 数据源专有配置    initialSize: 5    minIdle: 5    maxActive: 20    maxWait: 60000    timeBetweenEvictionRunsMillis: 60000    minEvictableIdleTimeMillis: 300000    validationQuery: SELECT 1 FROM DUAL    testWhileIdle: true    testOnBorrow: false    testOnReturn: false    poolPreparedStatements: true
    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j    filters: stat,wall,log4j    maxPoolPreparedStatementPerConnectionSize: 20    useGlobalDataSourceStat: true    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

3、测试数据库是否连接成功!

4、创建实体类,导入 Lombok!

Department.java

package com.kuang.pojo;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
@Data@NoArgsConstructor@AllArgsConstructorpublic class Department {
    private Integer id;    private String departmentName;
}

5、创建mapper目录以及对应的 Mapper 接口

DepartmentMapper.java

//@Mapper : 表示本类是一个 MyBatis 的 Mapper@Mapper@Repositorypublic interface DepartmentMapper {
    // 获取所有部门信息    List<Department> getDepartments();
    // 通过id获得部门    Department getDepartment(Integer id);
}

6、对应的Mapper映射文件

DepartmentMapper.xml

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kuang.mapper.DepartmentMapper">
    <select id="getDepartments" resultType="Department">       select * from department;    </select>
    <select id="getDepartment" resultType="Department" parameterType="int">       select * from department where id = #{id};    </select>
</mapper>

7、maven配置资源过滤问题

<resources>    <resource>        <directory>src/main/java</directory>        <includes>            <include>**/*.xml</include>        </includes>        <filtering>true</filtering>    </resource></resources>

8、编写部门的 DepartmentController 进行测试!

@RestControllerpublic class DepartmentController {        @Autowired    DepartmentMapper departmentMapper;        // 查询全部部门    @GetMapping("/getDepartments")    public List<Department> getDepartments(){        return departmentMapper.getDepartments();    }
    // 查询全部部门    @GetMapping("/getDepartment/{id}")    public Department getDepartment(@PathVariable("id") Integer id){        return departmentMapper.getDepartment(id);    }    }

启动项目访问进行测试!

我们增加一个员工类再测试下,为之后做准备

1、新建一个pojo类 Employee ;

@Data@AllArgsConstructor@NoArgsConstructorpublic class Employee {
    private Integer id;    private String lastName;    private String email;    //1 male, 0 female    private Integer gender;    private Integer department;    private Date birth;
    private Department eDepartment; // 冗余设计
}

2、新建一个 EmployeeMapper 接口

//@Mapper : 表示本类是一个 MyBatis 的 Mapper@Mapper@Repositorypublic interface EmployeeMapper {
    // 获取所有员工信息    List<Employee> getEmployees();
    // 新增一个员工    int save(Employee employee);
    // 通过id获得员工信息    Employee get(Integer id);
    // 通过id删除员工    int delete(Integer id);
}

3、编写 EmployeeMapper.xml 配置文件

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kuang.mapper.EmployeeMapper">
    <resultMap id="EmployeeMap" type="Employee">        <id property="id" column="eid"/>        <result property="lastName" column="last_name"/>        <result property="email" column="email"/>        <result property="gender" column="gender"/>        <result property="birth" column="birth"/>        <association property="eDepartment"  javaType="Department">            <id property="id" column="did"/>            <result property="departmentName" column="dname"/>        </association>    </resultMap>
    <select id="getEmployees" resultMap="EmployeeMap">        select e.id as eid,last_name,email,gender,birth,d.id as did,d.department_name as dname        from department d,employee e        where d.id = e.department    </select>
    <insert id="save" parameterType="Employee">        insert into employee (last_name,email,gender,department,birth)        values (#{lastName},#{email},#{gender},#{department},#{birth});    </insert>
    <select id="get" resultType="Employee">        select * from employee where id = #{id}    </select>
    <delete id="delete" parameterType="int">        delete from employee where id = #{id}    </delete>
</mapper>

4、编写EmployeeController类进行测试

@RestControllerpublic class EmployeeController {
    @Autowired    EmployeeMapper employeeMapper;
    // 获取所有员工信息    @GetMapping("/getEmployees")    public List<Employee> getEmployees(){        return employeeMapper.getEmployees();    }
    @GetMapping("/save")    public int save(){        Employee employee = new Employee();        employee.setLastName("kuangshen");        employee.setEmail("qinjiang@qq.com");        employee.setGender(1);        employee.setDepartment(101);        employee.setBirth(new Date());        return employeeMapper.save(employee);    }
    // 通过id获得员工信息    @GetMapping("/get/{id}")    public Employee get(@PathVariable("id") Integer id){        return employeeMapper.get(id);    }
    // 通过id删除员工    @GetMapping("/delete/{id}")    public int delete(@PathVariable("id") Integer id){        return employeeMapper.delete(id);    }
}

测试结果完成!
select * from employee where id = #{id}
delete from employee where id = #{id}


4、编写EmployeeController类进行测试

```java
@RestControllerpublic class EmployeeController {
    @Autowired    EmployeeMapper employeeMapper;
    // 获取所有员工信息    @GetMapping("/getEmployees")    public List<Employee> getEmployees(){        return employeeMapper.getEmployees();    }
    @GetMapping("/save")    public int save(){        Employee employee = new Employee();        employee.setLastName("kuangshen");        employee.setEmail("qinjiang@qq.com");        employee.setGender(1);        employee.setDepartment(101);        employee.setBirth(new Date());        return employeeMapper.save(employee);    }
    // 通过id获得员工信息    @GetMapping("/get/{id}")    public Employee get(@PathVariable("id") Integer id){        return employeeMapper.get(id);    }
    // 通过id删除员工    @GetMapping("/delete/{id}")    public int delete(@PathVariable("id") Integer id){        return employeeMapper.delete(id);    }
}

测试结果完成!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懒惰的coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值