Java 系列之 Springboot

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、Springboot 简介?
    • 1.1 什么是启动器?
    • 1.2 Springboot 优点
    • 1.3 Springboot 核心
  • 二、搭建方式
    • 2.1 搭建方式一
    • 2.2 搭建方式二
    • 2.3 搭建方式三
  • 三、启动原理
    • 3.1 初始化SrpingApplication对象
    • 3.2 执行run()方法
      • 1. 加载监听器
      • 2. 构造上下文环境
      • 3. 初始化应用上下文
      • 4. 刷新应用上下文准备阶段
      • 5. 刷新上下文
      • 6. 自动装配原理总结
  • 四、项目配置
    • 4.1 properties配置文件
    • 4.2 yml配置文件
    • 4.3 配置目录及优先级
      • 1. 配置文件存放位置
      • 2. 配置文件存放读取优先级
      • 3. 配置文件
      • 4. springboot 项目结构
  • 五、整合Mybatis
    • 5.1 导入依赖
    • 5.2 编写配置文件: appliction.yml中添加如下配置
    • 5.3 编写功能代码
  • 六、整合logback
  • 七、整合PageHelper
    • 7.1 PageHelper插件
    • 7.2 实现原理
    • 7.3 使用方法
    • 7.4 PageInfo对象和Page对象的区别
  • 八、整合Druid
  • 九、整合JSP
  • 十、整合FreeMarker
    • 10.1 介绍
    • 10.2 使用
    • 10.3 常用指令
  • 十一、整合Thymeleaf
    • 11.1 介绍
    • 11.2 基础语法
    • 11.3 内置对象
  • 十二、 模板引擎总结
    • 12.1 jsp
    • 12.2 freemarker
    • 12.3 Thymeleaf
  • 十三、项目打包部署
    • 13.1 打包 jar
    • 13.2 打包war
  • 十四、异常处理
    • 14.1 简介
    • 14.2 具体实现
  • 十五、单元测试类
  • 十六、bean管理
  • 十七、拦截器
  • 十八、 其他拓展
    • 18.1 注解拓展
    • 18.2 静态资源类
    • 18.3 文件上传
    • 18.4 MyBatis-plus
    • 18.5 JUnit5单元测试
  • 总结


前言

  • Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。

一、Springboot 简介?

  • Spring Boot实际上是利用Spring Framework 4 自动配置特性完成。编写项目时不需要编写xml文件。发展到现在,Spring Boot已经具有很很大的生态圈,各种主流技术已经都提供了Spring Boot的启动器。

1.1 什么是启动器?

  • Spring框架在项目中作用是Spring整合各种其他技术,让其他技术使用更加方便。Spring Boot的启动器实际上就是一个依赖。这个依赖中包含了整个这个技术的相关jar包,还包含了这个技术的自动配置,以前绝大多数XML配置都不需要配置了。当然了,启动器中自动配置无法实现所有内容的自动配置,在使用Spring Boot时还需要进行少量的配置(这个配置不是在xml中了,而是在properties或yml中即可)。如果是Spring自己封装的启动器的artifact id名字满足:spring-boot-starter-xxxx,如果是第三方公司提供的启动满足:xxxx-spring-boot-starter。以后每次使用Spring Boot整合其他技术时首先需要考虑导入启动器。

1.2 Springboot 优点

  1. 使用Spring Boot可以创建独立的Spring应用程序
  2. 在Spring Boot中直接嵌入了Tomcat、Jetty、Undertow等Web 容器,在使用SpringBoot做Web开发时不需要部署WAR文件
  3. 通过提供自己的启动器(Starter)依赖,简化项目构建配置
  4. 尽量的自动配置Spring和第三方库
  5. 绝对没有代码生成,也不需要XML配置文件

1.3 Springboot 核心

  • 起步依赖- 起步依赖本质上是一个Maven项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。 简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能。
    自动配置 -Spring Boot的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素,才决定 Spring配置应该用哪个,不该用哪个。该过程是Spring自动完成的。

二、搭建方式

2.1 搭建方式一

  1. 导入依赖1

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <!--继承父项目方式-->
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.4.5</version>
        </parent>
        <groupId>com.msb</groupId>
        <artifactId>springboot01</artifactId>
        <version>1.0-SNAPSHOT</version>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.4.5</version>
            </dependency>
        </dependencies>
    </project>
    
  2. 依赖传递
    Java 系列之 Springboot_第1张图片

  3. 创建一个controller

package com.msb.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
 * @Author: bingwoo
 */
@Controller
public class FirstController {
    @RequestMapping("/firstController")
    @ResponseBody
    public String firstController(){
        return "hello springboot";
    }
}
  1. 创建启动类
  • Spring Boot的启动类的作用是启动Spring Boot项目,是基于Main方法来运行的。
  • 注意:启动类在启动时会做注解扫描(@Controller、@Service、@Repository…),扫描位置为同包或者子包下的注解,所以启动类的位置应放于包的根下。
  1. 启动类与启动器区别
  • 启动类表示项目的启动入口

  • 启动器表示jar包的坐标
    必须在包中新建这个类,不能直接放入到java文件夹。
    在com.msb下新建自定义名称的类(规范:XXXXApplication),可以是项目上下文路径Application

    package com.msb;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    //启动类
    //可以自动扫描当前类所在包及子包的注解
    //注意:此类要放入到包中  在 和controller包同一个层次即可
    @SpringBootApplication
    public class Springboot01Application {
        public static void main(String[] args) {
            SpringApplication.run(Springboot02Application.class, args);
        }
    }
    
  1. 启动日志
    Java 系列之 Springboot_第2张图片

2.2 搭建方式二

  • 在公司中可能会出现必须继承某个项目,如果Spring Boot用了继承就不能继承别的项目了。所以Spring Boot还提供了依赖的方式。

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.msb</groupId>
        <artifactId>springboot01</artifactId>
        <version>1.0-SNAPSHOT</version>
        
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>2.4.5</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.4.5</version>
            </dependency>
        </dependencies>
    </project>
    
  • 使用idea自带springBoot项目初始化插件
    Java 系列之 Springboot_第3张图片
    Java 系列之 Springboot_第4张图片
    Java 系列之 Springboot_第5张图片
    Java 系列之 Springboot_第6张图片
    Java 系列之 Springboot_第7张图片

2.3 搭建方式三

  • 在spingboot官网,直接创建:官网地址
    Java 系列之 Springboot_第8张图片

三、启动原理

  • 简单了解启动原理可以看这边文章:文字地址,想详细了解:建议耐心看完本

  • SpringBoot的入口是从SpringApplication.run()传入我们的主启动类开始

    @SpringBootApplication
    public class LeeSpringbootApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(LeeSpringbootApplication.class, args);
        }
    }
    
  • run()方法

    1. 初始化SrpingApplication对象
    2. 执行run() 方法(primarySources:主启动类class)
      public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
          return new SpringApplication(primarySources).run(args);
      }
      

3.1 初始化SrpingApplication对象

  1. 设置应用类型,后面会根据类型初始化对应的环境,常用的一般都是servlet环境

  2. 加载系统中引导器Bootstrapper(从META-INF/spring.factories中加载)

  3. 初始化classpath下 META-INF/spring.factories 中已配置的ApplicationContextInitalizer

  4. 初始化classpath下所以已配置的 ApplicationListener

  5. 根据调用栈,设置 main 方法的类名

    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        //设置资源加载器为null
        this.resourceLoader = resourceLoader;
        //断言加载资源不能为null
        Assert.notNull(primarySources, "PrimarySources must not be null");
        //将primarySources数组转换为list,最后放到LinkedHashSet集合中
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
        // 1.1 推断应用类型,后面会根据类型初始化对应的环境,常用的一般都是servlet环境
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        // 1.2 加载系统中引导器Bootstrapper
        this.bootstrapRegistryInitializers = new ArrayList<>(
                getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
        // 1.3 初始化classpath下 META-INF/spring.factories 中已配置的ApplicationContextInitalizer
        setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        // 1.4 初始化classpath下所以已配置的 ApplicationListener
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        // 1.5 根据调用栈,设置 main 方法的类名
        this.mainApplicationClass = deduceMainApplicationClass();
    }
    
  • 在执行 getSpringFactoriesInstances(BootstrapRegistryInitializer. class ) 中会调用 loadSpringFactories() 方法遍历所有jar包中classpath下 META-INF/spring.factories文件,并保存在缓存中

    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = cache.get(classLoader);
        if (result != null) {
            return result;
        }
    
        result = new HashMap<>();
        try {
            Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                for (Map.Entry<?, ?> entry : properties.entrySet()) {
                    String factoryTypeName = ((String) entry.getKey()).trim();
                    String[] factoryImplementationNames =
                            StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                    for (String factoryImplementationName : factoryImplementationNames) {
                        result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                                .add(factoryImplementationName.trim());
                    }
                }
            }
    
            // Replace all lists with unmodifiable lists containing unique elements
            result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
                    .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
            cache.put(classLoader, result);
        }
        catch (IOException ex) {
            throw new IllegalArgumentException("Unable to load factories from location [" +
                    FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
        return result;
    }
    

3.2 执行run()方法

  1. 获取并启动监听器

  2. 构造上下文环境

  3. 初始化应用上下文

  4. 刷新应用上下文前的准备阶段

  5. 刷新上下文

  6. 刷新应用上下文后的扩展接口

    public ConfigurableApplicationContext run(String... args) {
        //记录程序运行时间
        long startTime = System.nanoTime();
        // 创建 DefaultBootstrapContext 的一项
        DefaultBootstrapContext bootstrapContext = createBootstrapContext();
        // ConfigurableApplicationContext spring的上下文
        ConfigurableApplicationContext context = null;
        configureHeadlessProperty();
        // 1、获取并启动监听器
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting(bootstrapContext, this.mainApplicationClass);
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            // 2、构造上下文环境
            ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            // 处理需要忽略的Bean
            configureIgnoreBeanInfo(environment);
            // 打印banner (springboot图标)
            Banner printedBanner = printBanner(environment);
            // 3、初始化应用上下文
            context = createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
            // 4、刷新应用上下文前的准备阶段
            prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            // 5、刷新上下文
            refreshContext(context);
            // 6、刷新应用上下文后的扩展接口
            afterRefresh(context, applicationArguments);
            // 记录执行时间
            Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
            }
            listeners.started(context, timeTakenToStartup);
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, listeners);
            throw new IllegalStateException(ex);
        }
        try {
            Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
            listeners.ready(context, timeTakenToReady);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, null);
            throw new IllegalStateException(ex);
        }
        return context;
    }
    

1. 加载监听器

  • 加载META-INF/spring.factories 中的 SpringApplicationRunListener,SpringApplicationRunListeners负责在springBoot启动的不同阶段,广播出不同的消息,传递给ApplicationListener监听器实现类

    private SpringApplicationRunListeners getRunListeners(String[] args) {
        Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
        return new SpringApplicationRunListeners(logger,
                getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
                this.applicationStartup);
    }
    
  • 在 getSpringFactoriesInstances 中加载构建监听器对象并根据order进行排序

    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = getClassLoader();
        // Use names and ensure unique to protect against duplicates
        Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }
    

2. 构造上下文环境

  • 根据之前标记的应用类型(SERVLET)创建相应的环境,并根据配置文件,配置相应的系统环境

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
                DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
        // Create and configure the environment
        //  创建并配置相应环境
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        // 根据用户配置,配置系统环境
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach(environment);
        // 启动监听器,其中一个重要的监听器 ConfigFileApplicationListener 加载项目配置文件的监听器
        listeners.environmentPrepared(bootstrapContext, environment);
        DefaultPropertiesPropertySource.moveToEnd(environment);
        Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
                "Environment prefix cannot be set via properties.");
        bindToSpringApplication(environment);
        if (!this.isCustomEnvironment) {
            environment = convertEnvironment(environment);
        }
        ConfigurationPropertySources.attach(environment);
        return environment;
    }
    

3. 初始化应用上下文

  • 根据配置的应用类型( SERVLET )创建对应的context ( AnnotationConfigServletWebServerApplicationContext ) 并在父类 GenericApplicationContext 的构造方法中创建了DefaultListableBeanFactory(ioc容器)

    protected ConfigurableApplicationContext createApplicationContext() {
       return this.applicationContextFactory.create(this.webApplicationType);
    }
    
    ApplicationContextFactory DEFAULT = (webApplicationType) -> {
        try {
            switch (webApplicationType) {
            case SERVLET:
                return new AnnotationConfigServletWebServerApplicationContext();
            case REACTIVE:
                return new AnnotationConfigReactiveWebServerApplicationContext();
            default:
                return new AnnotationConfigApplicationContext();
            }
        }
        catch (Exception ex) {
            throw new IllegalStateException("Unable create a default ApplicationContext instance, "
                    + "you may need a custom ApplicationContextFactory", ex);
        }
    };
    

4. 刷新应用上下文准备阶段

  • 主要完成应用上下文属性设置,并且将启动类生成实例对象保存到容器中。

    private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, Banner printedBanner) {
        // 设置容器环境
        context.setEnvironment(environment);
        // 执行容器后置处理(主要设置转换器)
        postProcessApplicationContext(context);
        // 应用初始化器,执行容器中的 ApplicationContextInitializer 包括spring.factories
        applyInitializers(context);
        // 向各个容器中发送容器已经准备好的事件
        listeners.contextPrepared(context);
        bootstrapContext.close(context);
        if (this.logStartupInfo) {
            logStartupInfo(context.getParent() == null);
            logStartupProfileInfo(context);
        }
        // Add boot specific singleton beans
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        // 将main函数中的args参数封装成单例Bean,注册到容器
        beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
        if (printedBanner != null) {
            // 将printedBanner 封装成单例Bean 注册到容器
            beanFactory.registerSingleton("springBootBanner", printedBanner);
        }
        if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
            ((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(this.allowCircularReferences);
        if (beanFactory instanceof DefaultListableBeanFactory) {
            ((DefaultListableBeanFactory) beanFactory)
                .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
        }
        }
        if (this.lazyInitialization) {
            context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
        }
        // Load the sources
        // 获取主启动类
        Set<Object> sources = getAllSources();
        Assert.notEmpty(sources, "Sources must not be empty");
        // 加载启动类,将启动类注册到容器
        load(context, sources.toArray(new Object[0]));
        // 发布容器中已加载的事件
        listeners.contextLoaded(context);
    }
    
  • postProcessApplicationContext(context) 设置转换器

    protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
        if (this.beanNameGenerator != null) {
            context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,
                    this.beanNameGenerator);
        }
        if (this.resourceLoader != null) {
            if (context instanceof GenericApplicationContext) {
                ((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
            }
            if (context instanceof DefaultResourceLoader) {
                ((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
            }
        }
        if (this.addConversionService) {
            context.getBeanFactory().setConversionService(context.getEnvironment().getConversionService());
        }
    }
    
  • 应用ApplicationContextInitializer

    protected void applyInitializers(ConfigurableApplicationContext context) {
        for (ApplicationContextInitializer initializer : getInitializers()) {
            Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
                    ApplicationContextInitializer.class);
            Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
            initializer.initialize(context);
        }
    }
    
  • getAllSource() 获取主启动类

    public Set<Object> getAllSources() {
        Set<Object> allSources = new LinkedHashSet<>();
        if (!CollectionUtils.isEmpty(this.primarySources)) {
            allSources.addAll(this.primarySources);
        }
        if (!CollectionUtils.isEmpty(this.sources)) {
            allSources.addAll(this.sources);
        }
        return Collections.unmodifiableSet(allSources);
    }
    
  • load() 主要将主启动类生成实例对象保存在容器中,spring容器在启动的时候会将类解析成spring内部的BeanDefinition结构,并将BeanDefinition存储到DefaultListableBeanFactory的map中。

    protected void load(ApplicationContext context, Object[] sources) {
        if (logger.isDebugEnabled()) {
            logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
        }
        // 创建 BeanDefinitionLoader
        BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
        if (this.beanNameGenerator != null) {
            loader.setBeanNameGenerator(this.beanNameGenerator);
        }
        if (this.resourceLoader != null) {
            loader.setResourceLoader(this.resourceLoader);
        }
        if (this.environment != null) {
            loader.setEnvironment(this.environment);
        }
        // 将启动类生成实例对象保存到容器中
        loader.load();
    }
    
    //getBeanDefinitionRegistry(context) 将上下文转换为 BeanDefinitionRegistry 类型
    private BeanDefinitionRegistry getBeanDefinitionRegistry(ApplicationContext context) {
        if (context instanceof BeanDefinitionRegistry) {
            return (BeanDefinitionRegistry) context;
        }
        if (context instanceof AbstractApplicationContext) {
            return (BeanDefinitionRegistry) ((AbstractApplicationContext) context).getBeanFactory();
        }
        throw new IllegalStateException("Could not locate BeanDefinitionRegistry");
    }
    
  • createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources) 创建 BeanDefinitionLoader,其中创建一些Bean定义读取器 。

    protected BeanDefinitionLoader createBeanDefinitionLoader(BeanDefinitionRegistry registry, Object[] sources) {
        return new BeanDefinitionLoader(registry, sources);
    }
    
    BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
        Assert.notNull(registry, "Registry must not be null");
        Assert.notEmpty(sources, "Sources must not be empty");
        this.sources = sources;
        // 创建注解形式的Bean定义读取器, eg:@Configuration @Bean @Component @Controller等
        this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
        // 创建xml形式的Bean定义读取器
        this.xmlReader = (XML_ENABLED ? new XmlBeanDefinitionReader(registry) : null);
        this.groovyReader = (isGroovyPresent() ? new GroovyBeanDefinitionReader(registry) : null);
        // 创建类路径扫描器
        this.scanner = new ClassPathBeanDefinitionScanner(registry);
        // 扫描器添加排除过滤器
        this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));
    }
    
  • loader.load()将启动类生成实例对象保存在容器中 。

    void load() {
        for (Object source : this.sources) {
        //source 为启动类
            load(source);        
        }
    }
    
    private void load(Object source) {
        Assert.notNull(source, "Source must not be null");
        if (source instanceof Class<?>) {
        // 从class中加载
            load((Class<?>) source);
            return;
        }
        if (source instanceof Resource) {
        // 从 Resource 中加载
            load((Resource) source);
            return;
        }
        if (source instanceof Package) {
        // 从 Package 中加载
            load((Package) source);
            return;
        }
        if (source instanceof CharSequence) {
        // 从 CharSequence 中加载
            load((CharSequence) source);
            return;
        }
        throw new IllegalArgumentException("Invalid source type " + source.getClass());
    }
    
    private void load(Class<?> source) {
        if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
            // Any GroovyLoaders added in beans{} DSL can contribute beans here
            GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
            ((GroovyBeanDefinitionReader) this.groovyReader).beans(loader.getBeans());
        }
        if (isEligible(source)) {
            // 将启动类的 BeanDefinition 注册到 BeanDefinitionMap 中
            this.annotatedReader.register(source);
        }
    }
    

5. 刷新上下文

  • 主要逻辑为AbstractApplicationContext 对象的 refresh() 方法,进行整个容器的刷新过程,会调用spring中的refresh()方法,其中有13个关键方法,来完成整个SpringBoot应用程序的启动。

    private void refreshContext(ConfigurableApplicationContext context) {
        if (this.registerShutdownHook) {
            shutdownHook.registerApplicationContext(context);
        }
        refresh(context);
    }
    
    protected void refresh(ConfigurableApplicationContext applicationContext) {
        applicationContext.refresh();
    }
    
    public final void refresh() throws BeansException, IllegalStateException {
        try {
            super.refresh();
        }
        catch (RuntimeException ex) {
            WebServer webServer = this.webServer;
            if (webServer != null) {
                webServer.stop();
            }
            throw ex;
        }
    }
    
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
    
            // Prepare this context for refreshing.
            //1:准备刷新上下文环境
            prepareRefresh();
    
            // Tell the subclass to refresh the internal bean factory.
            //2:获取告诉子类初始化Bean工厂  不同工厂不同实现
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    
            // Prepare the bean factory for use in this context.
            // 对bean工厂进行填充属性
            prepareBeanFactory(beanFactory);
    
            try {
                // Allows post-processing of the bean factory in context subclasses.
                // 执行beanFactroy后置处理器
                postProcessBeanFactory(beanFactory);
    
                StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
                // Invoke factory processors registered as beans in the context.
                // 调用我们的bean工厂的后置处理器. 1. 会在此将class扫描成beanDefinition  2.bean工厂的后置处理器调用
                invokeBeanFactoryPostProcessors(beanFactory);
    
                // Register bean processors that intercept bean creation.
                // 注册我们bean的后置处理器
                registerBeanPostProcessors(beanFactory);
                beanPostProcess.end();
    
                // Initialize message source for this context.
                // 初始化国际化资源处理器.
                initMessageSource();
    
                // Initialize event multicaster for this context.
                // 创建事件多播器
                initApplicationEventMulticaster();
    
                // Initialize other special beans in specific context subclasses.
                // 这个方法同样也是留个子类实现的springboot也是从这个方法进行启动tomcat的.
                onRefresh();
    
                // Check for listener beans and register them.
                //把我们的事件监听器注册到多播器上
                registerListeners();
    
                // Instantiate all remaining (non-lazy-init) singletons.
                // 实例化我们剩余的单实例bean.
                finishBeanFactoryInitialization(beanFactory);
    
                // Last step: publish corresponding event.
                // 最后容器刷新 发布刷新事件(Spring cloud也是从这里启动的)
                finishRefresh();
            }
    
            catch (BeansException ex) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
                }
    
                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();
    
                // Reset 'active' flag.
                cancelRefresh(ex);
    
                // Propagate exception to caller.
                throw ex;
            }
    
            finally {
                // Reset common introspection caches in Spring's core, since we
                // might not ever need metadata for singleton beans anymore...
                resetCommonCaches();
                contextRefresh.end();
            }
        }
    }
    
  • invokeBeanFactoryPostProcessors(beanFactory) 改方法会解析核心启动类中 @SpringBootApplication实现自动配置

  • Ioc容器的初始化包括三个步骤,该三个步骤在 invokeBeanFactoryPostProcessors 中完成

  • Resource定位

    • 在SpringBoot中,包扫描是从主类所在包开始扫描,prepareContext()方法中,会将主类解析成BeanDefinition保存在容器中,然后在refresh()方法的 invokeBeanFactoryPostProcessors ()方法中解析主类的BeanDefinition获取basePackage的路径。这样就完成了定位的过程。
    • SpringBoot的各种starter是通过SPI扩展机制实现的自动装配,SpringBoot的自动装配同样也是在 invokeBeanFactoryPostProcessors() 方法中实现的。在SpringBoot中有很多的@EnableXXX注解,其底层是@Import注解,在 invokeBeanFactoryPostProcessors() 方法中也实现了对该注解指定的配置类的定位加载。
    • 常规在SpringBoot中有三种定位方法:主类所在的包、SPI扩展机制实现的自动装配、@Import注解指定的类
    • SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类, 也可以这样理解 : SPI是“基于接口的编程+策略模式+配置文件”组成实现的动态加载机制。
  • BeanDefinition的载入

    • SpringBoot会将通过定位得到的basePackage的路径拼装成 classpath:com/***/.class 的形式,然后 PathMatchingResourcePatternResolver类会将该路径下所有的 .class 文件加载进来,然后进行遍历判断是否含有 @Component 注解,如果有就是要装载的 BeanDefinition。
  • 注册Beanfinition

    • 注册过程是将载入过程中解析得到的BeanDefinition向IOC容器进行注册。通过上下文分析,在容器中将BeanDefinition注入到一个ConcurrenHashMap中,IOC容器通过这个map来保存BeanDefinition数据。

      protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
          PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
      
          // Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
          // (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
          if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
              beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
              beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
          }
      }
      
      // PostProcessorRegistrationDelegate
      public static void invokeBeanFactoryPostProcessors(
              ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
      
          // WARNING: Although it may appear that the body of this method can be easily
          // refactored to avoid the use of multiple loops and multiple lists, the use
          // of multiple lists and multiple passes over the names of processors is
          // intentional. We must ensure that we honor the contracts for PriorityOrdered
          // and Ordered processors. Specifically, we must NOT cause processors to be
          // instantiated (via getBean() invocations) or registered in the ApplicationContext
          // in the wrong order.
          //
          // Before submitting a pull request (PR) to change this method, please review the
          // list of all declined PRs involving changes to PostProcessorRegistrationDelegate
          // to ensure that your proposal does not result in a breaking change:
          // https://github.com/spring-projects/spring-framework/issues?q=PostProcessorRegistrationDelegate+is%3Aclosed+label%3A%22status%3A+declined%22
      
          // Invoke BeanDefinitionRegistryPostProcessors first, if any.
          Set<String> processedBeans = new HashSet<>();
      
          if (beanFactory instanceof BeanDefinitionRegistry) {
              BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
              List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
              List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();
      
              for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
                  if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
                      BeanDefinitionRegistryPostProcessor registryProcessor =
                              (BeanDefinitionRegistryPostProcessor) postProcessor;
                      registryProcessor.postProcessBeanDefinitionRegistry(registry);
                      registryProcessors.add(registryProcessor);
                  }
                  else {
                      regularPostProcessors.add(postProcessor);
                  }
              }
      
              // Do not initialize FactoryBeans here: We need to leave all regular beans
              // uninitialized to let the bean factory post-processors apply to them!
              // Separate between BeanDefinitionRegistryPostProcessors that implement
              // PriorityOrdered, Ordered, and the rest.
              List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();
      
              // First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.
              String[] postProcessorNames =
                      beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
              for (String ppName : postProcessorNames) {
                  if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
                      currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
                      processedBeans.add(ppName);
                  }
              }
              sortPostProcessors(currentRegistryProcessors, beanFactory);
              registryProcessors.addAll(currentRegistryProcessors);
              invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
              currentRegistryProcessors.clear();
      
              // Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
              postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
              for (String ppName : postProcessorNames) {
                  if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
                      currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
                      processedBeans.add(ppName);
                  }
              }
              sortPostProcessors(currentRegistryProcessors, beanFactory);
              registryProcessors.addAll(currentRegistryProcessors);
              invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
              currentRegistryProcessors.clear();
      
              // Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
              boolean reiterate = true;
              while (reiterate) {
                  reiterate = false;
                  postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
                  for (String ppName : postProcessorNames) {
                      if (!processedBeans.contains(ppName)) {
                          currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
                          processedBeans.add(ppName);
                          reiterate = true;
                      }
                  }
                  sortPostProcessors(currentRegistryProcessors, beanFactory);
                  registryProcessors.addAll(currentRegistryProcessors);
                  invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
                  currentRegistryProcessors.clear();
              }
      
              // Now, invoke the postProcessBeanFactory callback of all processors handled so far.
              invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
              invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
          }
      
          else {
              // Invoke factory processors registered with the context instance.
              invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
          }
      
          // Do not initialize FactoryBeans here: We need to leave all regular beans
          // uninitialized to let the bean factory post-processors apply to them!
          String[] postProcessorNames =
                  beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);
      
          // Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
          // Ordered, and the rest.
          List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
          List<String> orderedPostProcessorNames = new ArrayList<>();
          List<String> nonOrderedPostProcessorNames = new ArrayList<>();
          for (String ppName : postProcessorNames) {
              if (processedBeans.contains(ppName)) {
                  // skip - already processed in first phase above
              }
              else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
                  priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
              }
              else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
                  orderedPostProcessorNames.add(ppName);
              }
              else {
                  nonOrderedPostProcessorNames.add(ppName);
              }
          }
      
          // First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
          sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
          invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);
      
          // Next, invoke the BeanFactoryPostProcessors that implement Ordered.
          List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
          for (String postProcessorName : orderedPostProcessorNames) {
              orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
          }
          sortPostProcessors(orderedPostProcessors, beanFactory);
          invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);
      
          // Finally, invoke all other BeanFactoryPostProcessors.
          List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
          for (String postProcessorName : nonOrderedPostProcessorNames) {
              nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
          }
          invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);
      
          // Clear cached merged bean definitions since the post-processors might have
          // modified the original metadata, e.g. replacing placeholders in values...
          beanFactory.clearMetadataCache();
      }
      
      private static void invokeBeanDefinitionRegistryPostProcessors(
              Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) {
      
          for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
              StartupStep postProcessBeanDefRegistry = applicationStartup.start("spring.context.beandef-registry.post-process")
                      .tag("postProcessor", postProcessor::toString);
              // 解析注解
              postProcessor.postProcessBeanDefinitionRegistry(registry);
              postProcessBeanDefRegistry.end();
          }
      }
      
  • 实现自动装配:

    • invokeBeanFactoryPostProcessors ()方法主要是对 ConfigurationClassPostProcessor 类的处理,这是BeanDefinitionRegistryPostProcessor的子类,BeanDefinitionRegistryPostProcessor 是BeanDefinitionRegistryPostProcessor 的子类,调用BeanDefinitionRegistryPostProcessor中的postProcessBeanDefinitionRegistry()方法,会解析 @PropertySource @ComponentScans @ComponentScan @Bean @Import 等注解
      refresh() -> AbstractApplicationContext.invokeBeanFactoryPostProcessors(beanFactory) 
        -> invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()) 
        -> postProcessor.postProcessBeanDefinitionRegistry(registry) (ConfigurationClassPostProcessor类下的方法) 
        -> processConfigBeanDefinitions(registry) -> new ConfigurationClassParser() (解析@Configuration 标注的类) 
        -> parser.parse(candidates) (解析启动类上的注解)
        -> this.reader.loadBeanDefinitions(configClasses) (生效自动配置类)
      //     ConfigurationClassPostProcessor
      public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
          int registryId = System.identityHashCode(registry);
          if (this.registriesPostProcessed.contains(registryId)) {
              throw new IllegalStateException(
                      "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
          }
          if (this.factoriesPostProcessed.contains(registryId)) {
              throw new IllegalStateException(
                      "postProcessBeanFactory already called on this post-processor against " + registry);
          }
          this.registriesPostProcessed.add(registryId);
          
          processConfigBeanDefinitions(registry);
      }
      public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
          List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
          String[] candidateNames = registry.getBeanDefinitionNames();
      
          for (String beanName : candidateNames) {
              BeanDefinition beanDef = registry.getBeanDefinition(beanName);
              if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
                  if (logger.isDebugEnabled()) {
                      logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
                  }
              }
              else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
                  configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
              }
          }
      
          // Return immediately if no @Configuration classes were found
          if (configCandidates.isEmpty()) {
              return;
          }
      
          // Sort by previously determined @Order value, if applicable
          configCandidates.sort((bd1, bd2) -> {
              int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
              int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
              return Integer.compare(i1, i2);
          });
      
          // Detect any custom bean name generation strategy supplied through the enclosing application context
          SingletonBeanRegistry sbr = null;
          if (registry instanceof SingletonBeanRegistry) {
              sbr = (SingletonBeanRegistry) registry;
              if (!this.localBeanNameGeneratorSet) {
                  BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
                          AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
                  if (generator != null) {
                      this.componentScanBeanNameGenerator = generator;
                      this.importBeanNameGenerator = generator;
                  }
              }
          }
      
          if (this.environment == null) {
              this.environment = new StandardEnvironment();
          }
      
          // Parse each @Configuration class
          ConfigurationClassParser parser = new ConfigurationClassParser(
                  this.metadataReaderFactory, this.problemReporter, this.environment,
                  this.resourceLoader, this.componentScanBeanNameGenerator, registry);
      
          Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
          Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
          do {
              StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
              //获取所有bean的全路径(解析各类注解)
              parser.parse(candidates);
              parser.validate();
      
              Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
              configClasses.removeAll(alreadyParsed);
      
              // Read the model and create bean definitions based on its content
              if (this.reader == null) {
                  this.reader = new ConfigurationClassBeanDefinitionReader(
                          registry, this.sourceExtractor, this.resourceLoader, this.environment,
                          this.importBeanNameGenerator, parser.getImportRegistry());
              }
              // 使自动配置类生效
              this.reader.loadBeanDefinitions(configClasses);
              alreadyParsed.addAll(configClasses);
              processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();
      
              candidates.clear();
              if (registry.getBeanDefinitionCount() > candidateNames.length) {
                  String[] newCandidateNames = registry.getBeanDefinitionNames();
                  Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
                  Set<String> alreadyParsedClasses = new HashSet<>();
                  for (ConfigurationClass configurationClass : alreadyParsed) {
                      alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
                  }
                  for (String candidateName : newCandidateNames) {
                      if (!oldCandidateNames.contains(candidateName)) {
                          BeanDefinition bd = registry.getBeanDefinition(candidateName);
                          if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
                                  !alreadyParsedClasses.contains(bd.getBeanClassName())) {
                              candidates.add(new BeanDefinitionHolder(bd, candidateName));
                          }
                      }
                  }
                  candidateNames = newCandidateNames;
              }
          }
          while (!candidates.isEmpty());
      
          // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
          if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
              sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
          }
      
          if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
              // Clear cache in externally provided MetadataReaderFactory; this is a no-op
              // for a shared cache since it'll be cleared by the ApplicationContext.
              ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
          }
      }
      
    • parser.parse(candidates) 从启动类开始解析各种注解( @PropertySource @ComponentScan @Import @ImportResource @Bean),加载配置类,在processImports(configClass , sourceClass , getImports(sourceClass) , filter , true)中对启动类进行解析,加载其中的@Import注解的类
      public void parse(Set<BeanDefinitionHolder> configCandidates) {
          for (BeanDefinitionHolder holder : configCandidates) {
              BeanDefinition bd = holder.getBeanDefinition();
              try {
                  if (bd instanceof AnnotatedBeanDefinition) {
                      parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
                  }
                  else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
                      parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
                  }
                  else {
                      parse(bd.getBeanClassName(), holder.getBeanName());
                  }
              }
              catch (BeanDefinitionStoreException ex) {
                  throw ex;
              }
              catch (Throwable ex) {
                  throw new BeanDefinitionStoreException(
                          "Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
              }
          }
      
          this.deferredImportSelectorHandler.process();
      }
      
      protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
          processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
      }
      protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
          if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
              return;
          }
      
          ConfigurationClass existingClass = this.configurationClasses.get(configClass);
          if (existingClass != null) {
              if (configClass.isImported()) {
                  if (existingClass.isImported()) {
                      existingClass.mergeImportedBy(configClass);
                  }
                  // Otherwise ignore new imported config class; existing non-imported class overrides it.
                  return;
              }
              else {
                  // Explicit bean definition found, probably replacing an import.
                  // Let's remove the old one and go with the new one.
                  this.configurationClasses.remove(configClass);
                  this.knownSuperclasses.values().removeIf(configClass::equals);
              }
          }
      
          // Recursively process the configuration class and its superclass hierarchy.
          SourceClass sourceClass = asSourceClass(configClass, filter);
          do {
              sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
          }
          while (sourceClass != null);
      
          this.configurationClasses.put(configClass, configClass);
      }
      
      protected final SourceClass doProcessConfigurationClass(
              ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
              throws IOException {
      
          if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
              // Recursively process any member (nested) classes first
              processMemberClasses(configClass, sourceClass, filter);
          }
      
          // Process any @PropertySource annotations
          for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
                  sourceClass.getMetadata(), PropertySources.class,
                  org.springframework.context.annotation.PropertySource.class)) {
              if (this.environment instanceof ConfigurableEnvironment) {
                  processPropertySource(propertySource);
              }
              else {
                  logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                          "]. Reason: Environment must implement ConfigurableEnvironment");
              }
          }
      
          // Process any @ComponentScan annotations
          // 对启动类下的所有 @ComponentScan 进行解析加载,包含(@RestController @Service等)
          Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
                  sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
          if (!componentScans.isEmpty() &&
                  !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
              for (AnnotationAttributes componentScan : componentScans) {
                  // The config class is annotated with @ComponentScan -> perform the scan immediately
                  Set<BeanDefinitionHolder> scannedBeanDefinitions =
                          this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
                  // Check the set of scanned definitions for any further config classes and parse recursively if needed
                  for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                      BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                      if (bdCand == null) {
                          bdCand = holder.getBeanDefinition();
                      }
                      if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                          parse(bdCand.getBeanClassName(), holder.getBeanName());
                      }
                  }
              }
          }
      
          // Process any @Import annotations
          processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
      
          // Process any @ImportResource annotations
          AnnotationAttributes importResource =
                  AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
          if (importResource != null) {
              String[] resources = importResource.getStringArray("locations");
              Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
              for (String resource : resources) {
                  String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
                  configClass.addImportedResource(resolvedResource, readerClass);
              }
          }
      
          // Process individual @Bean methods
          Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
          for (MethodMetadata methodMetadata : beanMethods) {
              configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
          }
      
          // Process default methods on interfaces
          processInterfaces(configClass, sourceClass);
      
          // Process superclass, if any
          if (sourceClass.getMetadata().hasSuperClass()) {
              String superclass = sourceClass.getMetadata().getSuperClassName();
              if (superclass != null && !superclass.startsWith("java") &&
                      !this.knownSuperclasses.containsKey(superclass)) {
                  this.knownSuperclasses.put(superclass, configClass);
                  // Superclass found, return its annotation metadata and recurse
                  return sourceClass.getSuperClass();
              }
          }
      
          // No superclass -> processing is complete
          return null;
      }
      
  • 通过getImport(sourceClass) 解析启动类上的注解,获取到其中被@Import注解的类,即AutoConfigurationPackages、AutoConfigurationImportSelector

  • 启动类上@SpringBootApplication注解为组合注解

    • @SpringBootConfiguration:其实质是一个 @Configuration 注解,表明该类是一个配置类
    • @EnableAutoConfiguration:开启了自动配置功能
    • @AutoConfigurationPackage:被该注解标注的类即主配置类,将主配置类所在的包当作base-package
    • @ComponentScan:直接向容器中注入指定的组件
  • 在解析@Import注解时,会有一个getImports()方法,从启动类开始递归解析注解,把所有包含@Import的注解都解析到,然后再processImport()方法中对@Import注解的类进行分类,此处主要识别的是AutoConfigurationImportSelector 归属于ImportSelector的子类,在后续的过程中会调用 DeferredImprotSelectorHandler中的process()方法,来完成EnableAutoConfiguration的加载。

    private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
        Set<SourceClass> imports = new LinkedHashSet<>();
        Set<SourceClass> visited = new LinkedHashSet<>();
        collectImports(sourceClass, imports, visited);
        return imports;
    }
        private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
            throws IOException {
    
        if (visited.add(sourceClass)) {
            for (SourceClass annotation : sourceClass.getAnnotations()) {
                String annName = annotation.getMetadata().getClassName();
                if (!annName.equals(Import.class.getName())) {
                    collectImports(annotation, imports, visited);
                }
            }
            imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
        }
    }
    
  • 执行 this. deferredImportSelectorHandler.process() 方法进行实现自动装配

    public void process() {
        List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
        this.deferredImportSelectors = null;
        try {
            if (deferredImports != null) {
                DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
                deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
                deferredImports.forEach(handler::register);
                handler.processGroupImports();
            }
        }
        finally {
            this.deferredImportSelectors = new ArrayList<>();
        }
    }
    
    // ConfigurationClassParser
    public void processGroupImports() {
        for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
            Predicate<String> exclusionFilter = grouping.getCandidateFilter();
            grouping.getImports().forEach(entry -> {
                ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
                try {
                    // 处理配置类上的注解
                    processImports(configurationClass, asSourceClass(configurationClass, exclusionFilter),
                            Collections.singleton(asSourceClass(entry.getImportClassName(), exclusionFilter)),
                            exclusionFilter, false);
                }
                catch (BeanDefinitionStoreException ex) {
                    throw ex;
                }
                catch (Throwable ex) {
                    throw new BeanDefinitionStoreException(
                            "Failed to process import candidates for configuration class [" +
                                    configurationClass.getMetadata().getClassName() + "]", ex);
                }
            });
        }
    }
    
    
    // DeferredImportSelectorGrouping
    public Iterable<Group.Entry> getImports() {
        for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
            // 遍历DeferredImportSelectorHolder对象集合deferredImports,deferrdImports集合装了各种ImportSelector(AutoConfigurationImportSelect)
            this.group.process(deferredImport.getConfigurationClass().getMetadata(),
                    deferredImport.getImportSelector());
        }
        // 经过上面处理,然后再进行选择导入哪写配置类
        return this.group.selectImports();
    }
    //AutoConfigurationImportSelector
    public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
        Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
                () -> String.format("Only %s implementations are supported, got %s",
                        AutoConfigurationImportSelector.class.getSimpleName(),
                        deferredImportSelector.getClass().getName()));
        // 获取自动配置类放入 AutoConfigurationEntry 对象中
        AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
                .getAutoConfigurationEntry(annotationMetadata);
        // 将封装了自动配置类的 AutoConfigurationEntry 对象装进 autoConfigurationEntries 集合
        this.autoConfigurationEntries.add(autoConfigurationEntry);
        // 遍历刚获取的自动配置类
        for (String importClassName : autoConfigurationEntry.getConfigurations()) {
            // 将符合条件的自动配置类作为 key,annotationMetadata作为值放进 entries 集合中
            this.entries.putIfAbsent(importClassName, annotationMetadata);
        }
    }
    // AutoConfigurationImportSelector
    public Iterable<Entry> selectImports() {
        if (this.autoConfigurationEntries.isEmpty()) {
            return Collections.emptyList();
        }
                .getAutoConfigurationEntry(annotationMetadata);
        // 得到所有要排除的自动配置类集合
        Set<String> allExclusions = this.autoConfigurationEntries.stream()
                .map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
                .getAutoConfigurationEntry(annotationMetadata);
        // 得到经过过滤后所有符合条件的自动配置类集合
        Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
                .map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream)
                .collect(Collectors.toCollection(LinkedHashSet::new));
                .getAutoConfigurationEntry(annotationMetadata);
        // 移除需要排除的自动配置类
        processedConfigurations.removeAll(allExclusions);
    
                .getAutoConfigurationEntry(annotationMetadata);
        // 对标注有 @Order注解的自动配置类进行排序
        return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
                .map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
                .collect(Collectors.toList());
    }
    
    private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
            Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
            boolean checkForCircularImports) {
    
        if (importCandidates.isEmpty()) {
            return;
        }
    
        if (checkForCircularImports && isChainedImportOnStack(configClass)) {
            this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
        }
        else {
            this.importStack.push(configClass);
            try {
                for (SourceClass candidate : importCandidates) {
                    if (candidate.isAssignable(ImportSelector.class)) {
                        // Candidate class is an ImportSelector -> delegate to it to determine imports
                        Class<?> candidateClass = candidate.loadClass();
                        ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
                                this.environment, this.resourceLoader, this.registry);
                        Predicate<String> selectorFilter = selector.getExclusionFilter();
                        if (selectorFilter != null) {
                            exclusionFilter = exclusionFilter.or(selectorFilter);
                        }
                        if (selector instanceof DeferredImportSelector) {
                            this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
                        }
                        else {
                            String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                            Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
                            processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
                        }
                    }
                    else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                        // Candidate class is an ImportBeanDefinitionRegistrar ->
                        // delegate to it to register additional bean definitions
                        Class<?> candidateClass = candidate.loadClass();
                        ImportBeanDefinitionRegistrar registrar =
                                ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
                                        this.environment, this.resourceLoader, this.registry);
                        configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
                    }
                    else {
                        // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
                        // process it as an @Configuration class
                        this.importStack.registerImport(
                                currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                        processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
                    }
                }
            }
            catch (BeanDefinitionStoreException ex) {
                throw ex;
            }
            catch (Throwable ex) {
                throw new BeanDefinitionStoreException(
                        "Failed to process import candidates for configuration class [" +
                        configClass.getMetadata().getClassName() + "]", ex);
            }
            finally {
                this.importStack.pop();
            }
        }
    }
    
  • 执行 this. reader.loadBeanDefinitions(configClasses) 对自动配置类进行生效,生成Bean对象。

6. 自动装配原理总结

  1. 当启动SpringBoot应用程序时,会创建 SpringApplication 对象,在对象的构造方法中进行某些参数的初始化工作,最主要的是判断当前应用程序的类型及初始化器和监听器,在这个过程中会加载整个应用程序的 META-INF/spring.factories 文件,将文件的内容保存到缓存中(Map> cache ),方便后续获取。
  2. SpringApplication对象创建完成后,开始执行run() 方法来完成整个启动。启动过程中最主要的是有两个方法:prepareContext()、refreshContext(),在这两个方法中完成了自动装配的核心功能。在其之前的处理逻辑中包含了上下文对象的创建,banner的打印等各个准备工作。
  3. 在prepareContext()方法中主要完成的是对上下文对象的初始化操作,包含了属性值的设置(比如环境对象)。在整个过程中load()方法完成将当前启动类作为一个BeanDefinition注册到registry中,方便后续在进行BeanFactory调用执行时找到对应的主类,来完成对 @SpringBootApplication @EnableAutoConfiguration等注解的解析工作。
  4. 在 refreshContext()方法中会进行整个容器的刷新过程,会调用spring中的refresh()方法。refresh()中有13个关键方法,在自动装配过程中,会调用 invokeBeanFactoryPostProcessors()方法主要是对 ConfigurationClassPostProcessor 类的处理,这是BeanDefinitionRegistryPostProcessor的子类,BeanDefinitionRegistryPostProcessor 是BeanDefinitionRegistryPostProcessor 的子类,调用BeanDefinitionRegistryPostProcessor中的postProcessBeanDefinitionRegistry()方法,会解析@PropertySource @ComponentScans @ComponentScan @Bean @Import等注解。
  5. 在解析@Import注解时,会有一个getImports()方法,从启动类开始递归解析注解,把所有包含@Import的注解都解析到,然后再processImport()方法中对@Import注解的类进行分类,此处主要识别的是AutoConfigurationImportSelector 归属于ImportSelector的子类,在后续的过程中会调用 DeferredImprotSelectorHandler中的process()方法,来完成EnableAutoConfiguration的加载。

四、项目配置

4.1 properties配置文件

  • SpringBoot默认读取项目下名字为application开头的 yml yaml properties配置文件
    <resource>
        <directory>${basedir}/src/main/resources</directory>
        <filtering>true</filtering>
        <includes>
          <include>**/application*.yml</include>
          <include>**/application*.yaml</include>
          <include>**/application*.properties</include>
        </includes>
      </resource>
      <resource>
        <directory>${basedir}/src/main/resources</directory>
        <excludes>
          <exclude>**/application*.yml</exclude>
          <exclude>**/application*.yaml</exclude>
          <exclude>**/application*.properties</exclude>
        </excludes>
      </resource>
    </resources>
    
  • 在项目下的application.properties里修改端口号和项目上下文路径
    Java 系列之 Springboot_第9张图片
    Java 系列之 Springboot_第10张图片注意:这里的每一个. 都代表一个层级。SpringBoot常见配置:查看官网文档
    Java 系列之 Springboot_第11张图片
  • 常见配置如下
    Java 系列之 Springboot_第12张图片

4.2 yml配置文件

  • properties转换成yml之后,使用缩进代表层级关系,基本格式要求:
    1. 大小写敏感
    2. 使用缩进代表层级关系
    3. 相同的部分只出现一次
    4. 注意空格
      Java 系列之 Springboot_第13张图片
      Java 系列之 Springboot_第14张图片
  1. 普通数据类型

    server:
       port: 8888
    
  2. 配置对象类型数据

    person:
      name: zs
      age: 12
      sex: 男
    #或者写成json格式
    person2: {name: zss,age: 18 }
    
  3. 配置数组类型

    city:
      - beijing
      - tianjin
      - shanghai
      - chongqing
    #或者
    city2: [beijing,tianjin,shanghai,chongqing]
    

4.3 配置目录及优先级

  • 如果同一个目录下,有application.yml也有application.properties,默认先读取application.properties。
  • 如果同一个配置属性,在多个配置文件都配置了,默认使用第1个读取到的,后面读取的不覆盖前面读取到的。

1. 配置文件存放位置

1 . 当前项目根目录中
Java 系列之 Springboot_第15张图片
2. 当前项目根目录下的一个/config子目录中
Java 系列之 Springboot_第16张图片
3. 项目的resources即classpath根路径中
Java 系列之 Springboot_第17张图片
4. 项目的resources即classpath根路径下的/config目录中
Java 系列之 Springboot_第18张图片

2. 配置文件存放读取优先级

  1. 当前项目根目录下的一个/config子目录中(最高)

    • config/application.properties
    • config/application.yml
  2. 当前项目根目录中(其次)

    • application.properties
    • application.yml
  3. 项目的resources即classpath根路径下的/config目录中(一般)

    • resources/config/application.properties
    • resources/config/application.yml
  4. 项目的resources即classpath根路径中(最后)

    • resources/application.properties
    • resources/application.yml

3. 配置文件

  1. Spring Boot 中有两种上下文对象,一种是 bootstrap, 另外一种是application(ServletContext), bootstrap 是应用程序的父上下文,也就是说 bootstrap 加载优先于 applicaton。bootstrap 主要用于从额外的资源来加载配置信息,还可以在本地外部配置文件中解密属性。这两个上下文共用一个环境,它是任何Spring应用程序的外部属性的来源。bootstrap 里面的属性会优先加载,它们默认也不能被本地相同配置覆盖。

  2. bootstrap配置文件特征

    • boostrap 由父 ApplicationContext 加载,比 applicaton 优先加载。
    • boostrap 里面的属性不能被覆盖。
  3. bootstrap与 application 的应用场景

    • application 配置文件主要用于 Spring Boot 项目的自动化配置。
    • bootstrap 配置文件有以下几个应用场景。
      • 使用 SpringCloudConfig 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息。
      • 一些固定的不能被覆盖的属性。
      • 一些加密/解密的场景。

4. springboot 项目结构

Java 系列之 Springboot_第19张图片

-- 项目名
	--src
		--main
			--java
				java代码
			--resources
				--public 公共资源。所有共享的内容。对外公开的内容。
				--static静态资源。图片、js、css。不会被服务器解析。
					--js
					-- jquery.js 访问:http://ip:port/js/jquery.js
					注意:该目录是SpringBoot可以直接识别的目录,会将其中的
					静态资源编译到web项目中,并放到tomcat中使用。静态资源的
					访问路径中无需声明static 例如:localhost:8080/a.png
				--templates 
				   FreeMarker  thymeleaf 页面所在目录。
				--webapp 只有当页面使用jsp时才有。
					--WEB-INF
  • 设置WEB-INF
    Java 系列之 Springboot_第20张图片

五、整合Mybatis

5.1 导入依赖

  • 依赖
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.21</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
        <scope>provided</scope>
    </dependency>
    

5.2 编写配置文件: appliction.yml中添加如下配置

  • mapper-locations: classpath:mybatis/*.xml mapper映射文件包扫描
    type-aliases-package 实体类别名包扫描
    spring:
      datasource:
        url: jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: root
    mybatis:
      mapper-locations: classpath:mybatis/*.xml
      type-aliases-package: com.msb.pojo
    

5.3 编写功能代码

  1. 在启动类上添加注解,表示mapper接口所在位置
    @SpringBootApplication
    public class MyApplication {
        public static void main(String[] args) {
            SpringApplication.run(MyApplication.class,args);
        }
    }
    
  2. 定义mapper接口
  • 如果不在MyApplication启动类上添加@MapperScan必须在UserMapper接口上添加@Mapper注解。

    //@Mapper
    public interface UserMapper {
        List<User> selectAll();
    }
    
  1. 定义mapper.xml映射文件
  • 在resource下新建mybatis文件夹,mapper.xml文件名没有要求了,不需要和接口名完全对应了,是根据namespace去找接口。但是最好还是和接口名字保持一致
  1. controller层代码

    package com.msb.controller;
    import com.msb.pojo.User;
    import com.msb.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import java.util.List;
    /**
     * @Author: bingwoo
     */
    @Controller
    @RequestMapping("/user")
    public class UserController {
        @Autowired
        private UserService userService;
        @RequestMapping("/findAll")
        @ResponseBody
        public List<User> findAll(){
            return userService.findAll();
        }
    }
    
  2. service层代码

package com.msb.service.impl;
import com.msb.pojo.User;
import com.msb.mapper.UserMapper;
import com.msb.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * @Author: bingwoo
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public List<User> findAll() {
        return userMapper.findAll();
    }
}
  1. idea中往往会误报代码错误,如果我们确定代码无问题,可以通过降低idea检查代码的严格程度来消除报错:快捷键: ctrl+alt+shift+h

六、整合logback

  • Spring Boot默认使用Logback组件作为日志管理。Logback是由log4j创始人设计的一个开源日志组件。

  • 在Spring Boot项目中我们不需要额外的添加Logback的依赖,因为在spring-boot-starter或者spring-boot-starter-web中已经包含了Logback的依赖。

  • Logback读取配置文件的步骤:
    (1)在classpath下查找文件logback-test.xml
    (2)如果文件不存在,则查找logback.xml
    Java 系列之 Springboot_第21张图片

    <?xml version="1.0" encoding="UTF-8" ?>
     <configuration>
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->  
        <property name="LOG_HOME" value="${catalina.base}/logs/" />  
        <!-- 控制台输出 -->   
        <appender name="Stdout" class="ch.qos.logback.core.ConsoleAppender">
           <!-- 日志输出格式 -->  
            <layout class="ch.qos.logback.classic.PatternLayout">   
                 <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> 
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n   
                </pattern>   
            </layout>   
        </appender>   
        <!-- 按照每天生成日志文件 -->   
        <appender name="RollingFile"  class="ch.qos.logback.core.rolling.RollingFileAppender">   
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!--日志文件输出的文件名-->
                <FileNamePattern>${LOG_HOME}/server.%d{yyyy-MM-dd}.log</FileNamePattern>   
                <MaxHistory>30</MaxHistory>
            </rollingPolicy>   
            <layout class="ch.qos.logback.classic.PatternLayout">  
                <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> 
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n   
                </pattern>   
           </layout> 
            <!--日志文件最大的大小-->
           <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
             <MaxFileSize>10MB</MaxFileSize>
           </triggeringPolicy>
        </appender>     
        <!-- 日志输出级别 -->
        <root level="info">   
            <appender-ref ref="Stdout" />   
            <appender-ref ref="RollingFile" />   
        </root>
        <logger name="com.msb.mapper" level="DEBUG"></logger>
    <!--日志异步到数据库 -->  
    <!--<appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
            日志异步到数据库 
            <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
               连接池 
               <dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
                  <driverClass>com.mysql.jdbc.Driver</driverClass>
                  <url>jdbc:mysql://127.0.0.1:3306/databaseName</url>
                  <user>root</user>
                  <password>root</password>
                </dataSource>
            </connectionSource>
      </appender> -->
    </configuration>
    

七、整合PageHelper

7.1 PageHelper插件

  • 我们在正常的查询业务之中,只需要加上一行代码就可以实现分页的数据的封装处理

7.2 实现原理

  • PageHelper方法使用了静态的ThreadLocal参数,分页参数和线程是绑定的。内部流程是ThreadLocal中设置了分页参数(pageIndex,pageSize),之后在查询执行的时候,获取当线程中的分页参数,执行查询的时候通过拦截器在sql语句中添加分页参数,之后实现分页查询,查询结束后在 finally 语句中清除ThreadLocal中的查询参数

7.3 使用方法

  1. 调用PageHelper方法:PageHelper.startPage(pageNum, pageSize)
  2. MyBatis 查询方法
    注意:只要你可以保证在PageHelper方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为PageHelper在finally代码段中自动清除了ThreadLocal存储的对象。
  • 添加PageHelper启动器依赖

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.2.12</version>
    </dependency>
    
  • 控制器

    package com.msb.controller;
    import com.msb.pojo.Emp;
    import com.msb.service.EmpService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import java.util.List;
    /**
     * @Author: bingwoo
     */
    @Controller
    @RequestMapping("/emp")
    public class EmpController {
        @Autowired
        private EmpService empService;
        @RequestMapping("/findByPage/{pageNum}/{pageSize}")
        @ResponseBody
        public List<Emp> findByPage(@PathVariable("pageNum") Integer pageNum,@PathVariable("pageSize") Integer pageSize){
            return empService.findByPage(pageNum,pageSize);
        }
    }
    
  • Service层代码编写

    package com.msb.service.impl;
    import com.github.pagehelper.Page;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.msb.mapper.EmpMapper;
    import com.msb.pojo.Emp;
    import com.msb.service.EmpService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import java.util.List;
    /**
     * @Author: bingwoo
     */
    @Service
    public class EmpServiceImpl implements EmpService {
        @Autowired
        private EmpMapper empMapper;
        @Override
        public List<Emp> findByPage(Integer pageNum, Integer pageSize) {
            Page<Emp> page = PageHelper.startPage(pageNum, pageSize);
            List<Emp> list =empMapper.findAll();
            // 方式1
            System.out.println("当前页:"+page.getPageNum());
            System.out.println("总页数"+page.getPages());
            System.out.println("页大小:"+page.getPageSize());
            System.out.println("总记录数:"+page.getTotal());
            System.out.println("当前页数据"+page.getResult());
            // 方式2
            PageInfo<Emp> pi =new PageInfo<>(list);
            System.out.println("当前页"+pi.getPageNum());
            System.out.println("总页数"+pi.getPages());
            System.out.println("页大小"+pi.getSize());
            System.out.println("总记录数"+pi.getTotal());
            System.out.println("当前页数据"+pi.getList());
            return list;
        }
    }
    

7.4 PageInfo对象和Page对象的区别

  1. Page对象解析参数

    private int pageNum;	//当前页码
    private int pageSize; 	//每页数据的数量
    private int startRow;	//始页首行行号
    private int endRow;		//尾页尾行行号
    private long total;		//总记录数
    private int pages;		//总页数
    private Boolean reasonable; //分页合理化
    private Boolean pageSizeZero; //当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
    
  2. PageInfo对象解析参数

    private int pageNum;   			//当前页
    private int pageSize;			//每页显示数据条数
    private int size;				//当前页的数量
    private int startRow; 			//始页首行行号
    private int endRow;				//尾页尾行行号
    private long total;				//总记录数
    private int pages;				//总页数
    private List<T> list;			//查询结果的数据
    private int firstPage;			//首页
    private int prePage;			//上一页
    private int nextPage;			// 下一页
    private int lastPage;			//最后一页
    private boolean isFirstPage;	//是不是第一页
    private boolean isLastPage;		//是不是最后一页
    private boolean hasPreviousPage;//有没有上一页
    private boolean hasNextPage;	//有没有下一页
    private int navigatePages;		//所有导航页号
    private int[] navigatepageNums;	//导航页码数
    

八、整合Druid

  1. 介绍:Druid是由阿里巴巴推出的数据库连接池。它结合了C3P0、DBCP、PROXOOL等数据库连接池的优点。之所以从众多数据库连接池中脱颖而出,还有一个重要的原因就是它包含控制台,很方便的帮助我们实现对于sql执行的监控。

  2. 添加依赖

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    
  3. 修改配置文件application.yml

    spring:
      datasource:
        # 使用阿里的Druid连接池
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 填写你数据库的url、登录名、密码和数据库名
        url: jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        username: root
        password: root
      druid:
        # 连接池的配置信息
        # 初始化大小,最小,最大
        initial-size: 5
        min-idle: 5
        maxActive: 20
        # 配置获取连接等待超时的时间
        maxWait: 60000
        # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        timeBetweenEvictionRunsMillis: 60000
        # 配置一个连接在池中最小生存的时间,单位是毫秒
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        # 打开PSCache,并且指定每个连接上PSCache的大小
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall,slf4j
        # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
        connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
        # 配置DruidStatFilter
        web-stat-filter:
          enabled: true
          url-pattern: "/*"
          exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
        # 配置DruidStatViewServlet
        stat-view-servlet:
          url-pattern: "/druid/*"
          # IP白名单(没有配置或者为空,则允许所有访问)
          allow: 127.0.0.1,192.168.8.109
          # IP黑名单 (存在共同时,deny优先于allow)
          deny: 192.168.1.188
          #  禁用HTML页面上的“Reset All”功能
          reset-enable: false
          # 登录名
          login-username: admin
          # 登录密码
          login-password: 123456
    

九、整合JSP

  1. 添加依赖
<!--JSP依赖-->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>
  1. 添加webapps目录并设置目录
    Java 系列之 Springboot_第22张图片
  2. 设置工作目录,如果在IDEA中项目结构为聚合工程。那么在运行jsp是需要指定路径。如果项目结构为独立项目则不需要。
    Java 系列之 Springboot_第23张图片
  3. 在 yml配置文件中配置视图解析器参数
    Java 系列之 Springboot_第24张图片
  4. 在控制类中声明单元方法请求转发jsp资源
    package com.msb.controller;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    /**
     * @Author: bingwoo
     */
    @Controller
    public class PageController {
        @RequestMapping("/{uri}")
        public String getPage(@PathVariable("uri") String uri){
            return uri;
        }
    }
    

十、整合FreeMarker

10.1 介绍

  • FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
  • FreeMarker是免费的,基于Apache许可证2.0版本发布。其模板编写为FreeMarker Template Language(FTL),属于简单、专用的语言。需要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,主要用于如何展现数据, 而在模板之外注意于要展示什么数据。
    Java 系列之 Springboot_第25张图片
  • 常用的java模板引擎还有哪些?
    • Jsp、Freemarker、Thymeleaf 、Velocity 等。
    • 模板+数据模型=输出
    • freemarker并不关心数据的来源,只是根据模板的内容,将数据模型在模板中显示并输出文件(通常为html,也可以生成其它格式的文本文件)freemarker作为springmvc一种视图格式,默认情况下SpringMVC支持freemarker视图格式。 需要创建Spring Boot+Freemarker工程用于测试模板。

10.2 使用

  1. 导入依赖

    <!--freeMaker依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    
  2. 创建controller

    @RequestMapping("/freemarker")
    @Controller
    public class FreemarkerController {
        @RequestMapping("/show")
        public String freemarker(Map<String, Object> map){
            map.put("name","msb");
           //返回模板文件名称
            return "show";
        }
    }
    
  3. 通过查阅配置信息发现,默认前缀为 ‘’ ,后缀为.ftlh,默认路径为templates
    Java 系列之 Springboot_第26张图片

  4. templates目录下创建模板文件

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    this is showftlh
    <br/>
    ${name}
    <img src="img/a.jpg"></img>
    </body>
    </html>
    

10.3 常用指令

  1. A遍历List集合
    1. 注释,即<#‐‐和‐‐>,介于其之间的内容会被freemarker忽略
    2. 插值(Interpolation):即 . . 部分 , f r e e m a r k e r 会用真实的值代替 {..}部分,freemarker会用真实的值代替 ..部分,freemarker会用真实的值代替{…}
    3. FTL指令:和HTML标记类似,名字前加#予以区分,Freemarker会解析标签中的表达式或逻辑。
    4. 文本,仅文本信息,这些不是freemarker的注释、插值、FTL指令的内容会被freemarker忽略解析,直接输出内容。
      @Controller
      public class FreemarkerController {
          @Autowired
          private EmpService empService;
          @RequestMapping("/showEmp")
          public ModelAndView testList(){
              ModelAndView mv =new ModelAndView();
              List<Emp> list =empService.findAll();
              mv.addObject("empList", list);
              mv.setViewName("showEmp");
              return mv;
          }
      }
      
  • 页面代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <style type="text/css">
            #empTable{
                width: 80%;
                border: 1px solid blue;
                margin: 0px auto;
            }
            #empTable th,td{
                border: 1px solid green;
                text-align: center;
            }
        </style>
    </head>
    <body>
    <table id="empTable" cellpadding="0px" cellspacing="0px">
        <tr>
            <th>索引</th>
            <th>工号</th>
            <th>姓名</th>
            <th>岗位</th>
            <th>薪资</th>
            <th>部门号</th>
        </tr>
        <#list empList as emp>
            <tr>
                <td>${emp_index}</td>
                <td>${emp.empno}</td>
                <td>${emp.ename}</td>
                <td>${emp.job}</td>
                <td>${emp.sal}</td>
                <td>${emp.deptno}</td>
            </tr>
        </#list>
    </table>
    </body>
    </html>
    
  • 说明: _index:得到循环的下标,使用方法是在stu后边加"_index",它的值是从0开始
  1. 遍历Map数据

    1. 遍历输出指定内容controller
      @Controller
      public class FreemarkerController {
          @Autowired
          private EmpService empService;
          @RequestMapping("/showEmpMap")
          public ModelAndView testMap(){
              ModelAndView mv =new ModelAndView();
              List<Emp> list =empService.findAll();
              Map<String,Emp> empMap =new HashMap<>();
              for (Emp emp : list) {
                  empMap.put(emp.getEmpno().toString(), emp);
              }
              mv.addObject("empMap", empMap);
              mv.setViewName("showEmpMap");
              return mv;
          }
      }
      
    2. 页面(FreeMarker在遍历map集合是,key必须是String)
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
          <style type="text/css">
              #empTable{
                  width: 80%;
                  border: 1px solid blue;
                  margin: 0px auto;
              }
              #empTable th,td{
                  border: 1px solid green;
                  text-align: center;
              }
          </style>
      </head>
      <body>
      输出7521员工信息:<br/>
      工号:${empMap['7521'].empno}<br/>
      姓名:${empMap['7521'].ename}<br/>
      岗位:${empMap['7521'].job}<br/>
      薪资:${empMap['7521'].sal}<br/>
      部门号:${empMap['7521'].deptno}<br/>
      <br/>
      遍历EmpMap
      <table id="empTable" cellpadding="0px" cellspacing="0px">
          <tr>
              <th>索引</th>
              <th>工号</th>
              <th>姓名</th>
              <th>岗位</th>
              <th>薪资</th>
              <th>部门号</th>
          </tr>
          <#list empMap?keys as k>
              <tr>
                  <td>${k_index}</td>
                  <td>${k}</td>
                  <td>${empMap[k].ename}</td>
                  <td>${empMap[k].job}</td>
                  <td>${empMap[k].sal}</td>
                  <td>${empMap[k].deptno}</td>
              </tr>
          </#list>
      </table>
      </body>
      </html>
      
  2. if指令:

    • if 指令即判断指令,是常用的FTL指令,freemarker在解析时遇到if会进行判断,条件为真则输出if中间的内容,否 则跳过内容不再输出。 if中支持的运算符

    • 算数运算符 FreeMarker表达式中完全支持算术运算,FreeMarker支持的算术运算符包括:+, - , * , / , %

    • 逻辑运算符有如下几个: 逻辑与:&& 逻辑或:|| 逻辑非:! 逻辑运算符只能作用于布尔值,否则将产生错误
      c比较运算符有如下几个:
      ① =或者==:判断两个值是否相等.
      ② !=:判断两个值是否不等.
      ③ > 或者gt:判断左边值是否大于右边值
      ④ >=或者gte:判断左边值是否大于等于右边值
      ⑤ <或者lt:判断左边值是否小于右边值
      ⑥ <=或者lte:判断左边值是否小于等于右边值
      注意: =和!=可以用于字符串,数值和日期来比较是否相等,但=和!=两边必须是相同类型的值,否则会产生错误,而且FreeMarker是精确比较,“x”,"x ","X"是不等的.其它的运行符可以作用于数字和日期,但不能作用于字符串,大部分的时候,使用gt等字母运算符代替>会有更好的效果,因为 FreeMarker会把>解释成FTL标签的结束字符,当然,也可以使用括号来避免这种情况,如:<#if (x>y)>

    • 如何判断空值

      1. 判断某变量是否存在使用 “??” 用法为:variable??,如果该变量存在,返回true,否则返回false 例:为防止stus为空报错可以加上判断如下
      2. 缺失变量默认值使用 “!” 使用!要以指定一个默认值,当变量为空时显示默认值。例: ${name!‘’}表示如果name为空显示空字符串。如果是嵌套对象则建议使用()括起来。
        <#if empList??>
                <#list empList as emp>
                    <tr <#if emp_index%2 ==0 > style="background-color: gray" </#if>>
                        <td>${emp_index}</td>
                        <td>${emp.empno}</td>
                        <td <#if emp.ename == 'KING'> style="color: aqua" </#if>>${emp.ename}</td>
                        <td>${emp.job}</td>
                        <td>${emp.mgr!'无'}</td>
                        <td <#if emp.sal gte 2000.0> style="color: red" </#if>>${emp.sal}</td>
                        <td>${emp.comm!'0'}</td>
                        <td>${emp.deptno}</td>
                    </tr>
                </#list>
            </#if>
        
  3. 内置函数

    • 内建函数语法格式: 变量+?+函数名称
      1. 内建函数获取某个集合的大小: ${集合名?size}
      2. 内建函数日期格式化
        显示年月日: ${today?date}
        显示时分秒: t o d a y ? t i m e 显示日期 + 时间: {today?time} 显示日期+时间: today?time显示日期+时间:{today?datetime}
        自定义格式化: ${today?string(“yyyy年MM月”)}
      3. 内建函数将json字符串转成对象
        <#assign text="{'bank':'工商银行','account':'10101920201920212'}" />
        <#assign data=text?eval />
        开户行:${data.bank} 账号:${data.account}
        
      • 其中用到了 assign标签,assign的作用是定义一个变量。
      • 页面
        员工人数:${empList?size}
        <table id="empTable" cellpadding="0px" cellspacing="0px">
            <tr>
                <th>索引</th>
                <th>工号</th>
                <th>姓名</th>
                <th>岗位</th>
                <th>上级</th>
                <th>入职日期a</th>
                <th>入职日期b</th>
                <th>入职日期c</th>
                <th>入职日期d</th>
                <th>薪资</th>
                <th>补助</th>
                <th>部门号</th>
            </tr>
            <#if empList??>
                <#list empList as emp>
                    <tr <#if emp_index%2 ==0 > style="background-color: gray" </#if>>
                        <td>${emp_index}</td>
                        <td>${emp.empno}</td>
                        <td <#if emp.ename == 'KING'> style="color: aqua" </#if>>${emp.ename}</td>
                        <td>${emp.job}</td>
                        <td>${emp.mgr!'无'}</td>
                        <td>${emp.hiredate?date}</td>
                        <td>${emp.hiredate?time}</td>
                        <td>${emp.hiredate?datetime}</td>
                        <td>${emp.hiredate?string("yyyy年MM月dd日")}</td>
                        <td <#if emp.sal gte 2000.0> style="color: red" </#if>>${emp.sal}</td>
                        <td>${emp.comm!'0'}</td>
                        <td>${emp.deptno}</td>
                    </tr>
                </#list>
            </#if>
        </table>
        

十一、整合Thymeleaf

11.1 介绍

  • Thymeleaf的主要目标是将优雅的自然模板带到开发工作流程中,并将HTML在浏览器中正确显示,并且可以作为静态原型,让开发团队能更容易地协作。Thymeleaf能够处理HTML,XML,JavaScript,CSS甚至纯文本。

  • 长期以来,jsp在视图领域有非常重要的地位,随着时间的变迁,出现了一位新的挑战者:Thymeleaf,Thymeleaf是原生的,不依赖于标签库.它能够在接受原始HTML的地方进行编辑和渲染.因为它没有与Servelet规范耦合,因此Thymeleaf模板能进入jsp所无法涉足的领域。

  • Thymeleaf在Spring Boot项目中放入到resources/templates中。这个文件夹中的内容是无法通过浏览器URL直接访问的(和WEB-INF效果一样),所有Thymeleaf页面必须先走控制器。

  • 创建项目,准备配置文件及各层级代码,项目中添加依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.4.5</version>
        </dependency>
    </dependencies>
    
  • 在resources下新建templates文件夹。新建index.html

    package com.msb.controller;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    /**
     * @Author: bingwoo
     */
    @Controller
    public class ThymeleafController {
        @RequestMapping("showIndex")
        public String showIndex(){
            return "index";
        }
    }
    

11.2 基础语法

  • Thymeleaf通过标准变量表达式完成数据的展示和处理
    1. 标准变量表达式必须依赖标签,不能独立使用
    2. 标准变量表达式一般在开始标签中,以 th开头
    3. 语法为:
    4. 表达式中可以通过${}取出域中的值并放入标签的指定位置
    5. ${}在这里不能单独使用,必须在 th:后面的双引号里使用
  1. th:text属性

    <!--向span双标签内部添加文本-->
        <span th:text="pageMessage"></span> <br/>
        <!--从域中根据参数名取出参数值放在双标签中-->
        <span th:text="${msg}"></span>      <br/>
    
  2. th:value 获取值

    <!--向input标签中的value属性赋值-->
    <input type="text"  th:value="pageMessage"/>
    <!--从域中根据参数名取出参数值 向input标签中的value属性赋值-->
    <input type="text"  th:value="${msg}"/>
    
  3. th:if 判断

    <span th:if="${name}!='张三'">会显示</span>
    
  4. 循环遍历.th:each

    - th:each="u,i :${list}" 其中i表示迭代状态。
    
    1. index:当前迭代器的索引 从0开始
    2. count:当前迭代对象的计数 从1开始
    3. size:被迭代对象的长度
    4. even/odd:布尔值,当前循环是否是偶数/奇数 从0开始
    5. first:布尔值,当前循环的是否是第一条,如果是返回true否则返回false
    6. last:布尔值,当前循环的是否是最后一条,如果是则返回true否则返回false
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <style type="text/css">
            #empTable{
                width: 80%;
                border: 1px solid blue;
                margin: 0px auto;
            }
            #empTable th,td{
                border: 1px solid green;
                text-align: center;
            }
        </style>
    </head>
    <body>
    //展示单个员工信息:
    <span th:if="${emp}!=null">
        工号:<span th:text="${emp.empno}"></span><br/>
        姓名:<span th:text="${emp.ename}"></span><br/>
        职务:<span th:text="${emp.job}"></span><br/>
        上级:<span th:text="${emp.mgr}"></span><br/>
        入职日期:<span th:text="${emp.hiredate}"></span><br/>
        工资:<span th:text="${emp.sal}"></span><br/>
        补助:<span th:text="${emp.comm}"></span><br/>
        部门号:<span th:text="${emp.deptno}"></span><br/>
    </span>
    <hr/>
    <span  th:if="${empList}!=null">
        <span  th:if="${empList.size()} ne 0">
            工号:<span th:text="${empList[0].empno}"></span><br/>
            姓名:<span th:text="${empList[0].ename}"></span><br/>
            职务:<span th:text="${empList[0].job}"></span><br/>
            上级:<span th:text="${empList[0].mgr}"></span><br/>
            入职日期:<span th:text="${empList[0].hiredate}"></span><br/>
            工资:<span th:text="${empList[0].sal}"></span><br/>
            补助:<span th:text="${empList[0].comm}"></span><br/>
            部门号:<span th:text="${empList[0].deptno}"></span><br/>
        </span>
    </span>
    <table  id="empTable" cellpadding="0px" cellspacing="0px">
        <tr>
            <th>索引</th>
            <th>序号</th>
            <th>总人数</th>
            <th>偶数索引?</th>
            <th>奇数索引?</th>
            <th>第一?</th>
            <th>最后?</th>
            <th>工号</th>
            <th>姓名</th>
            <th>职务</th>
            <th>上级</th>
            <th>入职日期</th>
            <th>工资</th>
            <th>补助</th>
            <th>部门号</th>
        </tr>
        <tr th:each="emp,i:${empList}">
            <td th:text="${i.index}"></td>
            <td th:text="${i.count}"></td>
            <td th:text="${i.size}"></td>
            <td th:text="${i.odd}"></td>
            <td th:text="${i.even}"></td>
            <td th:text="${i.first}"></td>
            <td th:text="${i.last}"></td>
            <td th:text="${emp.empno}"></td>
            <td th:text="${emp.ename}"></td>
            <td th:text="${emp.job}"></td>
            <td th:text="${emp.mgr}"></td>
            <td th:text="${emp.hiredate}"></td>
            <td th:text="${emp.sal}"></td>
            <td th:text="${emp.comm}"></td>
            <td th:text="${emp.deptno}"></td>
        </tr>
    </table>
    </body>
    </html>
    
  5. 算数运算符:+ , - , * , / , %

    <span th:text="1+1"></span>
    <span th:text="'1'+1"></span>
    <span th:text="${emp.empno}+1"></span>
    <span th:text="${emp.empno+1}"></span>
    
  6. 关系运算符

    1 gt:     great than(大于)>
    2 ge:    great equal(大于等于)>=
    3 eq:    equal(等于)==
    4 lt:    less than(小于)<
    5 le:    less equal(小于等于)<=
    6 ne:    not equal(不等于)!= 
    
  7. 逻辑运算符:&& 或 and: 表示并且 || 或 or : 表示或者

    <div th:text="1>0 and 2<3"></div>
    <div th:text="1>0 and 2>3"></div>
    <div th:text="1>0 or 2<3"></div>
    <div th:text="1>0 or 2>3"></div>
    <hr/>
    <div th:text="${emp.sal ge 800}"></div>
    <div th:text="${emp.sal } ge 800"></div>
    <div th:text="${emp.sal ge 800} and ${emp.deptno eq 20}"></div>
    <div th:text="(${emp.sal }ge 800) or (${emp.deptno } ne 20)"></div>
    <div th:text="${emp.sal ge 800 or emp.deptno ne 20 }"></div>
    
  8. 三目运算符

    <tr th:each="emp,i:${empList}" th:class="${i.odd}?a:b">
    
    
    对空值作出处理
    <tr th:each="emp,i:${empList}" th:class="${i.odd}?a:b">
       <td th:text="${i.index}"></td>
        <td th:text="${i.count}"></td>
        <td th:text="${emp.mgr} eq null ?老板:${emp.mgr}"></td>
        <td th:text="${emp.hiredate}"></td>
        <td th:text="${emp.sal}"></td>
        <td th:text="${emp.comm} eq null ?0:${emp.comm}"></td>
        <td th:text="${emp.deptno}"></td>
    </tr>
    
  9. th:href

    <a th:href="@{/getParam(id=1,name='msb')}" >跳转</a>
    <!-- 获取作用域值-->
    <a th:href="@{/getParam(name=${stu.name},age=${stu.age})}">跳转二</a>
    
  10. th:onclick :给元素绑定事件,单击事件并传递参数

//写法1:仅仅支持数字和布尔类型参数的传递,字符串不支持
<a href="javascript:viod(0)"  th:onclick="'del('+${emp.empno}+')'">删除</a>
//写法2:支持数字和文本类型的参数传递
<a href="javascript:void(0)" th:onclick="delEmp([[${emp.empno}]],[[${emp.ename}]])">删除</a>

11.3 内置对象

  1. Thymeleaf提供了一些内置对象,内置对象可直接在模板中使用。这些对象是以#引用的。使用内置对象的语法
  • 引用内置对象需要使用#

  • 大部分内置对象的名称都以s结尾。如:strings、numbers、dates

  • 常见内置对象如下

    #arrays:数组操作的工具;
    #aggregates:操作数组或集合的工具;
    #bools:判断boolean类型的工具;
    #calendars:类似于#dates,但是是java.util.Calendar类的方法;
    #ctx:上下文对象,可以从中获取所有的thymeleaf内置对象;
    #dates:日期格式化内置对象,具体方法可以参照java.util.Date;
    #numbers: 数字格式化;#strings:字符串格式化,具体方法可以参照String,如startsWith、contains等;
    #objects:参照java.lang.Object;
    #lists:列表操作的工具,参照java.util.List;
    #sets:Set操作工具,参照java.util.Set;#maps:Map操作工具,参照java.util.Map;
    #messages:操作消息的工具。
    
  1. strings对象
    Java 系列之 Springboot_第27张图片

  2. dates对象
    Java 系列之 Springboot_第28张图片

  3. #numbers

    #numbers.formatDecimal(numbwe,整数位,整数位千分位标识符,小数位,小数位表示符)
    
    ${#numbers.formatDecimal(num,1,'COMMA',2,'POINT')}
    
    显示:99,999,999.99
    
    1:表示整数位至少一位,不足以0补齐,如:num = 0.00,
    
    ${#numbers.formatDecimal(num,0,'COMMA',2,'POINT')}则显示 .00
    
    ${#numbers.formatDecimal(num,1,'COMMA',2,'POINT')}则显示 0.00
    
    COMMA:','
    
    POINT:‘.
  4. 域对象
    Java 系列之 Springboot_第29张图片

  5. 代码示例

    @RequestMapping("showIndex")
    public String  showIndex(Map<String,Object> map, HttpServletRequest req, HttpSession session){
        // 向request域放数据
        req.setAttribute("msg", "requestMessage");
        // 向session域放数据
        session.setAttribute("msg", "sessionMessage");
        // 向application域放数据
        req.getServletContext().setAttribute("msg", "applicationMessage");
        // 对象List集合数据
        List<Emp> empList = empService.findAll();
        map.put("empList", empList);
        return "index";
    }
    
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <style type="text/css">
            #empTable{
                width: 80%;
                border: 1px solid blue;
                margin: 0px auto;
            }
            #empTable th,td{
                border: 1px solid green;
                text-align: center;
            }
            .a{
                background-color: antiquewhite;
            }
            .b{
                background-color: gray;
            }
        </style>
    </head>
    <body>
    <table  id="empTable" cellpadding="0px" cellspacing="0px">
        <tr>
            <th>索引</th>
            <th>序号</th>
            <th>总人数</th>
            <th>偶数索引?</th>
            <th>奇数索引?</th>
            <th>第一?</th>
            <th>最后?</th>
            <th>工号</th>
            <th>姓名</th>
            <th>职务</th>
            <th>上级</th>
            <th>入职日期</th>
            <th>入职年</th>
            <th>入职月</th>
            <th>入职日</th>
            <th>工资</th>
            <th>补助</th>
            <th>部门号</th>
            <th>操作</th>
        </tr>
        <tr th:each="emp,i:${empList}" th:class="${i.odd}?a:b">
            <td th:text="${i.index}"></td>
            <td th:text="${i.count}"></td>
            <td th:text="${i.size}"></td>
            <td th:text="${i.odd}"></td>
            <td th:text="${i.even}"></td>
            <td th:text="${i.first}"></td>
            <td th:text="${i.last}"></td>
            <td th:text="${emp.empno}"></td>
            <td th:text="${emp.ename}"></td>
            <td th:text="${emp.job}"></td>
            <td th:text="${#strings.isEmpty(emp.mgr)}?老板:${emp.mgr}"></td>
            <td th:text="${#dates.format(emp.hiredate,'yyyy-MM-dd HH:mm:ss')}"></td>
            <td th:text="${#dates.year(emp.hiredate)}"></td>
            <td th:text="${#dates.month(emp.hiredate)}"></td>
            <td th:text="${#dates.day(emp.hiredate)}"></td>
            <td th:text="${#numbers.formatDecimal(emp.sal,7,'COMMA',2,'POINT')}"></td>
            <td th:text="${#strings.isEmpty(emp.comm)}?0:${#numbers.formatDecimal(emp.sal,7,'COMMA',2,'POINT')}"></td>
            <td th:text="${emp.deptno}"></td>
            <td>
                <a href="javascript:void(0)" th:onclick="removeEmp([[${emp.empno}]],[[${emp.ename}]])">删除</a>
            </td>
        </tr>
        
    </table>
    <script>
        function removeEmp(empno,ename){
            var resulet =confirm("确定要删除编号为"+empno+"的"+ename);
            if(resulet){
                window.location.href="removeEmp?empno="+empno+"&ename="+ename;
            }
        }
    </script>
    <hr/>
    request:<br/>
    <span th:text="${#httpServletRequest.getAttribute('msg')}"></span><br/>
    <span th:text="${#request.getAttribute('msg')}"></span><br/>
    <span th:text="${msg}"></span><br/>
    session:<br/>
    <span th:text="${#httpSession.getAttribute('msg')}"></span><br/>
    <span th:text="${#session.getAttribute('msg')}"></span><br/>
    <span th:text="${session.msg}"></span><br/>
    application:<br/>
    <span th:text="${#servletContext.getAttribute('msg')}"></span><br/>
    <span th:text="${application.msg}"></span><br/>
    </body>
    </html>
    

十二、 模板引擎总结

12.1 jsp

  1. 优点:
    1. 功能强大,可以写java代码
    2. 支持jsp标签(jsp tag)
    3. 支持表达式语言(el)
    4. 官方标准,用户群广,丰富的第三方jsp标签库
  2. 缺点 :性能问题。不支持前后端分离

12.2 freemarker

  • FreeMarker是一个用Java语言编写的模板引擎,它基于模板来生成文本输出。FreeMarker与Web容器无关,即在Web运行时,它并不知道Servlet或HTTP。它不仅可以用作表现层的实现技术,而且还可以用于生成XML,JSP或Java 等。
    目前企业中:主要用Freemarker做静态页面或是页面展示
  1. 优点:
    1. 不能编写java代码,可以实现严格的mvc分离
    2. 性能非常不错
    3. 对jsp标签支持良好
    4. 内置大量常用功能,使用非常方便
    5. 宏定义(类似jsp标签)非常方便
    6. 使用表达式语言
  2. 缺点:
    1. 不是官方标准
    2. 用户群体和第三方标签库没有jsp多

12.3 Thymeleaf

  • hymeleaf是个XML/XHTML/HTML5模板引擎,可以用于Web与非Web应用。
  • Thymeleaf的主要目标在于提供一种可被浏览器正确显示的、格式良好的模板创建方式,因此也可以用作静态建模。你可以使用它创建经过验证的XML与HTML模板。相对于编写逻辑或代码,开发者只需将标签属性添加到模板中即可。接下来,这些标签属性就会在DOM(文档对象模型)上执行预先制定好的逻辑。Thymeleaf的可扩展性也非常棒。你可以使用它定义自己的模板属性集合,这样就可以计算自定义表达式并使用自定义逻辑。这意味着Thymeleaf还可以作为模板引擎框架。
  1. 优点:静态html嵌入标签属性,浏览器可以直接打开模板文件,便于前后端联调。springboot官方推荐方案。
  2. 缺点:模板必须符合xml规范

十三、项目打包部署

13.1 打包 jar

  • SpringBoot项目可以是jar类型的maven项目,也可以是一个war类型的maven项目,取决于我们要不要整合jsp使用。但是不管是哪种项目类型,已经不是我们传统意义上的项目结构了
  • 在本地使用SpringBoot的启动器即可访问我们开发的项目。如果我们将项目功能开发完成后,需要使用SpringBoot的打包功能来将项目进行打包。
  • SpringBoot项目打包在linux服务器中运行:
    1. jar类型项目会打成jar包:jar类型项目使用SpringBoot打包插件打包时,会在打成的jar中内置一个tomcat的jar。所以我们可以使用jdk直接运行该jar项目可,jar项目中有一个功能,将功能代码放到其内置的tomcat中运行。我们直接使用浏览器访问即可。
    2. war类型项目会打成war包:在打包时需要将内置的tomcat插件排除,配置servlet的依赖。将war正常的放到tomcat服务器中运行即可。
  1. 导入springboot打包插件
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <fork>true</fork>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
  2. 将项目导出成jar包并运行
    Java 系列之 Springboot_第30张图片
  3. 使用maven package指令打包即可
    Java 系列之 Springboot_第31张图片
  4. 打成包后,可以通过dos java -jar指令直接启动运行
    Java 系列之 Springboot_第32张图片

13.2 打包war

  • 将项目导出war包并运行,项目打包成war之后,要放在一个Tomcat上运行
  • 如果我们当前的maven项目本身就是war类型的项目,直接打包即可,但是如果我们当前的maven项目是jar类型的项目,我们需要将项目修改为war类型,修改项目的pom文件,使用packaging标签设置值为war.并且需要在项目中创建webapp文件夹,并设置为资源文件夹。
  1. 更改打包类型
    Java 系列之 Springboot_第33张图片

  2. webapp文件夹(可加可不加)
    Java 系列之 Springboot_第34张图片

  3. 排除项目中自带的所有的Tomcat插件和jsp servlet 依赖,因为这里要将项目放到一个Tomcat上运行

    <!--配置SpringBoot的web启动器-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <!--排除web启动中自动依赖的tomcat插件-->
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--
        手动依赖tomcat插件,但是表明项目打包时该依赖不会被打进去,目的主要是保证开发阶段本地SpringBoot
        项目可以正常运行
    -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <!--打包的时候可以不用包进去,别的设施会提供。事实上该依赖理论上可以参与编译,测试,运行等周期。
            相当于compile,但是打包阶段做了exclude操作-->
        <scope>provided</scope>
    </dependency>
    
  4. SpringBoot的启动类继承SpringBootServletInitializer,并重写configure

    @SpringBootApplication
    public class MyApplication extends SpringBootServletInitializer {
        //重写配置方法
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
            return application.sources(MyApplication.class);
        }
        public static void main(String[] args) {
            //启动SpringBoot
            SpringApplication.run(MyApplication.class,args);
        }
    }
    
  5. 使用install命令打包项目,并将war包放到tomcat下的webapps下,启动tomcat即可。
    Java 系列之 Springboot_第35张图片
    如果我们使用的是tomcat7则需要将javax.el-api-3.0.0.jar包放到tomcat下 的lib目录中。

十四、异常处理

14.1 简介

  • SpringMVC异常简介:系统中异常包括两类:预期异常(检查型异常)和运行时异常 RuntimeException,前者通过捕获异常从而获取异常信息, 后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。系统的 dao、service、controller 出现都通过 throws Exception 向上抛出,最后由 springmvc 前端控制器交由异常处理器进行异常处理,如下图
    Java 系列之 Springboot_第36张图片

  • 默认情况,Spring Boot项目错误页面如下。当项目实际上线,如果给用户显示这个页面就不是很友好。当系统出现异常时应该给用户显示更加友好的错误页面。

    1. 设置具体的状态码页面:在templates/下新建error文件夹,在error中新建:状态码.html的页面。例如当出现500时显示的页面为500.html

    2. 使用x进行模糊匹配:当出现5开头状态码的错误时,显示页面可以命名为5xx.html
      当出现50开头状态码的错误时,显示页面可以命名为50x.html

    3. 统一错误显示页面:在templates下新建error.html。如果项目中不存在具体状态码的页面或没有使用x成功匹配的页面时,显示error.html作为错误显示页面。

14.2 具体实现

  1. 使用@ExceptionHandler注解处理异常 缺点:只能处理当前Controller中的异常。

    @Controller
    public class ControllerDemo1 {
        @RequestMapping("test1.action")
        public String test1(){
            int i = 1/0;
            return "success";
        }
        @RequestMapping("test2.action")
        public String test2(){
            String s =null;
            System.out.println(s.length());
            return "success";
        }
        @ExceptionHandler(value ={ArithmeticException.class,NullPointerException.class} )
        public ModelAndView handelException(){
            ModelAndView mv =new ModelAndView();
            mv.setViewName("error1");
            return mv;
        }
    }
    
  2. 使用:@ControllerAdvice+@ExceptionHandler(此处优先级低于局部异常处理器)

    package com.msb.exceptionhandler;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.servlet.ModelAndView;
    /**
     * @Author: bingwoo
     */
    @ControllerAdvice
    public class GloableExceptionHandler1 {
        @ExceptionHandler(value ={ArithmeticException.class,NullPointerException.class} )
        public ModelAndView handelException(){
            ModelAndView mv =new ModelAndView();
            mv.setViewName("error1");
            return mv;
        }
    }
    
  3. 使用:SimpleMappingExceptionResolver(xml配置,配置类配置)

    /**
     * 全局异常
     */
    @Configuration
    public class GloableException2 {
        @Bean
        public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver(){
            SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
            Properties prop = new Properties();
            prop.put("java.lang.NullPointerException","error1");
            prop.put("java.lang.ArithmeticException","error2");
            resolver.setExceptionMappings(prop);
            return resolver;
        }
    }
    
  4. 自定义的HandlerExceptionResolver

    /**
     * 全局异常
     * HandlerExceptionResolve
     */
    @Configuration
    public class GloableException3 implements HandlerExceptionResolver {
      @Override
      public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
          ModelAndView mv = new ModelAndView();
          if(e instanceof NullPointerException){
                  mv.setViewName("error1");
          }
          if(e instanceof ArithmeticException){
                  mv.setViewName("error2");
          }
          mv.addObject("msg",e);
          return mv;
      }
    }
    

十五、单元测试类

  • 在src/main/test里面新建com.msb.项目上下文,注意:
    1. 测试类不能叫做Test,会和注解同名
    2. 测试方法必须是public
    3. 测试方法返回值必须是void
    4. 测试方法必须没有参数
    package com.msb;
    import com.msb.pojo.Emp;
    import com.msb.service.EmpService;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import java.util.List;
    @SpringBootTest(classes = Springboot03Application.class)
    class Springboot03AppliactionTests {
        @Autowired
        private EmpService empService;
        @Test
        public void testFindAll() {
            List<Emp> list = empService.findAll();
            list.forEach(System.out::println);
        }
    }
    

十六、bean管理

  • Spring Boot中Bean管理:
    • Spring Boot 由于没有XML文件,所以所有的Bean管理都放入在一个配置类中实现。
    • 配置类就是类上具有@Configuration的类。这个类就相当于之前的applicationContext.xml
  1. 新建配置类
    com.msb.config.MyConfig , 规范都是放入到config文件夹中。
    注意:配置类要有@Configuration,方法要有@Bean

    @Configuration
    public class MyConfig {
        //访问权限修饰符没有强制要求,一般是protected
        //返回值就是注入到Spring容器中实例类型。
        // 方法名没有强制要求,相当于中id属性。
        @Bean
        protected User getUser(){
            User user = new User();
            user.setId(1L);
            user.setName("张三");
            return user;
        }
        //自定义bean名称
        @Bean("user2")
        protected  User getUser2(){
            User user = new User();
            user.setId(2L);
            user.setName("李四");
            return user;
        }
    }
    
  2. 如果Spring容器中存在同类型的Bean通过Bean的名称获取到Bean对象。或结合@Qualifier使用

    @SpringBootTest
    public class TestGetBean {
        @Autowired
        @Qualifier("user2")
        private User user;
        @Test
        public void testGetUser(){
            System.out.println(user);
        }
    }
    
  3. 在配置类的方法中通过方法参数让Spring容器把对象注入。

    //自定义bean名称
    @Bean("user1")
    public  User getUser(){
        User user = new User();
        user.setId(2L);
        user.setName("李四");
        return user;
    }
    @Bean
    //可以直接从方法参数中取到。
    public People getPeople(User user1){
        People p = new People();
        p.setUser(user1);
        return p;
    }
    

十七、拦截器

  1. 新建拦截器类。(注意:不要忘记类上注解@Component)

    @Component
    public class DemoInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println("执行拦截器");
            return true;
        }
    }
    
  2. 配置拦截器(注意:类上有注解@Configuration。此类相当于SpringMVC配置文件。
    addPathPattern(): 拦截哪些URL。 /** 拦截全部excludePathPatterns(): 不拦截哪些URL。当和addPathPattern()冲突时,excludePathPatterns()生效。)

    @Configuration
    public class MyConfig implements WebMvcConfigurer {
        @Autowired
        private DemoInterceptor demoInterceptor;
        //配置拦截器的映射
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(demoInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
        }
    }
    

十八、 其他拓展

18.1 注解拓展

  • springboot默认已经帮助我们整合好了SpringMVC,同时也给我们默认配置了DispathcerServlet 和编码过滤器,同时也给我们配置好了WEB项目开发的常见组件
    查看容器中的所有组件
  1. @SpringBootApplication 注解

    /*
    * 默认扫描启动类所在包下的所有层级的子包
    * 可以通过scanBasePackages属性指定扫描路径
    * SpringBootApplication是一个合成注解,可以拆分为以下三个注解
    *   @SpringBootConfiguration
    *   @EnableAutoConfiguration
    *   @ComponentScan(basePackages = "com.msb")
    * */
    @SpringBootApplication
    public class Springboot04Application {
        public static void main(String[] args) {
            //返回一个spring容器
            ConfigurableApplicationContext context = SpringApplication.run(Springboot04Application.class, args);
            // 查看所有组件的名
            String[] names = context.getBeanDefinitionNames();
            for (String name : names) {
                System.out.println(name);
            }
        }
    }
    
  2. @Configuration 注解

    package com.msb.config;
    import com.msb.pojo.User;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    /**
     * @Author: bingwoo
     * MyConfig配置类本身也是一个spring容器中的bean
     * proxyBeanMethods=true 属性,给MyConfig对象产生一个代理对象
     * 通过代理对象控制反复调用MyConfig里面的方法返回的是容器中的一个单实例
     * 如果proxyBeanMethods=false 那么我们拿到的MyConfig对象就不是一个代理对象
     * 那么这个时候反复调用MyConfig中的方法返回的就是多实例
     *
     * proxyBeanMethods=false 称之为Lite模式  特点启动快
     * proxyBeanMethods=true  称之为Full模式  特点依赖spring容器控制bean单例
     *
     */
    @Configuration(proxyBeanMethods = true)
    public class MyConfig {
        /*... ...*/
        @Bean // 向容器中添加一个Bean,以方法名作为Bean的id,返回值类型作为组件的类型
        public User user1(){
            return new User("zhangsan", 10);
        }
        /*... ...*/
        @Bean("user2") // 向容器中添加一个Bean,手动指定Bean的name属性,返回值类型作为组件的类型
        public User getUser(){
            return new User("lisi", 11);
        }
    }
    
  3. @Import 注解

    package com.msb.config;
    import com.msb.pojo.User;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Import;
    /*
    * @Import({User.class}) 在容器中自动创建Bean的注解
    * 通过传入字节码,默认调用bean的无参构造器,向容器中存放一个Bean
    * 默认组件的名字就是类的全路径名
    * @Import只要放到可以被扫描到的类之上就可以,不必非得是配置类或者Controller
    * */
    @Import({User.class})
    @Configuration(proxyBeanMethods = true)
    public class MyConfig {
    }
    
    @SpringBootApplication(scanBasePackages = "com.msb")
    public class Springboot04Application {
        public static void main(String[] args) {
            //启动SpringBoot, 返回一个spring容器
            ConfigurableApplicationContext context = SpringApplication.run(Springboot04Application.class, args);
            // 根据类型获取Bean
            User bean = context.getBean(User.class);
            System.out.println(bean);
            // 获取属性User类的所有bean的name
            String[] beanNamesForType = context.getBeanNamesForType(User.class);
            for (String s : beanNamesForType) {
                System.out.println(s);
            }
        }
    }
    
  4. @Conditional 条件装配 注解

18.2 静态资源类

  1. 默认无前缀,如果想指定静态资源前缀,可以 通过spring.mvc.static-path-pattern配置
    Java 系列之 Springboot_第37张图片

  2. springboot还支持静态资源webjars 的处理方式,就是将静态资源打成jar导入

    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>jquery</artifactId>
        <version>3.6.0</version>
    </dependency>
    
  3. 拦截器静态资源放行

    package com.msb.config;
    import com.msb.interceptor.LoginInterceptor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    /**
     * @Author: bingwoo
     */
    @Configuration
    public class MyInterceptorRegist implements WebMvcConfigurer {
        @Autowired
        private LoginInterceptor loginInterceptor;
        //配置拦截器的映射
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
        	//放行地址
            registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/login","/login.html","/css/**","/js/**","/img/**","/font/**");
        }
    }
    

18.3 文件上传

  1. 导入依赖

    <dependency>
      <groupId>com.sun.jersey</groupId>
      <artifactId>jersey-client</artifactId>
      <version>1.19</version>
    </dependency>
    
  2. 页面代码

    <html>
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <style>
            .progress {
                width: 200px;
                height: 10px;
                border: 1px solid #ccc;
                border-radius: 10px;
                margin: 10px 0px;
                overflow: hidden;
            }
            /* 初始状态设置进度条宽度为0px */
            .progress > div {
                width: 0px;
                height: 100%;
                background-color: yellowgreen;
                transition: all .3s ease;
            }
        </style>
        <script type="text/javascript" src="js/jquery.min.js"></script>
        <script type="text/javascript">
            $(function(){
                $("#uploadFile").click(function(){
                    // 获取要上传的文件
                    var photoFile =$("#photo")[0].files[0]
                    if(photoFile==undefined){
                        alert("您还未选中文件")
                        return;
                    }
                    // 将文件装入FormData对象
                    var formData =new FormData();
                    formData.append("headPhoto",photoFile)
                    // ajax向后台发送文件
                    $.ajax({
                        type:"post",
                        data:formData,
                        url:"file/upload",
                        processData:false,
                        contentType:false,
                        success:function(result){
                            // 接收后台响应的信息
                            alert(result.message)
                            // 图片回显
                            $("#headImg").attr("src",result.newFileName);
                        },
                        xhr: function() {
                            var xhr = new XMLHttpRequest();
                            //使用XMLHttpRequest.upload监听上传过程,注册progress事件,打印回调函数中的event事件
                            xhr.upload.addEventListener('progress', function (e) {
                                //loaded代表上传了多少
                                //total代表总数为多少
                                var progressRate = (e.loaded / e.total) * 100 + '%';
                                //通过设置进度条的宽度达到效果
                                $('.progress > div').css('width', progressRate);
                            })
                            return xhr;
                        }
                    })
                })
            })
        </script>
    </head>
    <body>
    <form action="addPlayer" method="get">
        <p>账号<input type="text" name="name"></p>
        <p>密码<input type="text" name="password"></p>
        <p>昵称<input type="text" name="nickname"></p>
        <p>头像:
            <br/>
            <input id="photo" type="file">
            <br/>
            <img id="headImg" style="width: 200px;height: 200px" alt="你还未上传图片">
            <br/>
            <div class="progress">
                <div></div>
            </div>
            <a id="uploadFile" href="javascript:void(0)">立即上传</a>
        </p>
        <p><input type="submit" value="注册"></p>
    </form>
    </body>
    </html>
    
  3. Controller代码

    package com.msb.controller;
    import com.sun.jersey.api.client.Client;
    import com.sun.jersey.api.client.WebResource;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.multipart.MultipartFile;
    import javax.servlet.http.HttpServletRequest;
    import java.io.File;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.UUID;
    /**
     * @Author: bingwoo
     */
    @Controller
    @RequestMapping("/file")
    public class FileController {
        // 文件存储位置
        private final static String FILESERVER="http://127.0.0.1:8090/upload/";
        @RequestMapping("/upload")
        @ResponseBody
        public Map<String,String> upload(MultipartFile headPhoto, HttpServletRequest req) throws IOException {
            Map<String,String> map=new HashMap<>();
            // 指定文件存储目录为我们项目部署环境下的upload目录
            String realPath = req.getServletContext().getRealPath("/upload");
            File dir = new File(realPath);
            // 如果不存在则创建目录
            if(!dir.exists()){
                dir.mkdirs();
            }
            // 获取文件名
            String originalFilename = headPhoto.getOriginalFilename();
            // 避免文件名冲突,使用UUID替换文件名
            String uuid = UUID.randomUUID().toString();
            // 获取拓展名
            String extendsName = originalFilename.substring(originalFilename.lastIndexOf("."));
            // 新的文件名
            String newFileName=uuid.concat(extendsName);
            // 创建 sun公司提供的jersey包中的client对象
            Client client=Client.create();
            WebResource resource = client.resource(FILESERVER + newFileName);
            //  文件保存到另一个服务器上去了
            resource.put(String.class, headPhoto.getBytes());
            // 上传成功之后,把文件的名字和文件的类型返回给浏览器
            map.put("message", "上传成功");
            map.put("newFileName", FILESERVER+newFileName);
            map.put("filetype", headPhoto.getContentType());
            return map;
        }
    }
    
    
  4. yml中配置文件大小限制

    spring:
      servlet:
        multipart:
          max-file-size: 10MB
          max-request-size: 100MB
    
  5. 多文件同步上传处理方式

    <form action="file/upload" method="post" enctype="multipart/form-data">
        <p>账号<input type="text" name="name"></p>
        <p>密码<input type="text" name="password"></p>
        <p>昵称<input type="text" name="nickname"></p>
        <p>头像:
            <br/>
            <input id="photo" name="photo"  type="file">
            <input id="photos" name="photos"  type="file" multiple>
            <br/>
            <img id="headImg" style="width: 200px;height: 200px" alt="你还未上传图片">
            <br/>
        <div class="progress">
            <div></div>
        </div>
        <a id="uploadFile" href="javascript:void(0)">立即上传</a>
        </p>
        <p><input type="submit" value="注册"></p>
    </form>
    
  6. 后台接收的处理单元参数处理

    public Map<String,String> upload(String name,
            String password,String nickname,
            @RequestPart("photo") MultipartFile photo,
            @RequestPart("photos") MultipartFile[] photos, HttpServletRequest req)
    

18.4 MyBatis-plus

  • MyBatis-plus是mybatis的增强工具,在MyBatis 上只做增强,不做改变,为简化开发,提高效率而生。
  1. 安装
    Java 系列之 Springboot_第38张图片

  2. 导入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.21</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.2</version>
    </dependency>
    
  3. 自动配置的内容:MyBatis PlusAutoConfiguration配置类,MyBatisPlusProperties配置项前缀 mybatis-plus: 就是对mybatis-plus的参数的设置SQLSessionFactory已经配置好 mapperlocations 自动配置好的,默认值是classpath:/mapper//*.xml 意为任意包路径下所有的mapper包下的xml文件 @Mapper建议替换成MapperScan:配置mybatisplus

    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
        username: root
        password: root
      druid:
        initial-size: 5
        min-idle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,wall,slf4j
        connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
        web-stat-filter:
          enabled: true
          url-pattern: "/*"
          exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
        stat-view-servlet:
          url-pattern: "/druid/*"
          allow: 127.0.0.1,192.168.8.109
          deny: 192.168.1.188
          reset-enable: false
          login-username: admin
          login-password: 123456
    mybatis-plus:
      type-aliases-package: com.msb.pojo
    
  4. 分页插件的使用

    package com.msb.config;
    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    /**
     * @Author: bingwoo
     */
    @Configuration
    public class MyBatisPlusConfig {
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor mybatisPlusInterceptor =new MybatisPlusInterceptor();
            PaginationInnerInterceptor paginationInnerInterceptor =new PaginationInnerInterceptor();
            // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
            //paginationInnerInterceptor.setOverflow(false);
            // 设置最大单页限制数量,默认 500 条,-1 不受限制
            //paginationInnerInterceptor.setMaxLimit(500L);
            // 设置数据库类型
            paginationInnerInterceptor.setDbType(DbType.MYSQL);
            mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
            return mybatisPlusInterceptor;
        }
    }
    
  5. 测试代码

    @Test
    public void testPage(){
        // 当前页  页大小
        QueryWrapper<Dept> queryWrapper=new QueryWrapper<>();
        //queryWrapper.likeRight("dname", "A");
        Page<Dept> page = deptService.page(new Page<>(1, 2), queryWrapper);
        // 当前页数据  总页数  总记录数  当前页  页大小 ... ..
        List<Dept> list = page.getRecords();
        list.forEach(System.out::println);
        System.out.println("总页数:"+page.getPages());
        System.out.println("总记录数:"+page.getTotal());
        System.out.println("当前页:"+page.getCurrent());
        System.out.println("页大小:"+page.getSize());
    }
    

18.5 JUnit5单元测试

  • springboot 2.2.0开始引入Junit5作为单元测试的默认库。
  • JUnit5和之前的版本有很大的不同,由单个子项目的几个不同模块组成。
  • JUnit Platform ,是在JVM上启动测试框架的技术,不仅支持Junit自己的测试引擎,其他的测试引擎也可以
  • JUnit Jupiter,提供了Junit5的最新的编程模型,是Junit5 的核心,内部包含了一个测试引擎,用于在Junit Platform上运行
  • JUnit Vintager: 提供了兼容Junit4/3 的测试引擎
  • Junit5 = JUnit Platform+ JUnit Jupiter+JUnit Vintager
  1. Junit支持Spring中的注解,测试起来比较方便, @Autowired @Transactional 等
    Java 系列之 Springboot_第39张图片
  • 测试代码

    package com.msb;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.msb.mapper.DeptMapper;
    import com.msb.pojo.Dept;
    import com.msb.service.DeptService;
    import org.junit.jupiter.api.*;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.junit.platform.commons.annotation.Testable;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
    import org.springframework.test.context.BootstrapWith;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    @SpringBootTest // 使用springboot的容器功能
    /*@BootstrapWith(SpringBootTestContextBootstrapper.class)
    @ExtendWith({SpringExtension.class})*/
    @DisplayName("Junit5测试类")// 测试类描述
    class SpringbootMybatisplusApplicationTests2 {
        @Autowired
        private DeptMapper deptMapper;
        @BeforeEach
        public void testForeach(){
            System.out.println("beforeach");
        }
        @AfterEach
        public void testAftereach(){
            System.out.println("aferEach");
        }
        @BeforeAll
        public static void beforeAll(){
            System.out.println("beforall");
        }
        @AfterAll
        public static void aferAll(){
            System.out.println("afterAll");
        }
        @RepeatedTest(3)// 重复测试3次
        @Timeout(value = 10000,unit = TimeUnit.MILLISECONDS)// 超时时间设置
        @DisplayName("Junit测试方法1")
        @Test
        public void test1(){
            System.out.println("a");
            System.out.println(deptMapper);
        }
        @Disabled// 设置不可用
        @DisplayName("Junit测试方法2") // 方法描述
        @Test
        public void test2(){
            System.out.println("b");
        }
    }
    
  1. 断言机制:断定某件事情,一定会发生,如果没有发生,那就是出现了问题,所欲的测试运行结束后,会有一个详细的断言报告用来对测试需要满足的条件进行验证,这些断言方法都是org.junit.jupiter.api.Assertions中的静态方法,简单断言
    Java 系列之 Springboot_第40张图片
  • 测试代码
    package com.msb;
    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import java.time.Duration;
    import java.util.concurrent.TimeUnit;
    @SpringBootTest
    @DisplayName("Junit5断言测试类")
    class SpringbootMybatisplusApplicationTests3 {
        @DisplayName("简单断言1")
        @Test
        public void testAssertions1(){
            int add = add(1, 2);
            Assertions.assertEquals(6,add,"add结果计算错误");
        }
        public int add(int a,int b){
            return a+b;
        }
        @DisplayName("简单断言2")
        @Test
        public void testAssertions2(){
            String s =new String("xxx");
            String s2=new String("abc");
            Assertions.assertEquals(s,s2,"String对象不一样");
        }
        // 组合断言
        @DisplayName("组合断言")
        @Test
        public void testAssertAll(){
            Assertions.assertAll("AssertAll",
                    ()-> Assertions.assertTrue(true&& false),
                    ()-> Assertions.assertEquals(1,2));
        }
        // 异常断言 认为应该会出现异常
        @DisplayName("异常断言")
        @Test
        public void testAssertException(){
            Assertions.assertThrows(ArithmeticException.class, ()->{ int i=1/0;}, "没有抛出异常");
        }
        // 超时断言 判断有没有超时
        @DisplayName("超时断言")
        @Test
        public void testAssertTimeOut(){
            Assertions.assertTimeout(Duration.ofMillis(1000),()-> Thread.sleep(5000));
        }
        // 快速失败
        @DisplayName("快速失败")
        @Test
        public void testFail(){
            if(true){
                Assertions.fail("测试 失败");
            }
        }
    }
    
  1. 前置条件(assumptions假设):类似于断言,不同在于,不满足断言回事方法测试失败,而不满足的前置条件会使得的是方法的执行中止,前置条件可以看成是测试方法执行的前提,当条件不满足时,就没有继续执行的必要

    package com.msb;
    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Assumptions;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import java.time.Duration;
    @SpringBootTest
    @DisplayName("Junit5测试前置条件")
    class SpringbootMybatisplusApplicationTests4 {
        @DisplayName("测试前提条件")
        @Test
        public void testAssumptions(){
            // 假设为true,才会执行
            Assumptions.assumeTrue(false,"结果不是true");
            System.out.println("后面的测试代码前提条件");
        }
        @DisplayName("简单断言1")
        @Test
        public void testAssertions1(){
            int add =10;
            Assertions.assertEquals(6,add,"add结果计算错误");
            System.out.println("后面的测试代码简单断言");
        }
    }
    
  2. 嵌套测试

    package com.msb;
    import org.junit.jupiter.api.*;
    import static org.junit.jupiter.api.Assertions.*;
    import java.util.EmptyStackException;
    import java.util.Stack;
    @DisplayName("嵌套测试")
    class SpringbootMybatisplusApplicationTests5 {
        Stack<Object> stack;
        @Test
        @DisplayName("is instantiated with new Stack()")
        void isInstantiatedWithNew() {
            new Stack<>();
            // 外层的测试不能驱动内层的测试方法
            assertNull(stack);
        }
        @Nested
        @DisplayName("when new")
        class WhenNew {
            @BeforeEach
            void createNewStack() {
                stack = new Stack<>();
            }
            @Test
            @DisplayName("is empty")
            void isEmpty() {
                assertTrue(stack.isEmpty());
            }
            @Test
            @DisplayName("throws EmptyStackException when popped")
            void throwsExceptionWhenPopped() {
                assertThrows(EmptyStackException.class, stack::pop);
            }
            @Test
            @DisplayName("throws EmptyStackException when peeked")
            void throwsExceptionWhenPeeked() {
                assertThrows(EmptyStackException.class, stack::peek);
            }
            @Nested
            @DisplayName("after pushing an element")
            class AfterPushing {
                String anElement = "an element";
                @BeforeEach // 内层Test可以驱动外层的BeforeEach
                void pushAnElement() {
                    stack.push(anElement);
                }
                @Test
                @DisplayName("it is no longer empty")
                void isNotEmpty() {
                    assertFalse(stack.isEmpty());
                }
                @Test
                @DisplayName("returns the element when popped and is empty")
                void returnElementWhenPopped() {
                    assertEquals(anElement, stack.pop());
                    assertTrue(stack.isEmpty());
                }
                @Test
                @DisplayName("returns the element when peeked but remains not empty")
                void returnElementWhenPeeked() {
                    assertEquals(anElement, stack.peek());
                    assertFalse(stack.isEmpty());
                }
            }
        }
    }
    
  3. 参数化测试

    package com.msb;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Nested;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.params.ParameterizedTest;
    import org.junit.jupiter.params.provider.MethodSource;
    import org.junit.jupiter.params.provider.ValueSource;
    import java.util.EmptyStackException;
    import java.util.Stack;
    import java.util.stream.Stream;
    import static org.junit.jupiter.api.Assertions.*;
    @DisplayName("参数化测试")
    class SpringbootMybatisplusApplicationTests6 {
        @ParameterizedTest
        @ValueSource(ints = { 1, 2, 3 })
        void testWithValueSource(int argument) {
            System.out.println(argument);
            assertTrue(argument > 0 && argument < 4);
        }
        @ParameterizedTest
        @MethodSource("stringProvider")
        void testWithExplicitLocalMethodSource(String argument) {
            assertNotNull(argument);
        }
        static Stream<String> stringProvider() {
            return Stream.of("apple", "banana");
        }
    }
    

总结

以上是总结的Spring boot 原理、搭建过程、整合Mybatis、整合logbacks、整合PageHelper、拓展了打包、异常处理、拦截器等,共自己及大家学习。

你可能感兴趣的:(原创,spring,boot,java,spring)