SpringBoot进阶

SpringBoot进阶

SpringBoot进阶

1.上节回顾

  1. springboot有哪些特点
  2. SpringBoot相比Spring而言有没有功能上的增强?
  3. 手动搭建springboot项目结合MyBatis
  4. 启动类的放置位置
  5. Lombok的使用

2.常见面试题

  1. Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?

3.本章重点

  1. SpringBoot核心注解解析以及SpringBoot自动配置原理是什么??
  2. SpringBoot的启动源码分析
  3. springboot集成单元测试
  4. 常见面试题解答

4.内容详解

4.1 SpringBoot核心注解 @SpringBootApplication 解析

  1. @SpringbootApplication是一个组合注解

    • @SpringBootConfiguration,此注解是springboot所必须要的基础配置(application.xml)
    • @EnableAutoConfiguration: 自动加载配置
  2. @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

  1. 因此@SpringBootApplication: 相当于@Configuration、@EnableAutoConfiguration、@ComponentScan的组合,是个等价的关系
  2. 补充点 @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

对之前留的思考题进行解答

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

  2. 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"),将其设置为多例模式

  3. 如何声明为一个多例的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);
        }
    }

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://championmi.cn/archives/springboot进阶md

Buy me a cup of coffee ☕.