SpringBoot进阶
1.上节回顾
- springboot有哪些特点
- SpringBoot相比Spring而言有没有功能上的增强?
- 手动搭建springboot项目结合MyBatis
- 启动类的放置位置
- Lombok的使用
2.常见面试题
- Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
3.本章重点
- SpringBoot核心注解解析以及SpringBoot自动配置原理是什么??
- SpringBoot的启动源码分析
- springboot集成单元测试
- 常见面试题解答
4.内容详解
4.1 SpringBoot核心注解 @SpringBootApplication 解析
-
@SpringbootApplication是一个组合注解
- @SpringBootConfiguration,此注解是springboot所必须要的基础配置(application.xml)
- @EnableAutoConfiguration: 自动加载配置
-
@EnableAutoConfiguration: 依然是一个组合注解,自动加载配置
-
@AutoConfigurationPackage:通过扫描包的形式自动加载(eg:mybatis的mapper扫描器)
-
@Import:导入(在application.xml配置文件中
),有使用方式,1,直接导入配置类(@Configuration 类)2,依据条件选择配置类(实现 ImportSelector 接口),如果并不确定引入哪个配置类,需要根据@Import注解所标识的类或者另一个注解(通常是注解)里的定义信息选择配置类的话,用这种方式。3,动态注册Bean(实现 ImportBeanDefinitionRegistrar 接口),一般只要用户确切知道哪些Bean需要放入容器的话,自己可以通过spring 提供的注解来标识就可以了,比如@Component,@Service,@Repository,@Bean等。 如果是不确定的类,或者不是spring专用的,所以并不想用spring的注解进行侵入式标识,那么就可以通过@Import注解,实现ImportBeanDefinitionRegistrar接口来动态注册Bean。 Mybatis 中大名鼎鼎的@MapperScan 也是如此 -
AutoConfigurationImportSelector,自动加载配置的选择器(根据条件进行自动加载配置),这个类中有一个方法,getCandidateConfigurations(根据某个特定的条件获取配置信息),方法体中loadFactoryNames():表示根据name的属性值来获取加载器信息,是根据了META-INF/spring.factories文件进行加载配置,根据factories文件映射到了配置类中
-
MybatisAutoConfiguration.java是经过xml配置文件转换来的,和xml配置文件一样的作用
DataSource,SqlSessionFactory,sqlSessionTemplate
- 因此@SpringBootApplication: 相当于@Configuration、@EnableAutoConfiguration、@ComponentScan的组合,是个等价的关系
- 补充点 @RestController 是个组合注解 是@Controller@ResponseBody 的组合,直接返回数据给前端
4.2 以上步骤画图解析springboot的自动配置流程
2,以mybatis的自动配置为例,追踪源码查看MyBatis的配置类,org.mybatis.spring.boot:mybatis-spring-boot-autoconfigure:2.1.1-->META-INF-->spring.factories-->MybatisAutoConfiguration
4.2 SpringBoot常用的starter
直接看官网,官网中全部的starter介绍
在springboot中会定义很多个starter,每一个starter都有自己不同的作用
4.3 springBoot 的配置文件
在官方文档中说明,可以使用文件进行配置数据源信息(就是springboot的主配置文件,也是以后操作最多的文件)常见的配置文件后缀名 properties,yml(ymal)
命名有规范:
文件名必须要使用application,否则springboot无法识别
application.properties
application.yml
存放位置有规定:
官方推荐把配置文件放在resources目录(classpath)-->config文件夹
如果config文件夹springboot没有检测到会从resources目录下去找
resources:classpath(根目录)
resources/config
以下给出yml配置文件的写法。注意冒号后面要有空格,一级一级的关系
#server 的配置
server:
port: 8087
#数据源的配置
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring01
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
#mybatis的配置信息
mybatis:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.qy105.aaa.model
mapper-locations: mapper/*Mapper.xml
4.4 SpringBoot集成单元测试
test测试类的使用案例
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceImplTest {
@Autowired
private UserMapper userMapper;
@Autowired
private WebApplicationContext applicationContext;
private MockMvc mockMvc;
@Before
public void setupMockMvc() {
mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext).build();
// mvc= MockMvcBuilders.standaloneSetup(new MyController()).build();
}
@Test
public void getName() throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/getUser");
MvcResult mvcResult = mockMvc.perform(builder)
.andDo(MockMvcResultHandlers.print())
.andReturn();
}
@org.junit.Test
public void getAccountById() {
Account accountById = userMapper.getAccountById(1);
System.out.println(accountById);
Account accountById1 = userMapper.getAccountById(1);
System.out.println(accountById1);
}
}
4.5 log的配置
代码中如何使用log
private static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
logging.file.name=demo.log
logging.level.root=debug
对之前留的思考题进行解答
-
spring与MyBatis集成之后一级缓存是否有效,什么时候会有效?
- 在未开启事务的情况之下,每次查询,spring都会关闭旧的sqlSession而创建新的sqlSession,因此此时的一级缓存是没有启作用的
- 在开启事务的情况之下,spring使用threadLocal获取当前资源绑定同一个sqlSession,因此此时一级缓存是有效的
一级缓存失效原因:
1,获得请求
2,spring检查到了这种需求,于是去申请一个mybatis的sqlsession(资源池),并将申请到的sqlsession与当前线程绑定,放入threadlocal里面
3,template从threadlocal获取到sqlsession,去执行查询
4,查询结束,清空threadlocal中与当前线程绑定的sqlsession,释放资源
5,我们又需要访问数据,拿到新的session
6,返回到步骤2
如何开启二级缓存:
xml配置:
maper接口:添加注解 @CacheNamespace
-
controller是单例的吗,单例会出现什么问题
以下为复习
spring bean作用域有以下5个:
singleton:单例模式,当spring创建applicationContext容器的时候,spring会欲初始化所有的该作用域实例,加上lazy-init就可以避免预处理;
prototype:原型模式,每次通过getBean获取该bean就会新产生一个实例,创建后spring将不再对其管理;
====下面是在web项目下才用到的===
**request:**搞web的大家都应该明白request的域了吧,就是每次请求都新产生一个实例,和prototype不同就是创建后,接下来的管理,spring依然在监听
session:每次会话,同上
global session:全局的web域,类似于servlet中的application
单例会出现什么问题?
如果其中定义了实例变量,会引起线程安全的问题
那为什么默认使用单例??:
为了性能,有些情况确实不需要多例
最佳实践:
-
不要在controller中定义成员变量(实例变量)。
-
万一必须要定义一个非静态成员变量时候,则通过注解@Scope("prototype"),将其设置为多例模式
-
-
如何声明为一个多例的controller
@Scope("prototype")
4.6 SpringApplication run()源码分析
SpringApplication 这个类应该算是 SpringBoot 框架 的“创新”产物了,原始的 Spring中并没有这个类,SpringApplication 里面封装了一套 Spring 应用的启动流程,然而这对用户完全透明,因此我们上手 SpringBoot 时感觉简洁、轻量。
一般来说默认的 SpringApplication 执行流程已经可以满足大部分需求,但是 若用户想干预这个过程,则可以通过 SpringApplication 在流程某些地方开启的 扩展点 来完成对流程的扩展,典型的扩展方案那就是使用 set 方法。
举个简单例子,用户想关闭小彩蛋
SpringApplication springApplication = new SpringApplication(WebApplication.class);
springApplication.setBannerMode(Banner.Mode.OFF);
springApplication.run(args);
这样一拆解后我们发现,我们也需要先构造 SpringApplication 类对象,然后调用该对象的 run() 方法。那么接下来就讲讲 SpringApplication 的构造过程 以及其 run() 方法的流程,搞清楚了这个,那么也就搞清楚了SpringBoot应用是如何运行起来的!
public ConfigurableApplicationContext run(String... args) {
//spring 提供的计时器
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
this.configureHeadlessProperty();
//1通过 SpringFactoriesLoader 加载 META-INF/spring.factories 文件,
//获取并创建 SpringApplicationRunListener 对象
SpringApplicationRunListeners listeners = this.getRunListeners(args);
//2然后由 SpringApplicationRunListener 来发出 starting 消息
listeners.starting();
Collection exceptionReporters;
try {
//3创建参数,并配置当前 SpringBoot 应用将要使用的 Environment
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//4完成之后,依然由 SpringApplicationRunListener 来发出 environmentPrepared 消息
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
//5创建 ApplicationContext
context = this.createApplicationContext();
//6初始化 ApplicationContext,并设置 Environment,加载相关配置等
exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
//7由 SpringApplicationRunListener 来发出 contextPrepared 消息,
//告知SpringBoot 应用使用的 ApplicationContext 已准备OK
//8将各种 beans 装载入 ApplicationContext,继续由 SpringApplicationRunListener
//来发出 contextLoaded 消息,告知 SpringBoot 应用使用的 ApplicationContext 已装填OK
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
//9 refresh ApplicationContext,完成IoC容器可用的最后一步
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
stopWatch.stop();//计时器结束
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}
//10.由 SpringApplicationRunListener 来发出 started 消息
listeners.started(context);
//11完成最终的程序的启动
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, exceptionReporters, listeners);
throw new IllegalStateException(var10);
}
try {
//12 SpringApplicationRunListener 来发出 running 消息,告知程序已运行起来了
listeners.running(context);
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}