51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

SpringBoot

SpringBoot笔记持续更新中!

SpringBoot {#SpringBoot}

发布者订阅者 {#发布者订阅者}

样例场景: 我们需要在用户注册后 给订阅的用户推送消息(例如前100名注册的 系统自动发放徽章等)

|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 | @Override @Transactional // 保证事件和用户注册的一致性 public Long register (User insert) { boolean save = userDao.save(insert); // 用户注册的事件 -> 谁订阅就给谁发送通知 (1.Mq 2.ApplicationEventPublisher) applicationEventPublisher.publishEvent( new UserRegisterEvent ( this , insert)); // this事件订阅者,发送端 return insert.getId(); } |

这个时候我们可以采用Spring的推送事件 ApplicationEventPublisher , 其中除此之外我们还可以采用MQ进行同等操作

  • 消息发布者 Event事件

我们定义一个事件, 继承 ApplicationEvent 然后定义构造函数, 我们此时场景需要用户的信息, 故我们在构造函数传入User即可

|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * @className : UserRegisterEvent * @author : Calyee * @description : 用户注册事件 * @version : 1.0 */ @Getter public class UserRegisterEvent extends ApplicationEvent { private User user; public UserRegisterEvent (Object source, User user) { super (source); this .user = user; } } |

  • 消息接收者

此项我们只需要在方法上使用注解 @EventListener 或 带事务的 TransactionalEventListener , 需不需要带事务则看具体应用场景, 此时的事务类型为枚举项, 事务为是否加入父事务执行, @Async异步执行

我们需要在注解处标明刚刚定义的 事件推送者 , 然后在方法处传入该事件, 对于 Object source 则传入this事件即可, 然后对其进行处理

|------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /** * @className : UserRegisterListener * @author : Calyee * @description : 用户注册监听 * @version : 1.0 */ @Component public class UserRegisterListener { @Autowired private IUserBackpackService userBackpackService; @Autowired private UserDao userDao; /** * 发送改名卡 * * @param userRegisterEvent 注册事件 */ @Async @TransactionalEventListener(classes = UserRegisterEvent.class, phase = TransactionPhase.AFTER_COMMIT) // 事务提交后 public void sendCard (UserRegisterEvent userRegisterEvent) { User user = userRegisterEvent.getUser(); userBackpackService.acquireItem(user.getId(), ItemEnum.MODIFY_NAME_CARD.getId(), IdempotentEnum.UID, user.getId().toString()); } /** * 发送 前10名/100名 注册的徽章 */ @Async @TransactionalEventListener(classes = UserRegisterEvent.class, phase = TransactionPhase.AFTER_COMMIT) public void sendBadge (UserRegisterEvent userRegisterEvent) { User user = userRegisterEvent.getUser(); int registeredUserCount = userDao.count(); if (registeredUserCount < 10 ){ userBackpackService.acquireItem(user.getId(), ItemEnum.REG_TOP10_BADGE.getId(), IdempotentEnum.UID, user.getId().toString()); } else if (registeredUserCount < 100 ){ userBackpackService.acquireItem(user.getId(), ItemEnum.REG_TOP100_BADGE.getId(), IdempotentEnum.UID, user.getId().toString()); } } } |

对于文件结构定义: 我们可以采用定义一个event包+event包下的listener包,

配置文件 {#配置文件}

读取YML配置文件信息 {#读取YML配置文件信息}

导入依赖:

|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 | <!-- 配置文件处理器: 是SpringBoot处理yml,yaml,properties配置文件 --> <!-- 1. 以流的方式读取 application.yml 2. 以反射的方式 将application.yml的值 存储到对象中(setXxx()方法) --> < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-configuration-processor </ artifactId > <!-- 可选,它将选择权交给上级应用 --> < optional > true </ optional > </ dependency > |

yml样例:

|---------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 | product: pname: apple price: 20.5 is-used: true man-date: 2021 /09/09 attributes: { 'color': 'red' , 'type' :'good' } address: province: 湖南省 city: 长沙 types: - 水果 - 零食 |

对应的实体类:

|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Data // Get Set // 当processor读取到application.yml配置文件以product,存储到当前对象中 @ConfigurationProperties(prefix = "product") @Component // IOC public class Product implements Serializable { private String pname; private Double price; private Boolean isUsed; // is-used private Date manDate; // man-date private Map<String, Object> attributes; private Address address; private List<String> types; } |

|-------------------|--------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | @Data public class Address implements Serializable { private String province; private String city; } |

测试类:

|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 | @RunWith(SpringRunner.class) @SpringBootTest() public class ProductTest { @Autowired private Product product; @Test public void testProduct () { Assert.assertNotNull(product); Assert.assertEquals( "apple" , product.getPname()); } } |

其中我们需要导入的junit的包是这个 import org.junit.Test;

不是 import org.junit.jupiter.api.Test;

不然会报错 Runner org.junit.internal.runners.ErrorReportingRunner does not support filtering and will ther

读取其他配置文件内容 {#读取其他配置文件内容}

1.读取例如properties配置 @PropertySource()

在配置文件中加入注解

|-------------|-----------------------------------------------------------------------------------------------------------------| | 1 2 | // 1.读取例如properties配置 并以key value存储 @PropertySource(value = {"classpath:db.properties"}) // value可以加多个 |

db.properties配置文件

|-------------|-------------------------------------------| | 1 2 | username = root password = 123456 |

如何在Spring中使用呢? 使用 @Value(${username}) 注解

|-------------|--------------------------------------------------------------| | 1 2 | @Value(${username}) private String username; // root |

2.读取例如xml配置 @ImportResource

在配置文件中加入注解

|-------------|-------------------------------------------------------------------------------------| | 1 2 | // 2.读取spring.xml配置文件 @ImportResource(locations = {"classpath:spring.xml"}) |

|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | <?xml version= "1.0" encoding= "UTF-8" ?> < beans xmlns = "http://www.springframework.org/schema/beans" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation = "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > < bean id = "us" class = "com.hang.springboot.entity.User" > < property name = "name" value = "zhang" > </ property > </ bean > </ beans > |

读取:

|---------------|--------------------------------------------------------------------------------------------------------------------------| | 1 2 3 | @Autowired @Qualifier(value = "us") // 根据bean的唯一id获取,因为在User类里面加了@Component注解,已经注入过了(id=user) private User user; |

在运行时修改配置文件信息 {#在运行时修改配置文件信息}

  1. 在命令行参数中配置

    |-----------|----------------------------------------------| | 1 | java -jar xxx.jar --server.port=9090 |

    此项使用必须建立在启动类的run方法中,传入了main函数中的args参数

    |-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 | @SpringBootApplication public class Application { // 命令行参数配置(传给SpringBoot的) --server.port=xxx public static void main (String[] args) { // 1. 传入参数的启动类,可以读取命令行配置 SpringApplication.run(Application.class, args); } } |

    2.传入虚拟机系统属性

|-----------|-----------------------------------------------| | 1 | java -Dserver.port= 9090 -jar xxx.jar |

-D 设置虚拟参数 其中server.port是自己需要修改的配置

|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | @SpringBootApplication public class Application { public static void main (String[] args) { // 没有传入参数 SpringApplication.run(Application.class); } } |

在最后,我们还能添加属性在配置文件里面,例如我需要加属性: name=qiuqiu

eg: java -jar xxx.jar --server.port=9090 --name=qiuqiu

java -Dserver.port=9090 -Dname=qiuqiu -jar xxx.jar

SpringBoot读取配置信息、环境变量 {#SpringBoot读取配置信息、环境变量}

  1. Environment基本的环境变量

|---------------|--------------------------------------------------------------------------------| | 1 2 3 | // 读取环境变量信息,spring启动时,自动加载 @Autowired private Environment environment; |

​ 2.ConfigurableEnvironment环境变量

|-------------|----------------------------------------------------------------------------| | 1 2 | @Resource private ConfigurableEnvironment configurableEnvironment; |

ConfigurableEnvironment是Environment的子接口,功能更多

注入之后,直接调用即可

多环境配置 多数据源配置 {#多环境配置-多数据源配置}

多环境:

开发时分为多个环境: 开发环境 (dev) , 测试环境 (test) , 生产环境 (prod)

系统默认 application.yml

1.通过在 application.xml 默认配置文件中配置

|-----------------|--------------------------------------------------------| | 1 2 3 4 | # 激活指定使用哪个环境配置文件 spring: profiles: active: dev |

2.通过注解配置 @Profile("dev") ,加入在带 @Configuration 注解的配置类中

多环境配置,如果设置的环境跟默认环境有相同的配置 , 则当前设置的环境会 覆盖默认环境的相同配置属性

多数据源:

@Configuration 修饰的配置类中配置数据源

|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @Value("${driverClassName}") private String driverName; @Value("${username}") private String username; @Value("${password}") private String password; @Value("${url}") private String url; @Bean // 表示指定了 spring.profiles.active: test的时候用这个数据源 @Profile("test") // 测试数据源 public DataSource localDataSource () { DriverManagerDataSource source = new DriverManagerDataSource (); source.setDriverClassName(driverName); source.setUsername(username); source.setPassword(password); source.setUrl(url); return source; } @Bean // 表示指定了 spring.profiles.active: prod的时候用这个数据源 @Profile("prod") public DataSource localDataSource () { DruidDataSource source = new DruidDataSource (); source.setDriverClassName(driverName); source.setUsername(username); source.setPassword(password); source.setUrl(url); return source; } |

此时则只会有一个数据源的bean

日志配置 {#日志配置}

基础配置

|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # 日志配置 logging: # 日志级别 level: # 根日志配置 日志级别 root: debug # 配置该路径下的日志级别 org.springframework: error org.apache: error # 日志输出 file: # name和path只能设置一个,默认输出在当前模块的根目录 name: spring.log path: logs/ |

滚动配置

暂无

Pom文件相关 {#Pom文件相关}

spring-boot-start-test

对于 spring-boot-starter-test 的依赖, 默认是引入junit4和junit5

|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-test </ artifactId > < scope > test </ scope > <!--排除junit4--> < exclusions > < exclusion > < groupId > org.junit.vintage </ groupId > < artifactId > junit-vintage-engine </ artifactId > </ exclusion > </ exclusions > </ dependency > |

docker打包

|------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | < plugin > < groupId > com.spotify </ groupId > < artifactId > dockerfile-maven-plugin </ artifactId > < version > 1.4.13 </ version > < executions > < execution > < id > default </ id > < goals > < goal > build </ goal > < goal > push </ goal > </ goals > </ execution > </ executions > < configuration > < repository > javastack/${project.name} </ repository > < tag > ${project.version} </ tag > < buildArgs > < JAR_FILE > ${project.build.finalName}.jar </ JAR_FILE > </ buildArgs > < dockerfile > Dockerfile </ dockerfile > </ configuration > </ plugin > |

测试类 {#测试类}

一、普通测试类 和 套件 {#一、普通测试类-和-套件}

例如此时我有一个service类需要测试:

|---------------------|-------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | @Component public class Calculator { public int add ( int a, int b) { return a + b; } } |

  1. 首先需要加注解@Component 交给Spring管理
  2. 然后在当前的包中右键,点击Go To -> Test -> Create New Test 一般点击OK即可
  3. 然后idea在test包下就会自动创建一个跟当前类同样结构的测试类
  4. 在测试类中需要引入注解

|---------------|-----------------------------------------------------------------------------------------------------------------| | 1 2 3 | // 获取springboot的启动类 @SpringBootTest(classes = SpringBootApplication.class) @RunWith(SpringRunner.class) |

测试(单方法,单类):

|------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @SpringBootTest() // 获取springboot的启动类 @RunWith(SpringRunner.class) public class CalculatorTest { // spring支持IOC: 控制反转 再进行DI注入 @Autowired private Calculator calculator; @Test void add () { int add = calculator.add( 1 , 2 ); // 传统sout输出 不推荐 // 推荐 断言 // 优点: 与预期结果一致,绿 不一致:红 便于捕捉没通过预期的方法 // System.out.println("add = " + add); Assert.assertEquals( 3 , add); // 绿 } } |

现在需求是: 假如有多个类,类里面有很多方法需要测试,那该怎么实现一起自动化测试?

: 使用SpringBoot的套件测试 , 顾名思义:把测试方法类全部放一起测试,就跟一个整体套件一样

启动当前类就可以了

|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 | /** * 当前方法是测试套件 * 就是把CalculatorTest 和 OtherTest * 两个类里面的方法 全部一起测试,假如用了断言 测试完成后 * 通过与未通过一目了然 */ @RunWith(Suite.class) @Suite .SuiteClasses({CalculatorTest.class,OtherTest.class}) // 测试指定类 public class SuiteTest { } |

二、其他的注解: {#二、其他的注解}

1) @Disabled {#1-Disabled}

:禁用测试方法, 用于当前方法现在不需要进行测试

|-------------------|-----------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | @Test @Disabled("This test is not ready yet.") public void disabledTest () { // 没有写完的逻辑 或是 已经不需要测试的逻辑 } |

2) @TestMethodOrder@Order {#2-TestMethodOrder-和-Order}

:配置测试方法的执行顺序

|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class OrderTest { @Order(1) @Test public void testMethod1 () { // 在这里写测试逻辑 } @Order(2) @Test public void testMethod2 () { // 在这里写测试逻辑 } } |

3) @BeforeAll@AfterAll {#3-BeforeAll-和-AfterAll}

:在测试类的所有测试方法前和后执行一次,可用于全局初始化和销毁资源

|---------------------------|--------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | @BeforeAll public static void initOperate () { // 初始化操作 } @AfterAll public static void destoryOperate () { // 资源销毁操作 } |

4) @BeforeEach@AfterEach {#4-BeforeEach-和-AfterEach}

:在测试类的每个测试方法前和后都会执行一次

|---------------------------|---------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | @BeforeEach public void beforeEachTest () { // 执行前的准备工作 } @AfterEach public void afterEachTest () { // 执行后的清理工作 } |

5) @RepeatedTest {#5-RepeatedTest}

:指定测试方法重复执行

|-----------------|--------------------------------------------------------------------------| | 1 2 3 4 | @RepeatedTest(6) public void repeatedTest () { // 该测试方法会重复执行6次 } |

6) @ParameterizedTest@ValueSource {#6-ParameterizedTest-和-ValueSource}

:用于参数化测试

|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | @ParameterizedTest @ValueSource(strings = { "name1", "name2", "name3" }) public void testParameter (String name) { // 使用参数化的名称进行测试 } |

7) @AutoConfigureMockMvc {#7-AutoConfigureMockMvc}

:启用MockMvc的自动配置,可用于测试接口

|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @SpringBootTest @AutoConfigureMockMvc public class MyControllerIntegrationTest { @Autowired private MockMvc mockMvc; @Test public void testController () throws Exception { mockMvc.perform(get( "/api/someendpoint" )) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } } |

切换服务器 {#切换服务器}

在pom.xml里面修改,例如需要把tomcat修改为undertow

|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-web </ artifactId > <!-- 1. 从starter-web中排除tomcat(原来的服务器)依赖 --> < exclusions > < exclusion > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-tomcat </ artifactId > </ exclusion > </ exclusions > </ dependency > <!-- 2. 加入新的服务器 --> < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-undertow </ artifactId > </ dependency > |

部分执行控制台输出如下

定时器 {#定时器}

  1. 在启动类中标明注解 @EnableScheduling
  2. 编写一个Job任务类,在类上面标明@Component注解给spring托管
  3. 在@ Scheduled 中写cron表达式设置定时

以Redis缓存文章浏览量同步数据库为例: (当前需要在 启动时 ,需要有缓存,需要看下面的 启动任务 )

|---------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Component public class UpdateViewCountJob { @Autowired private RedisCache redisCache; @Autowired private ArticleService articleService; @Scheduled(cron = "0 */10 * * * ?") // 开启定时任务 public void updateViewCountJob () { // 获取redis中存取的浏览量 Map<String, Integer> viewCountMap = redisCache.getCacheMap(SystemConstants.ARTICLE_VIEW_COUNT); // 双列集合不能用流,可以先转entrySet(键值对)或者keySet List<Article> articleList = viewCountMap.entrySet() .stream() // key(long) value(long) .map(entry -> new Article (Long.valueOf(entry.getKey()), entry.getValue().longValue())) .collect(Collectors.toList()); // 更新到数据库 articleService.updateBatchById(articleList); } } |

启动前预加载服务 {#启动前预加载服务}

|---------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /** * @ClassName ViewCountRunner * @Description 在启动之前对浏览量进行查询 然后缓存到redis里面 */ @Component public class ViewCountRunner implements CommandLineRunner { @Autowired private ArticleMapper articleMapper; @Autowired private RedisCache redisCache; @Override public void run (String... args) throws Exception { // 查询博客信息 id + viewCount key value List<Article> articleList = articleMapper.selectList( null ); Map<String, Integer> map = new HashMap <>(); // articleList.forEach(item->{ // map.put(String.valueOf(item.getId()),item.getViewCount()); // }); map = articleList.stream() .collect(Collectors.toMap(article -> article.getId().toString(), // key article -> { // value return article.getViewCount().intValue(); // 1L 在redis中要用Integer })); // 存储到redis redisCache.setCacheMap(SystemConstants.ARTICLE_VIEW_COUNT,map); } } |

自定义的Runner实现了 CommandLineRunner 类,重写run方法,该方法在SpringBoot启动时也会同步执行该任务,这个是SpringBoot提供的简单类

: 如果在上下文中,存在多个该接口实现类,可以通过@order注解,指定加载顺序

@Order写在实现类上 例如@Order(value = 1) 越前越快执行


SpringBoot的自动配置 {#SpringBoot的自动配置}

1、了解注解及一些类 {#1、了解注解及一些类}

@Configuration结合@Bean {#Configuration结合-Bean}

eg. @Configuration ,里面的 proxyBeanMethods 属性 true : false

proxyBeanMethods配置类是用来指定@Bean注解标注的方法是否使用代理, 默认是true使用动态代理 ,直接从IOC容器之中取得对象;
如果设置为false,也就是不使用注解,每次调用@Bean标注的方法获取到的对象和IOC容器中的都不一样,是一个新的对象,所以我们可以将此属性设置为false来 提高性能 ,但是不能扩展,例如AOP切面

优缺点 : 设置true动态代理的可以后期被切面增强,但是启动速度性能慢些,设置false则使用new对象,提高性能但是不能被增强

@EnableAutoConfiguration(xx.class) {#EnableAutoConfiguration-xx-class}

帮助SpringBoot应用将所有 符合条件@Configuration 配置都加载到当前SpringBoot,并创建对应 配置类的Bean ,并把该Bean实体交给IoC容器进行管理

该注解的功能更强大, 托管配置类并把bean托管 , 可以读取属性文件注入

@ConfigurationProperties(prefix = "") {#ConfigurationProperties-prefix}

一次性读取符合前缀的属性配置文件,只能注入属性

@Import {#Import}
  1. 导入外部的class文件

    |---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 | @Import({Apple.class,Product.class}) // 导入两个class // 此时的实例化注入对象,是这种形式的 例如 Apple类原来在外部文件中 org.friut中 // 那么由该方法实例化的对象的id是这样的 "org.friut.Apple" |

    2.导入一个包的多个类

假如需要导入的类多了,那么则需要使用该方式,我们需要创建一个类实现 ImportSelector 接口

|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 | public class ImportList implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { String url = "META-INF/" // TODO: File file = new File(url); // 将所有需要的全类名 封装到一个固定的文件内 在依次读取 // file.listFile(); return new String []{ // 把扫描到的文件名添加到此处 }; } } |

然后使用 @Import({ImportList.class,Other.class}) 导入刚刚的类

ApplicationContextAware接口在类中注入Spring容器对象 {#ApplicationContextAware接口在类中注入Spring容器对象}

|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @RestController public class SpringBeansGetInfoClass implements ApplicationContextAware { private ApplicationContext context; @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { this .context = applicationContext; } // 读取beanName @GetMapping("/getSpringBeanInfo") public void getBeanNameInfo () { for (String name : this .context.getBeanDefinitionNames()) { System.out.println(name); } } } |

实现 ApplicationContextAware 接口的类必须被Spring所管理

作用: 换句话说,就是这个类可以直接获取Spring配置文件中,所有有引用到的Bean对象

@Conditional {#Conditional}

条件判断注解

例如,我需要动态加载Tomcat或Jetty服务器,即 如果容器中有服务器某Bean则加载,当然还要判断是否有多个服务器判断(当前实例未做判断,具体看 自动配置WebServer服务

|------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /** * 这个是一个WebServer自动配置类 * 该案例为动态切换Tomcat和Jetty服务器的实例配置类 */ @Configuration // 交给Spring管理 public class WebServerAutoConfiguration { // Spring配置文件 // WebServer自动配置类 @Bean // 条件加载,根据TomcatCondition中约束的条件,动态注入Bean @Conditional(TomcatCondition.class) public TomcatWebServer tomcatWebServer () { return new TomcatWebServer (); } @Bean @Conditional(JettyCondition.class) // Jetty服务器条件加载判断 public JettyWebServer jettyWebServer () { return new JettyWebServer (); } } |

条件判断类 必须实现Condition接口,重写matches方法

|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class TomcatCondition implements Condition { @Override public boolean matches (ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { // 判断项目中是否有符合条件的依赖 try { // 利用类加载器,加载需要判断的类 conditionContext.getClassLoader().loadClass( "org.apache.catalina.startup.Tomcat" ); return true ; // 能走到这个地方 证明有 } catch (ClassNotFoundException e) { return false ; // 没有该类 } // // 如果返回true,则符合条件 // return false; } } |

2、自动配置start {#2、自动配置start}

样例一: DataSource {#样例一-DataSource}

以读取 spring.datasource 为例:

例如我的 application.yml 如下:

|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 serialization: write-dates-as-timestamps: false |

查看源码

我们简单的查看到,该类里面包含一些通用的数据源属性,并且通过注解 @ConfigurationProperties(prefix = "spring.datasource") 读取 application.yml 配置文件设置的值, 但是注意到,该属性在此处并未在spring容器中注入 ,那么spring是如何进行初始化的呢?

当前是自动装配,那当然是自动装配了,那么是在哪儿自动装配?

: 在此包中 , 很容易找到一个名为 DatasourceAutoConfiguration 的类 ,通过类名注意到这很显然是一个 自动装配数据源的类

找到自动配置类, 即 带 @EnableConfigurationProperties 注解的类, 通过此注解对刚刚已经注入属性的类进行IOC注册, 那么这个就被自动装配了


样例二: WebServer {#样例二-WebServer}

在此处,再举一个例子( 样例二 )如下,例如我需要对服务器的配置 , 比如 server.port=9090 服务器的端口等

步骤一: 对服务器的基础配置进行读取

文件在 org.springframework.bootspring-boot-autoconfigure 里面的 org.springframework.boot.autoconfigure.web.ServerProperties

ServerProperties类 里面同样可以看到注解 @ConfigurationProperties

|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @ConfigurationProperties( prefix = "server", // 读取配置文件属性 ignoreUnknownFields = true ) public class ServerProperties { private Integer port; private InetAddress address; @NestedConfigurationProperty private final ErrorProperties error = new ErrorProperties (); private ForwardHeadersStrategy forwardHeadersStrategy; private String serverHeader; private DataSize maxHttpHeaderSize = DataSize.ofKilobytes( 8L ); private Shutdown shutdown; @NestedConfigurationProperty private Ssl ssl; @NestedConfigurationProperty private final Compression compression; @NestedConfigurationProperty private final Http2 http2; private final Servlet servlet; private final Reactive reactive; private final Tomcat tomcat; private final Jetty jetty; private final Netty netty; private final Undertow undertow; // 方法省略 } |

步骤二: 对服务器配置完成的类在IOC容器中进行注册bean

在当前的package下的

org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration

我们可以看到熟悉的结构

|------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @AutoConfiguration @ConditionalOnWebApplication // 注册ServerProperties配置类 @EnableConfigurationProperties({ServerProperties.class}) // 主要注解 public class EmbeddedWebServerFactoryCustomizerAutoConfiguration { public EmbeddedWebServerFactoryCustomizerAutoConfiguration () { } @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({HttpServer.class}) public static class NettyWebServerFactoryCustomizerConfiguration { public NettyWebServerFactoryCustomizerConfiguration () { } @Bean public NettyWebServerFactoryCustomizer nettyWebServerFactoryCustomizer (Environment environment, ServerProperties serverProperties) { return new NettyWebServerFactoryCustomizer (environment, serverProperties); } } // 以下的省略 } |

两个样例总结 {#两个样例总结}

从以上步骤可以看出spring-boot-starter的配置流程较简单,简化一下流程即为:

1.添加 @ConfigurationProperties 读取application.yml配置文件
2.配置 @EnableAutoConfiguration 注解类,自动扫描package生成所需bean
3.添加 spring.factories 配置让spring-boot-autoconfigure对当前项目进行AutoConfiguration

SpringBoot自动配置 {#SpringBoot自动配置}

样例总结 说到读取 spring.factories ,那么springboot底层是如何进行读取,并且自动配置呢?

首先我们从启动类入口入手, @SpringBootApplication , 追踪发现注解 @EnableAutoConfiguration

|------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @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 { @AliasFor( annotation = EnableAutoConfiguration.class ) Class<?>[] exclude() default {}; // 略 } |

打开 @EnableAutoConfiguration

|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 | @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration" ; Class<?>[] exclude() default {}; String[] excludeName() default {}; } |

它导入了一个名为 AutoConfigurationImportSelector 的自动装配类选择器 , 并且追踪, 它实现了DeferredImportSelector 该类为 ImportSelector的子类

|------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | public class AutoConfigurationImportSelector implements DeferredImportSelector , BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry (); private static final String[] NO_IMPORTS = new String [ 0 ]; private static final Log logger = LogFactory.getLog(AutoConfigurationImportSelector.class); private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude" ; private ConfigurableListableBeanFactory beanFactory; private Environment environment; private ClassLoader beanClassLoader; private ResourceLoader resourceLoader; private ConfigurationClassFilter configurationClassFilter; // 此处方法构造器省略 } |

ImportSelector类,实现该类重写方法获取需要扫描的包 , 并且返回全类名

|-------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 | public interface ImportSelector { // 重写该方法 String[] selectImports(AnnotationMetadata importingClassMetadata); @Nullable default Predicate<String> getExclusionFilter () { return null ; } } |

在AutoConfigurationImportSelector中实现了selectImports方法

|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 | // 返回一个字符串数组,保存扫描类的路径 public String[] selectImports(AnnotationMetadata annotationMetadata) { if (! this .isEnabled(annotationMetadata)) { return NO_IMPORTS; } else { AutoConfigurationEntry autoConfigurationEntry = // 通过该方法(getAutoConfigurationEntry)获取自动配置的项 this .getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } } |

getAutoConfigurationEntry方法

|---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | protected AutoConfigurationEntry getAutoConfigurationEntry (AnnotationMetadata annotationMetadata) { if (! this .isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } else { AnnotationAttributes attributes = this .getAttributes(annotationMetadata); List<String> configurations = // 获取候选的配置属性 (点进方法) this .getCandidateConfigurations(annotationMetadata, attributes); configurations = this .removeDuplicates(configurations); Set<String> exclusions = this .getExclusions(annotationMetadata, attributes); this .checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = this .getConfigurationClassFilter().filter(configurations); this .fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry (configurations, exclusions); } } |

getCandidateConfigurations方法 获取候选的配置属性

|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | protected List<String> getCandidateConfigurations (AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = new // 加载工厂 bean -> loadFactoryNames() ArrayList(SpringFactoriesLoader.loadFactoryNames( this .getSpringFactoriesLoaderFactoryClass(), this .getBeanClassLoader())); // 读取第二个配置文件 -> load() ImportCandidates.load(AutoConfiguration.class, this .getBeanClassLoader()).forEach(configurations::add); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct." ); return configurations; } |

很容易发现 , springboot使用spring的类加载器,加载类 , 在断言处,发现不能为空的断言,分析: 没有自动配置类 找到在路径 META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中,也就是说 读取配置类是读取固定路径(这种机制叫做 SPI 机制),是一种 服务提供发现机制 , 然后读取了两个文件 其中这两个文件,包含了大量SpringBoot启动时需要读取的配置类

打开加载工厂bean方法 loadFactoryNames (该方法读取的是第一个配置文件 spring.factories )

通过类加载器,通过文件配置

读取第二个配置文件 通过 load()方法,点开发现

通过流的读取,把配置文件按行读取,形成一个数组

在第二个配置文件中读取到例如 org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration 的Web服务器类,spring则会去自动读取扫描配置Web服务的配置类并且注入托管,就是前面说到的[Web配置流程](#样例二: WebServer) ,经历 配置文件读取,配置类读取,配置类根据条件注入,bean托管等流程

SpringBoot Starter分析及自定义 {#SpringBoot-Starter分析及自定义}

SpringBoot Starter: 启动类 分析starter的启动流程:

依赖分析: 从SpringBoot starter开始 {#依赖分析-从SpringBoot-starter开始}

|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 | < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-web </ artifactId > </ dependency > |

追踪发现 包含依赖:

|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter </ artifactId > < version > 2.7.3 </ version > < scope > compile </ scope > </ dependency > |

再追踪: 然后发现了有一个依赖是 spring-boot-autoconfigure

|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | < dependency > < groupId > org.springframework.boot </ groupId > <!--自动配置依赖--> < artifactId > spring-boot-autoconfigure </ artifactId > < version > 2.7.3 </ version > < scope > compile </ scope > </ dependency > |

打开依赖 org.springframework.boot:spring-boot-starter

idea 中的 maven依赖结构如下:

spring-boot-starter

​ META-INF

​ LICENSE.txt

​ MANIFEST.MF

​ NOTICE.txt

所以:其实这个starter最重要的就是 pom.xml 文件,它引入了 autoconfiguration 的jar包

需要打开本地加载的依赖文件才能看到具体的结构

依赖结构:

标准的SpringBoot是将所有的自动配置类都写在了 xxx.factories ,但我们自定义的starter是将配置类都写在了 spring.factories 文件中

MyBatis Starter {#MyBatis-Starter}

结构如下

其中 mybatisspring.factories 的内容如下:

|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\ org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration |

|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ##### 两个案例总结: 包含两个模块: 一个是```starter```,一个是```autoconfiguration``` 其中在starter里面主要就是**引入autoconfiguration** 以及帮我们版本仲裁其它jar包 在```autoconfiguration```中是**托管配置类** , 比如我当前开发的Jdbc模块:那么我的starter则需要导入一些依赖,**重点是导入我的JdbcAutoConfiguration配置类**,其中它需要读取一些属性配置文件```Properties```,它包含了当前模块的配置属性信息, 位于```META-INF/spring.factories```的配置文件则是SpringBoot会自动读取的配置文件 ```java # Auto Configure 使用SpringBoot自动配置类 读取配置类 INF/spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hang.myAutoConfiguration.JdbcAutoConfiguration |

其中start中的pom文件 依赖了autoconfiguration

|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | <!-- starter只做了这一件事情: 引入AutoConfiguration配置类--> < dependency > < groupId > com.hang </ groupId > < artifactId > JdbcAutoConfiguration </ artifactId > < version > 1.0-SNAPSHOT </ version > </ dependency > |

至此: 我们只需要在SpringBoot项目中 引入自定义的starter即可

自定义starter {#自定义starter}

以日期格式化DateFormat为例

  1. 定义一个autoconfiguration模块(包含AutoConfiguration和Properties)

AutoConfiguration

|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | @Configuration @ConditionalOnClass({TimeFunction.class}) // 如果有这个类字节码才加载 @EnableConfigurationProperties({TimeProperties.class}) // IOC 属性类 public class DateFormatAutoConfiguration { @Bean public TimeFunction timeFunction () { return new TimeFunction (); } } |

Properties

|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | @ConfigurationProperties(prefix = "hang.time") public class TimeProperties { private String format = "yyyy-MM-dd HH:mm:ss" ; public String getFormat () { return format; } public void setFormat (String format) { this .format = format; } } |

yml配置 (日期格式化自定义)

|---------------|--------------------------------------------------| | 1 2 3 | hang: time: format: yyyy年MM月dd日 HH:mm:ss |

自定义函数

|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 | // 要托管这个类 -> @Bean注解将这个类在@Component public class TimeFunction { @Autowired private TimeProperties timeProperties; public String showTime (String name) { Date date = new Date (); DateFormat df = new SimpleDateFormat (timeProperties.getFormat()); String format = df.format(date); return "欢迎您:" + name + "现在是:" + format; } } |

  1. 定义一个starter模块

这个模块主要是引入 autoconfiguration 模块

  1. 定义spring.factories文件(位于autoconfiguration的 resource/META-INF 下)

|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 | # 使用SpringBoot自动配置类文件 读取配置类AutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hang.dateFormatAutoConfiguration.DateFormatAutoConfiguration |

如果不用starter自动配置,手动配置,需要配置属性文件解析Configuration-processor依赖

配置文件类通过@ConfigurationProperties(profix="")读取配置文件

通过@Component注入spring,方法等用@Bean注入

@ComponentScan {#ComponentScan}

排除过滤器 {#排除过滤器}
AutoConfigurationExcludeFilter {#AutoConfigurationExcludeFilter}

对于里面的过滤器分析:(案例)

如果排除中有一个符合条件, 则排除扫描排除

|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | @ComponentScan(excludeFilters = { @ComponentScan.Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ),@ComponentScan.Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} ) }) |

例如我有一个配置类

|-----------------------|-------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | @Configuration public class AppConfig (){ @Bean public OrderService orderService () { return new OrderService (); } } |

假如我在 resource下的META_INF下面的spring.factories中

|---------------|--------------------------------------------------------------------------------------------------------------| | 1 2 3 | # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hang.AppConfig |

众所周知, 在这个文件下配置了扫描路径也会被解析, 那么则会 被解析两次 .

如果配置了此项, 则只会被解析一次

TypeExcludeFilter {#TypeExcludeFilter}

配置此项需要单独定义一个排除过滤器, 继承自 TypeExcludeFilter, 重写match方法

例如我需要排除我的UserService

则通过重写类形参中的MetadataReader获取类信息然后判断即可

|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | public class HangTypeExcludeFilter extends TypeExcludeFilter { @Override public boolean match (MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) { return metadataReader.getClassMetadata().getClassName() .equals(UserService.class.getName()); } } |

光配置了此项还不够, 因为在执行当前方法过滤时, 并不知道容器中有没有扫描到或者注册bean(大概), 从而导致已经开始过滤, 然后导致失效

我们在扫描之间就得放置一些bean, 该怎么做呢?

在spring.factories中

|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 | # 初始化器在创建容器对象后,扫描前执行 + # Initializersa + org.springframework.context.ApplicationContextInitializer=\ + com.hang.ApplicationContextInitializer # Auto Configure # -> 刚刚在上一个案例写的 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hang.AppConfig |

自定义一个初始化器

HangApplicationContextInitializer

|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 | public class HangApplicationContextInitializer implements ApplicationContextInitializer { @Override public void initialize (ConfigurableApplicationContext applicationContext) { // 添加一个单例bean(排除过滤器) applicationContext.getBeanFactory() .regiseterSingleton( "HangTypeExcludeFilter" , new HangTypeExcludeFilter ()); } } |

此时则刚刚写的HangTypeExcludeFilter 中的 match匹配过滤则会生效了

SpringBoot Session整合Redis {#SpringBoot-Session整合Redis}

|---------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | <!--springboot整合redis session--> < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-data-redis </ artifactId > </ dependency > < dependency > < groupId > org.springframework.session </ groupId > < artifactId > spring-session-data-redis </ artifactId > </ dependency > |

修改Session未同步问题 {#修改Session未同步问题}

当前问题仅仅适用于当前案例情况

在Session未整合Redis之前, 对数据的操作为 引用数据类型 , 即修改已存在的session值, 自动同步 案例代码

|------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /** * 添加购物车 */ @PostMapping("/addCart") public Result addCart ( @RequestParam("fid") Integer fid, @RequestParam("num") Integer num, HttpSession session) { // 根据fid去查商品 Resfood food = resfoodService.getById(fid); if (Objects.isNull(food)) { return Result.fail( "没有此商品" ); } // 从session去Cart(map) Map<Integer, CartItem> cart = null ; if (Objects.nonNull(session.getAttribute( "cart" ))) { cart = (Map<Integer, CartItem>) session.getAttribute( "cart" ); } else { session.setAttribute( "cart" , cart); } CartItem cartItem; // 判断这个视频在map是否有 if (cart.containsKey(fid)) { cartItem = cart.get(fid); cartItem.setNum(cartItem.getNum() + 1 ); cart.put(fid, cartItem); } else { cartItem = new CartItem (); cartItem.setNum(num); cartItem.setResfood(food); cart.put(fid, cartItem); } // 处理数量 if (cartItem.getNum() <= 0 ) { cart.remove(fid); } // 问题出现处: 原代码没有加这个 + session.setAttribute( "cart" , cart); // 成功 return Result.*ok*(cart.values()); } |

样例中第37行处, 在没有把Session添加到Redis中前, 用户在对session的值修改后, tomcat自动同步更新session的数据, 即添加购物车不会出现 数量限制问题

但是将Session添加到Spring session data redis中的时候, 从一开始的引用类型变成了类似于值引用类型了, 即引用redis里面的session值, 并不会进行同步修改

经查源码: 在Spring整合Redis Session时采用了 事件监听 的方式, 即需要再次重复设置session的操作 session.setAttribute() , Redis中的Session才会同步修改

SpringBoot Mockito测试 {#SpringBoot-Mockito测试}

案例 Controller层 {#案例-Controller层}

当前测试环境:JDK8 , SpringBoot2.7.3 , Junit4, MyBatis-Plus3.x

首先第一步先创建一个简单的MVC框架的测试用例,我们在Controller中右键Go to创建测试用例

|------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/getById") public User getById ( @RequestParam("id") Integer id) { return userService.getUserInfoById(id); } @PostMapping("/save") public String save ( @RequestBody User user) { return userService.saveUserInfo(user) ? "保存成功" : "保存失败" ; } } |

测试包

|------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | @Slf4j @RunWith(SpringRunner.class) // Junit4 @SpringBootTest(classes = {MockApplication.class}, // 在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题 webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 自动配置和启用MockMvc配置 如果不加这个注解 下面是不能注入的 @AutoConfigureMockMvc(addFilters = false) public class UserControllerTest { @Autowired private MockMvc mockMvc; @Test public void getById () { // 预期对象 User user = new User ( 1 , "w" , "a" , "悟空" , "song@foxmail.com" , "13283718600" , "男" , 1 , null , "天子脚下" ); // 使用注入的mockMvc模拟发送请求 // MockMvcRequestBuilders: 请求构建器 try { Integer id = 1 ; // 在请求后面还可以指定请求类型 例如APPLICATION_JSON_VALUE mockMvc.perform(MockMvcRequestBuilders.get( "/user/getById" ) .contentType(MediaType.APPLICATION_JSON_VALUE) // 这里还能设置header .param( "id" , "1" ) ) // MockMvcResultMatchers: mockMvc结果匹配器 .andExpect(MockMvcResultMatchers.status().isOk()) // 例如当前为 匹配状态码为200或者其他可以表示成功的 // 这个是用于取返回body的 $.name代表取body里面的name字段 假如还有 .andExpect(MockMvcResultMatchers.jsonPath( "$.name" , Matchers.equalToIgnoringCase(user.getName()))) // 如果状态码符合预期 则进行print输出响应信息 .andDo(MockMvcResultHandlers.print()) // 真正请求得到响应的内容,真正执行的返回值 .andReturn(); } catch (Exception e) { log.error( "当前方法为:{},发生了异常" , "getById" ); throw new RuntimeException (e); } } @Test public void save () { } } |

在当前测试用例中

  1. 注意到 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) ,其中它代表 : 在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题

  2. @AutoConfigureMockMvc(addFilters = false) : 这个用于 自动配置和启用MockMvc配置

  3. 注入mockMvc

    |-------------|---------------------------------------------| | 1 2 | @Autowired private MockMvc mockMvc; |

  4. 在mockMvc实例中,使用 perform 执行一个HTTP请求, 入口参数为RequestBuilder,输出结果为ResultAction

  5. perform中 ,使用 MockMvcRequestBuilders 请求构建器构造请求, 可以使用get或者post请求,在get或post里面写入请求路径,在后面还可以加请求内容类型 contentType(MediaType.APPLICATION_JSON_VALUE) ,其中还能像SpringMVC一样设置其他的比如header,param

  6. andExpect 期望, 使用 MockMvcResultMatchers :mockMvc结果匹配器断言, 在我的测试用例中,使用了两个匹配断言

    6.1 、 .andExpect(MockMvcResultMatchers.status().isOk()) : 用于匹配返回状态码是否为ok,其中一般指200

    6.2、 .andExpect(MockMvcResultMatchers.jsonPath("$.name", Matchers.equalToIgnoringCase(user.getName()))) ,其中user为 自定义的期望对象 , 对于 jsonPath 对象,一般用于取返回的Body对象, 对于当前返回的结果如下

    |---------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type: "application/json" ] Content type = application/ json Body = { "uid" : 1 , "username" : "w" , "password" : "a" , "name" : "悟空" , "email" : "song@foxmail.com" , "phone" : "13283718600" , "sex" : "男" , "state" : 1 , "code" : null , "addr" : "天子脚下" } Forwarded URL = null Redirected URL = null Cookies = [] |

    假如Body中的json数据有嵌套,同样也是使用如: $.games[0].name 取game[0]对象的name属性

  7. .andDo(MockMvcResultHandlers.print())

如果符合预期结果 则进行print输出响应详细的信息:包括请求头,请求体等常见的字段,比如还有ModelAndView,Response

  1. .andReturn();

真正请求得到响应的内容,真正执行的返回值,返回值可以new一个 MockMvc 对象返回

  1. 其中还包括一些其他的函数,详情见 SpringBoot Test Doc 文档

如果使用这种方式发生注入错误,找不到一个Bean为MockMvc的,那么使用下面这种方案注入

|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @Slf4j @RunWith(SpringRunner.class) @SpringBootTest(classes = {MockApplication.class}, // 在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题 webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 自动配置和启用MockMvc配置 如果不加这个注解 下面是不能注入的 //@AutoConfigureMockMvc(addFilters = false) public class UserControllerTest { @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void setUp () { mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); } @Test @DisplayName("文本响应测试用例") public void getById () { // 测试代码 } @Test public void save () { } } |

  1. 注意到首先注入了 WebApplicationContext 对象,然后有一个没有初始化的MockMvc实例
  2. @Before 注解: 在每一个测试用例之前调用,每一次都会使用 MockMvcBuilders对 webApplicationContext 进行构建,从而得到一个 实例对象mockMvc
  3. @DisplayName("文本响应测试用例") 注解: 作用如图例

注入方式一: 自动配置在 org.springframework.boot:spring-boot-test-autoconfiguration 下的 web.servlet.MockMvcAutoConfiguration 配置类,

|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 | // 使用以下示例进行注入 @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @Before public void setUp () { mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); } |

注入方式二: 自动配置在 org.springframework.boot:spring-boot-test-autoconfiguration 下的 web.servlet.AutoConfigurationMockMvc 注解类

|-------------------|---------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | // 1. 在当前测试类上加注解 @AutoConfigureMockMvc(addFilters = false) // 2. AutoWired注入MockMvc @Autowired private MockMvc mockMvc; |

对于Get请求 示例

|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 | // 在请求后面还可以指定请求类型 例如APPLICATION_JSON_VALUE mockMvc.perform(MockMvcRequestBuilders.get( "/user/getById" ) .contentType(MediaType.APPLICATION_JSON_VALUE) .param( "id" , "1" ) ) // MockMvcResultMatchers: mockMvc结果匹配器 .andExpect(MockMvcResultMatchers.status().isOk()) // 例如当前为 匹配状态码为200或者其他可以表示成功的 // 这个是用于取返回body的 .andExpect(MockMvcResultMatchers.jsonPath( "$.name" , Matchers.equalToIgnoringCase(user.getName()))) // 如果状态码符合预期 则进行print输出响应信息 .andDo(print()) // 真正请求得到响应的内容,真正执行的返回值 .andReturn(); |

对于Post请求 示例

|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // JSON 格式 -> content mockMvc.perform(MockMvcRequestBuilders.post( "/user/save" ) .contentType(MediaType.APPLICATION_JSON) .content( "{\n" + " \"uid\": \"1\",\n" + " \"username\": \"w\"" + "}" ) // MockMvcResultMatchers: mockMvc结果匹配器 .accept(MediaType.APPLICATION_JSON)) .andDo(print()); // From 格式 -> param mockMvc.perform(MockMvcRequestBuilders.post( "/user/save" ) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param( "uid" , "1" ) .param( "username" , "w" ) .accept(MediaType.APPLICATION_JSON)) .andDo(print()); |

案例 Service层 {#案例-Service层}

|---------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | @Slf4j @RunWith(SpringRunner.class) @SpringBootTest(classes = {MockApplication.class}, // 在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题 webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class UserServiceTest { @MockBean // 模拟Bean:Mockito 会帮我们创建一个假的 Mock 对象,替换掉 Spring 中已存在的那个真实的 userDao Bean private UserMapper userMapper; @Autowired private UserService userService; @Test @DisplayName("测试没有dao数据访问层") public void testEmptyMapper () { User expectUser = new User ( 1 , "测试没有dao数据访问层" , "11" , "测试没有dao数据访问层" , "song@foxmail.com" , "13283718600" , "男" , 1 , null , "天子脚下" ); // Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 ) Mockito.when(userMapper.selectById( 1 )) .thenReturn( new User ( 1 , "测试没有dao数据访问层" , "11" , "测试没有dao数据访问层" , "song@foxmail.com" , "13283718600" , "男" , 1 , null , "天子脚下" )); User user = userService.getUserInfoById( 1 ); // 访问模拟的Dao Assert.assertEquals(expectUser,user); // 断言正确 } @Test @DisplayName("测试Service") public void testService () { // Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 ) Mockito.when(userService.getUserInfoById( 1 )) .thenReturn( new User ( 1 , "测试Service" , "11" , "测试没有dao数据访问层" , "song@foxmail.com" , "13283718600" , "男" , 1 , null , "天子脚下" )); User user = userService.getUserInfoById( 1 ); log.info(user.toString()); } } |

  1. @MockBean : 模拟Bean:Mockito 会帮我们创建一个假的 Mock 对象,替换掉 Spring 中已存在的那个真实的 userDao Bean

  2. when : 模拟引导访问的 对象.方法名()

  3. thenReturn : 引导访问的返回结果

当前样例1: Mockito.anyInt() ,任意的Int类型值

|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 | Mockito.when(userService.getUserById(Mockito.anyInt())) .thenReturn( new User ( 3 , "Calyee" )); User user1 = userService.getUserById( 3 ); // 回传的user的名字为Calyee User user2 = userService.getUserById( 200 ); // 回传的user的名字也为Calyee |

当前样例2: 指定的值

|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 | Mockito.when(userService.getUserById( 3 )).thenReturn( new User ( 3 , "Calyee" )); User user1 = userService.getUserById( 3 ); // 回传的user的名字为Calyee -> 为预设期盼 User user2 = userService.getUserById( 200 ); // 回传的user为null |

当前样例3: 当调用Insert的时候 不论传入的User字节码对象 是谁,都返回100

|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 | Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn( 100 ); Integer i = userService.insertUser( new User ()); //会返回100 |

其他注解:

  1. thenThrow : 当请求方法传入的值为1时,抛出一个RuntimeException ( 适用于 有出参的方法 )

|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 | Mockito.when(userService.getUserById( 1 )) .thenThrow( new RuntimeException ( "mock throw exception" )); User user = userService.getUserById( 1 ); //会抛出一个RuntimeException |

  1. doThrow 如果方法没有返回值的话(即是方法定义为 public void myMethod() {...}),要改用 doThrow() 抛出 Exception, doThrow ( 适用于 没有出参的方法 )

|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 | Mockito.doThrow( new RuntimeException ( "mock throw exception" )) .when(userService).print(); userService.print(); //会抛出一个RuntimeException |

简单模拟SpringBoot {#简单模拟SpringBoot}

初始化Spring项目 {#初始化Spring项目}

1.创建maven工程,建两个 Module

  1. springboot模块,表示springboot框架的源码实现
  2. user包,表示用户业务系统,用来写业务代码来测试我们所模拟出来的SpringBoot

2.其中SpringBoot也是依赖于Spring,我们需要处理请求,故需要导入SpringMVC,当然还有Tomcat等依赖

导入依赖 (其中Java JDK版本为1.8)

|---------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | < dependencies > < dependency > < groupId > org.springframework </ groupId > < artifactId > spring-context </ artifactId > < version > 5.2.11.RELEASE </ version > </ dependency > < dependency > < groupId > org.springframework </ groupId > < artifactId > spring-web </ artifactId > < version > 5.2.11.RELEASE </ version > </ dependency > < dependency > < groupId > org.springframework </ groupId > < artifactId > spring-webmvc </ artifactId > < version > 5.2.11.RELEASE </ version > </ dependency > < dependency > < groupId > javax.servlet </ groupId > < artifactId > javax.servlet-api </ artifactId > < version > 4.0.1 </ version > </ dependency > < dependency > < groupId > org.apache.tomcat.embed </ groupId > < artifactId > tomcat-embed-core </ artifactId > < version > 9.0.65 </ version > </ dependency > </ dependencies > |

在User测试模块中引入刚刚创建的SpringBoot模块(这个是另外一个模块的)

|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | < dependencies > < dependency > < groupId > org.springboot </ groupId > < artifactId > springboot </ artifactId > < version > 1.0-SNAPSHOT </ version > </ dependency > </ dependencies > |

在User模块中创建一个标准的测试结构

Controller

|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 | @RestController public class UserController { @Autowired private UserService userService; @GetMapping("/test") public String test () { return userService.test(); } } |

Service

|---------------------|---------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | @Service public class UserService { public String test () { return "Hello My SpringBoot!" ; } } |

核心注解与核心类 {#核心注解与核心类}

启动类

|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | /** * @ClassName MyApplication * @Description 自定义SpringBoot * @Author QiuLiHang * @Version 1.0 */ @HangSpringBootApplication // 该注解里面包含了ComponentScan注解 public class MyApplication { public static void main (String[] args) { HangSpringApplication.run(MyApplication.class); } } |

在springboot模块中创建

启动类标记注解

|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Configuration @ComponentScan // 默认扫描当前解析类的包路径,传给run方法的类 public @interface HangSpringBootApplication { } |

启动SpringBoot应用的启动类 -> run()方法

|-----------------|-----------------------------------------------------------------------------------------| | 1 2 3 4 | public class HangSpringApplication { public static void run (Class clazz) { } } |

run方法 {#run方法}

首先我们需要的是,启动类执行完成,就可以像原生SpringBoot一样在浏览器上可以访问到 localhost:8081/test 方法

那么此时肯定要 启动Tomcat容器 ,要想处理请求,在Spring容器中添加创建的 DispatcherServlet 对象添加到Tomcat里面,最后启动Tomcat

创建Spring容器 {#创建Spring容器}

|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 | public class HangSpringApplication { public static void run (Class clazz) { // 创建Spring容器 AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext (); // 注册: clazz -> Spring容器的配置类 applicationContext.register(clazz); // 刷新容器,加载被扫描的bean applicationContext.refresh(); // 启动Tomcat (此处未实现启动其他服务器) startTomcat(applicationContext); } } |

通过 AnnotationConfigWebApplicationContext 创建的容器,把传入的clazz作为容器的配置类, 比如此时我传入的是 MyApplication.class 类,当前类作为配置类,由于当前类头上带有我们刚刚 自定义的启动类注解 @HangSpringBootApplication , 其中当前注解内部带有 扫描注解 @ComponentScan 那么将会自动默认扫描 当前解析类的包路径(传入run方法的类包路径) ,假如设置了明示了扫描路径,则扫描自定义路径, 那么则会扫描到我们的 UserService 和 UserController , 然后添加到Spring容器里面

一般情况 我们会 直接把启动类当成配置类

而不会进行如下操作: 在其他类中创建启动类,然后在main方法( 不是启动类了,没有带启动类标识注解 )中进行 HangSpringApplication.run(MyApplication.class) 操作 (启动类标识@HangSpringBootApplication当前标明在MyApplication类上)

启动Tomcat {#启动Tomcat}

对于SpringBoot,它使用的是内嵌Tomcat方式,对于内嵌Tomcat,我们需要进行配置

|---------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public static void startTomcat (WebApplicationContext applicationContext) { // 启动 Tomcat Tomcat tomcat = new Tomcat (); Server server = tomcat.getServer(); Service service = server.findService( "Tomcat" ); Connector connector = new Connector (); connector.setPort( 8081 ); // 连接端口 Engine engine = new StandardEngine (); engine.setDefaultHost( "localhost" ); // 本地测试 Host host = new StandardHost (); host.setName( "localhost" ); String contextPath = "" ; Context context = new StandardContext (); context.setPath(contextPath); context.addLifecycleListener( new Tomcat .FixContextListener()); host.addChild(context); engine.addChild(host); service.setContainer(engine); service.addConnector(connector); // SpringMVC 处理请求 注册DispatcherServlet tomcat.addServlet(contextPath, "dispatcher" , new DispatcherServlet (applicationContext)); // 传入Spring容器 context.addServletMappingDecoded( "/*" , "dispatcher" ); try { tomcat.start(); } catch (LifecycleException e) { e.printStackTrace(); } } |

此时就能启动Tomcat了,可以正常启动了,其中控制台会输出tomcat启动情况,例如刚刚设置的8081端口

浏览器访问 localhost:8081/test ,结果返回了 "Hello My SpringBoot!"

修改其他的服务器 {#修改其他的服务器}

1.在springboot中创建一个抽象方法为: WebServer

|---------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | /** * 对自定义服务器调用 实现抽象化 */ public interface WebServer { /** * 服务启动抽象接口 */ public void start (WebApplicationContext applicationContext) ; } |

2.定义其他服务器接口实现类

|---------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | /** * @ClassName TomcatWebServer * @Description Tomcat服务 * @Author QiuLiHang * @Version 1.0 */ public class TomcatWebServer implements WebServer { /** * Tomcat服务启动类 */ @Override public void start (WebApplicationContext applicationContext) { Tomcat tomcat = new Tomcat (); Server server = tomcat.getServer(); Service service = server.findService( "Tomcat" ); Connector connector = new Connector (); connector.setPort( 8081 ); Engine engine = new StandardEngine (); engine.setDefaultHost( "localhost" ); Host host = new StandardHost (); host.setName( "localhost" ); String contextPath = "" ; Context context = new StandardContext (); context.setPath(contextPath); context.addLifecycleListener( new Tomcat .FixContextListener()); host.addChild(context); engine.addChild(host); service.setContainer(engine); service.addConnector(connector); // SpringMVC 处理请求 tomcat.addServlet(contextPath, "dispatcher" , new DispatcherServlet (applicationContext)); // 传入Spring容器 context.addServletMappingDecoded( "/*" , "dispatcher" ); try { tomcat.start(); } catch (LifecycleException e) { e.printStackTrace(); } } } |

|---------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * @ClassName JettyWebServer * @Description Jetty服务启动类 * @Author QiuLiHang * @Version 1.0 */ public class JettyWebServer implements WebServer { @Override public void start (WebApplicationContext applicationContext) { System.out.println( "启动jetty" ); } } |

以上添加了两个示例服务实现类

再次修改run方法

|------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public class HangSpringApplication { public static void run (Class clazz) { // 创建Spring容器 AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext (); // 注册: clazz -> Spring容器的配置类 applicationContext.register(clazz); // 刷新容器,加载被扫描的bean applicationContext.refresh(); // 启动Tomcat -> 当前方法只能启动tomcat // startTomcat(applicationContext); // 原方法 // 那么如何优雅的启动自定义的服务器呢? 即pom中导入的服务器 // 1. 获取WebServer类 (多态) WebServer webServer = getWebServer(applicationContext); // 2. 调用子类启动服务 webServer.start(applicationContext); } /** * 获取Spring容器中的WebServer对象 */ private static WebServer getWebServer (WebApplicationContext webApplicationContext) { // key为beanName,value为Bean对象 Map<String,WebServer> webServers = webApplicationContext.getBeansOfType(WebServer.class); // 获取WebServer类型的对象 // 限制Spring容器中的WebServer对象 // 1.不能为空 if (webServers.isEmpty()){ throw new NullPointerException (); } // 2.只能存在一个 if (webServers.size() > 1 ){ throw new IllegalStateException (); } // 返回唯一的一个 return webServers.values().stream().findFirst().get(); } } |

此时,在启动类中添加一个Tomcat 的Bean

|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @HangSpringBootApplication // 该注解里面包含了ComponentScan注解 public class MyApplication { // Spring配置类中配置的bean // 会被加入到SpringIOC容器里面,在后面启动WebServer的时候 会被扫描到 // 然后通过多态 启动子类实现的服务接口 @Bean public TomcatWebServer tomcatWebServer () { return new TomcatWebServer (); } public static void main (String[] args) { HangSpringApplication.run(MyApplication.class); } } |

当然,自己在启动类中配置服务器的bean,显然耦合度太高了,至此我们需要引入新的方案

自动配置WebServer服务 {#自动配置WebServer服务}

首先在springboot模块中添加 WebServerAutoConfiguration

|------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /** * @ClassName WebServerAutoConfiguration * @Description WebServer自动配置类 * @Author QiuLiHang * @Version 1.0 */ @Configuration // 交给Spring管理 public class WebServerAutoConfiguration { // Spring配置文件 // WebServer自动配置类 @Bean @Conditional(TomcatCondition.class) public TomcatWebServer tomcatWebServer () { return new TomcatWebServer (); } @Bean @Conditional(JettyCondition.class) public JettyWebServer jettyWebServer () { return new JettyWebServer (); } } |

其中发现有一个注解为 @Conditional(TomcatCondition.class)

该注解,当该Condition.class判断返回的boolean结果,作为是否执行该方法的条件,如果返回true则该注解修饰的该方法可执行

其中 TomcatCondition 类需要自定义:

|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class TomcatCondition implements Condition { @Override public boolean matches (ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { // 判断项目中是否有符合条件的依赖 try { // 利用类加载器,加载需要判断的类 conditionContext.getClassLoader().loadClass( "org.apache.catalina.startup.Tomcat" ); return true ; // 能走到这个地方 证明有 } catch (ClassNotFoundException e) { return false ; // 没有该类 } // // 如果返回true,则符合条件 // return false; } } |

走到这了, 上面的目标就是,检查当前依赖是否包含修饰的依赖项, 实现自动判断

现在还会出现问题, 就是 不能像SpringBoot一样,默认Tomcat, 排除其他的服务器依赖给子项目, 此时要在父项目中的pom.xml中设置 <optional>true</optional> 属性: 该依赖在 项目之间依赖不传递

|---------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | < dependency > < groupId > org.apache.tomcat.embed </ groupId > < artifactId > tomcat-embed-core </ artifactId > < version > 9.0.65 </ version > </ dependency > < dependency > < groupId > org.eclipse.jetty </ groupId > < artifactId > jetty-maven-plugin </ artifactId > < version > 9.4.14.v20181114 </ version > <!-- 加了这个设置,禁止依赖传递,即传给子类 (因为SpringBoot也是默认传递Tomcat而不传递jetty等其他) --> < optional > true </ optional > </ dependency > |

此时, 如果在子模块中想切换 jetty ,在依赖父项目处使用 exclusion 排除父项目传递的依赖, 再添加其他依赖, 示例如下

|------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | < dependencies > < dependency > < groupId > com.hang </ groupId > < artifactId > springboot </ artifactId > < version > 1.0-SNAPSHOT </ version > <!--排除tomcat--> < exclusions > < exclusion > < groupId > org.apache.tomcat.embed </ groupId > < artifactId > tomcat-embed-core </ artifactId > </ exclusion > </ exclusions > </ dependency > <!--切换为jetty--> < dependency > < groupId > org.eclipse.jetty </ groupId > < artifactId > jetty-maven-plugin </ artifactId > < version > 9.4.14.v20181114 </ version > </ dependency > </ dependencies > |

模拟条件注解 {#模拟条件注解}

在上面的模拟方案, 基于两个服务器的选择从而定义了两个Condition类: 1.TomcatCondition 2.JettyCondition

大胆想象, 加入有很多服务器需要筛选, 然后发现全部都是重复的代码, 何不如封装一下呢

仿SpringBoot的 ConditionalOnClass 编写一个 HangConditionalOnClass .

|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 | @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Conditional(HangCondition.class) // 封装的条件排除类 public @interface HangConditionalOnClass { /** * 拿到的类名 * @return */ String value () ; } |

HangCondition类

|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class HangCondition implements Condition { @Override public boolean matches (ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { // AnnotatedTypeMetadata 里面有解析到HangConditionalOnClass注解的Value信息 // 因为是解析到HangConditionalOnClass注解才进来这里的 Map<String, Object> annotationAttributes = annotatedTypeMetadata.getAnnotationAttributes(HangConditionalOnClass.class.getName()); String values = (String) annotationAttributes.get( "value" ); // 拿到Value // 判断项目中是否有符合条件的依赖 try { // 利用类加载器,加载需要判断的类 conditionContext.getClassLoader().loadClass(values); return true ; // 能走到这个地方 证明有 } catch (ClassNotFoundException e) { return false ; // 没有该类 } } } |

自动配置类 {#自动配置类}

有了条件注解,我们就可以来使⽤它了,那如何实现呢?这⾥就要⽤到⾃动配置类的概念,我们先看代码:

|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 | @Configuration public class WebServiceAutoConfiguration { @Bean @HangConditionalOnClass("org.apache.catalina.startup.Tomcat") public TomcatWebServer tomcatWebServer () { return new TomcatWebServer (); } @Bean @HangConditionalOnClass("org.eclipse.jetty.server.Server") public JettyWebServer jettyWebServer () { return new JettyWebServer (); } } |

这个代码跟最开始的大同小异, 只不过加了我们的条件注解

要实现服务器自动配置只需要在过滤掉所有bean留下唯一的bean就行了

这样整体SpringBoot启动逻辑就是这样的:

  1. 创建⼀个AnnotationConfigWebApplicationContext容器
  2. 解析MyApplication类,然后进⾏扫描
  3. 通过getWebServer⽅法从Spring容器中获取WebServer类型的Bean
  4. 调⽤WebServer对象的start⽅法

有了以上步骤,我们还差了⼀个关键步骤,就是Spring要能解析到WebServiceAutoConfiguration这个⾃动配置类,因为不管这个类⾥写了什么代码,Spring不去解析它,那都是没⽤的,此时我们需要SpringBoot在run⽅法中,能找到WebServiceAutoConfiguration这个配置类并添加到Spring容器中。

基于包扫描, 在SpringBoot中自己实现了一套SPI机制, 也就是熟悉的 ++spring.factories++

发现自动配置类 {#发现自动配置类}

基于SPI机制, SpringBoot约定在项目的resource目录下的META_INF中创建spring.factories, 配置SpringBoot中所需扫描的类

并且提供一个接口

HangAutoConfiguration

|-------------|----------------------------------------------------| | 1 2 | public interface HangAutoConfiguration { } |

然后在WebServiceAutoConfiguration实现此接口

|---------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 | @Configuration public class WebServiceAutoConfiguration implements HangAutoConfiguration { @Bean @HangConditionalOnClass("org.apache.catalina.startup.Tomcat") public TomcatWebServer tomcatWebServer () { return new TomcatWebServer (); } @Bean @HangConditionalOnClass("org.eclipse.jetty.server.Server") public JettyWebServer jettyWebServer () { return new JettyWebServer (); } } |

然后利用spring中的@Import技术导入这些配置类, 我们在@HangSpringBootApplication的定义上增加

|-----------|-----------------------------------------| | 1 | @Import(HangImportSelect.class) |

|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 | public class ZhouyuImportSelect implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { ServiceLoader<AutoConfiguration> serviceLoader = ServiceLoader.load(AutoConfiguration.class); List<String> list = new ArrayList <>(); for (AutoConfiguration autoConfiguration : serviceLoader) { list.add(autoConfiguration.getClass().getName()); } return list.toArray( new String [ 0 ]); } } |

这就完成了从com.zhouyu.springboot.AutoConfiguration⽂件中获取⾃动配置类的名字,并导⼊到Spring容器中,从⽽Spring容器就知道了这些配置类的存在,⽽对于user项⽬⽽⾔,是不需要修改代码的。

赞(0)
未经允许不得转载:工具盒子 » SpringBoot