纷杂的spring-boot-starter

时间:2024-10-01 16:45:32

纷杂的spring-boot-starter

  • 简述
  • 应用日志和spring-boot-starter-logging
  • 快速Web应用开发与spring-boot-starter-web
    • 项目结构层面的约定
    • SpringMVC框架层面的约定和定制
    • 嵌入式Web容器层面的约定和定制
  • 数据访问与spring-boot-starter-jdbc
    • SpringBoot应用的数据库版本化管理
  • spring-boot-starter-aop及其使用场景说明
    • spring-boot-starter-aop在构建spring-boot-starter-metrics自定义模块中的应用
  • 应用安全与spring-boot-starter-security
    • SpringSecurity基本设计
    • 定制spring-boot-starter-security
  • 应用监控与spring-boot-starter-actuator
    • 自定义应用的健康状态检查
    • 开放的endpoints

简述

1)基于Spring框架的“约定优先于配置(COC)”理念以及最佳实践之路。
2)提供了针对日常企业应用研发各种场景的spring-boot-starter自动配置依赖模块,如此多“开箱即用”的依赖模块,使得开发各种场景的Spring应用更加快速和高效。

SpringBoot提供的这些“开箱即用”的依赖模块都约定以spring-boot-starter-作为命名的前缀,并且皆位于包或者命名空间下(虽然SpringBoot的官方参考文档中提到不建议大家使用spring-boot-starter-来命名自己写的类似的自动配置依赖模块,但实际上,配合不同的groupId,这不应该是什么问题 )。

所有的spring-boot-starter都有约定俗成的默认配置,但允许我们调整这些配置以改变默认的配置行为,即“约定优先于配置”。在介绍相应的spring-boot-starter的默认配置(约定)以及可调整配置之前,我们有必要对SpringBoot应用的配置约定先做一个简单的介绍。

简单来讲,我们可以将对SpringBoot的行为可以进行干预的配置方式划分为几类:

  • 命令行参数(Command Line Args)。
  • 系统环境变量(Environment Variables)。
  • 位于文件系统中的配置文件。
  • 位于classpath中的配置文件。
  • 固化到代码中的配置项。

为了简化,其他比较少见场景的配置方式不在这里罗列。总的来说,以上几种方式按照优先级从高到低排列,高优先级方式提供的配置项可以覆盖或者优先生效,比如通过命令行参数传入的配置项会覆盖通过环境变量传入的同一配置项,当然也会覆盖其他后面几种方式给出的同一配置项。

不管是位于文件系统还是classpath,SpringBoot应用默认的配置文件名叫作,可以直接放在当前项目的根目录下或者名称为config的子目录下。

以上是关于SpringBoot应用配置方式的简单介绍,基本可以满足我们后面讲解的需要,所以,现在让我们进入纷杂的spring-boot-starter探索之旅吧!

应用日志和spring-boot-starter-logging

Java的日志系统多种多样,从默认提供的日志支持,到log4j,log4j2,commons logging等,复杂繁多,所以,应用日志系统的配置就会比较特殊,从而spring-boot-starter-logging也比较特殊一些,下面将其作为我们第一个了解的自动配置依赖模块。

假如maven依赖中添加了spring-boot-starter-logging:

<dependency>
    <groupId></groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

那么,我们的SpringBoot应用将自动使用logback作为应用日志框架,SpringBoot启动的时候,由-Application-Listener根据情况初始化并使用。

SpringBoot为我们提供了很多默认的日志配置,所以,只要将spring-boot-starter-logging作为依赖加入到当前应用的classpath,则“开箱即用”,不需要做任何多余的配置,但假设我们要对默认SpringBoot提供的应用日志设定做调整,则可以通过几种方式进行配置调整:

·遵循logback的约定,在classpath中使用自己定制的配置文件。

·在文件系统中任何一个位置提供自己的配置文件,然后通过配置项指向这个配置文件来启用它,比如在中指定如下的配置。

=/{}/
  • 1

注意 SpringBoot默认允许我们通过在配置文件或者命令行等方式使用和来自定义日志文件的名称和存放路径,不过,这只是允许我们在SpringBoot框架预先定义的默认日志系统设定的基础上做有限的设置,如果我们希望更灵活的配置,最好通过框架特定的配置方式提供相应的配置文件,然后通过来启用。

如果大家更习惯使用log4j或者log4j2,那么也可以采用类似的方式将它们对应的spring-boot-starter依赖模块加到Maven依赖中即可:

<dependency>
    <groupId></groupId>
    <artifactId>spring-boot-starter-log4j</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

或者

<dependency>
    <groupId></groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

但一定不要将这些完成同一目的的spring-boot-starter都加到依赖中。

快速Web应用开发与spring-boot-starter-web

在这个互联网时代,使用Spring框架除了开发少数的独立应用,大部分情况下实际上在使用SpringMVC开发web应用,为了帮我们简化快速搭建并开发一个Web项目,SpringBoot为我们提供了spring-boot-starter-web自动配置模块。

只要将spring-boot-starter-web加入项目的maven依赖:

<dependency>
    <groupId></groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

我们就得到了一个直接可执行的Web应用,当前项目下运行mvn spring-boot:run就可以直接启动一个使用了嵌入式tomcat服务请求的Web应用,只不过,我们还没有提供任何服务Web请求的Controller,所以,访问任何路径都会返回一个SpringBoot默认提供的错误页面(一般称其为whitelabel error page),我们可以在当前项目下新建一个服务根路径Web请求的Controller实现:

@RestController
public class IndexController {
    @RequestMapping("/")
    public String index() {
        return "hello, there";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

重新运行mvn spring-boot:run并访问http://localhost:8080,错误页面将被我们的Controller返回的消息所替代,一个简单的Web应用就这样完成了。

但是,简单的背后,其实却有很多“潜规则”(约定),我们只有充分了解了这些“潜规则”,才能更好地应用spring-boot-starter-web。

项目结构层面的约定

项目结构层面与传统打包为war的Java Web应用的差异在于,静态文件和页面模板的存放位置变了,原来是放在src/main/webapp目录下的一系列资源,现在都统一放在src/main/resources相应子目录下,比如:
1)src/main/resources/static用于存放各类静态资源,比如css,js等。
2)src/main/resources/templates用于存放模板文件,比如*.vm。

注意 当然,如果还是希望以war包的形式,而不是SpringBoot推荐使用的独立jar包形式发布Web应用,也可以继续原来Java Web应用的项目结构约定。

SpringMVC框架层面的约定和定制

spring-boot-starter-web默认将为我们自动配置如下一些SpringMVC必要组件:

  • 必要的ViewResolver,比如ContentNegotiatingViewResolver和Bean-NameViewResolver。

  • 将必要的Converter、GenericConverter和Formatter等bean注册到IoC容器。

  • 添加一系列的HttpMessageConverter以便支持对Web请求和相应的类型转换。

  • 自动配置和注册MessageCodesResolver。

  • 其他。

任何时候,如果我们对默认提供的SpringMVC组件设定不满意,都可以在IoC容器中注册新的同类型的bean定义来替换,或者直接提供一个基于WebMvcConfigurerAdapter类型的bean定义来定制,甚至直接提供一个标注了@EnableWebMvc的@Configuration配置类完全接管所有SpringMVC的相关配置,自己完全重新配置。

嵌入式Web容器层面的约定和定制

spring-boot-starter-web默认使用嵌入式tomcat作为web容器对外提供HTTP服务,默认将使用8080端口对外监听和提供服务:

1)假设我们不想使用默认的嵌入式tomcat(spring-boot-starter-tomcat自动配置模块),那么可以引入spring-boot-starter-jetty或者spring-boot-starter-undertow作为替代方案。

2)假设我们不想使用默认的8080端口,那么我们可以通过更改配置项使用自己指定的端口,比如:

=9000
  • 1

spring-boot-starter-web提供了很多以server.为前缀的配置项用于对嵌入式Web容器提供配置,比如:

  • .*
  • .*

如果这些依然无法满足需求,SpringBoot甚至允许我们直接对嵌入式的Web容器实例进行定制,这可以通过向IoC容器中注册一个EmbeddedServletContainerCustomizer类型的组件来对嵌入式Web容器进行定制:

public class UnveilSpringEmbeddedTomcatCustomizer implements Embed-dedServletContainerCustomizer {
    @Override
    public void customize(ConfigurableEmbeddedServletContainer container) {
        container.setPort(9999);
        container.setContextPath("/unveil-spring-chapter3");
        // ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

再深入的定制则需要针对特定的嵌入式Web容器,使用实现对应的Factory并注册到IoC容器:

  • TomcatEmbeddedServletContainerFactory
  • JettyEmbeddedServletContainerFactory
  • UndertowEmbeddedServletContainerFactory

但是,笔者认为大家几乎没有走到这一步的必要,而且建议最好也不要走到这一步,目前来看,spring-boot-starter-web提供的配置项列表已经是最简单和完备的定制方式了。

数据访问与spring-boot-starter-jdbc

大部分Java应用都需要访问数据库,尤其是服务层,所以,SpringBoot会为我们自动配置相应的数据访问设施。

若想SpringBoot为我们自动配置数据访问的基础设施,那么,我们需要直接或者间接地依赖spring-jdbc,一旦spring-jdbc位于我们SpringBoot应用的classpath,即会触发数据访问相关的自动配置行为,最简单的做法就是把spring-boot-starter-jdbc加为应用的依赖。

默认情况下,如果我们没有配置任何DataSource,那么,SpringBoot会为我们自动配置一个基于嵌入式数据库的DataSource,这种自动配置行为其实很适合于测试场景,但对实际的开发帮助不大,基本上我们会自己配置一个DataSource实例,或者通过自动配置模块提供的配置参数对DataSource实例进行自定义的配置。

假设我们的SpringBoot应用只依赖一个数据库,那么,使用DataSource自动配置模块提供的配置参数是最方便的:

=jdbc:mysql://{database host}:3306/{databaseName}
={database username}
={database password}
  • 1
  • 2
  • 3

当然,自己配置一个DataSource也是可以的,SpringBoot也会智能地选择我们自己配置的这个DataSource实例(只不过必要性真不大)。

除了DataSource会自动配置,SpringBoot还会自动配置相应的JdbcTemplate、DataSourceTransactionManager等关联“设施”,可谓服务周到,我们只要在使用的地方注入就可以了:

class SomeDao {
    @Autowired
    JdbcTemplate jdbcTemplate;
    public <T> List<T> queryForList(String sql){
        // ...
    }
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

不过,spring-boot-starter-jdbc以及与其相关的自动配置也不总是带来便利,在某些场景下,我们可能会在一个应用中需要依赖和访问多个数据库,这个时候就会出现问题了。

假设我们在ApplicationContext中配置了多个DataSource实例指向多个数据库:

@Bean
public DataSource dataSource1() throws Throwable {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUrl(...);
    dataSource.setUsername(...);
    dataSource.setPassword(...);
    // TODO other settings if necessary in the future.
    return dataSource;
}
@Bean
public DataSource dataSource2() throws Throwable {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUrl(...);
    dataSource.setUsername(...);
    dataSource.setPassword(...);
    // TODO other settings if necessary in the future.
    return dataSource;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

那么,不好意思,启动SpringBoot应用的时候会抛出类似如下的异常(Exception):

Exception):
No qualifying bean of type [] is defined: expected single matching bean but found 2
  • 1
  • 2

为了避免这种情况的发生,我们需要在SpringBoot的启动类上做点儿“手脚”:

@SpringBootApplication(exclude = {
        DataSourceAutoConfiguration.class, 
        DataSourceTransactionManagerAutoConfiguration.class
})
public class UnveilSpringChapter3Application {
    public static void main(String[] args) {
        SpringApplication.run(UnveilSpringChapter3Application.class, args);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

也就是说,我们需要在这种场景下排除掉对SpringBoot默认提供的DataSource相关的自动配置

但如果我们还是想要享受SpringBoot提供的自动配置DataSource的机能,也可以通过为其中一个DataSource配置添加这个Annotation的方式以实现两全其美:

@Bean
@Primary
public DataSource dataSource1() throws Throwable {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUrl(...);
    dataSource.setUsername(...);
    dataSource.setPassword(...);
    // TODO other settings if necessary in the future.
    return dataSource;
}
@Bean
public DataSource dataSource2() throws Throwable {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUrl(...);
    dataSource.setUsername(...);
    dataSource.setPassword(...);
    // TODO other settings if necessary in the future.
    return dataSource;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

另外,SpringBoot还提供了很多其他数据访问相关的自动配置模块,比如spring-boot-starter-data-jpa、spring-boot-starter-data-mongodb等,大家可以根据自己数据访问的具体场景选择使用这些自动配置模块。

如果选择了spring-boot-starter-data-jpa等关系数据库相关的数据访问自动配置模块,并且还需要同时依赖访问多个数据库,那么,也需要相应的在SpringBoot启动类中排除掉这些自动配置模块中的AutoConfiguration实现类(对应spring-boot-starter-data-jpa是JpaRepositoriesAutoConfiguration),或者标注某个DataSource为@Primary。

SpringBoot应用的数据库版本化管理

如何针对数据库的变更进行版本化管理,从Ruby On Rails的migration支持,到Java的MyBatis Migrations,Flyway以及Liquibase,都给出了相应的最佳实践建议和方案,但是,从国内业界现状,数据库migrations的实践方式并没有在国内普遍应用起来,大部分都是靠人来解决,这或许可以用一句“成熟度不够”来解释,另外一个原因或许是职能明确分工后造成的局面。

如果仔细分析以上数据库migration方案就会发现,它们给出的应用场景和实践几乎都是单应用、单部署的,这在庞大单一部署单元(Monolith)的年代显然是很适合的,因为应用从开发到发布部署,再到启动,整个生命周期内,应用相关的所有“原材料”都集中在一起进行管理,而且国外开发者往往偏“特种作战”(Full-Stack Developer),一身多能,从而数据库migration这种实践自然可以成型并广泛应用。

但回到国内来看,我们往往是“集团军作战”,拼的是“大部队+明确分工”的模式,而且应用所面向的服务人数也往往更为庞大,所以,整个应用的交付链路上各个环节之间的衔接是不同的人,而应用最终部署的拓扑又往往是分布式部署居多

spring-boot-starter-aop及其使用场景说明

在经历了代码生成、动态代理、字节码增强甚至静态编译等不同时代的洗礼之后,Java平台上的AOP方案基本上已经以SpringAOP结合AspectJ的方式稳固下来

现在Spring框架提供的AOP方案倡导了一种各取所长的方案,即使用SpringAOP的面向对象的方式来编写和组织织入逻辑,并使用AspectJ的Pointcut描述语言配合Annotation来标注和指明织入点(Jointpoint)。

原则上来说,我们只要引入Spring框架中AOP的相应依赖就可以直接使用Spring的AOP支持了,不过,为了进一步为大家使用SpringAOP提供便利,SpringBoot还是“不厌其烦”地为我们提供了一个spring-boot-starter-aop自动配置模块

spring-boot-starter-aop自动配置行为由两部分内容组成:

  1. 位于spring-boot-autoconfigure的提供@Configuration配置类和相应的配置项。

  2. spring-boot-starter-aop模块自身提供了针对spring-aop、aspectjrt和aspectjweaver的依赖。

一般情况下,只要项目依赖中加入了spring-boot-starter-aop,其实就会自动触发AOP的关联行为,包括构建相应的AutoProxyCreator,将横切关注点织入(Weave)相应的目标对象等,不过AopAutoConfiguration依然为我们提供了可怜的两个配置项,用来有限地干预AOP相关配置:

=true
-target-class=false
  • 1
  • 2

对我们来说,这两个配置项的最大意义在于:允许我们投反对票,比如可以选择关闭自动的aop配置(=false),或者启用针对class而不是interface级别的aop代理(aop proxy)。

AOP的应用场景很多,我们不妨以当下最热门的APM(Application Performance Monitoring)为实例场景,尝试使用spring-boot-starter-aop的支持打造一个应用性能监控的工具原型。

spring-boot-starter-aop在构建spring-boot-starter-metrics自定义模块中的应用

对于应用性能监控来说,架构逻辑上其实很简单,基本上就是三步走
应用性能监控关键环节示意图

本节暂时只构建一个spring-boot-starter-metrics自定义的自动配置模块用来解决“应用性能数据采集”的问题。

在此之前,有几个原则我们需要先说明一下:

·虽然说采集应用性能数据可以帮助我们更好地分析和改进应用的性能指标,但这不意味着可以借着APM的名义对应用的核心职能形成侵害,加上应用性能数据采集功能一定会对应用的性能本身带来拖累,你拿到的所谓性能数据是分摊了你的数据采集方案带来的负担,所以,一般情况下,最好把应用性能数据采集模块的性能损耗控制在10%以内甚至更小。

·SpringAOP其实提供了多种横切逻辑织入机制(Weaving),性能损耗上也是各有差别,
运行期间的动态代理和字节码增强Weavng,到类加载期间的Weaving,甚至高冷的AspectJ二次静态编译Weaving,大家可以根据情况灵活把握。

·针对应用性能数据的采集,最好对应用开发者是透明的,通过配置外部化的形式,可以最大限度地剥离这部分对应用开发者来说非核心的关注点,只在部署和运行之前确定采集点并配置上线即可,虽然本节实例采用基于@Annotation的方式来标注性能采集点,但不意味着这是最优的方式,更多是基于技术方案(SpringAOP)的现状给出的一种实践方式。

下面我们正式着手构建spring-boot-starter-metrics自定义的自动配置模块的设计和实现方案。

本次的主角我们选择Java中的Dropwizard Metrics这个类库作为打造我们APM原型的起点。

Dropwizard Metrics为我们提供了多种不同类型的应用数据度量方案,且通过相应的数据处理算法在性能和批量状态的管理上做了很优秀的工作,只不过,如果我们直接用它的API来对自己的应用代码进行度量的话,那写起来代码太多,而且这些性能代码混杂在应用的核心逻辑执行路径上,一个是界面不友好,另外一个就是不容易维护:

public class MockService implements InitializingBean{
    @Autowired
    MetricRegistry metricRegistry;
    private Timer timer;
    private Counter counter;
    // define more other metrics...
    public void doSth(){
        counter.inc();
        Timer.Context context = timer.time();
        try{
            System.out.println("just do something.");
        }finally {
            context.stop();
        }
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        timer = metricRegistry.timer("timerToProfilingDoSthMethod");
        counter = metricRegistry.counter("counterForDoSthMethod");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

对于这些非功能性的性能度量代码,我们可以使用AOP的方式剥离到相应的Aspect中单独维护,而为了能够将这些性能度量的Aspect挂接到指定的待度量代码上,基于现有的方案选型,可以使用metrics-annotation提供的一系列Annotation来标注织入位置,这样,开发者只要在需要度量的代码位置上标注相应的Annotation,我们提供的spring-boot-starter-metrics自定义的自动配置模块就会自动地收集这些位置上指定的性能度量数据

我们通过/构建一个SpringBoot的脚手架项目
然后在创建好的SpringBoot脚手架项目的中添加如下必要配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="/POM/4.0.0" xmlns:xsi= "http:///2001/XMLSchema-instance"
       xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.">
    <modelVersion>4.0.0</modelVersion>
    <groupId></groupId>
    <artifactId>spring-boot-starter-metrics</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>spring-boot-starter-metrics</name>
    <description>auto configuration module for dropwizard metrics</description>
    <parent>
        <groupId></groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <>UTF-8</>
        <>1.8</>
        <>3.1.2</>
    </properties>
    <!--其他配置-->
    <dependencies>
        <dependency>
            <groupId></groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId></groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId></groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId></groupId>
            <artifactId>metrics-core</artifactId>
            <version>${}</version>
        </dependency>
        <dependency>
            <groupId></groupId>
            <artifactId>metrics-annotation</artifactId>
            <version>${}</version>
        </dependency>
        <dependency>
            <groupId></groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.7</version>
        </dependency>
    </dependencies>
</project>

  • 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
  • 53

中有几个关键配置需要关注:

  • 继承了spring-boot-starter-parent,用于加入springboot的相关依赖。
  • 添加了spring-boot-starter-aop依赖,目的你懂的,本章节的主角就是它。
  • 添加了下相应的依赖,用来引入dropwizard metrics类库和必要的Annotations。
  • 添加了spring-boot-starter-actuator,这个自动配置模块,主要是引入它对dropwizard metrics和JMX的一部分自动配置逻辑,比如针对MetricRegistry和MBeanServer的自动配置,这样我们就可以直接@Autowired来注入使用MetricRegistry和MBeanServer。
  • aspectjrt,是使用了最新的版本,原则上spring-boot-starter-aop已经有依赖,这里可以不用明确添加配置。

如果单单是一个提供必要依赖的自动配置模块,那么到这里其实就可以结束了,但我们的spring-boot-starter-metrics需要使用AOP提供相应的横切关注点逻辑,所以,还需要编写并提供一些必要的代码组件,因此,最少我们先要提供一个@Configuration配置类,用于将我们即将提供的这些AOP逻辑暴露给使用者

@Configuration
@ComponentScan({"", ""})
@AutoConfigureAfter(AopAutoConfiguration.class)
public class DropwizardMetricsMBeansAutoConfiguration {
    @Value("${:}")
    String metricsMBeansDomainName;
    @Autowired
    MBeanServer mbeanServer;
    @Autowired
    MetricRegistry metricRegistry;
    @Bean
    public JmxReporter jmxReporter() {
        JmxReporter reporter = JmxReporter
                .forRegistry(metricRegistry)
                .inDomain(metricsMBeansDomainName)
                .registerWith(mbeanServer)
                .build();
        return reporter;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

然后就是将这个配置类添加到META-INF/:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.keevol.springboot.metrics.autocfg.DropwizardMetricsMBeansAuto-ConfigurationOK, 

  • 1
  • 2
  • 3

DropwizardMetricsMBeansAutoConfiguration配置类,这个配置类的实现很简单,注入了MBeanServer和MetricRegistry的实例,并开放了一个配置属性(默认值)便于使用者指定自定义的MBean暴露和访问的命名空间,当然,这些其实都不是重点,因为它们都只是为了将我们要采集的性能数据指标以JMX的形式暴露出去而服务的,重点在于DropwizardMetricsMBeansAutoConfiguration头顶上的那几顶“帽子”:

  • @Configuration自然不必说了,这是一个JavaConfig配置类。

  • @ComponentScan({“”,“”}),为了简便,让@ComponentScan把这两个java package下的所有组件都加载到IoC容器中,这些组件就包括我们要提供的一系列与AOP和Dropwizard Metrics相关的实现逻辑。

  • @AutoConfigureAfter()告诉SpringBoot:“我希望DropwizardMetricsMBeansAutoConfiguration在AopAutoConfiguration完成之后进行配置”。

最后的秘密就隐藏在@ComponentScan背后的两个java package之下了。

首先是,在这个java package下面,我们只提供了一个AutoMetricsAspect,其定义如下:

@Component
@Aspect
public class AutoMetricsAspect {
    protected ConcurrentMap<String, Meter> meters = new ConcurrentHashMap<>();
    protected ConcurrentMap<String, Meter> exceptionMeters = new ConcurrentHashMap<>();
    protected ConcurrentMap<String, Timer> timers = new ConcurrentHashMap<>();
    protected ConcurrentMap<String, Counter> counters = new ConcurrentHashMap<>();
    @Autowired
    MetricRegistry metricRegistry;
    @Pointcut(value = "execution(public * *(..))")
    public void publicMethods() {
    }
    @Before("publicMethods() && @annotation(countedAnnotation)")
    public void instrumentCounted(JoinPoint jp, Counted countedAnnotation) {
        String name = name(jp.getTarget().getClass(), StringUtils.hasLength(countedAnnotation.name()) ? countedAnnotation.name() : jp.getSignature().getName(), "counter");
        Counter counter = counters.computeIfAbsent(name, key -> metricRegistry.counter(key));
        counter.inc();
    }
    @Before("publicMethods() && @annotation(meteredAnnotation)")
    public void instrumentMetered(JoinPoint jp, Metered meteredAnnotation) {
        String name = name(jp.getTarget().getClass(), StringUtils.hasLength(meteredAnnotation.name()) ? meteredAnnotation.name() : jp.getSignature().getName(), "meter");
        Meter meter = meters.computeIfAbsent(name, key -> metricRegistry.meter(key));
        meter.mark();
    }
    @AfterThrowing(pointcut = "publicMethods() && @annotation(exMe-teredAnnotation)", throwing = "ex")
    public void instrumentExceptionMetered(JoinPoint jp, Throwable ex, ExceptionMetered exMeteredAnnotation) {
        String name = name(jp.getTarget().getClass(), StringUtils.hasLength(exMeteredAnnotation.name()) ? exMeteredAnnotation.name() : jp.getSignature().getName(), "meter", "exception");
        Meter meter = exceptionMeters.computeIfAbsent(name, meterName -> metricRegistry.meter(meterName));
        meter.mark();
    }
    @Around("publicMethods() && @annotation(timedAnnotation)")
    public Object instrumentTimed(ProceedingJoinPoint pjp, Timed timedAnnotation) throws Throwable {
        String name = name(pjp.getTarget().getClass(), StringUtils.hasLength(timedAnnotation.name()) ? timedAnnotation.name() : pjp.getSignature().getName(), "timer");
        Timer timer = timers.computeIfAbsent(name, inputName -> metricRegistry.timer(inputName));
        Timer.Context tc = timer.time();
        try {
            return pjp.proceed();
        } finally {
            tc.stop();
        }
    }
}
  • 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

@Aspect+@Component的目的在于告诉Spring框架:“我是一个AOP的Aspect实现类并且你可以通过@ComponentScan把我加入IoC容器之中。”当然,这不是重点。

:metrics-annotation这个依赖包为我们提供了几个有趣的Annotation:

  • Timed
  • Gauge
  • Counted
  • Metered
  • ExceptionMetered

这些语义良好的Annotation定义可以用来标注相应的AOP逻辑扩展点,比如,针对同一个MockService,我们可以将性能数据的度量和采集简化为只标注一两个Annotation就可以了:

on.name()) ? meteredAnnotation.name() : jp.getSignature().getName(), "meter");
  • 1
@Component
public class MockService {
    @Timed
    @Counted
    public void doSth() {
        System.out.println("just do something.");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

但是,Annotation注定只是Annotation,它们只是一些标记信息,要让它们发挥作用,需要有“伯乐”的眷顾,所以,AutoMetricsAspect在这里就是这些Dropwizard Metrics Annotation的“伯乐”,通过拦截每一个public方法并检查方法上是否存在某个metrics annotation,我们就可以根据具体的metrics annotation的类型,为匹配的方法注入相应性能数据采集代码逻辑,从而完成整个基于AOP和dropwizard metrics的应用性能数据采集方案的实现。

至此,整个基于spring-boot-starter-aop的spring-boot-starter-metrics自定义自动配置模块宣告完工.

注意 对于想了解更多细节,或者想寻找直接可用方案的读者,可以参考开源项目/ryantenney/metrics-spring。

应用安全与spring-boot-starter-security

关于应用安全这种头疼的问题,Spring生态圈里也有现成的解决方案,即从Acegi发展起来的SpringSecurity。但是说实话,SpringSecurity在整个社区中的名声并不是太好,尤其是在开发者眼中,“复杂(Too Complicated),太重(Too Heavyweight)”,但实际上,如果大家真得扑进去了解,就会发现,其实SpringSecurity框架本身的设计还是挺优秀的,SpringSecurity可以任意裁剪,而且还提供了丰富的开箱即用的安全特性支持。这里其实存在一个常见的设计取舍,

spring-boot-starter-security主要面向Web应用安全,配合spring-boot-starter-web,要使用SpringBoot构建一个安全的对外提供服务的Web应用简直太容易了:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="/POM/4.0.0" xmlns:xsi="http:///2001/XMLSchema-instance"
    xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.keevol.unveilspring.chapter3</groupId>
    <artifactId>web-security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>web-security-demo</name>
    <description>web security demo project for Spring Boot</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--其他依赖-->
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • 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

在项目中只要添加需要的Controller实现,一个添加了基本安全防护的Web应用就诞生了。spring-boot-starter-security默认会提供一个基于HTTP Basic认证的安全防护策略,默认用户名为user,访问密码则在当前Web应用启动的时候,打印到控制台。
如果我们希望对HTTP Basic认证的用户名和密码进行定制,可以通过如下配置项进行:

security.user.name={个人希望设置的用户名}
security.user.password={个人希望使用的访问密码}

  • 1
  • 2
  • 3

除此之外,spring-boot-starter-security还会默认启用一些必要的Web安全防护,比如针对XSS、CSRF等常见针对Web应用的攻击,同时,也会将一些常见的静态资源路径排除在安全防护之外。

但是,说实话,spring-boot-starter-security提供的默认安全策略相对于真正的生产环境来说,还是太弱了,但也没办法,既要安全,又要便利,spring-boot-starter-security默认情况下已经尽量做到够好了。不过好在SpringSecurity扩展性不错,要在其上构建一套真正严谨有效的Web应用安全防护体系也并非难事,只不过,需要我们先能够从其架构设计上理解并把握它,然后再在SpringSecurity和SpringBoot的基础上构建一套符合自身需要的Web应用安全方案。

SpringSecurity基本设计

Spring Security的几个核心概念
Spring Security 核心概念

  • 访问者(Accessor)需要访问某个资源(Resources)是这个场景中最原始的需求,但并不是谁都可以访问资源,也不是任何资源都允许任何人来访问,所以,中间我们要加入一些检查和防护。

  • 在访问资源的所经之路上,可能需要上山、过桥、下海行船,不管怎么样,这些所经之路对于我们要防护的资源来说都是比较好的设置关卡点,对应上图就是FilterInvocation(对应Web应用场景)、MethodInvocation以及Joinpoint,这些在Spring Security框架中统称Secured Object(s)。

  • 我们知道了在哪里设置关卡最合适,下一步就是设置关卡,对应不同的所经之路,我们分别设置类似FilterSecurityInterceptor、Method-SecurityInterceptor以及AspectJSecurityInterceptor这样的关卡来负责拦截非法资源访问的闯入者们;而在Spring Security框架的设计中,关卡的概念统一抽象为AbstractSecurityInterceptor,而FilterSecurity-Interceptor、MethodSecurityInterceptor以及AspectJSecurityInterceptor都是它的具体实现类。

把门儿的倒是有了,可是他们不知道该拦谁,不该拦谁,所以,我们需要有类似神盾局()这样的机构,由这个机构来决定谁可以放行,谁必须阻截,而在SpringSecurity框架中AccessDecisionManager就是这个控制机构,AccessDecisionManager将决定谁可以访问哪些资源。

·谁怎么定义?我们总得知道当前这个访问者是谁才能告知AccessDecisionManager阻截还是放行,所以,SpringSecurity框架中的AuthenticationManager将解决的是访问者身份认证的问题,只有确定你在册了,才可以给你授权访问(除非我们运行匿名访问某些公共资源)。

AuthenticationManager、AccessDecisionManager和AbstractSecurityInterceptor属于Spring Security框架的基础铁三角,AuthenticationManager和Access-DecisionManager负责制定规则AbstractSecurityInterceptor负责执行。所有针对不同应用场景的安全方案,基本上都是在这个基础核心的基础上衍生出来的,比如,Web安全。

Spring Security的Web安全方案基于Java的Servlet API规范进行构建,所以,像Play Framework这种脱离Servlet规范的Web框架,则无法享受到SpringSecurity提供的默认的Web安全方案(当然,依然可以基于基本模型实现扩展方案)。

既然是基于Servlet API规范,那么,要实现关卡的“特效”,则非莫属了。在使用Spring框架开发Filter的时候,为了让Filter可以享受到依赖注入的好处,我们一般是实现GenericFilterBean并注册到IoC容器,为了能够启用这些注册到IoC容器的Filter,我们一般要在或者相应的JavaConfig的配置中声明一个,使其filter-name与IoC容器中我们希望启用的Filter对应“挂钩”,SpringSecurity的Web安全方案的启用也是这个原理。SpringSecurity默认会需要声明一个默认名称为“springSecurityFilterChain”的(方式或者JavaConfig方式),然后指向IoC容器中注册的一个实例,FilterChainProxy通过扩展GenericFilterBean间接实现了Filter接口,同时持有一组SecurityFilterChain,使它可以针对不同的Web资源进行特定的防护
在这里插入图片描述
这些还只是“骨架”,真正执行防护任务的其实是一个个中定义的一系列Filter:

public interface SecurityFilterChain {
    boolean matches(HttpServletRequest request);
    List<Filter> getFilters();
}
  • 1
  • 2
  • 3
  • 4

当我们经常看到如下的xml schema形式的配置格式的时候:

<http auto-config='true'>
  <intercept-url pattern="/*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
  <intercept-url pattern="/**" access="ROLE_USER" />
  <form-login login-page='/'/>
</http>
  • 1
  • 2
  • 3
  • 4
  • 5

其实一个个http元素背后对应的就是一个个SecurityFilterChain实例,而http元素的那些子元素,比如intercept-url,则对应的就是一个个Filter。
默认情况下,Spring Security为SecurityFilterChain中的Filter序列设定了一个注册框架,以100为间隔步长,按照一个合理的顺序来规划和排布常用的Filter实现

int order = 100;
put(ChannelProcessingFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
put(WebAsyncManagerIntegrationFilter.class, order);
order += STEP;
put(SecurityContextPersistenceFilter.class, order);
order += STEP;
put(HeaderWriterFilter.class, order);
order += STEP;
put(CsrfFilter.class, order);
order += STEP;
put(LogoutFilter.class, order);
order += STEP;
put(X509AuthenticationFilter.class, order);
order += STEP;
put(AbstractPreAuthenticatedProcessingFilter.class, order);
order += STEP;
filterToOrder.put("", order);
order += STEP;
put(UsernamePasswordAuthenticationFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
filterToOrder.put("", order);
order += STEP;
put(DefaultLoginPageGeneratingFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
put(DigestAuthenticationFilter.class, order);
order += STEP;
put(BasicAuthenticationFilter.class, order);
order += STEP;
put(RequestCacheAwareFilter.class, order);
order += STEP;
put(SecurityContextHolderAwareRequestFilter.class, order);
order += STEP;
put(JaasApiIntegrationFilter.class, order);
order += STEP;
put(RememberMeAuthenticationFilter.class, order);
order += STEP;
put(AnonymousAuthenticationFilter.class, order);
order += STEP;
put(SessionManagementFilter.class, order);
order += STEP;
put(ExceptionTranslationFilter.class, order);
order += STEP;
put(FilterSecurityInterceptor.class, order);
order += STEP;
put(SwitchUserFilter.class, order);
 
  • 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
  • 53

简单划分为几类,除个别Filter在每个SecurityFilterChain都需要,其他可以根据需要选用并添加

  • 信道与状态管理,比如ChannelProcessingFilter用于处理http或者https之间的切换,而SecurityContextPersistenceFilter用于重建或者销毁必要的SecurityContext状态。

  • 常见Web安全防护类,比如CsrfFilter。

  • 认证和授权,比如BasicAuthenticationFilter、CasAuthen-ticationFilter等。

  • 最需要重点关注的是FilterSecurityInterceptor,还记得我们前面说到的secured object吧。FilterSecurityInterceptor就属于放在Web访问路径上的那道“关卡”,现在,它的真实位置和效能终于浮出水面了。

ExceptionTranslationFilter属于另一个需要关注的核心类,它负责接待或者送客,如果访客来访,对方没有报上名来,那么,它会让访客去登记认证(去找AuthenticationManager做认证);如果对方报上名了,但认证失败,那么不好意思,请重新认证或者走人。当然,它拒绝访客的方式是抛出相应的Exception,所以名字叫ExceptionTranslationFilter。

最后,这个Filter序列因为间隔了100的步长,所以,我们可以在其中穿插自己的Filter实现类,为定制和扩展SpringSecurity的防护体系提供了机会。

以上就是关于Spring Security以及其Web安全相关的基础介绍,这些内容足以帮助我们理解并扩展spring-boot-starter-security。

定制spring-boot-starter-security

除了使用SecurityProperties暴露的配置项(以security.*开头)对spring-boot-starter-security进行简单的配置,我们还可以通过给出一个继承了WebSecurityConfigurerAdapter的JavaConfig配置类对spring-boot-starter-security的行为进行更深一级的定制。

使用WebSecurityConfigurerAdapter的好处在于,我们依然可以使用spring-boot-starter-security默认约定的一些行为,只需要对必要的行为进行调整,比如:

  • 使用其他的AuthenticationManager实例。
  • 对默认HttpSecurity定义的资源访问的规则进行重新定义。
  • 对默认提供的WebSecurity行为进行调整。

为了能够让这些调整生效,我们定义的WebSecurityConfigurerAdapter实现类一般在顺序上需要先于spring-boot-starter-security默认提供的配置,故此,一般配合@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)进行标注:

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class DemoSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected DemoSecurityConfiguration() {
        super(true); // 取消默认提供的安全相关Filters配置
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        // ...
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // ...
    }
    // 通过Override其他方法实现对web安全的定制
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

WebSecurityConfigurerAdapter其实是为我们预先设定了一个框架,并开放了有限的一些扩展点允许我们对Web安全相关的设定进行定制,某些场景下还是会感觉“掣肘”,或者,某些有“洁癖”的开发者,往往不想使用在某些场景下显得并非必要的默认设定。这个时候,我们可以直接实现并注册一个标注了@EnableWebSecurity的JavaConfig配置类到IoC容器,从而实现一种“颠覆性”的定制,即跟spring-boot-starter-security默认提供的Web安全相关配置一刀两断,完全自建:

@Configuration
@EnableWebSecurity
public class OverhaulSecurityConfiguration {
    @Bean
    public AuthenticationManager authenticationManager(){
        // ...
    }
    @Bean
    public AccessDecisionManager accessDecisionManager(){
        // ...
    }
    @Bean
    public SecurityFilterChain mySecurityFilterChain(){
        // ...
    }
    // 其他web安全相关组件和依赖配置
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这种方案需要开发者对Spring Security框架本身以及Web安全本身有很深的理解,不到迫不得已,最好不要这么做,威力大,风险也大。

应用监控与spring-boot-starter-actuator

为了保障其可以持续稳定的服务,我们通常需要对其进行监控,从而可以了解应用的运行状态,并根据情况决定是否需要对其运行状态进行调整,顺应需求,SpringBoot框架提供了spring-boot-starter-actuator自动配置模块用于支持SpringBoot应用的监控。
Sensor和Actuator示意图
为了能够感知应用的运行状态,我们通常会设置一些监控指标并采集分析,这些监控指标的采集需要在应用内部设置相应的监控点,这类监控点一般只是读取状态数据,我们通常称它们为Sensor,即中文一般称为“传感器”;应用的运行状态数据通过Sensors采集上来之后,我们通常会有专门的系统对这些数据进行分析和判断,一旦某个指标数据超出了预定的阈值,这往往意味着应用的运行状态在这个指标上出现了“不健康”的现象,我们希望对这个指标进行调整,而为了能够执行调整,我们需要预先在应用内部设置对应的执行调整逻辑的控制器(比如,直接关闭的开关,或者可以执行微调甚至像刹车一样直接快速拉低某个指标值的装置),这些控制器就称为Actuator

虽然我们日常天天在说“监控,监控”,但实际上“监”跟“控”是两个概念,Sensor更多服务于“监”的场景,而Actuator则服务于“控”的场景。

spring-boot-starter-actuator自动配置模块默认提供了很多endpoint,虽然自动配置模块名为spring-boot-starter-actuator,但实际上这些endpoint可以按照“监”和“控”划分为两类:

  1. Sensor类endpoints
  • autoconfig:这个endpoint会为我们提供一份SpringBoot的自动配置报告,告诉我们哪些自动配置模块生效了,以及哪些没有生效,原因是什么。
  • beans:给出当前应用的容器中所有bean的信息。
  • configprops:对现有容器中的ConfigurationProperties提供的信息进行“消毒”处理后给出汇总信息。
  • nfo:提供当前SpringBoot应用的任意信息,我们可以通过Environment或者等形式提供以info.为前缀的任何配置项,然后info这个endpoint就会将这些配置项的值作为信息的一部分展示出来。
  • health:针对当前SpringBoot应用的健康检查用的endpoint。
  • env:关于当前SpringBoot应用对应的Environment信息。
  • metrics:当前SprinBoot应用的metrics信息。
  • trace:当前SpringBoot应用的trace信息。
  • mapping:如果是基于SpringMVC的Web应用,mapping这个endpoint将给出@RequestMapping相关信息。
  1. Actuator类endpoints
  • shutdown:用于关闭当前SpringBoot应用的endpoint。

  • dump:用于执行线程的dump操作。

默认情况下,除了shutdown这个endpoint(因为比较危险,如果没有安全防护,谁都可以访问它,然后关闭应用),其他endpoints都是默认启用的。生产环境下,如果没有启用安全防护(比如没有依赖spring-boot-starter-security),那么,建议遵循Deny By Default原则,将所有的endpoints都关掉,然后根据具体情况单独启用某些endpoint:

=false
=true
=true
...
  • 1
  • 2
  • 3
  • 4

所有配置项以endpoints.为前缀,然后根据endpoint名称划分具体配置项。

大部分endpoints都是开箱即用,但依然有些endpoint提供给我们进一步扩展的权利,比如健康状态检查相关的endpoint(health endpoint)。

自定义应用的健康状态检查

pringBoot预先通过为我们提供了一些常见服务的监控检查支持,比如:

  • DataSourceHealthIndicator
  • DiskSpaceHealthIndicator
  • RedisHealthIndicator
  • SolrHealthIndicator
  • MongoHealthIndicator

如果这些默认提供的健康检查支持依然无法满足我们的需要,SpringBoot还允许我们提供更多的HealthIndicator实现,只要将这些HealthIndicator实现类注册到IoC容器,SpringBoot会自动发现并使用它们。

假设需要检查依赖的dubbo服务是否处于健康状态,我们可以实现一个DubboHealthIndicator:

package com.keevol.unveilspring.chapter3.healthcheck;
import com.alibaba.dubbo.config.spring.ReferenceBean;
import com.alibaba.dubbo.rpc.service.EchoService;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
public class DubboHealthIndicator extends AbstractHealthIndicator {
    private final ReferenceBean bean;
    public DubboHealthIndicator(ReferenceBean bean) {
        this.bean = bean;
    }
    @Override
    protected void doHealthCheck(Health.Builder builder) throws Exception {
        builder.withDetail("interface", bean.getObjectType());
        final EchoService service = (EchoService) bean.getObject();
        service.$echo("hi");
        builder.up();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

要实现一个自定义的HealthIndicator,一般我们不会直接实现(Implements)HealthIndicator接口,而是继承AbstractHealthIndicator:

public abstract class AbstractHealthIndicator implements HealthIndicator {
    @Override
    public final Health health() {
        Health.Builder builder = new Health.Builder();
        try {
            doHealthCheck(builder);
        }
        catch (Exception ex) {
            builder.down(ex);
        }
        return builder.build();
    }
    protected abstract void doHealthCheck(Health.Builder builder) throws Exception;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

我们只需实现doHealthCheck,在其中实现我们面向的具体服务的健康检查逻辑就可以了,因此,在DubboHealthIndicator实现类中,我们通过dubbo框架提供的EchoService直接检查相应的dubbo服务健康状态即可,只要没有任何异常抛出,我们就认为检查的dubbo服务是状态健康的,所以,最后会通过的up()方法标记服务状态为正常运行

为了完成对dubbo服务的健康检查,只实现一个DubboHealthIndicator是不够的,我们还需要将其注册到IoC容器中,但是一个一个单独注册太费劲了,而且还要自己提供针对某个dubbo服务的ReferenceBean依赖实例,所以,为了一劳永逸,也为了其他人能够同样方便地使用针对dubbo服务的健康检查支持,我们可以在DubboHealthIndicator的基础上实现一个spring-boot-starter-dubbo-health-indicator自动配置模块,即:

@Configuration
@ConditionalOnClass(name = {""})
public class DubboHealthIndicatorConfiguration {
    @Autowired
    HealthAggregator healthAggregator;
    @Autowired(required = false)
    Map<String, ReferenceBean> references;
    @Bean
    public HealthIndicator dubboHealthIndicator() {
        Map<String, HealthIndicator> indicators = new HashMap<>();
        for (String key : references.keySet()) {
            final ReferenceBean bean = references.get(key);
            indicators.put(key.startsWith("&") ? key.replaceFirst("&", "") : key, new DubboHealthIndicator(bean));
        }
        return new CompositeHealthIndicator(healthAggregator, indicators);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

然后我们在spring-boot-starter-dubbo-health-indicator的META-INF/文件中添加如下配置:

=\
...DubboHealthIndicatorConfiguration
  • 1
  • 2

现在,发布spring-boot-starter-dubbo-health-indicator并依赖它就可以自动享受到针对当前应用引用的所有dubbo服务进行健康检查的服务!

开放的endpoints

不管是spring-boot-starter-actuator默认提供的endpoint实现,还是我们自己给出的endpoint实现,如果只是实现了放在SpringBoot应用的“身体内部”,那么它们不会发挥任何作用,只有将它们采集的信息暴露开放给外部监控者,或者允许外部监控者访问它们,这些endpoints才会真正发挥出它们的最大“功效”。
首先,spring-boot-starter-actuator会通过将所有的实例以JMX MBean的形式开放给外部监控者使用,默认情况下,这些Endpoint对应的JMX MBean会放在命名空间下面,不过可以通过配置项进行更改,比如=。

EndpointMBeanExportAutoConfiguration为我们提供了一条很好的应用监控实践之路,既然它会把所有的实例都作为JMX Mbean开放出去,那么,我们就可以提供一批用于某些场景下的自定义Endpoint实现类,比如:

public class HelloEndpoint extends AbstractEndpoint<String> {
    public HelloEndpoint(String id) {
        super(id, false);
    }
    @Override
    public String invoke() {
        return "Hello, SpringBoot";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然后,将像HelloEndpoint这样的实现类注册到SpringBoot应用的IoC容器,就可以扩展SpringBoot的endpoints功能了。

注意 Endpoint其实更适合简单的Sensor场景(即用于读取或者提供信息),或者简单功能的actuator场景(不需要行为参数),如果需要对SpringBoot进行更细粒度的监控,可以考虑直接使用Spring框架的JMX支持。

除了可以使用JMX将spring-boot-starter-actuator提供的(或者我们自己提供的)endpoints开放访问,如果当前SpringBoot应用恰好又是一个Web应用,那么,这些endpoints还会通过HTTP协议开放给外部访问,与一般的Web请求处理一样,使用的也是Web应用使用的HTTP服务器和地址端口。因为每个Endpoint都有一个id作为唯一标识,所以,这些endpoints的默认访问路径其实就是它们的id,比如info这个endpoint的HTTP访问路径就是/info,而beans这个endpoint的HTTP访问路径则是/beans,以此类推。

SpringBoot允许我们通过management.为前缀的配置项对endpoints的HTTP开放行为进行调整:

·使用-path=设置自定义的endpoints访问上下文路径,默认直接根路径,即/info,/beans等形式。

·使用=配置单独的HTTP服务监听地址,比如只允许本地访问:

=127.0.0.1
  • 1

·使用=设置单独的监听端口,默认与web应用的对外服务端口相同,我们可以通过=8888将管理接口的HTTP对外监听端口设置为8888,但如果=-1,则意味着我们将关闭管理接口的HTTP对外服务。

JMX和HTTP是endpoints对外开放访问最常用的方式,鉴于Java的序列化漏洞以及JMX的远程访问防火墙问题,建议用HTTP并配合安全防护将SpringBoot应用的endpoints开放给外部监控者使用。

endpoints属于spring-boot-starter-actuator提供的主要功能之一,除此之外,spring-boot-starter-actuator还提供了更多针对应用监控的支持和实现方案。

与spring-boot-starter-remote-shell

spring-boot-starter-actuator提供了基于CRaSH(/)的远程Shell(Remote Shell)支持,从笔者角度来看,这是一把双刃剑,不建议在生产环境使用,因为提供给自己便利的同时,也为黑客朋友们提供了便利。如果实在要用,请加强安全认证和防护。

不过,这里我们还是会为大家分析一下spring-boot-starter-actuator是如何提供针对CRaSH的支持的。

spring-boot-starter-actuator提供了自动配置类,该类会在出现在classpath中的时候生效,所以,只要将CRaSH作为依赖加入应用的classpath依赖就可以了,最简单直接的做法是让需要启用CRaSH的SpringBoot应用依赖spring-boot-starter-remote-shell自动配置模块,spring-boot-starter-remote-shell的主要功效就是提供了针对CRaSH的各项依赖。

的Metrics与Dropwizard的Metrics
SpringBoot框架的Metrics核心类设计示意图

SpringBoot提供了一套自己的针对系统指标的度量框架,这个框架的核心。
基本上,我们只需关注即可,它可以理解为提供一组Metric的集合,我们既可以通过PublicMetrics来汇总和管理Metric,也可以通过MetricRepository来存储和管理Metric。一旦使用了spring-boot-starter-actuator,只要当前SpringBoot应用的ApplicationContext中存在任何PublicMetrics实例,EndpointAutoConfiguration就会将这些PublicMetrics采集汇总到一起,然后通过MetricsEndpoint将它们开放出去。

spring-boot-starter-actuator提供的默认会把一个SystemPublicMetrics开放出来,用于提供系统各项指标的度量和状态采集,另外一个就是会把当前SpringBoot应用的ApplicationContext的实例中的所有Metric汇总并开放出去。默认如果用户没有给出任何自定义的MetricRepository,spring-boot-starter-actuator会提供一个InMemoryMetricRepository实现,如果我们将Dropwizard的Metrics类库作为依赖加入classpath,那么,Dropwizard Metrics的MetricRegistry中所有的度量指标项也会通过PublicMetrics的形式开发暴露出来。

虽然SpringBoot提供的metrics框架也能帮助我们完成系统和应用指标的度量,但笔者更倾向于使用Dropwizard这种特定场景下比较完善的方案,从metrics的类型,到外围系统的集成,Dropwizard metrics都更加成熟和完备。

与Trace
SpringBoot的Auditing和Trace支持都遵循数据/事件+Repository的设计
SpringBoot框架Audit和Trace功能支持核心类示意图
从设计上来说是很简单清晰的,也有很好的统一性,但实际应用过程中,我们依然会更加倾向于特定场景的方案选型,比如Auditing,我们可能只是通过打印日志时候的Logger名称来区分并记录Audit事件,然后通过日志采集通道汇总分析就可以了,而不用非要实现一个LogFileBasedAuditEventRepository或者ElasticSearchBasedAuditEventRepository之类的实现,否则看起来难免有些“学究”气。对于Trace来说也是同样道理,我们可能直接使用完备的APM方案而不是单一或者少量Trace事件的记录。