前言
笔者是于2021年07月15日正式加入菜鸟,然后于2021年10月13日晚上进行了试用期答辩。
运行一个springboot的项目,第一行弹出了警告:
| 1 | WARN 69220 --- [ restartedMain] o.s.boot.StartupInfoLogger : InetAddress.getLocalHost().getHostName() took 5006 milliseconds to respond. Please verify your network configuration (macOS machines may need to add entries to /etc/hosts). | 
可以看到,springboot警告,有一个InetAddress.getLocalHost().getHostName()方法执行了5秒,这太慢了,需要去确认一下网络配置,如果是macOS用户的需要去编辑一下/etc/hosts。
看到这个函数其实就知道了,是在获取我的电脑主机名字。那么为什么这个函数会耗时那么久呢?我在自己的idea里面试了一下,运行InetAddress.getLocalHost() 这个方法确实需要5秒。
这个应该是(猜测)会去访问DNS进行解析,但是一般来说个人的主机名字肯定是无法进行解析的,所以当时间到达了上限之后(5秒)就返回了结果。
这个问题解决的话也是很简单的,在macOS上面,只需要在终端中输入hostname就可以看到Mac主机名,然后编辑一下/etc/hosts这个文件,加入这两行即可:
| 1 | 127.0.0.1 localhost Mac主机名 | 
然后就可以了。如果发现还是不可以推荐使用dscacheutil -flushcache 并重启Mac。
苹果机器有三个名字,分别是:
 
其中的hostname命令输出的就是上面的HostName。其中一般的终端的提示也是hostname。
而另外两个名字则是在系统设置里面:

图中的电脑名称就是ComputerName,而下面的以.local结尾的则是LocalHostName,Mac会自动加上这个结尾。
如果希望修改这三个名字:
| 1 | sudo scutil --set ComputerName "newname" | 
把set改成get就可以获取对应的主机名字了(get不需要权限)。
其实我之前运行springboot一直好好的,最近在GitHub上面跑一个项目的时候才发现有这个问题。
我在网上搜索了一下,发现确实也有别的网友有这个问题,就是如果你没有手动设置过苹果的hostname,而只是设置了对应的主机名(刚购买的时候利用图形化界面设置的),那么这个hostname是可能会发生改变的,我自己就是hostname发生了变化,导致了/etc/hosts/ 中对应的条目无法生效。网上看到的是如果你的hostname中有除了大小写字母的其它字符(包括.),那么macOS就会帮你修改…
所以推荐使用上面的命令进行hostname的设定,这样就不会改变了。
在springboot的GitHub的项目下,有一个issue就是这个问题。下面这个回答我觉得很有道理:
When you request a hostname, the JDK resolves it to IP addresses. It then tries a reverse lookup of those addresses and checks that at least one of the results maps back to the input host name. It’s this reverse lookup that’s slow. The slowness isn’t limited to the JVM. Anything on the OS that tries to perform such a reverse lookup will be slow without appropriate configuration in
/etc/hosts.JDK会把hostname解析成IP地址(可能会有多个),并且它会去确认至少其中一个IP地址能够重新映射回hostname,正是这个从IP再解析回hostname的过程非常慢。所以需要配置对应的映射文件,来进行加速
看代码的时候,发现很多类中有一个静态常量:
| 1 | private static final long serialVersionUID = 1L; | 
这个静态的常量是用来干什么的呢?
在Stack Overflow上面也有对应的问题:
Eclipse issues warnings when a
serialVersionUIDis missing.The serializable class Foo does not declare a static final serialVersionUID field of type long
What is
serialVersionUIDand why is it important? Please show an example where missingserialVersionUIDwill cause a problem.
当缺少这个常量的时候,Eclipse会进行警告,那么这个常量究竟是什么,它为什么重要呢?
java.io.Serializable 接口的文档很好的说明了这一点:
在运行序列化的时候,会将每个可序列化类与一个称为 serialVersionUID 的版本号相关联,在反序列化期间使用它来验证序列化对象的发送方和接收方是否已为该对象加载了与序列化兼容的类。
如果接收方为对象加载了一个与相应发送方类具有不同 serialVersionUID 的类,则反序列化将导致 InvalidClassException。 可序列化的类可以通过声明一个名为 serialVersionUID 的字段来显式声明它自己的 serialVersionUID,该字段必须是静态的、最终的且类型为 long
就算你自己没有提供这个,JVM也会为你的类生成一个,但是这样的话会因为不同的 JVM 而有所差异,可能导致InvalidClassException的发生。所以非常推荐自己在类中定义一个。
我们在实际中会有这种需求:当某些事件发生的时候,就调用某些方法。这个时候就需要用到观察者这一设计模式。
找的网上的一个例子,现在有一个天气预报中心,它能够向外部广播出自己的信息,如果有对象需要天气信息,那么就自己去订阅。
所以从上面的这些信息,不难首先定义出天气预报中心作为一个接口:
| 1 | public interface WeatherCenter { | 
然后把所有的订阅者作为Observer,抽象出对应的接口:
| 1 | public interface Observer { | 
然后可以有多个对象,它们实现这个Observer接口。
最后我只需要去创建一个对象,实现上面的天气预报中心,并且它持有一个集合,然后把这些所有的订阅者都放到里面,这样在 publishWeatherInfo 方法里面,去挨个调用它们的 sendWeatherWarning 方法即可。
观察者模式的核心在于事件的发布者,其实是它通知了所有的这些观察者(由它主动调用了它们的方法),并不是监听这些事件的人执行了这段代码,它们仅仅是提前定义好了如果发生这件事怎么做,真正调用这段代码的是被观察者。
所以,观察者其实是一个被动的角色,而是由被观察者来主动通知观察者,观察者只需要在自己的代码里实现 一旦发生了,就干什么 的逻辑就好了,而不用关心是什么时候发生的,事件是怎么到自己这里来的。
有了上面的例子,我们不难抽象出观察者设计模式中的角色:
Observer 接口的类。Subject 的实现类,在里面实现对应的方法。从上面的设计模式中我们可以发现,其实就是两个接口,然后实现就可以了。JDK自然为我们实现了对应的观察者模式,分别抽象出了 Observer 接口 和 Observable 类。
由于它们俩是在是有点太简单了,这里就直接跳过了,直接进入到第二步:事件模型。
上述的观察者模式还是很好理解的,于是就可以在此基础上更抽象一步, 抽象出事件驱动模型。其中观察者对应监听器(Listener),而被观察者对应的是事件源source,其中事件源被封存在事件event中。JDK为事件抽象出了两个接口:EventObject 和 EventListener。
| 1 | 
 | 
这里值得注意的是,EventObject  是一个普通的类,所以事件都需要继承自它。而 EventListener 则是一个空接口,我们一般是自己定义一个自己的Listener接口来继承这个接口,然后再去实现它。
除此之外我们可以发现,最为重要的是 source 这个对象,所有事件都是通过引用它来进行构造的,也就是这个source其实就是上面观察者模式中的 Observable 类,也就是事件的源。
比如A通知B一件事情,那么这个事情本身就是一个EventObject,然后A就是其中的source,而B则是对应的listener。本质上和观察者完全一致,只不过它又抽象出事件这个概念,并且让事件持有事件源这个对象,这样当listener获得事件的时候,也可以顺手获取到事件源。
假设老师需要向学生发布做作业的event,那么毫无疑问,做作业这个事件就是event,而老师是source,学生是listener。
首先先抽象出这个event:
| 1 | public class HomeworkEventObject extends EventObject { | 
一般来说,事件中的source就是监听器(学生)能够获取到的对象(老师),所以可以好好封装(比如封装老师需要传递给学生的信息)。当然你也可以在event中持有别的对象引用,这样也行。
接着抽象出Listener(学生),这里推荐的是自己首先抽象出接口去继承JDK提供的,然后具体的监听器再去实现它:
| 1 | public interface HomeworkListener extends EventListener { | 
| 1 | public class Student implements HomeworkListener { | 
最后才去实现整个事件的源:
| 1 | public class Teacher { | 
可以看到比较难的就是事件源的编写,因为它需要自己维护所有的监听器,并且还需要调用对应的方法。
我们在自己使用JDK的事件机制的时候(虽然基本也没机会用),可以遵循先抽象出事件、然后定义对应监听器,最后自己去实现事件源的顺序去做。
Spring 同样抽象出了两个接口,分别是 ApplicationEvent 和 ApplicationListener ,分别对应JDK的两个接口。 
当然在spring中做了简化,只需要bean实现了Listener接口并且注册到了容器中,那么每次有ApplicationEvent发生,这个bean就可以获得通知,也就是事件源不再需要自己去维护对应的集合了,配合注解来说就更加简单了。
还是上面的例子,首先第一步还是先抽象出对应的事件:
| 1 | public class HomeworkEvent extends ApplicationEvent { | 
然后是对应的listener:
| 1 | @Component | 
对应的event的source这里就省略了,我自己定义的就是一个空对象,然后自己定义了一个Service进行测试。这里其实event的source也可以是一个publisher,这样可能更加符合常识。
发送事件在spring中特别简单,只需要在容器中自动注入一个ApplicationEventPublisher的对象(ApplicationContext实现了这个接口,所以其实可以直接使用ApplicationContext进行事件的发布) ,然后利用这个对象进行事件的发布即可。
所以我们在使用spring中只需要定义好事件,写好监听器的处理逻辑,最后只需要在你需要发送该事件的时候,注入ApplicationEventPublisher并进行事件的发送即可。但是需要注意的是,发送事件和处理事件是在同一个线程中进行处理的,而且默认情况下事件发送之后,需要等待事件处理完成之后,才会接下去处理。
spring中的事件机制一共有四个角色:
ApplicationEvent,它本身是一个抽象类,继承自JDK的EventObject。除了原有的source之外,只是额外新增了一个时间戳而已。ApplicationListener,它是一个接口继承自JDK的EventListener,作为一个函数型接口,只有一个方法,也就是当事件发生的时候会进行的逻辑,所以我们可以使用lambda表达式。ApplicationEventPublisher,这也是和JDK最与众不同的地方,spring抽象出了这个类,用这个类来进行事件的发布。ApplicationEventMulticaster,从名字上看,它是把事件发送给所有的监听器,但是其实它是用来对上面的三者进行管理的对象。所有的事件监听器都注册到广播器中,这样广播器就能从全局视角知道所有事件和所有监听器之间的关系了。spring的事件机制默认就是同步的,即事件发布之后,假设有三个监听器在监听,那么需要等监听器1处理完其中的逻辑之后,然后监听器2的逻辑才会进行处理,然后等到监听器2的处理逻辑处理完,监听器3的逻辑才开始处理… 等到所有的监听器的逻辑都走完之后,才回到发送事件之后的代码之后去继续执行。总体流程可参考下图:

如果我们不希望它按照默认的逻辑来做,那么可以在对应的监听器的处理逻辑中,新开一个线程进行逻辑的处理,或者直接交给线程池进行处理。
在spring默认的状态下,事件监听器默认是同步的,那么必然有个顺序,spring默认的是bean注册到容器中的顺序进行的。当然是可以通过实现Ordered接口来进行人为的操控的,只要实现Ordered接口,然后自己实现getOrder()就可以了。order的实现机制是,数字越小优先级越高,所以优先级最高的是Integer.MIN_VALUE。当然如果两个bean的order是一样的,那么就继续按照bean的加载顺序来。
除此之外spring本身还有另外一个接口PriorityOrdered,它继承了Ordered接口,在spring中的机制是:任何实现了PriorityOrdered接口的类都会优先于Ordered接口,最后就是那些没有设置过Order的类。
所有的这一切,肯定需要首先深入spring 的 ApplicationContext 中才可以解密。在spring的refresh()方法中,可以发现:
| 1 | // 其它流程 | 
很明显,第一行方法是初始化了事件广播器,最后一行注册了所有的监听器,那我们就把目光放在这两个函数中。
| 1 | protected void initApplicationEventMulticaster() { | 
可以发现,如果用户没有自定义广播器,那么就会使用SimpleApplicationEventMulticaster作为默认的广播器。
| 1 | protected void registerListeners() { | 
简单来说就是找到所有的监听器,然后把它们都加入到广播器中就行了。
因为ApplicationContext接口本身就是一个publisher,所以我们可以通过直接使用它进行事件的发布。实际的实现在AbstractApplicationContext中:
| 1 | protected void publishEvent(Object event, @Nullable ResolvableType eventType) { | 
| 1 | @Override | 
最后invokeListener就是调用了listener的对应的方法。还有我自己特意往容器中注入了一个executor,但是发现spring还是用的同步的方式。