springboot中接口实例化_这份3万字的Spring Boot知识清单,请查收

作者:CHEN川原文:https://www.jianshu.com/p/83693d3d0a65

在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架。或许从命名上就能看出这个框架的设计初衷:快速的启动Spring应用。因而Spring Boot应用本质上就是一个基于Spring框架的应用,它是Spring对“约定优先于配置”理念的最佳实践产物,它能够帮助开发者更快速高效地构建基于Spring生态圈的应用。

那Spring Boot有何魔法?自动配置起步依赖Actuator命令行界面(CLI) 是Spring Boot最重要的4大核心特性,其中CLI是Spring Boot的可选特性,虽然它功能强大,但也引入了一套不太常规的开发模型,因而这个系列的文章仅关注其它3种特性。如文章标题,本文是这个系列的第一部分,将为你打开Spring Boot的大门,重点为你剖析其启动流程以及自动配置实现原理。要掌握这部分核心内容,理解一些Spring框架的基础知识,将会让你事半功倍。

一、抛砖引玉:探索Spring IoC容器

如果有看过SpringApplication.run()方法的源码,Spring Boot冗长无比的启动流程一定会让你抓狂,透过现象看本质,SpringApplication只是将一个典型的Spring应用的启动流程进行了扩展,因此,透彻理解Spring容器是打开Spring Boot大门的一把钥匙。

1.1、Spring IoC容器

可以把Spring IoC容器比作一间餐馆,当你来到餐馆,通常会直接招呼服务员:点菜!至于菜的原料是什么?如何用原料把菜做出来?可能你根本就不关心。IoC容器也是一样,你只需要告诉它需要某个bean,它就把对应的实例(instance)扔给你,至于这个bean是否依赖其他组件,怎样完成它的初始化,根本就不需要你关心。

作为餐馆,想要做出菜肴,得知道菜的原料和菜谱,同样地,IoC容器想要管理各个业务对象以及它们之间的依赖关系,需要通过某种途径来记录和管理这些信息。BeanDefinition对象就承担了这个责任:容器中的每一个bean都会有一个对应的BeanDefinition实例,该实例负责保存bean对象的所有必要信息,包括bean对象的class类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的bean实例。

原材料已经准备好(把BeanDefinition看着原料),开始做菜吧,等等,你还需要一份菜谱,BeanDefinitionRegistry和BeanFactory就是这份菜谱,BeanDefinitionRegistry抽象出bean的注册逻辑,而BeanFactory则抽象出了bean的管理逻辑,而各个BeanFactory的实现类就具体承担了bean的注册以及管理工作。它们之间的关系就如下图:

04145395764f71430893f106ddac75b6.png

BeanFactory、BeanDefinitionRegistry关系图(来自:Spring揭秘)

DefaultListableBeanFactory作为一个比较通用的BeanFactory实现,它同时也实现了BeanDefinitionRegistry接口,因此它就承担了Bean的注册管理工作。从图中也可以看出,BeanFactory接口中主要包含getBean、containBean、getType、getAliases等管理bean的方法,而BeanDefinitionRegistry接口则包含registerBeanDefinition、removeBeanDefinition、getBeanDefinition等注册管理BeanDefinition的方法。

下面通过一段简单的代码来模拟BeanFactory底层是如何工作的:

d3c45a47288128fdc96c11f61a07d65b.png

②、Bean的实例化阶段

经过第一阶段,所有bean定义都通过BeanDefinition的方式注册到BeanDefinitionRegistry中,当某个请求通过容器的getBean方法请求某个对象,或者因为依赖关系容器需要隐式的调用getBean时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。如果没有,则会根据注册的BeanDefinition所提供的信息实例化被请求对象,并为其注入依赖。当该对象装配完毕后,容器会立即将其返回给请求方法使用。

BeanFactory只是Spring IoC容器的一种实现,如果没有特殊指定,它采用采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。而在实际场景下,我们更多的使用另外一种类型的容器:ApplicationContext,它构建在BeanFactory之上,属于更高级的容器,除了具有BeanFactory的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的bean,在容器启动时全部完成初始化和依赖注入操作。

1.2、Spring容器扩展机制

IoC容器负责管理容器中所有bean的生命周期,而在bean生命周期的不同阶段,Spring提供了不同的扩展点来改变bean的命运。在容器的启动阶段,BeanFactoryPostProcessor允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做一些额外的操作,比如修改bean定义的某些属性或者增加其他信息等。

如果要自定义扩展类,通常需要实现org.springframework.beans.factory.config.BeanFactoryPostProcessor接口,与此同时,因为容器中可能有多个BeanFactoryPostProcessor,可能还需要实现org.springframework.core.Ordered接口,以保证BeanFactoryPostProcessor按照顺序执行。Spring提供了为数不多的BeanFactoryPostProcessor实现,我们以PropertyPlaceholderConfigurer来说明其大致的工作流程。

在Spring项目的XML配置文件中,经常可以看到许多配置项的值使用占位符,而将占位符所代表的值单独配置到独立的properties文件,这样可以将散落在不同XML文件中的配置集中管理,而且也方便运维根据不同的环境进行配置不同的值。这个非常实用的功能就是由PropertyPlaceholderConfigurer负责实现的。

根据前文,当BeanFactory在第一阶段加载完所有配置信息时,BeanFactory中保存的对象的属性还是以占位符方式存在的,比如${jdbc.mysql.url}。当PropertyPlaceholderConfigurer作为BeanFactoryPostProcessor被应用时,它会使用properties配置文件中的值来替换相应的BeanDefinition中占位符所表示的属性值。当需要实例化bean时,bean定义中的属性值就已经被替换成我们配置的值。当然其实现比上面描述的要复杂一些,这里仅说明其大致工作原理,更详细的实现可以参考其源码。

与之相似的,还有BeanPostProcessor,其存在于对象实例化阶段。跟BeanFactoryPostProcessor类似,它会处理容器内所有符合条件并且已经实例化后的对象。简单的对比,BeanFactoryPostProcessor处理bean的定义,而BeanPostProcessor则处理bean完成实例化后的对象。BeanPostProcessor定义了两个接口:

public interface BeanPostProcessor { // 前置处理 Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; // 后置处理 Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;}

为了理解这两个方法执行的时机,简单的了解下bean的整个生命周期:

a5c8ef02dadecd8ca507e00515757638.png

Bean的实例化过程(来自:Spring揭秘)

postProcessBeforeInitialization()方法与postProcessAfterInitialization()分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了bean对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。注解、AOP等功能的实现均大量使用了BeanPostProcessor,比如有一个自定义注解,你完全可以实现BeanPostProcessor的接口,在其中判断bean对象的脑袋上是否有该注解,如果有,你可以对这个bean实例执行任何操作,想想是不是非常的简单?

再来看一个更常见的例子,在Spring中经常能够看到各种各样的Aware接口,其作用就是在对象实例化完成以后将Aware接口定义中规定的依赖注入到当前实例中。比如最常见的ApplicationContextAware接口,实现了这个接口的类都可以获取到一个ApplicationContext对象。当容器中每个对象的实例化过程走到BeanPostProcessor前置处理这一步时,容器会检测到之前注册到容器的ApplicationContextAwareProcessor,然后就会调用其postProcessBeforeInitialization()方法,检查并设置Aware相关依赖。看看代码吧,是不是很简单:

// 代码来自:org.springframework.context.support.ApplicationContextAwareProcessor// 其postProcessBeforeInitialization方法调用了invokeAwareInterfaces方法private void invokeAwareInterfaces(Object bean) { if (bean instanceof EnvironmentAware) { ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); } if (bean instanceof ApplicationContextAware) { ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); } // ......}

最后总结一下,本小节内容和你一起回顾了Spring容器的部分核心内容,限于篇幅不能写更多,但理解这部分内容,足以让您轻松理解Spring Boot的启动原理,如果在后续的学习过程中遇到一些晦涩难懂的知识,再回过头来看看Spring的核心知识,也许有意想不到的效果。也许Spring Boot的中文资料很少,但Spring的中文资料和书籍有太多太多,总有东西能给你启发。

二、夯实基础:JavaConfig与常见Annotation

2.1、JavaConfig

我们知道bean是Spring IOC中非常核心的概念,Spring容器负责bean的生命周期的管理。在最初,Spring使用XML配置文件的方式来描述bean的定义以及相互间的依赖关系,但随着Spring的发展,越来越多的人对这种方式表示不满,因为Spring项目的所有业务类均以bean的形式配置在XML文件中,造成了大量的XML文件,使项目变得复杂且难以管理。

后来,基于纯Java Annotation依赖注入框架Guice出世,其性能明显优于采用XML方式的Spring,甚至有部分人认为,Guice可以完全取代Spring(Guice仅是一个轻量级IOC框架,取代Spring还差的挺远)。正是这样的危机感,促使Spring及社区推出并持续完善了JavaConfig子项目,它基于Java代码和Annotation注解来描述bean之间的依赖绑定关系。比如,下面是使用XML配置方式来描述bean的定义:

而基于JavaConfig的配置形式是这样的:

@Configurationpublic class MoonBookConfiguration { // 任何标志了@Bean的方法,其返回值将作为一个bean注册到Spring的IOC容器中 // 方法名默认成为该bean定义的id @Bean public BookService bookService() { return new BookServiceImpl(); }}

如果两个bean之间有依赖关系的话,在XML配置中应该是这样:

而在JavaConfig中则是这样:

537be9ec52b099f2434b980dd1df784a.png

你可能注意到这个示例中,有两个bean都依赖于dependencyService,也就是说当初始化bookService时会调用dependencyService(),在初始化otherService时也会调用dependencyService(),那么问题来了?这时候IOC容器中是有一个dependencyService实例还是两个?这个问题留着大家思考吧,这里不再赘述。

2.2、@ComponentScan

@ComponentScan注解对应XML配置形式中的元素,表示启用组件扫描,Spring会自动扫描所有通过注解配置的bean,然后将其注册到IOC容器中。我们可以通过basePackages等属性来指定@ComponentScan自动扫描的范围,如果不指定,默认从声明@ComponentScan所在类的package进行扫描。正因为如此,SpringBoot的启动类都默认在src/main/java下。

2.3、@Import

@Import注解用于导入配置类,举个简单的例子:

@Configurationpublic class MoonBookConfiguration { @Bean public BookService bookService() { return new BookServiceImpl(); }}

现在有另外一个配置类,比如:MoonUserConfiguration,这个配置类中有一个bean依赖于MoonBookConfiguration中的bookService,如何将这两个bean组合在一起?借助@Import即可:

@Configuration// 可以同时导入多个配置类,比如:@Import({A.class,B.class})@Import(MoonBookConfiguration.class)public class MoonUserConfiguration { @Bean public UserService userService(BookService bookService) { return new BookServiceImpl(bookService); }}

需要注意的是,在4.2之前,@Import注解只支持导入配置类,但是在4.2之后,它支持导入普通类,并将这个类作为一个bean的定义注册到IOC容器中。

2.4、@Conditional

@Conditional注解表示在满足某种条件后才初始化一个bean或者启用某些配置。它一般用在由@Component、@Service、@Configuration等注解标识的类上面,或者由@Bean标记的方法上。如果一个@Configuration类标记了@Conditional,则该类中所有标识了@Bean的方法和@Import注解导入的相关类将遵从这些条件。

在Spring里可以很方便的编写你自己的条件类,所要做的就是实现Condition接口,并覆盖它的matches()方法。举个例子,下面的简单条件类表示只有在Classpath里存在JdbcTemplate类时才生效:

public class JdbcTemplateCondition implements Condition { @Override public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { try { conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate"); return true; } catch (ClassNotFoundException e) { e.printStackTrace(); } return false; }}

当你用Java来声明bean的时候,可以使用这个自定义条件类:

@Conditional(JdbcTemplateCondition.class)@Servicepublic MyService service() { ......}

这个例子中只有当JdbcTemplateCondition类的条件成立时才会创建MyService这个bean。也就是说MyService这bean的创建条件是classpath里面包含JdbcTemplate,否则这个bean的声明就会被忽略掉。

Spring Boot定义了很多有趣的条件,并把他们运用到了配置类上,这些配置类构成了Spring Boot的自动配置的基础。Spring Boot运用条件化配置的方法是:定义多个特殊的条件化注解,并将它们用到配置类上。下面列出了Spring Boot提供的部分条件化注解:

6f85146558c55eb48fd063e9a6554679.png

2.5、@ConfigurationProperties与@EnableConfigurationProperties

当某些属性的值需要配置的时候,我们一般会在application.properties文件中新建配置项,然后在bean中使用@Value注解来获取配置的值,比如下面配置数据源的代码。

// jdbc configjdbc.mysql.url=jdbc:mysql://localhost:3306/sampledbjdbc.mysql.username=rootjdbc.mysql.password=123456......// 配置数据源@Configurationpublic class HikariDataSourceConfiguration { @Value("jdbc.mysql.url") public String url; @Value("jdbc.mysql.username") public String user; @Value("jdbc.mysql.password") public String password;  @Bean public HikariDataSource dataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(url); hikariConfig.setUsername(user); hikariConfig.setPassword(password); // 省略部分代码 return new HikariDataSource(hikariConfig); }}

使用@Value注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot提供了更优雅的实现方式,那就是@ConfigurationProperties注解。我们可以通过下面的方式来改写上面的代码:

@Component// 还可以通过@PropertySource("classpath:jdbc.properties")来指定配置文件@ConfigurationProperties("jdbc.mysql")// 前缀=jdbc.mysql,会在配置文件中寻找jdbc.mysql.*的配置项pulic class JdbcConfig { public String url; public String username; public String password;}@Configurationpublic class HikariDataSourceConfiguration { @AutoWired public JdbcConfig config;  @Bean public HikariDataSource dataSource() { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setJdbcUrl(config.url); hikariConfig.setUsername(config.username); hikariConfig.setPassword(config.password); // 省略部分代码 return new HikariDataSource(hikariConfig); }}

@ConfigurationProperties对于更为复杂的配置,处理起来也是得心应手,比如有如下配置文件:

#Appapp.menus[0].title=Homeapp.menus[0].name=Homeapp.menus[0].path=/app.menus[1].title=Loginapp.menus[1].name=Loginapp.menus[1].path=/loginapp.compiler.timeout=5app.compiler.output-folder=/temp/app.error=/error/

可以定义如下配置类来接收这些属性

@Component@ConfigurationProperties("app")public class AppProperties { public String error; public List
menus = new ArrayList<>(); public Compiler compiler = new Compiler(); public static class Menu { public String name; public String path; public String title; } public static class Compiler { public String timeout; public String outputFolder; }}

@EnableConfigurationProperties注解表示对@ConfigurationProperties的内嵌支持,默认会将对应Properties Class作为bean注入的IOC容器中,即在相应的Properties类上不用加@Component注解。

三、削铁如泥:SpringFactoriesLoader详解

JVM提供了3种类加载器:BootstrapClassLoader、ExtClassLoader、AppClassLoader分别加载Java核心类库、扩展类库以及应用的类路径(CLASSPATH)下的类库。JVM通过双亲委派模型进行类的加载,我们也可以通过继承java.lang.classloader实现自己的类加载器。

何为双亲委派模型?当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。

采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证Java 核心库的类型安全,比如,加载位于rt.jar包中的java.lang.Object类,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象。查看ClassLoader的源码,对双亲委派模型会有更直观的认识:

protected Class> loadClass(String name, boolean resolve) { synchronized (getClassLoadingLock(name)) { // 首先,检查该类是否已经被加载,如果从JVM缓存中找到该类,则直接返回 Class> c = findLoadedClass(name); if (c == null) { try { // 遵循双亲委派的模型,首先会通过递归从父加载器开始找, // 直到父类加载器是BootstrapClassLoader为止 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {} if (c == null) { // 如果还找不到,尝试通过findClass方法去寻找 // findClass是留给开发者自己实现的,也就是说 // 自定义类加载器时,重写此方法即可 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }}

但双亲委派模型并不能解决所有的类加载器问题,比如,Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些SPI的接口由核心类库提供,却由第三方实现,这样就存在一个问题:SPI 的接口是 Java 核心库的一部分,是由BootstrapClassLoader加载的;SPI实现的Java类一般是由AppClassLoader来加载的。BootstrapClassLoader是无法找到 SPI 的实现类的,因为它只加载Java的核心库。它也不能代理给AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。

线程上下文类加载器(ContextClassLoader)正好解决了这个问题。从名称上看,可能会误解为它是一种新的类加载器,实际上,它仅仅是Thread类的一个变量而已,可以通过setContextClassLoader(ClassLoader cl)和getContextClassLoader()来设置和获取该对象。如果不做任何的设置,Java应用的线程的上下文类加载器默认就是AppClassLoader。在核心类库使用SPI接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。但在JDBC中,你可能会看到一种更直接的实现方式,比如,JDBC驱动管理java.sql.Driver中的loadInitialDrivers()方法中,你可以直接看到JDK是如何加载驱动的:

for (String aDriver : driversList) { try { // 直接使用AppClassLoader Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); }}

其实讲解线程上下文类加载器,最主要是让大家在看到Thread.currentThread().getClassLoader()和Thread.currentThread().getContextClassLoader()时不会一脸懵逼,这两者除了在许多底层框架中取得的ClassLoader可能会有所不同外,其他大多数业务场景下都是一样的,大家只要知道它是为了解决什么问题而存在的即可。

类加载器除了加载class外,还有一个非常重要功能,就是加载资源,它可以从jar包中读取任何资源文件,比如,ClassLoader.getResources(String name)方法就是用于读取jar包中的资源文件,其代码如下:

public Enumeration getResources(String name) throws IOException { Enumeration[] tmp = (Enumeration[]) new Enumeration>[2]; if (parent != null) { tmp[0] = parent.getResources(name); } else { tmp[0] = getBootstrapResources(name); } tmp[1] = findResources(name); return new CompoundEnumeration<>(tmp);}

是不是觉得有点眼熟,不错,它的逻辑其实跟类加载的逻辑是一样的,首先判断父类加载器是否为空,不为空则委托父类加载器执行资源查找任务,直到BootstrapClassLoader,最后才轮到自己查找。而不同的类加载器负责扫描不同路径下的jar包,就如同加载class一样,最后会扫描所有的jar包,找到符合条件的资源文件。

类加载器的findResources(name)方法会遍历其负责加载的所有jar包,找到jar包中名称为name的资源文件,这里的资源可以是任何文件,甚至是.class文件,比如下面的示例,用于查找Array.class文件:

// 寻找Array.class文件public static void main(String[] args) throws Exception{ // Array.class的完整路径 String name = "java/sql/Array.class"; Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(name); while (urls.hasMoreElements()) { URL url = urls.nextElement(); System.out.println(url.toString()); }}

运行后可以得到如下结果:

$JAVA_HOME/jre/lib/rt.jar!/java/sql/Array.class

根据资源文件的URL,可以构造相应的文件来读取资源内容。

看到这里,你可能会感到挺奇怪的,你不是要详解SpringFactoriesLoader吗?上来讲了一堆ClassLoader是几个意思?看下它的源码你就知道了:

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";// spring.factories文件的格式为:key=value1,value2,value3// 从所有的jar包中找到META-INF/spring.factories文件// 然后从文件中解析出key=factoryClass类名称的所有value值public static List loadFactoryNames(Class> factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); // 取得资源文件的URL Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List result = new ArrayList(); // 遍历所有的URL while (urls.hasMoreElements()) { URL url = urls.nextElement(); // 根据资源文件URL解析properties文件 Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); // 组装数据,并返回 result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result;}

有了前面关于ClassLoader的知识,再来理解这段代码,是不是感觉豁然开朗:从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。来简单看下spring.factories文件的内容吧:

// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories// EnableAutoConfiguration后文会讲到,它用于开启Spring Boot自动配置功能org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration

执行loadFactoryNames(EnableAutoConfiguration.class, classLoader)后,得到对应的一组@Configuration类,

我们就可以通过反射实例化这些类然后注入到IOC容器中,最后容器里就有了一系列标注了@Configuration的JavaConfig形式的配置类。

这就是SpringFactoriesLoader,它本质上属于Spring框架私有的一种扩展方案,类似于SPI,Spring Boot在Spring基础上的很多核心功能都是基于此,希望大家可以理解。

四、另一件武器:Spring容器的事件监听机制

过去,事件监听机制多用于图形界面编程,比如:点击按钮、在文本框输入内容等操作被称为事件,而当事件触发时,应用程序作出一定的响应则表示应用监听了这个事件,而在服务器端,事件的监听机制更多的用于异步通知以及监控和异常处理。Java提供了实现事件监听机制的两个基础类:自定义事件类型扩展自java.util.EventObject、事件的监听器扩展自java.util.EventListener。来看一个简单的实例:简单的监控一个方法的耗时。

首先定义事件类型,通常的做法是扩展EventObject,随着事件的发生,相应的状态通常都封装在此类中:

public class MethodMonitorEvent extends EventObject { // 时间戳,用于记录方法开始执行的时间 public long timestamp; public MethodMonitorEvent(Object source) { super(source); }}

事件发布之后,相应的监听器即可对该类型的事件进行处理,我们可以在方法开始执行之前发布一个begin事件,在方法执行结束之后发布一个end事件,相应地,事件监听器需要提供方法对这两种情况下接收到的事件进行处理:

// 1、定义事件监听接口public interface MethodMonitorEventListener extends EventListener { // 处理方法执行之前发布的事件 public void onMethodBegin(MethodMonitorEvent event); // 处理方法结束时发布的事件 public void onMethodEnd(MethodMonitorEvent event);}// 2、事件监听接口的实现:如何处理public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener { @Override public void onMethodBegin(MethodMonitorEvent event) { // 记录方法开始执行时的时间 event.timestamp = System.currentTimeMillis(); } @Override public void onMethodEnd(MethodMonitorEvent event) { // 计算方法耗时 long duration = System.currentTimeMillis() - event.timestamp; System.out.println("耗时:" + duration); }}

事件监听器接口针对不同的事件发布实际提供相应的处理方法定义,最重要的是,其方法只接收MethodMonitorEvent参数,说明这个监听器类只负责监听器对应的事件并进行处理。有了事件和监听器,剩下的就是发布事件,然后让相应的监听器监听并处理。通常情况,我们会有一个事件发布者,它本身作为事件源,在合适的时机,将相应的事件发布给对应的事件监听器:

public class MethodMonitorEventPublisher { private List listeners = new ArrayList(); public void methodMonitor() { MethodMonitorEvent eventObject = new MethodMonitorEvent(this); publishEvent("begin
表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页