SpringBoot笔记持续更新中!
SpringBoot 发布者订阅者 样例场景: 我们需要在用户注册后 给订阅的用户推送消息(例如前100名注册的 系统自动发放徽章等)
1 2 3 4 5 6 7 8 @Override @Transactional public Long register (User insert) { boolean save = userDao.save(insert); applicationEventPublisher.publishEvent(new UserRegisterEvent (this , insert)); return insert.getId(); }
这个时候我们可以采用Spring的推送事件ApplicationEventPublisher
, 其中除此之外我们还可以采用MQ进行同等操作
我们定义一个事件, 继承ApplicationEvent
然后定义构造函数, 我们此时场景需要用户的信息, 故我们在构造函数传入User即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @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 @Component public class UserRegisterListener { @Autowired private IUserBackpackService userBackpackService; @Autowired private UserDao userDao; @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()); } @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配置文件信息 导入依赖:
1 2 3 4 5 6 7 8 9 10 11 <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 @ConfigurationProperties(prefix = "product") @Component public class Product implements Serializable { private String pname; private Double price; private Boolean isUsed; private Date manDate; 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 @PropertySource(value = {"classpath:db.properties"})
db.properties配置文件
1 2 username =root password =123456
如何在Spring中使用呢? 使用@Value(${username})
注解
1 2 @Value(${username}) private String username;
2.读取例如xml配置 @ImportResource
在配置文件中加入注解
1 2 @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") private User user;
在运行时修改配置文件信息
在命令行参数中配置
1 java -jar xxx.jar --server.port=9090
此项使用必须建立在启动类的run方法中,传入了main函数中的args参数
1 2 3 4 5 6 7 8 @SpringBootApplication public class Application { public static void main (String[] args) { 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读取配置信息、环境变量
Environment基本的环境变量
1 2 3 @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 @Profile("test") public DataSource localDataSource () { DriverManagerDataSource source = new DriverManagerDataSource (); source.setDriverClassName(driverName); source.setUsername(username); source.setPassword(password); source.setUrl(url); return source; } @Bean @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: spring.log path: logs/
滚动配置
暂无
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 > <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; } }
首先需要加注解@Component 交给Spring管理
然后在当前的包中右键,点击Go To -> Test -> Create New Test 一般点击OK即可
然后idea在test包下就会自动创建一个跟当前类同样结构的测试类
在测试类中需要引入注解
1 2 3 @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() @RunWith(SpringRunner.class) public class CalculatorTest { @Autowired private Calculator calculator; @Test void add () { int add = calculator.add(1 , 2 ); Assert.assertEquals(3 , add); } }
现在需求是: 假如有多个类,类里面有很多方法需要测试,那该怎么实现一起自动化测试?
: 使用SpringBoot的套件测试 , 顾名思义:把测试方法类全部放一起测试,就跟一个整体套件一样
启动当前类就可以了
1 2 3 4 5 6 7 8 9 10 @RunWith(Suite.class) @Suite .SuiteClasses({CalculatorTest.class,OtherTest.class}) public class SuiteTest {}
二、其他的注解: 1) @Disabled
:禁用测试方法, 用于当前方法现在不需要进行测试
1 2 3 4 5 @Test @Disabled("This test is not ready yet.") public void disabledTest () { }
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
:在测试类的所有测试方法前和后执行一次,可用于全局初始化和销毁资源
1 2 3 4 5 6 7 8 9 @BeforeAll public static void initOperate () { } @AfterAll public static void destoryOperate () { }
4) @BeforeEach
和 @AfterEach
:在测试类的每个测试方法前和后都会执行一次
1 2 3 4 5 6 7 8 9 @BeforeEach public void beforeEachTest () { } @AfterEach public void afterEachTest () { }
5) @RepeatedTest
:指定测试方法重复执行
1 2 3 4 @RepeatedTest(6) public void repeatedTest () { }
6) @ParameterizedTest
和 @ValueSource
:用于参数化测试
1 2 3 4 5 @ParameterizedTest @ValueSource(strings = { "name1", "name2", "name3" }) public void testParameter (String name) { }
:启用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 > <exclusions > <exclusion > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-undertow</artifactId > </dependency >
部分执行控制台输出如下
定时器
在启动类中标明注解@EnableScheduling
编写一个Job任务类,在类上面标明@Component注解给spring托管
在@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 () { Map<String, Integer> viewCountMap = redisCache.getCacheMap(SystemConstants.ARTICLE_VIEW_COUNT); List<Article> articleList = viewCountMap.entrySet() .stream() .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 @Component public class ViewCountRunner implements CommandLineRunner { @Autowired private ArticleMapper articleMapper; @Autowired private RedisCache redisCache; @Override public void run (String... args) throws Exception { List<Article> articleList = articleMapper.selectList(null ); Map<String, Integer> map = new HashMap <>(); map = articleList.stream() .collect(Collectors.toMap(article -> article.getId().toString(), article -> { return article.getViewCount().intValue(); })); redisCache.setCacheMap(SystemConstants.ARTICLE_VIEW_COUNT,map); } }
自定义的Runner实现了CommandLineRunner
类,重写run方法,该方法在SpringBoot启动时也会同步执行该任务,这个是SpringBoot提供的简单类
: 如果在上下文中,存在多个该接口实现类,可以通过@order注解,指定加载顺序
@Order写在实现类上 例如@Order(value = 1) 越前越快执行
SpringBoot的自动配置 1、了解注解及一些类 @Configuration结合@Bean
eg. @Configuration
,里面的proxyBeanMethods
属性 true : false
proxyBeanMethods配置类是用来指定@Bean注解标注的方法是否使用代理,默认是true使用动态代理 ,直接从IOC容器之中取得对象; 如果设置为false,也就是不使用注解,每次调用@Bean标注的方法获取到的对象和IOC容器中的都不一样,是一个新的对象,所以我们可以将此属性设置为false来提高性能 ,但是不能扩展,例如AOP切面
优缺点 : 设置true动态代理的可以后期被切面增强,但是启动速度性能慢些,设置false则使用new对象,提高性能但是不能被增强
@EnableAutoConfiguration(xx.class)
帮助SpringBoot应用将所有符合条件 的@Configuration
配置都加载到当前SpringBoot,并创建对应配置类的Bean ,并把该Bean实体交给IoC容器进行管理
该注解的功能更强大,托管配置类并把bean托管 ,可以读取属性文件注入
@ConfigurationProperties(prefix = "")
一次性读取符合前缀的属性配置文件,只能注入属性
@Import
导入外部的class文件
1 2 3 @Import({Apple.class,Product.class})
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/" return new String []{ }; } }
然后使用@Import({ImportList.class,Other.class})
导入刚刚的类
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; } @GetMapping("/getSpringBeanInfo") public void getBeanNameInfo () { for (String name : this .context.getBeanDefinitionNames()) { System.out.println(name); } } }
实现ApplicationContextAware
接口的类必须被Spring所管理
作用: 换句话说,就是这个类可以直接获取Spring配置文件中,所有有引用到的Bean对象
@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 @Configuration public class WebServerAutoConfiguration { @Bean @Conditional(TomcatCondition.class) public TomcatWebServer tomcatWebServer () { return new TomcatWebServer (); } @Bean @Conditional(JettyCondition.class) 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 ; } } }
2、自动配置start 样例一: 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 在此处,再举一个例子(样例二 )如下,例如我需要对服务器的配置 , 比如server.port=9090
服务器的端口等
步骤一: 对服务器的基础配置进行读取
文件在org.springframework.boot
的 spring-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 @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自动配置 样例总结 说到读取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 = 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 ArrayList(SpringFactoriesLoader.loadFactoryNames(this .getSpringFactoriesLoaderFactoryClass(), this .getBeanClassLoader())); 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: 启动类 分析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
的spring.factories
的内容如下:
1 2 3 4 5 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 <dependency > <groupId > com.hang</groupId > <artifactId > JdbcAutoConfiguration</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
至此: 我们只需要在SpringBoot项目中引入自定义的starter即可
自定义starter 以日期格式化DateFormat为例
定义一个autoconfiguration模块(包含AutoConfiguration和Properties)
AutoConfiguration
1 2 3 4 5 6 7 8 9 @Configuration @ConditionalOnClass({TimeFunction.class}) @EnableConfigurationProperties({TimeProperties.class}) 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 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; } }
定义一个starter模块
这个模块主要是引入autoconfiguration 模块
定义spring.factories文件(位于autoconfiguration的resource/META-INF
下)
1 2 3 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hang.dateFormatAutoConfiguration.DateFormatAutoConfiguration
如果不用starter自动配置,手动配置,需要配置属性文件解析Configuration-processor依赖
配置文件类通过@ConfigurationProperties(profix=””)读取配置文件
通过@Component注入spring,方法等用@Bean注入
@ComponentScan
排除过滤器 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, 重写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) { applicationContext.getBeanFactory() .regiseterSingleton("HangTypeExcludeFilter" ,new HangTypeExcludeFilter ()); } }
此时则刚刚写的HangTypeExcludeFilter 中的 match匹配过滤则会生效了
SpringBoot Session整合Redis 1 2 3 4 5 6 7 8 9 <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未整合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) { Resfood food = resfoodService.getById(fid); if (Objects.isNull(food)) { return Result.fail("没有此商品" ); } 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; 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测试 案例 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) @SpringBootTest(classes = {MockApplication.class}, // 在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题 webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @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 , "天子脚下" ); try { Integer id = 1 ; mockMvc.perform(MockMvcRequestBuilders.get("/user/getById" ) .contentType(MediaType.APPLICATION_JSON_VALUE) .param("id" ,"1" ) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.name" , Matchers.equalToIgnoringCase(user.getName()))) .andDo(MockMvcResultHandlers.print()) .andReturn(); } catch (Exception e) { log.error("当前方法为:{},发生了异常" , "getById" ); throw new RuntimeException (e); } } @Test public void save () { } }
在当前测试用例中
注意到@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
,其中它代表 :在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题
@AutoConfigureMockMvc(addFilters = false)
: 这个用于自动配置和启用MockMvc配置
注入mockMvc
1 2 @Autowired private MockMvc mockMvc;
在mockMvc实例中,使用perform
执行一个HTTP请求, 入口参数为RequestBuilder,输出结果为ResultAction
在perform中
,使用MockMvcRequestBuilders
请求构建器构造请求, 可以使用get或者post请求,在get或post里面写入请求路径,在后面还可以加请求内容类型contentType(MediaType.APPLICATION_JSON_VALUE)
,其中还能像SpringMVC一样设置其他的比如header,param
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属性
.andDo(MockMvcResultHandlers.print())
如果符合预期结果 则进行print输出响应详细的信息:包括请求头,请求体等常见的字段,比如还有ModelAndView,Response
.andReturn();
真正请求得到响应的内容,真正执行的返回值,返回值可以new一个MockMvc
对象返回
其中还包括一些其他的函数,详情见 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) 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 () { } }
注意到首先注入了WebApplicationContext
对象,然后有一个没有初始化的MockMvc实例
@Before
注解: 在每一个测试用例之前调用,每一次都会使用 MockMvcBuilders对webApplicationContext
进行构建,从而得到一个实例对象mockMvc
@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 @AutoConfigureMockMvc(addFilters = false) @Autowired private MockMvc mockMvc;
对于Get请求 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 mockMvc.perform(MockMvcRequestBuilders.get("/user/getById" ) .contentType(MediaType.APPLICATION_JSON_VALUE) .param("id" , "1" ) ) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.name" , Matchers.equalToIgnoringCase(user.getName()))) .andDo(print()) .andReturn();
对于Post请求 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 mockMvc.perform(MockMvcRequestBuilders.post("/user/save" ) .contentType(MediaType.APPLICATION_JSON) .content("{\n" + " \"uid\": \"1\",\n" + " \"username\": \"w\"" + "}" ) .accept(MediaType.APPLICATION_JSON)) .andDo(print()); mockMvc.perform(MockMvcRequestBuilders.post("/user/save" ) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("uid" , "1" ) .param("username" , "w" ) .accept(MediaType.APPLICATION_JSON)) .andDo(print());
案例 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 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(userMapper.selectById(1 )) .thenReturn(new User (1 , "测试没有dao数据访问层" , "11" , "测试没有dao数据访问层" ,"song@foxmail.com" , "13283718600" , "男" , 1 , null , "天子脚下" )); User user = userService.getUserInfoById(1 ); Assert.assertEquals(expectUser,user); } @Test @DisplayName("测试Service") public void testService () { 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 user2 = userService.getUserById(200 );
当前样例2: 指定的值
1 2 3 Mockito.when(userService.getUserById(3 )).thenReturn(new User (3 , "Calyee" )); User user1 = userService.getUserById(3 ); User user2 = userService.getUserById(200 );
当前样例3: 当调用Insert的时候不论传入的User字节码对象 是谁,都返回100
1 2 Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100 ); Integer i = userService.insertUser(new User ());
其他注解:
4.thenThrow
: 当请求方法传入的值为1时,抛出一个RuntimeException (适用于 有出参的方法 )
1 2 3 Mockito.when(userService.getUserById(1 )) .thenThrow(new RuntimeException ("mock throw exception" )); User user = userService.getUserById(1 );
5.doThrow
如果方法没有返回值的话(即是方法定义为 public void myMethod() {…}),要改用 doThrow() 抛出 Exception, doThrow ( 适用于 没有出参的方法 )
1 2 3 Mockito.doThrow(new RuntimeException ("mock throw exception" )) .when(userService).print(); userService.print();
简单模拟SpringBoot 初始化Spring项目 1.创建maven工程,建两个Module
springboot模块,表示springboot框架的源码实现
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 @HangSpringBootApplication 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 public @interface HangSpringBootApplication {}
启动SpringBoot应用的启动类 -> run()方法
1 2 3 4 public class HangSpringApplication { public static void run (Class clazz) { } }
run方法 首先我们需要的是,启动类执行完成,就可以像原生SpringBoot一样在浏览器上可以访问到localhost:8081/test
方法
那么此时肯定要启动Tomcat容器 ,要想处理请求,在Spring容器中添加创建的DispatcherServlet
对象添加到Tomcat里面,最后启动Tomcat
创建Spring容器 1 2 3 4 5 6 7 8 9 10 11 12 13 public class HangSpringApplication { public static void run (Class clazz) { AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext (); applicationContext.register(clazz); applicationContext.refresh(); startTomcat(applicationContext); } }
通过AnnotationConfigWebApplicationContext
创建的容器,把传入的clazz作为容器的配置类, 比如此时我传入的是MyApplication.class
类,当前类作为配置类,由于当前类头上带有我们刚刚自定义的启动类注解 @HangSpringBootApplication
, 其中当前注解内部带有扫描注解 @ComponentScan
那么将会自动默认扫描当前解析类的包路径(传入run方法的类包路径) ,假如设置了明示了扫描路径,则扫描自定义路径, 那么则会扫描到我们的UserService 和 UserController
, 然后添加到Spring容器里面
一般情况 我们会直接把启动类当成配置类
而不会进行如下操作: 在其他类中创建启动类,然后在main方法(不是启动类了,没有带启动类标识注解 )中进行HangSpringApplication.run(MyApplication.class)
操作 (启动类标识@HangSpringBootApplication当前标明在MyApplication类上)
启动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 = 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); tomcat.addServlet(contextPath, "dispatcher" , new DispatcherServlet (applicationContext)); 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 public class TomcatWebServer implements WebServer { @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); tomcat.addServlet(contextPath, "dispatcher" , new DispatcherServlet (applicationContext)); context.addServletMappingDecoded("/*" , "dispatcher" ); try { tomcat.start(); } catch (LifecycleException e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 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) { AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext (); applicationContext.register(clazz); applicationContext.refresh(); WebServer webServer = getWebServer(applicationContext); webServer.start(applicationContext); } private static WebServer getWebServer (WebApplicationContext webApplicationContext) { Map<String,WebServer> webServers = webApplicationContext.getBeansOfType(WebServer.class); if (webServers.isEmpty()){ throw new NullPointerException (); } 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 public class MyApplication { @Bean public TomcatWebServer tomcatWebServer () { return new TomcatWebServer (); } public static void main (String[] args) { HangSpringApplication.run(MyApplication.class); } }
当然,自己在启动类中配置服务器的bean,显然耦合度太高了,至此我们需要引入新的方案
自动配置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 @Configuration public class WebServerAutoConfiguration { @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 ; } } }
走到这了, 上面的目标就是,检查当前依赖是否包含修饰的依赖项, 实现自动判断
现在还会出现问题, 就是 不能像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 > <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 > <exclusions > <exclusion > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-core</artifactId > </exclusion > </exclusions > </dependency > <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 { 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) { Map<String, Object> annotationAttributes = annotatedTypeMetadata.getAnnotationAttributes(HangConditionalOnClass.class.getName()); String values = (String) annotationAttributes.get("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启动逻辑就是这样的:
创建⼀个AnnotationConfigWebApplicationContext容器
解析MyApplication类,然后进⾏扫描
通过getWebServer⽅法从Spring容器中获取WebServer类型的Bean
调⽤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项⽬⽽⾔,是不需要修改代码的。