0xCAFEBABE

talk is cheap show me the code

0%

SpringBoot入门指南

以spring为基础的应用搭建一站式解决方案

解决了传统spring应用配置繁杂的痛点 只需要少量或无需配置 (内嵌web容器) 就能构建一个产品级应用 极大简化开发

底层使用了大量的自动配置

与微服务架构(将一个大型应用拆分成若干个小服务 每个服务通过http协议进行通信)通过spring cloud连接 通过springboot 可以快速构建一个微服务单元

快速入门

构建

  1. 使用spring官网的启动器生成一个项目 并导入IDE
  2. 使用IDEA的spring initializr创建 项目基于maven

目录一览

springboot项目目录结构

  • mian 主要代码

    • java 代码目录
    • resources 资源目录
      • static 静态资源目录 (web)
      • templates 模板目录 (web)
      • application.properties 配置文件
  • test 测试代码

启动类

包含main方法 可启动一个spring boot项目

1
2
3
4
5
6
7
8
9
10
11
/**
* spring boot 启动类
*由此启动一个spring boot应用
* @SpringBootApplication 此注解担任 bean配置 启用自动配置 bean扫描(扫描同包下)等功能
*/
@SpringBootApplication
public class SpringBootHelloworldApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootHelloworldApplication.class, args);
}
}

项目打包

可通过插件将spring boot项目打包成jar包

1
2
3
4
5
6
7
8
9
10
11
 <!--
此插件可将项目打成jar包
-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

项目依赖

springboot依赖与各个框架的依赖关系及版本

  • 版本仲裁

    springboot只用一个父项目来管理各依赖版本

    1
    2
    3
    4
    5
    6
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
    </parent>
  • 场景启动器

    根据不同的使用场景 将相关依赖抽取为一个个starter 要使用该场景时只需导入相关的starter

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

启动流程

springboot启动流程

  • main方法启动

    1
    2
    3
    public static void main(String[] args) {
    SpringApplication.run(SpringbootJpaApplication.class, args);
    }
  • 创建SpringbootApplication实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    // 将主配置类添加至集合
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    //判断是否为web应用
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 找到类路径下META-INF/spring.factories下所有的ApplicationContextInitializer
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 从META-INF/spring.factories找到所有的ApplicationListener
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    //找到带main方法的配置类
    this.mainApplicationClass = deduceMainApplicationClass();
    }
  • run方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    //从META-INF/spring.factories 获取 SpringApplicationRunListeners
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 回调每个SpringApplicationRunListener的starting方法
    listeners.starting();
    try {
    // 封装命令行参数
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    // 准备环境 从中回调每个SpringApplicationRunListener的environmentPrepared方法
    ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
    configureIgnoreBeanInfo(environment);
    // 打印springboot横幅
    Banner printedBanner = printBanner(environment);
    // 根据环境创建ioc容器
    context = createApplicationContext();
    // 错误报告
    exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
    new Class[] { ConfigurableApplicationContext.class }, context);
    //准备上下文环境 其中的applyInitializers调用所有ApplicationContextInitializer的initialize方法 最后会调用所有的contextLoaded方法
    prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    // 刷新容器 如果为web应用 还会启动web容器
    refreshContext(context);
    // 从ioc容器中获取ApplicationRunner和 CommandLineRunner并回调
    afterRefresh(context, applicationArguments);
    stopWatch.stop();
    if (this.logStartupInfo) {
    new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
    }
    // 回调所有listener的started方法
    listeners.started(context);
    f(context, applicationArguments);
    }
    catch (Throwable ex) {
    handleRunFailure(context, ex, exceptionReporters, listeners);
    throw new IllegalStateException(ex);
    }

    try {
    // 回调所有running方法
    listeners.running(context);
    }
    catch (Throwable ex) {
    handleRunFailure(context, ex, exceptionReporters, null);
    throw new IllegalStateException(ex);
    }
    return context;
    }

事件监听

springboot通过事件监听机制 在应用启动时完成对项目的配置

通过类路径下META-INF声明的spring.factories文件 中指定的一系列监听器 可以在springboot启动过程中进行定制化

1
2
3
4
5
6
7
8
9
10
11
12
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
# 需要在IOC中注册
# ApplicationRunner CommandLineRunner

基本配置

spring boot自身通过导入大量自动配置类完成自动配置

自动配置

通过SpringBootApplication注解 可启用自动配置自动导入org.springframework.boot.autoconfigure 包下的自动配置 通过查找类路径下的META-INF/spring.factories 将自动配置类导入

启用自动配置

  • @EnableAutoConfiguration

    1
    2
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {

    通过AutoConfigurationImportSelector导入组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
    }
    // 加载类路径下META-INF/spring-autoconfigure-metadata.properties所声明的自动配置类信息
    AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
    .loadMetadata(this.beanClassLoader);
    // 获取所有符合条件的自动配置
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
    annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
    AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 获取候候选配置
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = filter(configurations, autoConfigurationMetadata);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
    }

自动配置类

每个xxxAutoConfiguration都是一个自动配置类 在应用启动时会加入容器 进行自动配置工作

可通过debug=true开启debug模式 查看哪些自动配置类生效

  • @HttpEncodingAutoConfiguration为例

    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration(proxyBeanMethods = false)
    //启用自动配置属性类 并将该类加入容器
    @EnableConfigurationProperties(HttpProperties.class)
    // @ConditionalOnXxx 根据不同条件使配置生效
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    @ConditionalOnClass(CharacterEncodingFilter.class)
    @ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
    public class HttpEncodingAutoConfiguration {

    配置属性类 自定义配置文件内容由配置类决定 XxxProperties

    1
    2
    @ConfigurationProperties(prefix = "spring.http")
    public class HttpProperties {
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Bean
    // 在容器确缺失此bean时添加
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
    CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    // bean的属性从配置类中获取
    filter.setEncoding(this.properties.getCharset().name());
    filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
    filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
    return filter;
    }

条件配置

springboot扩展了很多由@Conditional派生而来的注解 自动配置需要配满足一定条件才能生效

名称 作用
@ConditionalOnBean 容器中是否存在bean
@ConditionalOnClass 是否存在指定的类
@ConditionalOnCloudPlatform 云平台是否处于激活状态
@ConditionalOnEnabledResourceChain 是否开启spring资源处理链
@ConditionalOnExpression 依赖于SpringEL表达式的值
@ConditionalOnJava 是否匹配java版本
@ConditionalOnJndi 是否开启JNDI
@ConditionalOnMissingBean 容器中是否缺失bean
@ConditionalOnMissingClass 类路径下是否缺失指定类
@ConditionalOnMissingFilterBean 是否缺失指定类型的过滤器bean
@ConditionalOnNotWebApplication 是否不为web应用
@ConditionalOnProperty 检查指定属性是否有指定的值
@ConditionalOnRepositoryType 是否启用了指定的spring data持久化类型
@ConditionalOnResource 是否指定的配置文件在类路径下
@ConditionalOnSingleCandidate 是否存在单个候选bean
@ConditionalOnWebApplication 是否为web应用

手动配置

通过properties或yml文件修改默认配置

配置文件

  • properties

    经典java配置文件 基于key-value

    1
    server.port=8081
  • yaml

    以数据驱动 以层级关系为结构的配置文件 以空格缩进来表示层级关系 大小写敏感

    可以表示i

    • 字面量

      “” 非转义字符串 ‘’ 转义字符串

      1
      2
      3
      4
      5
      name:
      firstName: "das \n"
      lastName: 'ada\n'
      isMale: false
      age: 19
    • 对象 map

      用key-value表示对象

      1
      2
      3
      user:
      username: "root"
      password: "admin123"

      行内写法

      1
      user: {username: "root",password: admin123}
  • 集合

    用- 表示数组

    1
    2
    3
    4
    companies:
    - apple
    - google
    - amazon

    行内写法

    1
    companies: [apple, google, amazon]
  • 混合写法

    对象+数组

    1
    2
    3
    users:
    - {u: a, p: a}
    - {u:a, p: b}

配置绑定

将配置文件绑定到bean

  1. 添加配置处理器依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
    </dependency>
  2. 在要绑定属性的bean上添加@ConfigurationProperties并将该bean注册到容器 支持松散语法 驼峰命名转”-“连接 支持JSR303数据校验

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @ConfigurationProperties(prefix = "userinfo")
    @Component
    public class UserInfo {
    private Integer id;
    private Integer userId;
    private String nickname;
    private String age;
    private String description;
    private List<String> favorites;
  3. 重启应用(让ide有提示)

  4. 配置属性

    1
    2
    3
    4
    5
    userinfo:
    id: 12
    nickname: javabean
    address:
    province: 湖南
  5. @Value配置

    通过此注解可将单个配置文件值注入到bean 如果与配置文件中已有值的冲突 则优先使用配置文件

    1
    2
    3
    4
    @Value("#{6*6}") // spring el
    private Integer userId;
    @Value("${description}") // 从配置文件中获取
    private String description;
  6. @PropertiesSource 指定properties配置文件

    1
    @PropertySource(value = {"classpath:user.properties"})

    当与主配置文件存在相同配置时 优先使用主配置文件

  7. @ImportResource 导入spring配置文件

    用在配置类上 可导入基于xml的bean配置

    1
    2
    3
    4
    @SpringBootApplication
    @ImportResource(locations = {"classpath:bean.xml"})
    public class SpringBootHelloworldApplication {
    public static void main(String[] args) {
  8. 配置占位符

    spring boot提供了多种内置配置占位符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 随机字符串
    description=${random.value}
    # uuid
    description=${random.uuid}
    # 随机int值
    description=${random.int}
    # 随机long值
    description=${random.long}
    # 获取前面已经定义的值
    desc=${description}

注解配置

使用注解替代xml

  • @Configuration

    将类定义为配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * 配置类
    *
    */
    @Configuration
    public class RootConfig {
    /**
    * @Bean 将方法定义为生产bean的方法
    * @return
    */
    @Bean
    public Address address0(){
    return new Address();
    }
    }

配置文件优先级

当存在多个配置文件时的优先级

  • 应用在启动时会按照如下顺序加载配置文件 (优先级从高到低) 高优先级会覆盖低优先级 最终会取并集

    1. ./config 工程目录
    2. ./ 工程目录
    3. classpath:./config 类路径
    4. classpath:./ 类路径
  • 默认配置文件

    在项目打包后 可通过命令行参数 –spring.config.location 指定配置文件 此时该配置文件优先级最高

  • 配置加载顺序

    springboot支持多种外部配置方式

    优先级从高到低以如下顺序加载配置

    1. 命令行参数
    2. 来自java:comp.env的JNDI属性
    3. 来自系统属性System.getProperties()
    4. 操作系统环境变量
    5. RadomValuePropertySource配置的random.*属性值
    6. jar包外部的带profile配置的application.properties/yml文件
    7. jar包内部带profile配置的application.properties/yml文件
    8. jar包外部不带profile配置的application.properties/yml文件
    9. jar包内部不带profile配置的application.properties/yml文件
    10. @Configuration注解类上的@PropertySource
    11. 通过SpringApplication.setDefaultProperties指定的默认属性

多环境配置

根据环境切换配置

多Profile配置文件

通过多个appliaction-{profile}指定多个环境的配置文件 默认不指定环境配置文件为application

在properties配置的环境在yml文件中同样适用

  • properties激活特定环境

    spring.profiles.active属性指定环境 appliaction-{profile}配置多个环境配置文件

    1
    spring.profiles.active=prod
  • yaml切换环境

    通过文档块分割 实现一个文件切换多个环境

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    server:
    port: 8080
    spring:
    profiles:
    active: dev # 切换以下环境
    ---
    server:
    servlet:
    context-path: /prod
    spring:
    profiles: prod
    ---
    server:
    servlet:
    context-path: /dev
    spring:
    profiles: dev
  • 命令行方式切换

    通过命令行参数 –spring.profiles.active=切换环境

    通过虚拟机参数-Dspring.profiles.active= 切换环境

日志

spring boot 通过桥接和委托的方式将日志框架统一

spring默认底层使用jcl日志框架

springboot 使用了slf4j与logback组合

在使用非sfl4j实现的日志框架时 需要导入适配器包

在springboot中引入其他框架时 需要排除日志依赖

日志框架统一

springboot通过starter的方式将日志框架统一为slf4j

  1. 排除其他非slf4j的日志门面
  2. 导入中间包替换原有日志框架
  3. 导入slf4j的实现, 或导入桥接器包将非slf4j实现转为slf4j实现

日志配置

springboot默认日志级别为info

可通过logging.* 配置日志

1
2
3
4
5
6
7
8
9
10
11
# 修改某个包的日志级别
logging.level.com.yong=trace
# 日志文件存放目录 如果为相对路径 则相对于该项目
logging.file.name=application.log
# 日志存放目录 如果为相对路径 则相对于该项目 如果为绝对路径 则绝对与该项目所在的磁盘
# 如果与logging.file.name同时存在 则会此配置不会生效
logging.file.path=/spring
# 控制台日志格式
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%10t] %-5level %logger{3} - %msg%n
# 文件日志格式
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss}--[%t]--[%5level]--%logger: %msg%n

自定义配置

可通过在类路径下放上各个日志的配置文件 让日志框架直接读取

也可以在文件名后追加xxx.-spring.xx 启动springboot提供的高级日志功能 根据配置环境选择配置文件

1
2
3
4
5
6
<springProfile name="dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss}-[%t]-[%level]-%c: %m%n</pattern>
</springProfile>
<springProfile name="prod">
<pattern>[%level]-->%d{yyyy-MM-dd HH:mm:ss}-[%t]--%c: %m%n</pattern>
</springProfile>

切换日志框架

spring支持通过starter的方式切换日志框架

支持slf4j和log4j

WEB

使用springboot进行web开发时 只需要导入相应的starter 就能通过少量配置进行相关功能的开发

静态资源

springboot web应用如果以jar打包 需要配置静态资源路径

  • 静态资源映射规则

    • webjars

      静态资源通过jar包的方式引入

      查找路径: classpath:META-INF/resources/webjars

      1
      2
      3
      4
      5
      <dependency>
      <groupId>org.wbjars</groupId>
      <artifactId>jquery</artifactId>
      <version>3.3.2</version>
      </dependency>
    • 自定义资源

      默认从以下路径映射和当前项目的根路径静态资源

      1
      2
      private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
      "classpath:/resources/", "classpath:/static/", "classpath:/public/" };

Thymeleaf

springboot推荐的模板引擎 通过starter的方式引入

详情thymeleaf官方手册

  • 自动配置

    1
    2
    3
    4
    5
    @ConfigurationProperties(prefix = "spring.thymeleaf")
    public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
  • 手动配置

    1
    2
    # 禁用模板缓存 模板引擎实时生效 需要在修改后按ctrl+f9重新编译(IDEA)
    spring.thymeleaf.cache=false
  • 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
  • 基本语法

    • 内置对象

      1
      2
      3
      4
      5
      6
      7
      #ctx : the context object. 
      #vars: the context variables.
      #locale : the context locale.
      #request : (only in Web Contexts) the HttpServletRequest object.
      #response : (only in Web Contexts) the HttpServletResponse object.
      #session : (only in Web Contexts) the HttpSession object.
      #servletContext : (only in Web Contexts) the ServletContext object.
    • 工具对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      #execInfo : information about the template being processed. 
      #messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
      #uris : methods for escaping parts of URLs/URIs
      #conversions : methods for executing the configured conversion service (if any). #dates : methods for java.util.Date objects: formatting, component extraction, etc.
      #calendars : analogous to #dates , but for java.util.Calendar objects.
      #numbers : methods for formatting numeric objects.
      #strings : methods for String objects: contains, startsWith, prepending/appending, etc.
      #objects : methods for objects in general.
      #bools : methods for boolean evaluation.
      #arrays : methods for arrays.
      #lists : methods for lists.
      #sets : methods for sets.
      #maps : methods for maps.
      #aggregates : methods for creating aggregates on arrays or collections.
      #ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).
  • 消息表达式

    获取国际化内容

    1
    <p th:utext="#{home.welcome}">Welcome to our grocery store!</p>
  • 变量表达式

    1
    <p>Today is: <span th:text="${today}">13 february 2011</span>.</p>
  • 选择器表达式

    1
    2
    3
    4
    5
    <div th:object="${session.user}">
    <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
    </div>
  • url表达式

    1
    2
    3
    <a href="details.html"    th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>
    <!-- 带参数的url -->
    <a th:href="@{${url}(orderId=${o.id})}">view</a> <a th:href="@{'/details/'+${user.login}(orderId=${o.id})}">view</a>
  • 循环

    1
    2
    3
    4
    5
    <tr th:each="prod : ${prods}">      
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
    </tr>
  • 引入片段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div th:fragment="copy">      
    &copy; 2011 The Good Thymes Virtual Grocery
    </div>
    <!-- 插入 -->
    <div th:insert="footer :: copy"></div>
    <!-- 替换 -->
    <div th:replace="footer :: copy"></div>
    <!-- 包含 只包含其内容 -->
    <div th:include="footer :: copy"></div>

webmvc

springboot自动配置了spring mvc

自动配置

spirngboot默认配置了如下内容:

  • 视图解析器

  • 静态资源映射

  • 类型转换器 日志格式化器

    日期格式化器默认格式yyyy/MM/dd 可通过spring.mvc.date-format修改

  • 消息转换器 用于转换http请求和响应

  • 错误代码解析器

  • 数据绑定器

附加配置

用过自定义配置类进行附加配置

不能使用@EnableWebMvc 否则springboot的spring mvc自动配置会失效

1
2
3
// 自定义web配置 
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

相当于@EnableWebWvc

1
2
@Configuration(proxyBeanMethods = false)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {

查找自定义配置类

1
2
3
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

代理配置类导入其他自定义配置

1
2
3
4
5
6
7
class WebMvcConfigurerComposite implements WebMvcConfigurer {
private final List<WebMvcConfigurer> delegates = new ArrayList<>();
public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.delegates.addAll(configurers);
}
}

当使用@EnableWebMvc全面接管spring mvc时

1
2
3
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

会使自动配置类条件不成立而失效

1
2
3
4
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
  • 国际化配置

    1
    2
    # 国际化资源路径
    spring.messages.basename=i18n.index
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 自定义Locale解析器
    public class MyLocaleResolver implements LocaleResolver {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
    String localeStr = request.getParameter("locale");
    logger.info("locale", localeStr);
    if(localeStr!=null && !StringUtils.isEmpty(localeStr)){
    String[] split = localeStr.split("_");
    return new Locale(split[0], split[1]);
    }
    return Locale.getDefault();
    }

错误处理

springboot可根据不同的访问设备返回不同的错误信息 html或json

根据请求头判断应该返回响应类型

  • 自动配置

    springboot通过ErrorMvcAutoConfiguration自动配置错误页面

    主要通过添加以下组件对错误进行处理

    • DefaultErrorAttributes

      页面错误信息

    • BasicErrorController

      处理/error下的请求

    • ErrorPageCustomizer

      定制错误页面

    • DefaultErrorViewResolver

      错误视图解析器

  • 定制错误页面

    • 错误页面

      在有模板解析的情况下 可在模板目录放上error/状态码.html 错误页面 或 4xx 5xx 模糊匹配 可获取错误信息

      如果没有模板引擎 在静态资源目录下找

      如果都没有 来到默认错误页面

    • json

      需要自定义异常处理器 返回json数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 只能返回json数据
      @ResponseBody
      @ExceptionHandler(RuntimeException.class)
      public Map<String, Object> handleException(Throwable error){
      Map<String, Object> map = new HashMap<>();
      map.put("status", "error");
      map.put("message", error.getMessage());
      return map;
      }

      错误页面与json共存

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @ExceptionHandler(RuntimeException.class)
      public String handleException(HttpServletRequest request, Throwable error){
      Map<String, Object> map = new HashMap<>();
      request.setAttribute("javax.servlet.error.status_code", 500);
      map.put("status", "error");
      map.put("message", "error");
      // 添加错误信息 可以通过自定义ErrorAttribute取出并放入
      request.setAttribute("ext", map);
      return "forward:/error";
      }
  • 自定义ErrorAttribute

    通过自定义ErrorAttributes可以放入自定义的错误信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
    Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
    // 将自定义错误处理器中的附加错误信息取出
    map.put("ext",webRequest.getAttribute("ext", RequestAttributes.SCOPE_REQUEST));
    return map;
    }
    }

定制化WEB容器

springboot支持多种web嵌入式容器 通过starter方式引入

默认为tomcat

容器配置

修改各个容器的配置

  • 基本配置类

    所有的容器配置都在此 各个容器的配置通过静态内部类的形式展现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Configuration(proxyBeanMethods = false)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
    @ConditionalOnClass(ServletRequest.class)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @EnableConfigurationProperties(ServerProperties.class)
    // 导入各个容器的配置类
    @Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
    ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
    ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
    ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
    public class ServletWebServerFactoryAutoConfiguration {
  • 容器配置(以tomcat为例)

    • 配置文件

      1
      2
      3
      4
      server:
      port: 8080
      tomcat:
      uri-encoding: UTF-8
    • 配置类

      可通过自定义WebServerFactoryCustomizer配置类进行定制化配置

      1
      2
      3
      4
      5
      6
      7
      @Component
      public class MyServletContainerCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
      @Override
      public void customize(ConfigurableServletWebServerFactory factory) {
      factory.setPort(3323);
      }
      }
  • 容器自动配置原理

    通过bean后置处理器注册器向容器中注册WebServerFactoryCustomizerBeanPostProcessor 容器定制化器后置处理器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
    BeanDefinitionRegistry registry) {
    if (this.beanFactory == null) {
    return;
    }
    registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor",
    WebServerFactoryCustomizerBeanPostProcessor.class);
    registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor",
    ErrorPageRegistrarBeanPostProcessor.class);
    }

    在容器定制化器中 应用每个定制化器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    if (bean instanceof WebServerFactory) {
    postProcessBeforeInitialization((WebServerFactory) bean);
    }
    return bean;
    }
    private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
    // 遍历并应用每个定制化器
    LambdaSafe.callbacks(WebServerFactoryCustomizer.class, getCustomizers(), webServerFactory)
    .withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
    .invoke((customizer) -> customizer.customize(webServerFactory));
    }
    // 从bean工厂中获取所有容器定制器类
    private Collection<WebServerFactoryCustomizer<?>> getWebServerFactoryCustomizerBeans() {
    return (Collection) this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false).values();
    }
  • 容器启动过程

    ServletWebServerApplicationContext容器初始化在调用onRefresh方法时会启动容器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Override
    protected void onRefresh() {
    super.onRefresh();
    try {
    createWebServer();
    }catch (Throwable ex) {
    throw new ApplicationContextException("Unable to start web server", ex);
    }
    }

createWebServer()

1
this.webServer = factory.getWebServer(getSelfInitializer()); // 容器工厂创建容器

最终调用initialize()方法完成容器的启动

1
2
// Start the server to trigger initialization listeners
this.tomcat.start();

注册三大组件

springboot支持注册三大组件 filter listener

  • servlet

    1
    2
    3
    4
    5
    6
    7
    @Bean
    ServletRegistrationBean<HttpServlet> servletRegistrationBean(){
    ServletRegistrationBean<HttpServlet> bean = new ServletRegistrationBean<>();
    bean.setServlet(new MyServlet());
    bean.addUrlMappings("/servlet");
    return bean;
    }
  • filter

    1
    2
    3
    4
    5
    6
    7
    @Bean
    FilterRegistrationBean<HttpFilter> filterFilterRegistrationBean(){
    FilterRegistrationBean<HttpFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new MyFilter());
    registrationBean.addUrlPatterns("/*");
    return registrationBean;
    }
  • listener

    1
    2
    3
    4
    5
    6
    @Bean
    ServletListenerRegistrationBean<MySeesionListener> seesionListenerServletListenerRegistrationBean(){
    ServletListenerRegistrationBean<MySeesionListener> seesionListenerServletListenerRegistrationBean = new ServletListenerRegistrationBean<>();
    seesionListenerServletListenerRegistrationBean.setListener(new MySeesionListener());
    return seesionListenerServletListenerRegistrationBean;
    }

切换web容器

springboot支持切换容器

通过starter方式切换容器

默认为tomcat

  1. 排除默认容器

    1
    2
    3
    4
    5
    6
    <exclusions>
    <exclusion>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    </exclusion>
    </exclusions>
  2. 引入其他容器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- jetty -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
    <!-- undertow 不支持jsp -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>

使用外部容器

通过打war包的方式 使用外部容器

通过servlet3.0的spi模式 在tomcat启动时 通过@HandlesTypes注解查找WebApplicationInitializer 的实现类进行DispatcherServlet和容器的初始化

1
2
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

执行每一个WebApplicationInitializeronStartup方法 完成初始化

1
2
3
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
  • 启动过程 先启动容器 再启动应用

    将主启动类添加至应用

    1
    2
    3
    4
    5
    6
    public class ServletInitializer extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    return application.sources(SpringbootWebappApplication.class);
    }
    }

    onStartup方法创建ioc容器

    1
    2
    3
    4
    5
    6
    7
    public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
    // ...
    WebApplicationContext rootAppContext = createRootApplicationContext(servletContext);
    // ...
    }

    启动应用

    1
    2
    3
    4
    5
    6
    protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
    // ...
    builder = configure(builder);
    // ...
    return run(application);
    }

数据访问

springboot 通过starter的方式 引入持久层框架 与spring data集成

JDBC

基本的jdbc配置 使用数据源

  • 导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!-- starter -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <!-- jdbc驱动 -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>
  • 连接配置

    默认使用com.zaxxer.hikari.HikariDataSource(springboot 2.x) 作为数据源

    springboot内置支持 Tomcat Hikari DBCP 数据源 还可通过spring.datasource.type 指定自己的数据源

    1
    2
    3
    4
    spring.datasource.username=root
    spring.datasource.password=admin123
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai

    对于自定义数据源 通过反射创建对象 并绑定配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "spring.datasource.type")
    static class Generic {
    @Bean
    DataSource dataSource(DataSourceProperties properties) {
    return properties.initializeDataSourceBuilder().build();
    }
    }
  • 数据源初始化

    通过spring的事件监听机制 对数据源进行初始化

    1
    class DataSourceInitializerInvoker implements ApplicationListener<DataSourceSchemaCreatedEvent>, InitializingBean {

    在bean的所有属性复赋值后调用initialize初始化方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override
    public void afterPropertiesSet() {
    DataSourceInitializer initializer = getDataSourceInitializer();
    if (initializer != null) {
    boolean schemaCreated = this.dataSourceInitializer.createSchema();
    if (schemaCreated) {
    initialize(initializer);
    }
    }
    }

    初始化时执行所有的sql脚本

    1
    2
    3
    4
    5
    6
    7
    8
    private void initialize(DataSourceInitializer initializer) {
    //..
    // 发布事件
    this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer.getDataSource()));
    // The listener might not be registered yet, so don't rely on it.
    this.dataSourceInitializer.initSchema();
    // ..
    }

    initSchema 执行所有脚本

    1
    2
    3
    4
    5
    6
    7
    void initSchema() {
    // 获取所有脚本文件路径
    List<Resource> scripts = getScripts("spring.datasource.data", this.properties.getData(), "data");
    // ..
    runScripts(scripts, username, password);
    // ..
    }

    事件发生时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override
    public void onApplicationEvent(DataSourceSchemaCreatedEvent event) {
    // NOTE the event can happen more than once and
    // the event datasource is not used
    DataSourceInitializer initializer = getDataSourceInitializer();
    if (!this.initialized && initializer != null) {
    initializer.initSchema();
    this.initialized = true;
    }
    }

    启用启动时执行sql脚本

    1
    2
    3
    4
    5
    6
    7
    # 启用脚本执行 如果ALWAYS 会查找类路径下的schema.*.sql 和data.*.sql 建表和出插入数据
    spring.datasource.initialization-mode=ALWAYS
    # 指定sql文件
    # 执行建表语句
    spring.datasource.schema=classpath:schema.sql
    # 执行插入语句
    spring.datasource.data=classpath:data.sql
  • JdbcTemplate

    自动配置了JdbcTemplate和NamedParameterJdbcTemplate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Bean
    @Primary
    NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) {
    return new NamedParameterJdbcTemplate(jdbcTemplate);
    }
    @Bean
    @Primary
    JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    JdbcProperties.Template template = properties.getTemplate();
    jdbcTemplate.setFetchSize(template.getFetchSize());
    jdbcTemplate.setMaxRows(template.getMaxRows());
    if (template.getQueryTimeout() != null) {
    jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
    }
    return jdbcTemplate;
    }

数据源监控

Druid数据源提供基于servlet的监控功能

  • 引入druid starter

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.21</version>
    </dependency>
  • 数据源监控配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 用户名密码
    spring.datasource.druid.stat-view-servlet.login-username=root
    spring.datasource.druid.stat-view-servlet.login-password=admin123
    # 是否启用
    spring.datasource.druid.stat-view-servlet.enabled=true
    # 监控页面入口
    spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
    spring.datasource.druid.web-stat-filter.enabled=true
    # 路径排除
    spring.datasource.druid.web-stat-filter.exclusions=*.css,*.js,/druid/*
    # 过滤器拦截路径
    spring.datasource.druid.web-stat-filter.url-pattern=/*

整合MyBatis

springboot通过starter的方式引入MyBatis自动配置

  • 导入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
    </dependency>
  • 自定义配置

    1
    2
    3
    4
    mybatis:
    configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
  • 自定义配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Mapper扫描
    @MapperScan("com.yong.springboot.mapper")
    @org.springframework.context.annotation.Configuration
    public class MyBatisConfig {
    @Bean
    ConfigurationCustomizer configurationCustomizer() {
    return new ConfigurationCustomizer() {
    @Override
    public void customize(Configuration configuration) {
    // TODO Auto-generated method stub
    configuration.setMapUnderscoreToCamelCase(true);
    }
    };
    }
    }
  • 自动配置

    springboot自动配置好了 SqlSessionFactory SqlSessionTemplate MapperScannerConfigurer

    1
    2
    3
    4
    5
    6
    @org.springframework.context.annotation.Configuration
    @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
    @ConditionalOnSingleCandidate(DataSource.class)
    @EnableConfigurationProperties(MybatisProperties.class)
    @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
    public class MybatisAutoConfiguration implements InitializingBean {

Mapper 配置

  • 注解

    1
    2
    3
    4
    //@Mapper
    public interface UserMapper {
    @Select("select * from t_user")
    List<User> findAll();
  • xml

    1
    2
    3
    4
    5
    6
    7
    8
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.yong.springboot.mapper.UserMapper">
    <select id="findAll">
    select * from t_user
    </select>

    配置映射和配置文件路径

    1
    2
    3
    4
    mybatis:
    check-config-location: true
    config-location: classpath:mybatis-config.xml
    mapper-locations: classpath:com/yong/springboot/mapper/*.xml

JPA

spring data jpa 提供对关系型数据库的统一API 简化操作 底层使用hibernate

JPA是JSR107规范 用于简化持久层开发

入门

  • 导入依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
  • 配置文件

    1
    2
    3
    4
    5
    spring:
    jpa:
    show-sql: true
    hibernate:
    ddl-auto: update

注解关系映射

使用注解完成实体类的数据库关系隐射

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@Table(name="t_emp")
public class Employee {
@Id // 主键
@GeneratedValue(strategy=GenerationType.IDENTITY) // 自增
private Integer id;
@Column
private String name;
@Column // 列
private Integer age;
@Column
private Integer deptId;

Repository

spring data jpa 提供统一的CRUD接口

顶层接口

1
2
3
@Indexed
public interface Repository<T, ID> {
}

CRUD接口

1
2
@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {

分页接口

1
2
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

JPA接口

1
2
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
  • Dao实现

    1
    public interface EmployeeRepository extends JpaRepository<Employee, Integer> {}

自定义starter

可按照springboot的自动配置规范 自定义starter

一般的starter由 starter与autoconfigurer两部分组成

starter依赖autoconfigurer

使用时只需导入starter

  • 新建boot项目autoconfigure 引入唯一依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    </dependency>
  • 新建空maven项目starter 引入autoconfigure

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.yong</groupId>
    <artifactId>magic-spring-boot-autoconfigure</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    </dependency>
  • 类路径下新建WEB-INF/spring.factories 将自动配置类引入 在应用启动时会查找该类并加入ioc容器

    1
    2
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.yong.spring.boot.autoconfigure.MagicAutoConfigure

缓存

springboot提供了统一的缓存接口(Spring Data) 与JSR107规范类似 更易于使用

starter

springboot通过starter的方式引入缓存支持

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • 自动配置

    springboot提供了对缓存的自动配置

    主要支持如下缓存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
    mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
    mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
    mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
    mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
    mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
    mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
    mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
    mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
    mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);

    默认SimpleCacheConfiguration生效 底层使用ConcurrentHashMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(CacheManager.class)
    @Conditional(CacheCondition.class)
    class SimpleCacheConfiguration {
    @Bean
    ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
    CacheManagerCustomizers cacheManagerCustomizers) {
    ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
    List<String> cacheNames = cacheProperties.getCacheNames();
    if (!cacheNames.isEmpty()) {
    cacheManager.setCacheNames(cacheNames);
    }
    return cacheManagerCustomizers.customize(cacheManager);
    }
    }

    @Cacheable方法执行前 会先尝试获取缓存时按照cacheNames获取Cache组件 如果不存在会创建缓存并将方法的返回结果放入缓存 整体通过AOP实现 默认使用SimpleKeyGenerator

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Override
    @Nullable
    public Cache getCache(String name) {
    Cache cache = this.cacheMap.get(name);
    if (cache == null && this.dynamic) {
    synchronized (this.cacheMap) {
    cache = this.cacheMap.get(name);
    if (cache == null) {
    cache = createConcurrentMapCache(name);
    this.cacheMap.put(name, cache);
    }
    }
    }
    return cache;
    }

注解驱动缓存

注解方式配置缓存

需要在配置类中开启@EnableCaching注解驱动缓存

  • @Cacheable

    缓存方法的返回值

    参数一览 第二次不会执行直接走缓存

    • cacheNames/value 指定缓存组件的名字

    • key 缓存数据使用的key 默认使用方法参数值 1 使用方法的返回值 可写SpEL

      #root.methodName 当前方法名

      #root.method 当前方法

      #root.args 当前方法的参数列表

      #root.target 当前调用方法的对象

      #root.targetClass 当前调用方法所属的类

      #root.caches 当前方法使用的缓存列表

      #参数名 获取参数值

      #result 方法返回结果

    • keyGenerator key生成器 可以自己指定key的生成器组件id 与key二选一

      自定义KeyGenerator

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Bean
      KeyGenerator keyGenerator() {
      return new KeyGenerator() {
      @Override
      public Object generate(Object target, Method method, Object... params) {
      // TODO Auto-generated method stub
      return method.getName()+"["+Arrays.toString(params)+"]";
      }
      };
      }
  • cacheManager 指定缓存管理器

  • condition 指定符合条件的情况下缓存

  • unless 否定缓存 当为true时方法的返回值不会被缓存 可获取方法的返回值进行判断

  • sync 是否异步缓存

    1
    2
    3
    4
    @Cacheable(cacheNames= {"emp"}, key="#root.method")
    public List<Employee> getAll(){
    return employeeDao.findAll();
    }
  • @CachePut

    方法执行完毕后更新缓存

    应该与相应的查询方法使用相同的缓存才能正确跟新缓存

  • @CacheEvict

    删除缓存

    • allEntries 默认false 是否删除所有缓存
    • beforeInvocation 默认false 是否崽方法执行前删除 如果方法抛异常 之后不会删除缓存
  • @Caching 定义复杂缓存规则

  • @CacheConfig 定义公共缓存配置

整合Redis

通过starter的方式整合Redis(Spring data redis)

  • 导入starter

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  • 自动配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RedisOperations.class)
    @EnableConfigurationProperties(RedisProperties.class)
    @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
    public class RedisAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
    throws UnknownHostException {
    // 默认采用JDK序列化机制 要求实体类实现Serializable接口
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
    }
    @Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
    throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
    }
    }
  • 自定义CacheManager 实现json序列化 如果存在多个CacheManager 需要使用@Primary指定一个主要CacheManager

    1
    2
    3
    4
    5
    6
    7
    @Bean
    RedisCacheManager empCacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
    defaultCacheConfiguration = defaultCacheConfiguration.serializeValuesWith(SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<Employee>(Employee.class))); // 使用json序列化
    RedisCacheManager cacheManager = new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), defaultCacheConfiguration);
    return cacheManager;
    }

消息中间件

springboot 与消息队列整合

通过使用消息队列 可以

实现消息的生产与消费异步执行

微服务之间的通信

缓解后端服务压力(流量削降)

概述

通过使用消息中间件可以提升应用的异步通信 扩展解耦能力

  • 消息服务

    • 消息代理

      消息发送者发送消息后 将由消息代理接管 消息代理保证消息传递到指定目的地

    • 目的地

      消息接收者

      目的地的主要形式有

      • 队列 点对点消息通信
      • 主题 发布/订阅 消息通信
  • 消息服务规范

    • JMS

      基于JVM消息代理规范

      例如ActiveMQ HornetMQ

    • AMQP

      高级消息队列协议 兼容JMQ 支持跨语言和平台

      例如RabbitMQ

RabbitMQ

AMQP协议的消息中间件

  • 核心概念

    • Message 消息 由消息头和消息体组成

    • Publisher 消息生产者

    • Exchange 交换器 用于将生产者发送的消息交给消息队列

      消息分发类型

      • direct 路由键完全匹配 才会派发到相应的消息队列
      • fanout 将消息派发到每个消息队列
      • topic 路由键模糊匹配 # 匹配0个或多个单词 * 匹配一个单词
    • Queue 消息队列 保存消息知道发送给消费者

    • Binding 绑定 用于消息队列与交换器之间的关联 消息头的路由键与队列的映射关系

    • Conncection TCP连接

    • Channel 信道 用于多路复用 消息传输通过信道完成 一个TCP连接上可承载多个信道

    • Comsumer 消费者 消息的接收者

    • Virtual Host 虚拟主机 表示一批交换器 消息队列 和相关对象 每个虚拟主机相互隔离

    • Broker 表示消息队列服务器实体

  • 安装

    1. 官网下载安装程序 下载链接
    2. 点击安装
    3. cmd到安装目录/sbin
    4. 安装web管理插件 rabbitmq-plugins enable rabbitmq_management
    5. 启动服务器 rabbitmq-server
    6. 浏览器输入 http://localhost:15672/ 初始用户名密码都为guest

springboot整合

  • 引入starter

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  • 自动配置类

    导入了

    • CachingConnectionFactory 连接工厂

    • AmqpAdmin 管理组件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // 添加队列
      Queue queue = new Queue("myqueue");
      amqpAdmin.declareQueue(queue);
      // 添加交换器
      Exchange exchange = new DirectExchange("");
      amqpAdmin.declareExchange(exchange);
      //添加Binding
      Binding binding = new Binding("amqpadmin.main", DestinationType.QUEUE, "amqpadmin", "amqpadmin", null);
      amqpAdmin.declareBinding(binding);
  • RabbitTemplate 消息操作组件

    1
    2
    3
    4
    5
    6
    Map<String, Object> msg = new HashMap<>();
    msg.put("msg", "from spring");
    // 向指定交换器发送消息
    rabbitTemplate.convertAndSend("yong.main", "yong.main", msg);
    // 接收指定队列的消息
    Object object = rabbitTemplate.receiveAndConvert("main");
  • 事件监听

    开启Rabbit注解驱动

    1
    2
    3
    @SpringBootApplication
    @EnableRabbit
    public class SpringbootAmqpApplication {

    监听指定队列 一旦有消息 立即调用

    1
    2
    3
    4
    @RabbitListener(queues= {"main"})
    void onReceived(Message msg) {
    System.out.println(msg);
    }

全文检索

springboot整合spring data elasticserach 提供检索支持

ElasticSerach

一种分布式搜索服务 为大型应用提供全文检索支持 基于RESTFull API 建立在 Lucene 之上

通过HTTP请求对数据进行存储和检索

数据通过文档的方式储存 并通过索引和类型进行归类

PUT: 添加和更新

GET: 获取

DELETE: 删除索引

springboot整合

通过starter完成整合

提供两种方式操作es

  1. Jest
  2. SpringData ElasticSearch
  • 导入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
  • 自动配置

    jest

    1
    2
    3
    4
    5
    6
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(JestClient.class)
    @EnableConfigurationProperties(JestProperties.class)
    @AutoConfigureAfter(GsonAutoConfiguration.class)
    @Deprecated
    public class JestAutoConfiguration {

    rest

    1
    2
    3
    4
    5
    6
    7
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RestClient.class)
    @EnableConfigurationProperties(RestClientProperties.class)
    @Import({ RestClientConfigurations.RestClientBuilderConfiguration.class,
    RestClientConfigurations.RestHighLevelClientConfiguration.class,
    RestClientConfigurations.RestClientFallbackConfiguration.class })
    public class RestClientAutoConfiguration {

    spring data

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ ElasticsearchTemplate.class })
    @AutoConfigureAfter({ ElasticsearchAutoConfiguration.class, RestClientAutoConfiguration.class,
    ReactiveRestClientAutoConfiguration.class })
    @Import({ ElasticsearchDataConfiguration.BaseConfiguration.class,
    ElasticsearchDataConfiguration.TransportClientConfiguration.class,
    ElasticsearchDataConfiguration.RestClientConfiguration.class,
    ElasticsearchDataConfiguration.ReactiveRestClientConfiguration.class })
    public class ElasticsearchDataAutoConfiguration {
  • 操作类

    • Jest

      1
      2
      3
      4
      Index index = new Index.Builder(news).index("love").type("news").id("1").build();
      jestClient.execute(index);
      Get get = new Get.Builder("love", "1").type("news").build();
      DocumentResult documentResult = jestClient.execute(get);
    • Repository

      1
      2
      bookRepo.index(book);
      Optional<Book> optional = bookRepo.findById(1);
    • ElasticsearchTemplate

      1
      2
      3
      4
      5
      6
      IndexQuery query = new IndexQuery();
      query.setIndexName("yong");
      query.setType("news");
      query.setObject(news);
      query.setId("1");
      elasticsearchTemplate.index(query);

任务

spring boot 支持各种任务

异步任务

通过注解异步任务支持

  • @Async 标注一个方法将异步执行 (开启一个线程)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Async
    public void doAsync() {
    logger.info("starting.......");
    try {
    TimeUnit.SECONDS.sleep(3);
    } catch (Exception e) {
    // TODO Auto-generated catch block
    throw new RuntimeException(e);
    }
    logger.info("done.......");
    }
  • @EnableAync 开启异步支持 需要标在主配置类上

定时任务

通过注解驱动

  • @Scheduled 标记一个方法为定制任务 该方法不可带参数和返回值

    • corn 时间间隔表达式

      秒 分 时 日 月 周

      特殊字符

      • , 枚举
      • - 区间
      • * 任意
      • / 步长 0/4 从0秒启动 每4秒执行一次
      • ? 日 /星期 冲突匹配
      • L 最后
      • W 工作日
      • C 于calendar联系后计算的值
      • # 星期 4#2 第二个星期四
    1
    2
    3
    4
    @Scheduled(cron="* * * * * MON-FRI")
    public void doSchedule() {
    logger.info("do schedule.....");
    }
  • @EnableScheduling 开启定时任务功能

邮件任务

springboot提供了邮件相关starter

  • 导入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
  • 邮件配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 邮件用户名
    spring.mail.username=2455499971@qq.com
    # 授权码
    spring.mail.password=
    # 邮件服务器地址
    spring.mail.host=smtp.qq.com
    # 开启ssl
    spring.mail.properties.mail.smtp.ssl.enable=true
    # 端口
    spring.mail.port=
  • 发送邮件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Autowired
    JavaMailSenderImpl sender;
    // 发送简单邮件
    SimpleMailMessage mail = new SimpleMailMessage();
    mail.setTo("asdfghjklhcy@hotmail.com");
    mail.setFrom("2455499971@qq.com");
    mail.setSubject("test");
    mail.setText("这是一封测试邮件");
    sender.send(mail);
    // 带附件的邮件
    MimeMessage mimeMessage = sender.createMimeMessage();
    MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
    helper.setTo("asdfghjklhcy@hotmail.com");
    helper.setFrom("2455499971@qq.com");
    helper.setText("<h1 style='color:red'>大新闻</h1>", true);
    helper.setSubject("breaking news");
    helper.addAttachment("test.jpg", new File(""));
    sender.send(mimeMessage);

安全

springboot 整合了 spring security 通过starter呈现

spring security

安全框架 整合了权限验证等等安全手段

  • 依赖导入

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  • demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // 定义spring security 配置类
    @EnableWebSecurity
    public class MySecurityConfig extends WebSecurityConfigurerAdapter{
    /**
    * 定义授权规则
    *
    */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    // TODO Auto-generated method stub
    // url 权限验证 规则
    http.authorizeRequests().antMatchers("/level1/**").hasRole("l1");
    http.formLogin().loginProcessingUrl("/auth").successForwardUrl("/level1");
    // 自定义登陆页面
    // http.formLogin().loginPage("");
    // 注销url 默认 /logout
    http.logout();
    // 记住我 (启用后 cookie在注销后会销毁)
    http.rememberMe();
    }
    /**
    * 认证规则
    *
    */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    /**
    * 自定义用户角色
    *
    */
    @SuppressWarnings("deprecation")
    UserDetails user = User.withDefaultPasswordEncoder()
    .username("root")
    .password("admin123")
    .roles("l1")
    .build();
    // 存储在内存中
    auth.inMemoryAuthentication().withUser(user);
    }
    }

分布式

根据微服务思想 将一个大型应用拆分成若干个小模块 将通过分布式服务框架组成分布式系统

常见的框架包括

  1. ZooKeeper

    注册中心 协调微服务

  2. Dubbo

    调用框架 负责调用微服务

ZooKeeper+Dubbo

解决两个服务之间的远程调用问题

通过接口共享实现远程调用

  1. 创建两个子项目分为生产者和消费者 和一个公共api

  2. 为生产者和消费者导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.5</version>
    </dependency>
    <dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.13.0</version>
    </dependency>
  3. 生产者实现公共api

    1
    2
    3
    4
    5
    6
    7
    8
    @Component
    @Service // 将服务注册到zookeeper容器
    public class ProviderServiceImpl implements ProviderService {
    @Override
    public String doProvide() {
    return "getting product";
    }
    }
  4. 消费者从容器中获取接口实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    public class CustomerService {
    @Reference // 从容器中获取
    ProviderService providerService;
    public void doCustome(){
    System.out.println(providerService.getClass());
    String s = providerService.doProvide();
    System.out.println(s);
    }
    }
  5. dubbo配置

    1
    2
    3
    dubbo.application.name=provider
    dubbo.registry.address=zookeeper://localhost:2181
    dubbo.scan.base-packages=com.yong.springboot.service

Spring Cloud

spring提供的分布式整体解决方案

  • 五大组件

    • Netflix Eureka 服务发现
    • Netflix Ribbon 客户端负载均衡
    • Netflix Hystrix 断路器
    • Netflix Zuul 服务网关
    • Spring Cloud Config 分布式配置
  • Eureka

    基于http协议的远程调用

    注册中心(服务器)配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server:
    port: 8000
    eureka:
    instance:
    hostname: euraka-server
    client:
    register-with-eureka: false # 注册中心本身不注册
    fetch-registry: false # 不获取注册信息
    service-url:
    defaultZone: http://localhost:8000/eureka/ # 服务注册地址

    生产者与消费者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    server:
    port: 8001
    eureka:
    instance:
    prefer-ip-address: true # 注册服务时 使用服务的IP地址
    appname: producer
    client:
    service-url:
    defaultZone: http://localhost:8000/eureka/

    激活eureka服务器

    1
    2
    3
    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaServerApplication

    启用服务发现

    1
    2
    3
    @SpringBootApplication
    @EnableDiscoveryClient
    public class EurekaConsumerApplication {

    远程调用

    1
    2
    3
    4
    @GetMapping("/consume")
    String consume(){
    return restTemplate.getForObject("http://PRODUCER/produce", String.class);
    }

热部署

通过引入springboot提供的devtool实现热重载

对于IDEA 修改后按Ctrl+f9 重载

对Eclipse Ctrl + s保存后重载

1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

状态监控

springboot提供了actuat状态监控组件

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • 配置信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # autoconfig 所有自动配置信息
    # auditevents 审计事件
    # beans 所有bean信息
    # configprops 所有配置属性
    # threaddump 线程信息
    # env 当前环境信息
    # health 应用健康状况
    # info 应用信息
    # metrics 应用的各项指标
    # mappings 应用@RequestMapping映射路径
    # shutdown 关闭当前应用
    # trace 追踪信息
    # heapdump 堆转储文件
    # 暴露端点
    management.endpoints.web.exposure.include=health,info,beans,env
    # 启用某个端点
    management.endpoint.shutdown.enabled=true
    # 入口url
    management.endpoints.web.base-path=
    # 指定端口
    management.server.port=8081
    # 总是显示健康状态细节
    management.endpoint.health.show-details=always

自定义HealthIndicator

通过实现HealthIndicator 可自定义健康状态信息 命名必须符合 xxxHealthIndicator

1
2
3
4
5
6
7
@Component
public class MyAppHealthIndicator implements HealthIndicator {
@Override
public Health health() {
return Health.down().withDetail("error", "internal error").build();
}
}