简栈文化

Java技术人的成长之路~

分支管理

总览(一张流程图给大家先镇镇惊)

http://static.cyblogs.com/git分支总图概览.jpg

它主要体现了Git对我们源代码版本的管理。

(转载者加)一般情况:

  • masterdevelop并行。
  • master上始终是最稳定的代码,develop是正在开发的代码。
  • feature则是某个开发为了自己的功能拉的分支。
    不一般情况:
  • develop正在开发,如果你上线突然被拒绝了,这时候就要从master上开一个热分支,或者release分支也行,改好之后在分别合并到其他分支。但,本人感觉release通常意味着终止。别在从release上拉分支了。

为何是Git?

​ 对于Git与其他集中式代码管理工具相比的优缺点的全面讨论,请参见这里。这样的争论总是喋喋不休。作为一个开发者,与现今的其他开发工具相比较,我更喜欢GitGit真得改变了开发者对于合并和分支的思考。我曾经使用经典的CVS/Subversion,然而每次的合并/分支和其他行为总让人担惊受怕(“小心合并里的冲突,简直要命!”)。
但是对于Git来说,这些行为非常简单和搞笑,它们被认为是日常工作中的核心部分。例如,在很多CVS/Subversion书里,分支与合并总是在后面的章节中被讨论(对于高级用户使用),然而在每个Git书中,在第3章就已经完全涵盖了(作为基础)。
简单和重复的特性带来的结果是:分支与合并不再是什么可以害怕的东西。分支/合并被认为对于版本管理工具比其他功能更重要。
关于工具,不再多说,让我们直接看开发模型吧。这个模型并不是如下模型:在管理软件开发进度方面,面对每个开发过程,每个队员必须按一定次序开发。

分布式而非集中式

​ 对于这种分支模型,我们设置了一个版本库,它运转良好,这是一个”事实上” 版本库。不过请注意,这个版本库只是被认为是中心版本库(因为Git是一个分布式版本管理系统,从技术上来讲,并没有一个中心版本库)。我们将把这个版本库称为原始库,这个名字对所有的Git用户来说都很容易理解。

http://static.cyblogs.com/git分布式集中式.jpg

每个开发者都对origin库拉代码和提交代码。但是除了集中式的存取代码关系,每个开发者也可以从子团队的其他队友那里获得代码版本变更。例如,对于2个或多个开发者一起完成的大版本变更,为了防止过早地向origin库提交工作内容,这种机制就变得非常有用。在上述途中,有如下子团队:Alice和Bob,AliceDavidClairDavid
从技术上将,这意味着,Alice创建了一个Git的远程节点,而对于Bob,该节点指向了Bob的版本库,反之亦然。

主分支

http://static.cyblogs.com/develop与master分支的关系.jpg

在核心部分,研发模型很大程度上靠其他现有模型支撑的。中心库有2个可一直延续的分支:

  • master分支
  • develop分支

每个Git用户都要熟悉原始的master分支。与master分支并行的另一个分支,我们称之为develop分支。
我们把原始库/master库认作为主分支,HEAD的源代码存在于此版本中,并且随时都是一个预备生产状态。

我们把origin/develop库认为是主分支,该分支HEAD源码始终体现下个发布版的最新软件变更。有人称这个为“集成分支”,而这是每晚自动构建得来的。
develop分支的源码到达了一个稳定状态待发布,所有的代码变更需要以某种方式合并到master分支,然后标记一个版本号。如何操作将在稍后详细介绍。
所以,每次变更都合并到了master,这就是新产品的定义。在这一点,我们倾向于严格执行这一点,从而,理论上,每当对master有一个提交操作,我们就可以使用Git钩子脚本来自动构建并且发布软件到生产服务器。

辅助性分支

我们的开发模型使用了各种辅助性分支,这些分支与关键分支(master和develop)一起,用来支持团队成员们并行开发,使得易于追踪功能,协助生产发布环境准备,以及快速修复实时在线问题。与关键分支不同,这些分支总是有一个有限的生命期,因为他们最终会被移除。
我们用到的分支类型包括:

  • 功能分支
  • 发布分支
  • 热修复分支

每一种分支有一个特定目的,并且受限于严格到规则,比如:可以用哪些分支作为源分支,哪些分支能作为合并目标。我们马上将进行演练。
从技术角度来看,这些分支绝不是特殊分支。分支的类型基于我们使用的方法来进行分类。它们理所当然是普通的Git分支。

功能分支

http://static.cyblogs.com/20181126103335995_580-0.jpg

可能是develop分支的分支版本,最终必须合并到develop分支中。
分支命名规则:除了masterdeveloprelease-hotfix-之外,其他命名均可。
功能分支(有时被称为topic分支)通常为即将发布或者未来发布版开发新的功能。当新功能开始研发,包含该功能的发布版本在这个还是无法确定发布时间的。功能版本的实质是只要这个功能处于开发状态它就会存在,但是最终会或合并到develop分支(确定将新功能添加到不久的发布版中)或取消(譬如一次令人失望的测试)。
功能分支通常存在于开发者的软件库,而不是在源代码库中。
创建一个功能分支
开始一项功能的开发工作时,基于develop创建分支。

1
2
$ git checkout -b myfeature develop
Switched to a new branch "myfeature"

合并一个功能到develop分支
完成的功能可以合并进develop分支,以明确加入到未来的发布:

1
2
3
4
5
6
7
8
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff myfeature
Updating ea1b82a..05e9557
(Summary of changes)
$ git branch -d myfeature
Deleted branch myfeature (was 05e9557).
$ git push origin develop

--no-ff标志导致合并操作创建一个新commit对象,即使该合并操作可以fast-forward。这避免了丢失这个功能分支存在的历史信息,将该功能的所有提交组合在一起。 比较:

http://static.cyblogs.com/t_70_580-0.jpg

后一种情况,不可能从Git历史中看到哪些提交一起实现了一个功能——你必须手工阅读全部的日志信息。如果对整个功能进行回退 (比如一组提交),后一种方式会是一种真正头痛的问题,而使用--no-ff flag的情况则很容易.
是的,它会创建一个新的(空)提交对象,但是收益远大于开销。
不幸的是,我还没找到一种方法,让–no-ff时作为合并操作的默认选项,但它应该是可行的。

Release 分支

Release分支可能从develop分支分离而来,但是一定要合并到develop和master分支上,它的习惯命名方式为:release-*
Release分支是为新产品的发布做准备的。它允许我们在最后时刻做一些细小的修改。他们允许小bugs的修改和准备发布元数据(版本号,开发时间等等)。当在Release分支完成这些所有工作以后,对于下一次打的发布,develop分支接收features会更加明确。
develop分支创建新的Release分支的关键时刻是develop分支达到了发布的理想状态。至少所有这次要发布的features必须在这个点及时合并到develop分支。对于所有未来准备发布的features必须等到Release分支创建以后再合并。
Release分支创建的时候要为即将发行版本分配一个版本号,一点都不早。直到那时,develop分支反映的变化都是为了下一个发行版,但是在Release分支创建之前,下一个发行版到底叫0.3还是1.0是不明确的。这个决定是在Release分支创建时根据项目在版本号上的规则制定的。

创建一个release分支
Release分支是从develop分支创建的。例如,当前产品的发行版本号为1.1.5,同事我们有一个大的版本即将发行。develop 分支已经为下次发行做好了准备,我们得决定下一个版本是1.2(而不是1.1.6或者2.0)。所以我们将Release分支分离出来,给一个能够反映新版本号的分支名。

1
2
3
4
5
6
7
$ git checkout -b release-1.2 develop
Switched to a new branch "release-1.2"
$ ./bump-version.sh 1.2
Files modified successfully, version bumped to 1.2.
$ git commit -a -m "Bumped version number to 1.2"
[release-1.2 74d9424] Bumped version number to 1.2
1 files changed, 1 insertions(+), 1 deletions(-)

创建新分支以后,切换到该分支,添加版本号。这里,bump-version.sh 是一个虚构的shell脚本,它可以复制一些文件来反映新的版本(这当然可以手动改变–目的就是修改一些文件)。然后版本号被提交。
这个新分支可能会存在一段时间,直到该发行版到达它的预定目标。在此期间,bug的修复可能被提交到该分支上(而不是提交到develop分支上)。在这里严格禁止增加大的新features。他们必须合并到develop分支上,然后等待下一次大的发行版。

完成一个release分支
当一个release分支准备好成为一个真正的发行版的时候,有一些工作必须完成。首先,release分支要合并到master上(因为每一次提交到master上的都是一个新定义的发行版,记住)。然后,提交到master上必须打一个标签,以便以后更加方便的引用这个历史版本。最后,在release分支上的修改必须合并到develop分支上,以便未来发行版也包含这些bugs的修复。
Git中的前两步是:

1
2
3
4
5
6
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff release-1.2
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2

发行版现在已经完成,为以后引用打上标签。
**编辑:**你可能也想使用the-sor-u <key>flags来标记你的标签。
为了是修改保持在release分支上,我们需要合并这些到develop分支上去,在Git上:

1
2
3
4
$ git checkout develop
Switched to branch 'develop'
$ git merge --no-ff release-1.2
Merge made by recursive.

(Summary of changes)
这个步骤可能会导致合并冲突(可能由于改变版本号更是如此)。如果是这样,修复它然后提交。
现在我们真正的完成了,这个release分支将被删除,因为我们不再需要它了。

1
2
$ git branch -d release-1.2
Deleted branch release-1.2 (was ff452fe).

热修复分支

http://static.cyblogs.com/t_70_580-1.jpg

可以基于master分支,必须合并回developmaster分支。
分支名约定:hotfix-*
热修复分支与发布分支很相似,他们都为新的生成环境发布做准备,尽管这是未经计划的。他们来自生产环境的处于异常状态压力。当生成环境验证缺陷必须马上修复是,热修复分支可以基于master分支上对应与线上版本的tag创建。
其本质是团队成员(在develop分支上)的工作可以继续,而另一个人准备生产环境的快速修复。
创建修补bug分支
hotfix branch(修补bug分支)是从Master分支上面分出来的。例如,1.2版本是当前生产环境的版本并且有bug。但是开发分支(develop)变化还不稳定。我们需要分出来一个修补bug分支(hotfix branch)来解决这种情况。

1
2
3
4
5
6
7
$ git checkout -b hotfix-1.2.1 master
Switched to a new branch "hotfix-1.2.1"
$ ./bump-version.sh 1.2.1
Files modified successfully, version bumped to 1.2.1.
$ git commit -a -m "Bumped version number to 1.2.1"
[hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1
1 files changed, 1 insertions(+), 1 deletions(-)

分支关闭的时侯不要忘了更新版本号(bump the version)
然后,修复bug,一次提交或者多次分开提交。

1
2
3
$ git commit -m "Fixed severe production problem"
[hotfix-1.2.1 abbe5d6] Fixed severe production problem
5 files changed, 32 insertions(+), 17 deletions(-)

完成一个hotfix分支
完成一个bugfix之后,需要把butfix合并到master和develop分支去,这样就可以保证修复的这个bug也包含到下一个发行版中。这一点和完成release分支很相似。
首先,更新master并对release打上tag

1
2
3
4
5
6
$ git checkout master
Switched to branch 'master'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.
(Summary of changes)
$ git tag -a 1.2.1

编辑:你可能也会想使用 -sor-u <key>参数来对你的tag进行加密
下一步,把bugfix添加到develop分支中:

1
2
3
4
$ git checkout develop 
Switched to branch 'develop'
$ git merge --no-ff hotfix-1.2.1
Merge made by recursive.

(Summary of changes)
规则的一个例外是: 如果一个release分支已经存在,那么应该把hotfix合并到这个release分支,而不是合并到develop分支。当release分支完成后, 将bugfix分支合并回release分支也会使得bugfix被合并到develop分支。(如果在develop分支的工作急需这个bugfix,等不到release分支的完成,那你也可以把bugfix合并到develop分支)
最后,删除临时分支:

(Summary of changes)
规则的一个例外是: 如果一个release分支已经存在,那么应该把hotfix合并到这个release分支,而不是合并到develop分支。当release分支完成后, 将bugfix分支合并回release分支也会使得bugfix被合并到develop分支。(如果在develop分支的工作急需这个bugfix,等不到release分支的完成,那你也可以把bugfix合并到develop分支)
最后,删除临时分支:

1
2
$ git branch -d hotfix-1.2.1
Deleted branch hotfix-1.2.1 (was abbe5d6).

摘要

尽管这个分支模型没有任何震撼的新东西, 文章开头的图表在我们的项目中表现出惊人的实用性。它形成了一个优雅的思维模型,易于领悟并使团队成员发展出对分支和发布过程的共同理解。

参考地址

一.简介

简单的说,RedisTemplateStringRedisTemplate的关系如下:

1.StringRedisTemplateRedisTemplate的子类。

2.StringRedisTemplate的各个序列化策略都是StringRedisSerializer,而RedisTemplate用的是JdkSerializationRedisSerializer

二.RedisTemplate和StringRedisTemplate的代码结构

RedisTemplate类说起。

RedisTemplate类中,定义了这样四个变量:

1
2
3
4
5
6
7
8
@Nullable
private RedisSerializer keySerializer = null;
@Nullable
private RedisSerializer valueSerializer = null;
@Nullable
private RedisSerializer hashKeySerializer = null;
@Nullable
private RedisSerializer hashValueSerializer = null;

分别代表了普通keyvalue,和Hash类型的key,value的序列化策略,可以分别设置。

另外定义变量,用来指定默认的序列化策略:

1
2
@Nullable
private RedisSerializer<?> defaultSerializer;

RedisTemplate类中,定义了afterPropertiesSet()方法,当Spring创建RedisTemplate类的对象时,会调用这个方法:

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
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (this.defaultSerializer == null) {
this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
}

if (this.enableDefaultSerializer) {
if (this.keySerializer == null) {
this.keySerializer = this.defaultSerializer;
defaultUsed = true;
}

if (this.valueSerializer == null) {
this.valueSerializer = this.defaultSerializer;
defaultUsed = true;
}

if (this.hashKeySerializer == null) {
this.hashKeySerializer = this.defaultSerializer;
defaultUsed = true;
}

if (this.hashValueSerializer == null) {
this.hashValueSerializer = this.defaultSerializer;
defaultUsed = true;
}
}

if (this.enableDefaultSerializer && defaultUsed) {
Assert.notNull(this.defaultSerializer, "default serializer null and not all serializers initialized");
}

if (this.scriptExecutor == null) {
this.scriptExecutor = new DefaultScriptExecutor(this);
}

this.initialized = true;
}

可以看到,在默认情况下,RedisTemplate使用的默认序列化策略是JdkSerializationRedisSerializer。包括RedisTemplate下的keyvaluehash-key,hash-value的序列化,都用这种策略。

再来看看StringRedisTemplate,他作为RedisTemplate的子类,只是修改了序列化策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StringRedisTemplate extends RedisTemplate<String, String> {
public StringRedisTemplate() {
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
this.setKeySerializer(stringSerializer);
this.setValueSerializer(stringSerializer);
this.setHashKeySerializer(stringSerializer);
this.setHashValueSerializer(stringSerializer);
}

public StringRedisTemplate(RedisConnectionFactory connectionFactory) {
this();
this.setConnectionFactory(connectionFactory);
this.afterPropertiesSet();
}

protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
return new DefaultStringRedisConnection(connection);
}
}

以上就是StringRedisTemplate整个类的内容,可以看到,在他的默认构造中,keyvaluehash-keyhash-value都使用的是StringRedisSerializer类作为序列化策略。这也就是StringRedisTemplate和他的父类RedisTemplate的主要区别。

三.序列化策略

更进一步,看一下这个序列化策略是什么。

上面提到的StringRedisSerializerJdkSerializationRedisSerializer都是序列化策略类,他们都实现了一个RedisSerializer<T>接口:

1
2
3
4
5
6
7
public interface RedisSerializer<T> {
@Nullable
byte[] serialize(@Nullable T var1) throws SerializationException;

@Nullable
T deserialize(@Nullable byte[] var1) throws SerializationException;
}

接口表达的意思很简单,两个方法,serialize用于序列化,把对象变为byte数组,deserialize用于反序列化,把byte数组转为对象。

StringRedisSerializer

看看StringRedisSerializer是怎么做的:

1.StringRedisSerializer的构造:

1
2
3
4
5
6
7
8
public StringRedisSerializer() {
this(StandardCharsets.UTF_8);
}

public StringRedisSerializer(Charset charset) {
Assert.notNull(charset, "Charset must not be null!");
this.charset = charset;
}

定义了编码格式,默认UTF_8

2.StringRedisSerializer的serialize和deserialize方法:

1
2
3
4
5
6
7
public String deserialize(@Nullable byte[] bytes) {
return bytes == null ? null : new String(bytes, this.charset);
}

public byte[] serialize(@Nullable String string) {
return string == null ? null : string.getBytes(this.charset);
}

可以看到,StringRedisSerializer采用的是字符串和对应编码下二进制数组之间的转换。

在这种编码格式下,如果我们向redis保存信息,然后用客户端访问Redis时,只要编码格式一致,就能看到保存信息的原文。保存字符串ABC,客户端看到的也是字符串ABC

JdkSerializationRedisSerializer

然后对比看看JdkSerializationRedisSerializer是怎么做的。

1.JdkSerializationRedisSerializer的构造:

1
2
3
4
5
6
7
8
9
10
private final Converter<Object, byte[]> serializer;
private final Converter<byte[], Object> deserializer;

public JdkSerializationRedisSerializer() {
this(new SerializingConverter(), new DeserializingConverter());
}

public JdkSerializationRedisSerializer(ClassLoader classLoader) {
this(new SerializingConverter(), new DeserializingConverter(classLoader));
}

可以看到,JdkSerializationRedisSerializer定义了两个变量,serializer和deserializer,显然是用来序列化和反序列化的,他们两个的类型是一样的,都是Converter接口,只是泛型不同。

Converter接口:

1
2
3
4
5
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
T convert(S source);
}

就一个方法。

另外在JdkSerializationRedisSerializer的构造中,对serializer和deserializer进行了初始化,使用SerializingConverter和DeserializingConverter作为实现类。

2.JdkSerializationRedisSerializer的serialize和deserialize方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Object deserialize(@Nullable byte[] bytes) {
if (SerializationUtils.isEmpty(bytes)) {
return null;
} else {
try {
return this.deserializer.convert(bytes);
} catch (Exception var3) {
throw new SerializationException("Cannot deserialize", var3);
}
}
}

public byte[] serialize(@Nullable Object object) {
if (object == null) {
return SerializationUtils.EMPTY_ARRAY;
} else {
try {
return (byte[])this.serializer.convert(object);
} catch (Exception var3) {
throw new SerializationException("Cannot serialize", var3);
}
}
}

其实就是调用了对应Converterconvert方法。

3.关于Converter

既然到这了,就再深入一步,看看SerializingConverter和DeserializingConverter的convert方法。

首先,序列化:

SerializingConverter的相关方法,贴一部分关键的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public SerializingConverter() {
this.serializer = new DefaultSerializer();
}

@Override
public byte[] convert(Object source) {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
try {
this.serializer.serialize(source, byteStream);
return byteStream.toByteArray();
}
catch (Throwable ex) {
throw new SerializationFailedException("Failed to serialize object using " +
this.serializer.getClass().getSimpleName(), ex);
}
}

可以看到,SerializingConverter类定义了serializer变量,用DefaultSerializer类实现,序列化的方式是调用DefaultSerializerserialize方法:

1
2
3
4
5
6
7
8
9
10
@Override
public void serialize(Object object, OutputStream outputStream) throws IOException {
if (!(object instanceof Serializable)) {
throw new IllegalArgumentException(getClass().getSimpleName() + " requires a Serializable payload " +
"but received an object of type [" + object.getClass().getName() + "]");
}
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(object);
objectOutputStream.flush();
}

DefaultSerializerserialize方法使用了ObjectOutputStream,调用writeObject方法序列化对象。

对应的,反序列化:

DeserializingConverter的convert方法,贴一下相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public DeserializingConverter() {
this.deserializer = new DefaultDeserializer();
}

@Override
public Object convert(byte[] source) {
ByteArrayInputStream byteStream = new ByteArrayInputStream(source);
try {
return this.deserializer.deserialize(byteStream);
}
catch (Throwable ex) {
throw new SerializationFailedException("Failed to deserialize payload. " +
"Is the byte array a result of corresponding serialization for " +
this.deserializer.getClass().getSimpleName() + "?", ex);
}
}

可见DeserializingConverter使用了DefaultDeserializer作为反序列化工具,调用了他的deserialize方法:

1
2
3
4
5
6
7
8
9
10
11
@Override
@SuppressWarnings("resource")
public Object deserialize(InputStream inputStream) throws IOException {
ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader);
try {
return objectInputStream.readObject();
}
catch (ClassNotFoundException ex) {
throw new NestedIOException("Failed to deserialize object type", ex);
}
}

对比SerializingConverterDeserializingConverter使用的是ConfigurableObjectInputStream,并调用他的readObject方法进行反序列化。

这种序列化方式,如果保存信息至redis,用客户端查看时,保存的信息看起来像是在原来的字符前面加了几个字符。

比如:

1
2
3
4
JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
StringRedisSerializer stringSerializer = new StringRedisSerializer();
byte[] jdkByteArr = jdkSerializer.serialize("CSDN博客");
byte[] stringByteArr = stringSerializer.serialize("CSDN博客");

这种情况下,得到的byte数组是:

jdkByteArr

{-84,-19,0,5,116,0,10,67,83,68,78,-27,-115,-102,-27,-82,-94}

stringByteArr:

{67,83,68,78,-27,-115,-102,-27,-82,-94}

StringRedisSerializer把字符串本身转化成byte数组,而JdkSerializationRedisSerializer在数组前面加了几个字符,这些字符也会被保存到redis中。

所以,从数据上来说,这两种序列化策略处理的数据是不会共通的,各人管各人的。

四.关于redisTemplate的Operations

使用redisTemplate时,除了调用execute方法并自定义RedisCallback之外,还可以使用redisTemplate提供的几个Operations接口。

redisTemplate中定义了以下几个Operations

1
2
3
4
5
6
7
8
9
10
11
12
@Nullable
private ValueOperations<K, V> valueOps;
@Nullable
private ListOperations<K, V> listOps;
@Nullable
private SetOperations<K, V> setOps;
@Nullable
private ZSetOperations<K, V> zSetOps;
@Nullable
private GeoOperations<K, V> geoOps;
@Nullable
private HyperLogLogOperations<K, V> hllOps;

这几个Operations接口,分别提供了对不同种类数据的操作方法。

ValueOperations为例,他提供的方法有:

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
void set(K var1, V var2);

void set(K var1, V var2, long var3, TimeUnit var5);

@Nullable
Boolean setIfAbsent(K var1, V var2);

void multiSet(Map<? extends K, ? extends V> var1);

@Nullable
Boolean multiSetIfAbsent(Map<? extends K, ? extends V> var1);

@Nullable
V get(Object var1);

@Nullable
V getAndSet(K var1, V var2);

@Nullable
List<V> multiGet(Collection<K> var1);

@Nullable
Long increment(K var1, long var2);

@Nullable
Double increment(K var1, double var2);

@Nullable
Integer append(K var1, String var2);

@Nullable
String get(K var1, long var2, long var4);

void set(K var1, V var2, long var3);

@Nullable
Long size(K var1);

@Nullable
Boolean setBit(K var1, long var2, boolean var4);

@Nullable
Boolean getBit(K var1, long var2);

其他的Operations提供的方法各有不同,但是这些Operations的使用方式都是相同的。

不同的Operations分别通过RedisTemplate的以下方法获取:

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
public ValueOperations<K, V> opsForValue() {
if (this.valueOps == null) {
this.valueOps = new DefaultValueOperations(this);
}

return this.valueOps;
}

public ListOperations<K, V> opsForList() {
if (this.listOps == null) {
this.listOps = new DefaultListOperations(this);
}

return this.listOps;
}

public SetOperations<K, V> opsForSet() {
if (this.setOps == null) {
this.setOps = new DefaultSetOperations(this);
}

return this.setOps;
}

public ZSetOperations<K, V> opsForZSet() {
if (this.zSetOps == null) {
this.zSetOps = new DefaultZSetOperations(this);
}

return this.zSetOps;
}

public GeoOperations<K, V> opsForGeo() {
if (this.geoOps == null) {
this.geoOps = new DefaultGeoOperations(this);
}

return this.geoOps;
}

可见,在这些获得Operations的方法中,都提供了一个默认实现类,并且把RedisTemplate对象本身当做参数传给了这个实现类。

还是以ValueOperations为例,RedisTemplate提供的默认实现类是DefaultValueOperations,看看这个类的源码:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package org.springframework.data.redis.core;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.AbstractOperations.ValueDeserializingRedisCallback;

class DefaultValueOperations<K, V> extends AbstractOperations<K, V> implements ValueOperations<K, V> {
DefaultValueOperations(RedisTemplate<K, V> template) {
super(template);
}

public V get(Object key) {
return this.execute(new AbstractOperations<K, V>.ValueDeserializingRedisCallback(key) {
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.get(rawKey);
}
}, true);
}

public V getAndSet(K key, V newValue) {
final byte[] rawValue = this.rawValue(newValue);
return this.execute(new AbstractOperations<K, V>.ValueDeserializingRedisCallback(key) {
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.getSet(rawKey, rawValue);
}
}, true);
}

public Long increment(K key, long delta) {
byte[] rawKey = this.rawKey(key);
return (Long)this.execute((connection) -> {
return connection.incrBy(rawKey, delta);
}, true);
}

public Double increment(K key, double delta) {
byte[] rawKey = this.rawKey(key);
return (Double)this.execute((connection) -> {
return connection.incrBy(rawKey, delta);
}, true);
}

public Integer append(K key, String value) {
byte[] rawKey = this.rawKey(key);
byte[] rawString = this.rawString(value);
return (Integer)this.execute((connection) -> {
Long result = connection.append(rawKey, rawString);
return result != null ? result.intValue() : null;
}, true);
}

public String get(K key, long start, long end) {
byte[] rawKey = this.rawKey(key);
byte[] rawReturn = (byte[])this.execute((connection) -> {
return connection.getRange(rawKey, start, end);
}, true);
return this.deserializeString(rawReturn);
}

public List<V> multiGet(Collection<K> keys) {
if (keys.isEmpty()) {
return Collections.emptyList();
} else {
byte[][] rawKeys = new byte[keys.size()][];
int counter = 0;

Object hashKey;
for(Iterator var4 = keys.iterator(); var4.hasNext(); rawKeys[counter++] = this.rawKey(hashKey)) {
hashKey = var4.next();
}

List<byte[]> rawValues = (List)this.execute((connection) -> {
return connection.mGet(rawKeys);
}, true);
return this.deserializeValues(rawValues);
}
}

public void multiSet(Map<? extends K, ? extends V> m) {
if (!m.isEmpty()) {
Map<byte[], byte[]> rawKeys = new LinkedHashMap(m.size());
Iterator var3 = m.entrySet().iterator();

while(var3.hasNext()) {
Entry<? extends K, ? extends V> entry = (Entry)var3.next();
rawKeys.put(this.rawKey(entry.getKey()), this.rawValue(entry.getValue()));
}

this.execute((connection) -> {
connection.mSet(rawKeys);
return null;
}, true);
}
}

public Boolean multiSetIfAbsent(Map<? extends K, ? extends V> m) {
if (m.isEmpty()) {
return true;
} else {
Map<byte[], byte[]> rawKeys = new LinkedHashMap(m.size());
Iterator var3 = m.entrySet().iterator();

while(var3.hasNext()) {
Entry<? extends K, ? extends V> entry = (Entry)var3.next();
rawKeys.put(this.rawKey(entry.getKey()), this.rawValue(entry.getValue()));
}

return (Boolean)this.execute((connection) -> {
return connection.mSetNX(rawKeys);
}, true);
}
}

public void set(K key, V value) {
final byte[] rawValue = this.rawValue(value);
this.execute(new AbstractOperations<K, V>.ValueDeserializingRedisCallback(key) {
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
connection.set(rawKey, rawValue);
return null;
}
}, true);
}

public void set(K key, V value, final long timeout, final TimeUnit unit) {
final byte[] rawKey = this.rawKey(key);
final byte[] rawValue = this.rawValue(value);
this.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
this.potentiallyUsePsetEx(connection);
return null;
}

public void potentiallyUsePsetEx(RedisConnection connection) {
if (!TimeUnit.MILLISECONDS.equals(unit) || !this.failsafeInvokePsetEx(connection)) {
connection.setEx(rawKey, TimeoutUtils.toSeconds(timeout, unit), rawValue);
}

}

private boolean failsafeInvokePsetEx(RedisConnection connection) {
boolean failed = false;

try {
connection.pSetEx(rawKey, timeout, rawValue);
} catch (UnsupportedOperationException var4) {
failed = true;
}

return !failed;
}
}, true);
}

public Boolean setIfAbsent(K key, V value) {
byte[] rawKey = this.rawKey(key);
byte[] rawValue = this.rawValue(value);
return (Boolean)this.execute((connection) -> {
return connection.setNX(rawKey, rawValue);
}, true);
}

public void set(K key, V value, long offset) {
byte[] rawKey = this.rawKey(key);
byte[] rawValue = this.rawValue(value);
this.execute((connection) -> {
connection.setRange(rawKey, rawValue, offset);
return null;
}, true);
}

public Long size(K key) {
byte[] rawKey = this.rawKey(key);
return (Long)this.execute((connection) -> {
return connection.strLen(rawKey);
}, true);
}

public Boolean setBit(K key, long offset, boolean value) {
byte[] rawKey = this.rawKey(key);
return (Boolean)this.execute((connection) -> {
return connection.setBit(rawKey, offset, value);
}, true);
}

public Boolean getBit(K key, long offset) {
byte[] rawKey = this.rawKey(key);
return (Boolean)this.execute((connection) -> {
return connection.getBit(rawKey, offset);
}, true);
}
}

所有Operations实现类都是AbstractOperations的子类,另外各自实现各自的接口。

实现类的方法中多数都是调用了this.execute()方法,这个方法在父类AbstractOperations中,最终调用的其实也是RedisTemplateexecute()方法。

以上面DefaultValueOperationsset()方法为例,看一下代码:

1
2
3
4
5
6
7
8
9
public void set(K key, V value) {
final byte[] rawValue = this.rawValue(value);
this.execute(new AbstractOperations<K, V>.ValueDeserializingRedisCallback(key) {
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
connection.set(rawKey, rawValue);
return null;
}
}, true);
}

首先是对value的处理,调用this.rawValue()方法,把value序列化成byte数组,这个方法在父类AbstractOperations中:

1
2
3
byte[] rawValue(Object value) {
return this.valueSerializer() == null && value instanceof byte[] ? (byte[])((byte[])value) : this.valueSerializer().serialize(value);
}

可见,代码用的是自己的valueSerializer来序列化value,这个valueSerializer来自RedisTemplate

回到set()方法,value序列化完成后,调用this.execute()方法,给此方法传递的第一个参数是:

1
2
3
4
5
6
new AbstractOperations<K, V>.ValueDeserializingRedisCallback(key) {
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
connection.set(rawKey, rawValue);
return null;
}
}

这个参数实际上是一个ValueDeserializingRedisCallback对象,在其中定义了inRedis()方法的实现。

this.execute()方法在父类AbstractOperations中:

1
2
3
4
@Nullable
<T> T execute(RedisCallback<T> callback, boolean b) {
return this.template.execute(callback, b);
}

其中this.template指的就是初始化时传入的RedisTemplate,其execute()方法是这样的:

1
2
3
4
@Nullable
public <T> T execute(RedisCallback<T> action, boolean exposeConnection) {
return this.execute(action, exposeConnection, false);
}

然后调用下面的方法:

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
@Nullable
public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) {
Assert.isTrue(this.initialized, "template not initialized; call afterPropertiesSet() before using it");
Assert.notNull(action, "Callback object must not be null");
RedisConnectionFactory factory = this.getRequiredConnectionFactory();
RedisConnection conn = null;

Object var11;
try {
if (this.enableTransactionSupport) {
conn = RedisConnectionUtils.bindConnection(factory, this.enableTransactionSupport);
} else {
conn = RedisConnectionUtils.getConnection(factory);
}

boolean existingConnection = TransactionSynchronizationManager.hasResource(factory);
RedisConnection connToUse = this.preProcessConnection(conn, existingConnection);
boolean pipelineStatus = connToUse.isPipelined();
if (pipeline && !pipelineStatus) {
connToUse.openPipeline();
}

RedisConnection connToExpose = exposeConnection ? connToUse : this.createRedisConnectionProxy(connToUse);
T result = action.doInRedis(connToExpose);
if (pipeline && !pipelineStatus) {
connToUse.closePipeline();
}

var11 = this.postProcessResult(result, connToUse, existingConnection);
} finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}

return var11;
}

方法初始化了RedisConnection,最后面调用了RedisCallbackdoInRedis()方法,也就是这一行:

1
T result = action.doInRedis(connToExpose);

这里的变量action就是在set()方法中自定义的new AbstractOperations<K, V>.ValueDeserializingRedisCallback(key)

ValueDeserializingRedisCallback类是AbstractOperations的内部抽象类,他的doInRedis()方法是这样的:

1
2
3
4
public final V doInRedis(RedisConnection connection) {
byte[] result = this.inRedis(AbstractOperations.this.rawKey(this.key), connection);
return AbstractOperations.this.deserializeValue(result);
}

可见调用了inRedis()方法,其第一个参数是序列化后的key,调用的是AbstractOperationsrawKey()方法,代码如下:

1
2
3
4
byte[] rawKey(Object key) {
Assert.notNull(key, "non null key required");
return this.keySerializer() == null && key instanceof byte[] ? (byte[])((byte[])key) : this.keySerializer().serialize(key);
}

这里把key进行序列化,keySerializer()方法从RedisTemplate中获取keySerializer,并由keySerializerkey进行序列化。

ValueDeserializingRedisCallback类中的inRedis()方法是抽象方法,具体的实现在DefaultValueOperationsset()方法中,也就是这一部分:

1
2
3
4
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
connection.set(rawKey, rawValue);
return null;
}

最终调用的是RedisConnectionset()方法,完成Redisset操作。

以上就是在RedisTemplate中使用ValueOperations进行set操作的全部代码流程。

Redis的不同操作分散在RedisTemplate的不同Operations中,只是调用的方法不同,调用流程都差不多。

参考地址

使用新版idea创建了一个springboot项目,运行build时发生如下报错。

1
2
3
4
5
6
Error:(3, 32) java: 程序包org.springframework.boot不存在
Error:(4, 46) java: 程序包org.springframework.boot.autoconfigure不存在
Error:(5, 40) java: 程序包org.springframework.boot.builder不存在
Error:(6, 52) java: 程序包org.springframework.boot.web.servlet.support不存在
Error:(9, 34) java: 找不到符号
符号: 类 SpringBootServletInitializer

http://static.cyblogs.com/QQ20201107-080822@2x.jpg

诡异的是,使用mvn打包却是正常的,能生成正确的jar并且能通过命令行启动 后来通过查阅资料得知新版IDEA需要在Setting里将 delegate IDE build/run actions to Maven勾选上即可。

http://static.cyblogs.com/QQ20201107-081022@2x.jpg

其它

在解决这个问题的过程中,还了解到使用spring-boot-maven-plugin这个插件打的jar的结构会和普通插件maven-jar-plugin的有些不一样。spring-boot-maven-plugin将所有应用启动运行所需的jar全部包含进来了,具备独立运行的条件。

1
2
3
4
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

spring-boot-maven-plugin打包的jar目录:

http://static.cyblogs.com/QQ20201107-081147@2x.jpg

而使用普通插件打包的结构如图:

http://static.cyblogs.com/QQ20201107-081242@2x.jpg

最后,这里补充一点。如果是你想在IDEA里面,不是通过Maven来依赖,这里就不用打钩,否则你每次都必须要install你的jar包到本地来。

参考地址

前言

使用spring开发时,进行配置主要有两种方式,一是xml的方式,二是java config的方式。spring技术自身也在不断的发展和改变,从当前springboot的火热程度来看,java config的应用是越来越广泛了,在使用java config的过程当中,我们不可避免的会有各种各样的注解打交道,其中,我们使用最多的注解应该就是@Autowired注解了。这个注解的功能就是为我们注入一个定义好的bean。那么,这个注解除了我们常用的属性注入方式之外还有哪些使用方式呢?它在代码层面又是怎么实现的呢?这是本篇文章着重想讨论的问题。

@Autowired注解用法

在分析这个注解的实现原理之前,我们不妨先来回顾一下@Autowired注解的用法。

@Autowired注解应用于构造函数,如以下示例所示

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private final CustomerPreferenceDao customerPreferenceDao;

@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}

// ...
}

@Autowired注释应用于setter方法

1
2
3
4
5
6
7
8
9
10
11
public class SimpleMovieLister {

private MovieFinder movieFinder;

@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// ...
}

@Autowired注释应用于具有任意名称和多个参数的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MovieRecommender {

private MovieCatalog movieCatalog;

private CustomerPreferenceDao customerPreferenceDao;

@Autowired
public void prepare(MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}

// ...
}

您也可以将@Autowired应用于字段,或者将其与构造函数混合,如以下示例所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MovieRecommender {

private final CustomerPreferenceDao customerPreferenceDao;

@Autowired
private MovieCatalog movieCatalog;

@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}

// ...
}

直接应用于字段是我们使用的最多的一种方式,但是使用构造方法注入从代码层面却是更加好的,原因可以参考这篇博客:spring重点知识。除此之外,还有以下不太常见的几种方式

@Autowired注释添加到需要该类型数组的字段或方法,则spring会从ApplicationContext中搜寻符合指定类型的所有bean,如以下示例所示:

1
2
3
4
5
6
7
public class MovieRecommender {

@Autowired
private MovieCatalog[] movieCatalogs;

// ...
}

数组可以,我们可以马上举一反三,那容器也可以吗,答案是肯定的,下面是set以及map的例子:

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private Set<MovieCatalog> movieCatalogs;

@Autowired
public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}

// ...
}
1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private Map<String, MovieCatalog> movieCatalogs;

@Autowired
public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}

// ...
}

以上就是@Autowired注解的主要使用方式,经常使用spring的话应该对其中常用的几种不会感到陌生。

@Autowired注解的作用到底是什么?

@Autowired这个注解我们经常在使用,现在,我想问的是,它的作用到底是什么呢?

首先,我们从所属范围来看,事实上这个注解是属于spring的容器配置的一个注解,与它同属容器配置的注解还有:@Required,@Primary, @Qualifier等等。因此@Autowired注解是一个用于容器(container)配置的注解。

其次,我们可以直接从字面意思来看,@autowired注解来源于英文单词autowire,这个单词的意思是自动装配的意思。

自动装配又是什么意思?这个词语本来的意思是指的一些工业上的用机器代替人口,自动将一些需要完成的组装任务,或者别的一些任务完成。而在spring的世界当中,自动装配指的就是使用将Spring容器中的bean自动的和我们需要这个bean的类组装在一起。

因此,笔者个人对这个注解的作用下的定义就是:将Spring容器中的bean自动的和我们需要这个bean的类组装在一起协同使用。

接下来,我们就来看一下这个注解背后到底做了些什么工作。

@Autowired注解是如何实现的

事实上,要回答这个问题必须先弄明白的是java是如何支持注解这样一个功能的。

java的注解实现的核心技术是反射,让我们通过一些例子以及自己实现一个注解来理解它工作的原理。

例子注解@Override

@Override注解的定义如下:

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

@Override注解使用java官方提供的注解,它的定义里面并没有任何的实现逻辑。注意,所有的注解几乎都是这样的,注解只能是被看作元数据,它不包含任何业务逻辑注解更像是一个标签,一个声明,表面被注释的这个地方,将具有某种特定的逻辑

那么,问题接踵而至,注解本身不包含任何逻辑,那么注解的功能是如何实现的呢?答案必然是别的某个地方对这个注解做了实现。以@Override注解为例,他的功能是重写一个方法,而他的实现者就是JVMjava虚拟机,java虚拟机在字节码层面实现了这个功能。

但是对于开发人员,虚拟机的实现是无法控制的东西,也不能用于自定义注解。 所以,如果是我们自己想定义一个独一无二的注解的话,则我们需要自己为注解写一个实现逻辑,换言之,我们需要实现自己注解特定逻辑的功能

自己实现一个注解

在自己写注解之前我们有一些基础知识需要掌握,那就是我们写注解这个功能首先是需要java支持的,javajdk5当中支持了这一功能,并且在java.lang.annotation包中提供了四个注解,仅用于编写注解时使用,他们是:

注解 作用
@Documented 表明是否在java doc中添加Annotation
@Retention 定义注释应保留多长时间,即有效周期。有以下几种策略: RetentionPolicy.SOURCE - 在编译期间丢弃。 编译完成后,这些注释没有任何意义,因此它们不会写入字节码。 示例@Override,@ SuppressWarnings RetentionPolicy.CLASS - 在类加载期间丢弃。 在进行字节码级后处理时很有用。 有点令人惊讶的是,这是默认值。 RetentionPolicy.RUNTIME - 不要丢弃。 注释应该可以在运行时进行反射。 这是我们通常用于自定义注释的内容。
@Target 指定可以放置注解的位置。 如果不指定,则可以将注解放在任何位置。若我们只想要其中几个,则需要定义对应的几个。下面是这8个属性:ElementType.TYPE(类,接口,枚举)ElementType.FIELD(实例变量)ElementType.METHODElementType.PARAMETERElementType.CONSTRUCTORElementType.LOCAL_VARIABLEElementType.ANNOTATION_TYPE(在另一个注释上)ElementType.PACKAGE(记住package-info.java)
@Inherited 控制注解是否对子类产生影响。

下面我们开始自己实现一个注解,注解仅支持 primitives, stringenumerations这三种类型。 注解的所有属性都定义为方法,也可以提供默认值。我们先实现一个最简单的注解。

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimpleAnnotation {
String value();
}

上面这个注释里面只定义了一个字符传,它的目标注释对象是方法,保留策略是在运行期间。下面我们定义一个方法来使用这个注解:

1
2
3
4
5
6
7
8
public class UseAnnotation {

@SimpleAnnotation("testStringValue")
public void testMethod(){
//do something here
}

}

我们在这里使用了这个注解,并把字符串赋值为:testStringValue,到这里,定义一个注解并使用它,我们就已经全部完成。

简单的不敢相信。但是,细心一想的话,我们虽然写了一个注解也用了它,可是它并没有产生任何作用啊。也没有对我们这里方法产生任何效果啊。是的现在确实是这样的,原因在于我们前面提到的一点,我们还没有为这个注解实现它的逻辑,现在我们就来为这个注解实现逻辑。

应该怎么做呢?我们不妨自己来想一想。首先,我想给标注了这个注解的方法或字段实现功能,我们必须得知道,到底有哪些方法,哪些字段使用了这个注解吧,因此,这里我们很容易想到,这里应该会用到反射。其次,利用反射,我们利用反射拿到这样目标之后,得为他实现一个逻辑,这个逻辑是这些方法本身逻辑之外的逻辑,这又让我们想起了代理,aop等知识,我们相当于就是在为这些方法做一个增强。事实上的实现主借的逻辑也大概就是这个思路。梳理一下大致步骤如下:

  1. 利用反射机制获取一个类的Class对象
  2. 通过这个class对象可以去获取他的每一个方法method,或字段Field等等
  3. MethodField等类提供了类似于getAnnotation的方法来获取这个一个字段的所有注解
  4. 拿到注解之后,我们可以判断这个注解是否是我们要实现的注解,如果是则实现注解逻辑

现在我们来实现一下这个逻辑,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
private static void annotationLogic() {

Class useAnnotationClass = UseAnnotation.class;
for(Method method : useAnnotationClass.getMethods()) {
SimpleAnnotation simpleAnnotation = (SimpleAnnotation)method.getAnnotation(SimpleAnnotation.class);
if(simpleAnnotation != null) {
System.out.println(" Method Name : " + method.getName());
System.out.println(" value : " + simpleAnnotation.value());
System.out.println(" --------------------------- ");
}
}
}

在这里我们实现的逻辑就是打印几句话。从上面的实现逻辑我们不能发现,借助于java的反射我们可以直接拿到一个类里所有的方法,然后再拿到方法上的注解,当然,我们也可以拿到字段上的注解。借助于反射我们可以拿到几乎任何属于一个类的东西。

关于反射更多的知识请参见这篇博客:java中的反射和多态实现原理详解以及对比

一个简单的注解我们就实现完了。现在我们再回过头来,看一下@Autowired注解是如何实现的。

@Autowired注解实现逻辑分析

知道了上面的知识,我们不难想到,上面的注解虽然简单,但是@Autowired和他最大的区别应该仅仅在于注解的实现逻辑,其他利用反射获取注解等等步骤应该都是一致的。先来看一下@Autowired这个注解在spring的源代码里的定义是怎样的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.springframework.beans.factory.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}

阅读代码我们可以看到,Autowired注解可以应用在构造方法,普通方法,参数,字段,以及注解这五种类型的地方,它的保留策略是在运行时。下面,我们不多说直接来看spring对这个注解进行的逻辑实现.

Spring源代码当中,Autowired注解位于包org.springframework.beans.factory.annotation之中,该包的内容如下:

img

经过分析,不难发现Springautowire注解的实现逻辑位于类:AutowiredAnnotationBeanPostProcessor之中,已在上图标红。其中的核心处理代码如下:

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
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<>();
Class<?> targetClass = clazz;//需要处理的目标类

do {
final LinkedList<InjectionMetadata.InjectedElement> currElements = new LinkedList<>();
/*通过反射获取该类所有的字段,并遍历每一个字段,并通过方法findAutowiredAnnotation遍历每一个字段的所用注解,并如果用autowired修饰了,则返回auotowired相关属性*/
ReflectionUtils.doWithLocalFields(targetClass, field -> {
AnnotationAttributes ann = findAutowiredAnnotation(field);
if (ann != null) {//校验autowired注解是否用在了static方法上
if (Modifier.isStatic(field.getModifiers())) {
if (logger.isWarnEnabled()) {
logger.warn("Autowired annotation is not supported on static fields: " + field);
}
return;
}//判断是否指定了required
boolean required = determineRequiredStatus(ann);
currElements.add(new AutowiredFieldElement(field, required));
}
});
//和上面一样的逻辑,但是是通过反射处理类的method
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
if (logger.isWarnEnabled()) {
logger.warn("Autowired annotation is not supported on static methods: " + method);
}
return;
}
if (method.getParameterCount() == 0) {
if (logger.isWarnEnabled()) {
logger.warn("Autowired annotation should only be used on methods with parameters: " +
method);
}
}
boolean required = determineRequiredStatus(ann);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new AutowiredMethodElement(method, required, pd));
}
});
//用@Autowired修饰的注解可能不止一个,因此都加在currElements这个容器里面,一起处理
elements.addAll(0, currElements);
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);

return new InjectionMetadata(clazz, elements);
}

博主在源代码里加了注释,结合注释就能看懂它做的事情了,最后这个方法返回的就是包含所有带有autowire注解修饰的一个InjectionMetadata集合。这个类由两部分组成:

1
2
3
4
public InjectionMetadata(Class<?> targetClass, Collection<InjectedElement> elements) {
this.targetClass = targetClass;
this.injectedElements = elements;
}

一是我们处理的目标类,二就是上述方法获取到的所以elements集合。

有了目标类,与所有需要注入的元素集合之后,我们就可以实现autowired的依赖注入逻辑了,实现的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public PropertyValues postProcessPropertyValues(
PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {

InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
}
catch (BeanCreationException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
}
return pvs;
}

它调用的方法是InjectionMetadata中定义的inject方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Collection<InjectedElement> checkedElements = this.checkedElements;
Collection<InjectedElement> elementsToIterate =
(checkedElements != null ? checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
if (logger.isTraceEnabled()) {
logger.trace("Processing injected element of bean '" + beanName + "': " + element);
}
element.inject(target, beanName, pvs);
}
}
}

其逻辑就是遍历,然后调用inject方法,inject方法其实现逻辑如下:

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
/**
* Either this or {@link #getResourceToInject} needs to be overridden.
*/
protected void inject(Object target, @Nullable String requestingBeanName, @Nullable PropertyValues pvs)
throws Throwable {

if (this.isField) {
Field field = (Field) this.member;
ReflectionUtils.makeAccessible(field);
field.set(target, getResourceToInject(target, requestingBeanName));
}
else {
if (checkPropertySkipping(pvs)) {
return;
}
try {
Method method = (Method) this.member;
ReflectionUtils.makeAccessible(method);
method.invoke(target, getResourceToInject(target, requestingBeanName));
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}

在这里的代码当中我们也可以看到,是inject也使用了反射技术并且依然是分成字段和方法去处理的。在代码里面也调用了makeAccessible这样的可以称之为暴力破解的方法,但是反射技术本就是为框架等用途设计的,这也无可厚非。

对于字段的话,本质上就是去set这个字段的值,即对对象进行实例化和赋值,例如下面代码:

1
2
@Autowired
ObjectTest objectTest;

那么在这里实现的就相当于给这个objecTest引用赋值了。

对于方法的话,本质就是去调用这个方法,因此这里调用的是method.invokegetResourceToInject方法的参数就是要注入的bean的名字,这个方法的功能就是根据这个bean的名字去拿到它。

以上,就是@Autowire注解实现逻辑的全部分析。结合源代码再看一遍的话,会更加清楚一点。下面是spring容器如何实现@AutoWired自动注入的过程的图:

img

总结起来一句话:使用@Autowired注入的bean对于目标类来说,从代码结构上来讲也就是一个普通的成员变量,@Autowiredspring一起工作,通过反射为这个成员变量赋值,也就是将其赋为期望的类实例。

问题

注解的有效周期是什么?

各种注释之间的第一个主要区别是,它们是在编译时使用,然后被丢弃(如@Override),还是被放在编译的类文件中,并在运行时可用(如Spring@Component)。这是由注释的“@Retention”策略决定的。如果您正在编写自己的注释,则需要决定该注释在运行时(可能用于自动配置)还是仅在编译时(用于检查或代码生成)有用。

当用注释编译代码时,编译器看到注释就像看到源元素上的其他修饰符一样,比如访问修饰符(public/private)或.。当遇到注释时,它运行一个注释处理器,就像一个插件类,表示对特定的注释感兴趣。注释处理器通常使用反射API来检查正在编译的元素,并且可以简单地对它们执行检查、修改它们或生成要编译的新代码。@Override是一个示例;它使用反射API来确保能够在其中一个超类中找到方法签名的匹配,如果不能,则使用@Override会导致编译错误。

注入的bean和用它的bean的关系是如何维护的?

无论以何种方式注入,注入的bean就相当于类中的一个普通对象应用,这是它的实例化是spring去容器中找符合的bean进行实例化,并注入到类当中的。他们之间的关系就是普通的一个对象持有另一个对象引用的关系。只是这些对象都是spring当中的bean而已。

为什么注入的bean不能被定义为static的?

从设计的角度来说 ,使用静态字段会鼓励使用静态方法。 静态方法是evil的。 依赖注入的主要目的是让容器为您创建对象并进行连接。 而且,它使测试更加容易。

一旦开始使用静态方法,您就不再需要创建对象的实例,并且测试变得更加困难。 同样,您不能创建给定类的多个实例,每个实例都注入不同的依赖项(因为该字段是隐式共享的,并且会创建全局状态)。

静态变量不是Object的属性,而是Class的属性。 springautowire是在对象上完成的,这样使得设计很干净。 在spring当中我们也可以将bean对象定义为单例,这样就能从功能上实现与静态定义相同的目的。

但是从纯粹技术的层面,我们可以这样做:

@Autowired可以与setter方法一起使用,然后可以让setter修改静态字段的值。但是这种做法非常不推荐。

参考地址

方式一

如果用普通的key/value结构来存储,主要有以下2种存储方式:

第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,

如:

1
2
3
4
set u001 "李三,18,20010101" 

127.0.0.1:6379> get u001
"\xe6\x9d\x8e\xe4\xb8\x89,18,20010101"

这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。

方式二

第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,

如:

1
2
3
4
5
6
7
8
mset user:001:name "李三" user:001:age 18 user:001:birthday "20010101"

127.0.0.1:6379> get user:001:name
"\xe6\x9d\x8e\xe4\xb8\x89"
127.0.0.1:6379> get user:001:age
"18"
127.0.0.1:6379> get user:001:birthday
"20010101"

虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。

最好的办法

那么Redis提供的Hash很好的解决了这个问题,RedisHash实际是内部存储的Value为一个HashMap

并提供了直接存取这个Map成员的接口,

如:

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> hmset user:001 name "李三" age 18 birthday "20010101"
OK
127.0.0.1:6379> hgetall user:001
1) "name"
2) "\xe6\x9d\x8e\xe4\xb8\x89"
3) "age"
4) "18"
5) "birthday"
6) "20010101"
127.0.0.1:6379> hget user:001 age
"18"

也就是说,Key仍然是用户ID,value是一个Map,这个Mapkey是成员的属性名,value是属性值, 这样对数据的修改和存取都可以直接通过其内部MapKey(Redis里称内部Mapkeyfield), 也就是通过

key(用户ID) + field(属性标签) 操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。

参考地址

Eureka 工作原理

Eureka 作为 Spring Cloud 体系中最核心、默认的注册中心组件,研究它的运行机制,有助于我们在工作中更好地使用它。

Eureka 核心概念

回到上节的服务注册调用示意图,服务提供者和服务的消费者,本质上也是 Eureka Client 角色。整体上可以分为两个主体:Eureka ServerEureka Client
http://static.cyblogs.com/20190703102014756.png

Eureka Server:注册中心服务端

注册中心服务端主要对外提供了三个功能:

服务注册
服务提供者启动时,会通过 Eureka ClientEureka Server 注册信息,Eureka Server 会存储该服务的信息,Eureka Server 内部有二层缓存机制来维护整个注册表

提供注册表
服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表

同步状态
Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态。

Eureka Client:注册中心客户端
Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互。Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。

Register: 服务注册
服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka ClientEureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。

Renew: 服务续约
Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka ServerEureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。

服务续约的两个重要属性

1
2
3
4
5
服务续约任务的调用间隔时间,默认为30
eureka.instance.lease-renewal-interval-in-seconds=30

服务失效的时间,默认为90秒。
eureka.instance.lease-expiration-duration-in-seconds=90

Eviction 服务剔除
Eureka ClientEureka Server 不再有心跳时,Eureka Server 会将该服务实例从服务注册列表中删除,即服务剔除。

Cancel: 服务下线
Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:

1
DiscoveryManager.getInstance().shutdownComponent();

GetRegisty: 获取注册列表信息
Eureka Client 从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与 Eureka Client 的缓存信息不同,Eureka Client 自动处理。

如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 则会重新获取整个注册表信息。 Eureka Server 缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka ClientEureka Server 可以使用 JSON/XML 格式进行通讯。在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息。

获取服务是服务消费者的基础,所以必有两个重要参数需要注意:

1
2
3
4
5
# 启用服务消费者从注册中心拉取服务列表的功能
eureka.client.fetch-registry=true

# 设置服务消费者从注册中心拉取服务列表的间隔
eureka.client.registry-fetch-interval-seconds=30

Remote Call: 远程调用
Eureka Client 从注册中心获取到服务提供者信息后,就可以通过 Http 请求调用对应的服务;服务提供者有多个时,Eureka Client 客户端会通过 Ribbon 自动进行负载均衡。

自我保护机制

默认情况下,如果 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳,会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,网络分区故障,导致此实例被注销。

固定时间内大量实例被注销,可能会严重威胁整个微服务架构的可用性。为了解决这个问题,Eureka 开发了自我保护机制,那么什么是自我保护机制呢?

Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 即会进入自我保护机制。

Eureka Server 触发自我保护机制后,页面会出现提示:

在这里插入图片描述

Eureka Server 进入自我保护机制,会出现以下几种情况:

  • (1) Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务
  • (2)Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
  • (3)当网络稳定时,当前实例新的注册信息会被同步到其它节点中

Eureka 自我保护机制是为了防止误杀服务而提供的一个机制。当个别客户端出现心跳失联时,则认为是客户端的问题,剔除掉客户端;当 Eureka 捕获到大量的心跳失败时,则认为可能是网络问题,进入自我保护机制;当客户端心跳恢复时,Eureka 会自动退出自我保护机制。

如果在保护期内刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,即会调用失败。对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。

通过在 Eureka Server 配置如下参数,开启或者关闭保护机制,生产环境建议打开:

1
eureka.server.enable-self-preservation=true

Eureka 集群原理

再来看看 Eureka 集群的工作原理。我们假设有三台 Eureka Server 组成的集群,第一台 Eureka Server 在北京机房,另外两台 Eureka Server 在深圳和西安机房。这样三台 Eureka Server 就组建成了一个跨区域的高可用集群,只要三个地方的任意一个机房不出现问题,都不会影响整个架构的稳定性。

http://static.cyblogs.com/20190703103823398.png

从图中可以看出 Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。

如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。

另外 Eureka Server 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。

Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。

Eureka 分区
Eureka 提供了 RegionZone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS:

  • region:可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region。
  • zone:可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone。

上图中的 us-east-1cus-east-1dus-east-1e 就代表了不同的 ZoneZone 内的 Eureka Client 优先和 Zone 内的 Eureka Server 进行心跳同步,同样调用端优先在 Zone 内的 Eureka Server 获取服务列表,当 Zone 内的 Eureka Server 挂掉之后,才会从别的 Zone 中获取信息。

Eurka 保证 AP

Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。

Eurka 工作流程

了解完 Eureka 核心概念,自我保护机制,以及集群内的工作原理后,我们来整体梳理一下 Eureka 的工作流程:

1、Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息

2、Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务

3、Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常

4、当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例

5、单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端

6、当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式

7、Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地

8、服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存

9、Eureka Client 获取到目标服务器信息,发起服务调用

10、Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除

讲了 Eureka 核心概念、Eureka 自我保护机制和 Eureka 集群原理。通过分析 Eureka 工作原理,我可以明显地感觉到 Eureka 的设计之巧妙,通过一些列的机制,完美地解决了注册中心的稳定性和高可用性。

Eureka 为了保障注册中心的高可用性,容忍了数据的非强一致性,服务节点间的数据可能不一致, Client-Server 间的数据可能不一致。比较适合跨越多机房、对注册中心服务可用性要求较高的使用场景。

参考地址

深度阅读的能力

我敢保证,看完这个回答之前,就有很多人会划走。

这绝逼不是因为我这个回答写的太无趣,或者他们不同意我的某些观点,而是因为——他们看什么东西都是这样。

他们根本不能沉浸式的阅读什么东西,一般超过几百字的内容就很少看了,除非是小黄文和超能力爽文。他们做的最多的是浮皮潦草的刷刷抖音、看看段子,稍微有点复杂的电影都看不下去,更何况那些晦涩枯燥的工具书了。

我不能说知乎用户就比抖音用户牛逼,但是经过我这些年的学习和生活,我发现能够全神贯注的阅读长篇文字,本身就是很强的一种能力,它能够轻而易举的影响到我们的学习效果和工作效果,它关乎到我们的注意力、记忆力、逻辑思维能力和理解能力。

你信也好,你不信也罢,越看书就会越聪明,一直看爽文、肥皂剧、抖音就会变得不善思考。

自我认知能力

不要觉得这个能力很菜,真没几个人有。

不服你可以冷静下来想想,你真的了解你自己么?你真的知道你自己在这个社会中处于怎样的一个阶层和水平么?如果你觉得你都了解,那我再问你一句,你的这些认知,都来源于哪里?

其实你对自己的评价往往都来源于他人对你的反馈,所以人其实是很容易被环境和他人的评价所左右的,不要觉得你不可能进入传销,当你周围的环境和人全都变了,你得到的反馈就完全乱了,你自己就会失去对自己的认知。

而更值得注意的是,很多人并没有进入什么传销组织,但是却在他人的吹捧之中失去了对自己的判断,很多商家就是利用这一点,忽悠了不知多少人前赴后继的去送钱。

持久且专注的坚持做一件事的能力

其实这世界上绝大多数对未来很有意义的事情,短期内往往不会看到成效。比如健身、读书、早睡早起。你健身一天不会有八块腹肌,读一本书不能洞悉人性,早睡早起一天也不能容光焕发。

这些从长期看来对一个人有利的行为,不会像你刷抖音、看段子一样,短短一两分钟内就能够带给你刺激的感觉。

而且我相信所有的人都知道读书有好处,但问题就是读一本往往看不到效果,所以我一直觉得牛逼的人和普通人之间最大的差距就是——牛逼的人能在看不到回馈的情况下继续坚持下去,持续且专注的坚持下去。

对于99%的普通人来说,长期执着于一件事,持续深耕,五年十年后,你就会得到巨大的回报,并且成为这个领域的专家。

控制情绪的能力

每个人的生活中,都有一大半决策是在情绪的影响下做出的,而且往往这样做出的决策都不是最佳决策。只要你能在情绪失控的时候,提醒自己不要做出决策,让自己平复一下,就能解决很多麻烦事儿。

生而为人,必然有很多事情让你很愤怒、很悲伤、很焦躁,但是我想说的是,你要学会别跟自己过不去,让自己活的轻松点!

这本身就是很强悍的一种能力,会影响自己的生活,并影响身边人对你的看法。

掌控自己欲望的能力

很多人都以为自律是限制自己的欲望,据我所知,恰恰相反,自律应该是更好的分配自己的欲望。

你觉得健身的人是没有食欲的人么?错了,他们只是为了满足自己“拥有好身材”的长期欲望而放弃了满足眼前美食的欲望而已。

所以一个真正长期自律的人实际上就是对自己欲望进行更好的规划的分配的人。

用语言表达自己思想的能力

知乎有一个很重要的特点就是会把质量好的回答都排在前面,你看多了就会觉得所有的人都很牛逼,但是你可以做这么一个实验:

先给我这个回答点个赞(别走丢了),然后随便翻几个情感、生活类的大问题,几千个回答的那种,按时间排序,再找找那些0赞的回答看一下,长长一段文字,要么在写自己家里的琐事,要么是生活中遇到的不良情绪,要么试图用自己蹩脚的生活经验来总结人生的大道理,读完以后再回来看看我这个回答我自己都觉得我是个大文豪。

不光如此,就连一个简单的提问,很多人也能问的乱七八糟,完全没有逻辑顺序,描述抓不住重点,最后连自己想要问什么都说不明白。

连话都说不明白,你说还谈什么能力?

共情能力

为什么经常有人会被说情商低?据我所知不是因为他们脑子笨或者不会说话,本质上是因为他不能体会到对方的情绪和感觉,不能通过对方的描述、表情、动作而产生共情。

你说人家在那儿跟你诉着苦,说的就差哭出来了,你突然get到了笑点,对着别人哈哈大笑,你说这是不是缺?

你女朋友含情脉脉的看着你,说她冷,结果你说,“活该,谁让你穿那么点,瞅你冻得那逼出”,我就问你抗不抗揍吧。

不要脸的能力

别觉得这不算什么能力,我很严肃的告诉你,这能力特别牛逼,而且非常重要。

不要脸这个词,表面上是骂人,其实你仔细想想,这里面还包含了一个人对外界评判的不在意,对别人眼光的不care,甚至还能够体现出一个人的大局观。

你有多少次想跟领导去提要求最后却打了退堂鼓?你有多少次内心一万只草泥马奔腾而过但是嘴上却答应了别人的请求?你有多少次因为对方说咱们这么好的交情而抹不开面子?

多少因为面子天天加班的,因为面子【顺路】帮别人送东西的,因为面子给别人办事儿丢了饭碗的,还有因为面子借给别人钱要不回来的。

接纳不同观点的能力

你有没有想过,为什么现在满大街都是教别人说话的和提高情商的书籍?但是却没有多少书是教人说大实话的?

因为很多人根本就听不下那些难听的大实话!

哪怕你是真的为他好,他也不爱听。

比如一个女生要买一件衣服,你觉得很难看,她穿上以后像块被马蹄子踩了的奶油蛋糕,但是她自己还觉得自己美美哒。

如果她能心平气和的接纳你的不同观点,显然对她有好处,她就不至于买那么难看的衣服,不至于乱花钱。

但事实是大部分人总是先入为主,根本听不进去不同观点,别人的好言相劝大多都被当做情商低和柠檬精。

参考地址

http 是无状态的,一次请求结束,连接断开,下次服务器再收到请求,它就不知道这个请求是哪个用户发过来的。当然它知道是哪个客户端地址发过来的,但是对于我们的应用来说,我们是靠用户来管理,而不是靠客户端。所以对我们的应用而言,它是需要有状态管理的,以便服务端能够准确的知道 http 请求是哪个用户发起的,从而判断他是否有权限继续这个请求。这个过程就是常说的会话管理。它也可以简单理解为一个用户从登录到退出应用的一段期间。本文总结了 3 种常见的实现 web 应用会话管理的方式:

1)基于 serversession 的管理方式

2)cookie-base 的管理方式

3)token-base 的管理方式

这些内容可以帮助加深对 web 中用户登录机制的理解,对实际项目开发也有参考价值,欢迎阅读与指正。

1. 基于 server 端 session 的管理

在早期 web 应用中,通常使用服务端 session 来管理用户的会话。快速了解服务端 session:

  1. 服务端 session 是用户第一次访问应用时,服务器就会创建的对象,代表用户的一次会话过程,可以用来存放数据。服务器为每一个 session 都分配一个唯一的 sessionid,以保证每个用户都有一个不同的 session 对象。

2)服务器在创建完 session 后,会把 sessionid 通过 cookie 返回给用户所在的浏览器,这样当用户第二次及以后向服务器发送请求的时候,就会通过 cookiesessionid 传回给服务器,以便服务器能够根据 sessionid 找到与该用户对应的 session 对象。

3)session 通常有失效时间的设定,比如 2 个小时。当失效时间到,服务器会销毁之前的 session,并创建新的 session 返回给用户。但是只要用户在失效时间内,有发送新的请求给服务器,通常服务器都会把他对应的 session 的失效时间根据当前的请求时间再延长 2 个小时。

4)session 在一开始并不具备会话管理的作用。它只有在用户登录认证成功之后,并且往 sesssion 对象里面放入了用户登录成功的凭证,才能用来管理会话。管理会话的逻辑也很简单,只要拿到用户的 session 对象,看它里面有没有登录成功的凭证,就能判断这个用户是否已经登录。当用户主动退出的时候,会把它的 session 对象里的登录凭证清掉。所以在用户登录前或退出后或者 session 对象失效时,肯定都是拿不到需要的登录凭证的。

以上过程可简单使用流程图描述如下:

http://static.cyblogs.com/459873-20161115231400951-1095594983.png

主流的 web 开发平台(java,.net,php)都原生支持这种会话管理的方式,而且开发起来很简单,相信大部分后端开发人员在入门的时候都了解并使用过它。它还有一个比较大的优点就是安全性好,因为在浏览器端与服务器端保持会话状态的媒介始终只是一个 sessionid 串,只要这个串够随机,攻击者就不能轻易冒充他人的 sessionid 进行操作;除非通过 CSRFhttp 劫持的方式,才有可能冒充别人进行操作;即使冒充成功,也必须被冒充的用户 session 里面包含有效的登录凭证才行。但是在真正决定用它管理会话之前,也得根据自己的应用情况考虑以下几个问题:

1)这种方式将会话信息存储在 web 服务器里面,所以在用户同时在线量比较多时,这些会话信息会占据比较多的内存;

2)当应用采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题。因为 session 是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建 session 的服务器,这样他就拿不到之前已经放入到 session 中的登录凭证之类的信息了;

3)多个应用要共享 session 时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好 cookie 跨域的处理。

针对问题 1 和问题 2,我见过的解决方案是采用 redis 这种中间服务器来管理 session 的增删改查,一来减轻 web 服务器的负担,二来解决不同 web 服务器共享 session 的问题。针对问题 3,由于服务端的 session 依赖 cookie 来传递 sessionid,所以在实际项目中,只要解决各个项目里面如何实现 sessionidcookie 跨域访问即可,这个是可以实现的,就是比较麻烦,前后端有可能都要做处理。

如果不考虑以上三个问题,这种管理方式比较值得使用,尤其是一些小型的 web 应用。但是一旦应用将来有扩展的必要,那就得谨慎对待前面的三个问题。如果真要在项目中使用这种方式,推荐结合单点登录框架如 CAS 一起用,这样会使应用的扩展性更强。

由于前一种方式会增加服务器的负担和架构的复杂性,所以后来就有人想出直接把用户的登录凭证直接存到客户端的方案,当用户登录成功之后,把登录凭证写到 cookie 里面,并给 cookie 设置有效期,后续请求直接验证存有登录凭证的 cookie 是否存在以及凭证是否有效,即可判断用户的登录状态。使用它来实现会话管理的整体流程如下:

1)用户发起登录请求,服务端根据传入的用户密码之类的身份信息,验证用户是否满足登录条件,如果满足,就根据用户信息创建一个登录凭证,这个登录凭证简单来说就是一个对象,最简单的形式可以只包含用户 id,凭证创建时间和过期时间三个值。

2)服务端把上一步创建好的登录凭证,先对它做数字签名,然后再用对称加密算法做加密处理,将签名、加密后的字串,写入 cookiecookie 的名字必须固定(如 ticket),因为后面再获取的时候,还得根据这个名字来获取 cookie 值。这一步添加数字签名的目的是防止登录凭证里的信息被篡改,因为一旦信息被篡改,那么下一步做签名验证的时候肯定会失败。做加密的目的,是防止 cookie 被别人截取的时候,无法轻易读到其中的用户信息。

3)用户登录后发起后续请求,服务端根据上一步存登录凭证的 cookie 名字,获取到相关的 cookie 值。然后先做解密处理,再做数字签名的认证,如果这两步都失败,说明这个登录凭证非法;如果这两步成功,接着就可以拿到原始存入的登录凭证了。然后用这个凭证的过期时间和当前时间做对比,判断凭证是否过期,如果过期,就需要用户再重新登录;如果未过期,则允许请求继续。

http://static.cyblogs.com/459873-20161120210043123-760641758.png

这种方式最大的优点就是实现了服务端的无状态化,彻底移除了服务端对会话的管理的逻辑,服务端只需要负责创建和验证登录 cookie 即可,无需保持用户的状态信息。对于第一种方式的第二个问题,用户会话信息共享的问题,它也能很好解决:因为如果只是同一个应用做集群部署,由于验证登录凭证的代码都是一样的,所以不管是哪个服务器处理用户请求,总能拿到 cookie 中的登录凭证来进行验证;如果是不同的应用,只要每个应用都包含相同的登录逻辑,那么他们也是能轻易实现会话共享的,不过这种情况下,登录逻辑里面数字签名以及加密解密要用到的密钥文件或者密钥串,需要在不同的应用里面共享,总而言之,就是需要算法完全保持一致。

这种方式由于把登录凭证直接存放客户端,并且需要 cookie 传来传去,所以它的缺点也比较明显:

1)cookie 有大小限制,存储不了太多数据,所以要是登录凭证存的消息过多,导致加密签名后的串太长,就会引发别的问题,比如其它业务场景需要 cookie 的时候,就有可能没那么多空间可用了;所以用的时候得谨慎,得观察实际的登录 cookie 的大小;比如太长,就要考虑是非是数字签名的算法太严格,导致签名后的串太长,那就适当调整签名逻辑;比如如果一开始用 4096 位的 RSA 算法做数字签名,可以考虑换成 10242048 位;

2)每次传送 cookie,增加了请求的数量,对访问性能也有影响;

3)也有跨域问题,毕竟还是要用 cookie

相比起第一种方式,cookie-based 方案明显还是要好一些,目前好多 web 开发平台或框架都默认使用这种方式来做会话管理,比如 php 里面 yii 框架,这是我们团队后端目前用的,它用的就是这个方案,以上提到的那些登录逻辑,框架也都已经封装好了,实际用起来也很简单;asp.net 里面 forms 身份认证,也是这个思路,这里有一篇好文章把它的实现细节都说的很清楚:

http://www.cnblogs.com/fish-li/archive/2012/04/15/2450571.html

前面两种会话管理方式因为都用到 cookie,不适合用在 native app 里面:native app 不好管理 cookie,毕竟它不是浏览器。这两种方案都不适合用来做纯 api 服务的登录认证。要实现 api 服务的登录认证,就要考虑下面要介绍的第三种会话管理方式。

3. token-based 的管理方式

这种方式从流程和实现上来说,跟 cookie-based 的方式没有太多区别,只不过 cookie-based 里面写到 cookie 里面的 ticket 在这种方式下称为 token,这个 token 在返回给客户端之后,后续请求都必须通过 url 参数或者是 http header 的形式,主动带上 token,这样服务端接收到请求之后就能直接从 http header 或者 url 里面取到 token 进行验证:

http://static.cyblogs.com/459873-20161120210044154-648255641.png

这种方式不通过 cookie 进行 token 的传递,而是每次请求的时候,主动把 token 加到 http header 里面或者 url 后面,所以即使在 native app 里面也能使用它来调用我们通过 web 发布的 api 接口。app 里面还要做两件事情:

1)有效存储 token,得保证每次调接口的时候都能从同一个位置拿到同一个 token

2)每次调接口的的代码里都得把 token 加到 header 或者接口地址里面。

看起来麻烦,其实也不麻烦,这两件事情,对于 app 来说,很容易做到,只要对接口调用的模块稍加封装即可。

这种方式同样适用于网页应用,token 可以存于 localStorage 或者 sessionStorage 里面,然后每发 ajax 请求的时候,都把 token 拿出来放到 ajax 请求的 header 里即可。不过如果是非接口的请求,比如直接通过点击链接请求一个页面这种,是无法自动带上 token 的。所以这种方式也仅限于走纯接口的 web 应用。

这种方式用在 web 应用里也有跨域的问题,比如应用如果部署在 a.comapi 服务部署在 b.com,从 a.com 里面发出 ajax 请求到 b.com,默认情况下是会报跨域错误的,这种问题可以用 CORS跨域资源共享)的方式来快速解决,相关细节可去阅读前面给出的 CORS 文章详细了解。

这种方式跟 cookie-based 的方式同样都还有的一个问题就是 ticket 或者 token 刷新的问题。有的产品里面,你肯定不希望用户登录后,操作了半个小时,结果 ticket 或者 token 到了过期时间,然后用户又得去重新登录的情况出现。这个时候就得考虑 tickettoken 的自动刷新的问题,简单来说,可以在验证 tickettoken 有效之后,自动把 tickettoken 的失效时间延长,然后把它再返回给客户端;客户端如果检测到服务器有返回新的 tickettoken,就替换原来的 tickettoken

4. 安全问题

web 应用里面,会话管理的安全性始终是最重要的安全问题,这个对用户的影响极大。

首先从会话管理凭证来说,第一种方式的会话凭证仅仅是一个 session id,所以只要这个 session id 足够随机,而不是一个自增的数字 id 值,那么其它人就不可能轻易地冒充别人的 session id 进行操作;第二种方式的凭证(ticket)以及第三种方式的凭证(token)都是一个在服务端做了数字签名,和加密处理的串,所以只要密钥不泄露,别人也无法轻易地拿到这个串中的有效信息并对它进行篡改。总之,这三种会话管理方式的凭证本身是比较安全的。

然后从客户端和服务端的 http 过程来说,当别人截获到客户端请求中的会话凭证,就能拿这个凭证冒充原用户,做一些非法操作,而服务器也认不出来。这种安全问题,可以简单采用 https 来解决,虽然可能还有 http 劫持这种更高程度的威胁存在,但是我们从代码能做的防范,确实也就是这个层次了。

最后的安全问题就是 CSRF(跨站请求伪造)。这个跟代码有很大关系,本质上它就是代码的漏洞,只不过一般情况下这些漏洞,作为开发人员都不容易发现,只有那些一门心思想搞些事情的人才会专门去找这些漏洞,所以这种问题的防范更多地还是依赖于开发人员对这种攻击方式的了解,包括常见的攻击形式和应对方法。不管凭证信息本身多么安全,别人利用 CSRF,就能拿到别人的凭证,然后用它冒充别人进行非法操作,所以有时间还真得多去了解下它的相关资料才行。举例来说,假如我们把凭证直接放到 url 后面进行传递,就有可能成为一个 CSRF 的漏洞:当恶意用户在我们的应用内上传了 1 张引用了他自己网站的图片,当正常的用户登录之后访问的页面里面包含这个图片的时候,由于这个图片加载的时候会向恶意网站发送 get 请求;当恶意网站收到请求的时候,就会从这个请求的 Reffer header 里面看到包含这个图片的页面地址,而这个地址正好包含了正常用户的会话凭证;于是恶意用户就拿到了正常用户的凭证;只要这个凭证还没失效,他就能用它冒充用户进行非法操作。

5. 总结

前面这三种方式,各自有各自的优点及使用场景,我觉得没有哪个是最好的,做项目的时候,根据项目将来的扩展情况和架构情况,才能决定用哪个是最合适的。本文的目的也就是想介绍这几种方式的原理,以便掌握 web 应用中登录验证的关键因素。

作为一个前端开发人员,本文虽然介绍了 3 种会话管理的方式,但是与前端关系最紧密的还是第三种方式,毕竟现在前端开发 SPA 应用以及 hybrid 应用已经非常流行了,所以掌握好这个方式的认证过程和使用方式,对前端来说,显然是很有帮助的。好在这个方式的技术其实早就有很多实现了,而且还有现成的标准可用,这个标准就是 JWT(json-web-token)

JWT 本身并没有做任何技术实现,它只是定义了 token-based 的管理方式该如何实现,它规定了 token 的应该包含的标准内容以及 token 的生成过程和方法。目前实现了这个标准的技术已经有非常多:

http://static.cyblogs.com/459873-20161120210045904-1163191341.png

参考地址

前言

循环依赖:就是N个类循环(嵌套)引用。 通俗的讲就是N个Bean互相引用对方,最终形成闭环。用一副经典的图示可以表示成这样(A、B、C都代表对象,虚线代表引用关系):

http://static.cyblogs.com/mxuio3xfqc.png

注意:其实可以N=1,也就是极限情况的循环依赖:自己依赖自己

另需注意:这里指的循环引用不是方法之间的循环调用,而是对象的相互依赖关系。(方法之间循环调用若有出口也是能够正常work的)

可以设想一下这个场景:如果在日常开发中我们用new对象的方式,若构造函数之间发生这种循环依赖的话,程序会在运行时一直循环调用最终导致内存溢出,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Main {

public static void main(String[] args) throws Exception {
System.out.println(new A());
}

}

class A {
public A() {
new B();
}
}

class B {
public B() {
new A();
}
}

运行报错:

1
Exception in thread "main" java.lang.StackOverflowError

这是一个典型的循环依赖问题。本文说一下Spring是如果巧妙的解决平时我们会遇到的三大循环依赖问题的~

Spring Bean的循环依赖

谈到Spring Bean的循环依赖,有的小伙伴可能比较陌生,毕竟开发过程中好像对循环依赖这个概念无感知。其实不然,你有这种错觉,权是因为你工作在Spring的襁褓中,从而让你“高枕无忧”~ 我十分坚信,小伙伴们在平时业务开发中一定一定写过如下结构的代码:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class AServiceImpl implements AService {
@Autowired
private BService bService;
...
}
@Service
public class BServiceImpl implements BService {
@Autowired
private AService aService;
...
}

这其实就是Spring环境下典型的循环依赖场景。但是很显然,这种循环依赖场景,Spring已经完美的帮我们解决和规避了问题。所以即使平时我们这样循环引用,也能够整成进行我们的coding之旅~

Spring中三大循环依赖场景演示

在Spring环境中,因为我们的Bean的实例化、初始化都是交给了容器,因此它的循环依赖主要表现为下面三种场景。为了方便演示,我准备了如下两个类:

1、构造器注入循环依赖
1
2
3
4
5
6
7
8
9
10
@Service
public class A {
public A(B b) {
}
}
@Service
public class B {
public B(A a) {
}
}

结果:项目启动失败抛出异常BeanCurrentlyInCreationException

1
2
3
4
5
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)

构造器注入构成的循环依赖,此种循环依赖方式是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。这也是构造器注入的最大劣势(它有很多独特的优势,请小伙伴自行发掘)

根本原因:Spring解决循环依赖依靠的是Bean的“中间态”这个概念,而这个中间态指的是已经实例化,但还没初始化的状态。而构造器是完成实例化的东东,所以构造器的循环依赖无法解决~~~

2、field属性注入(setter方法注入)循环依赖

这种方式是我们最最最最为常用的依赖注入方式(所以猜都能猜到它肯定不会有问题啦):

1
2
3
4
5
6
7
8
9
10
11
@Service
public class A {
@Autowired
private B b;
}

@Service
public class B {
@Autowired
private A a;
}

结果:项目启动成功,能够正常work

备注:setter方法注入方式因为原理和字段注入方式类似,此处不多加演示

3、prototype field属性注入循环依赖

prototype在平时使用情况较少,但是也并不是不会使用到,因此此种方式也需要引起重视。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class A {
@Autowired
private B b;
}

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class B {
@Autowired
private A a;
}

结果:需要注意的是本例中启动时是不会报错的(因为非单例Bean默认不会初始化,而是使用时才会初始化),所以很简单咱们只需要手动getBean()或者在一个单例Bean内@Autowired一下它即可

1
2
3
// 在单例Bean内注入
@Autowired
private A a;

这样子启动就报错:

1
2
3
4
5
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'mytest.TestSpringBean': Unsatisfied dependency expressed through field 'a'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'a': Unsatisfied dependency expressed through field 'b'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'b': Unsatisfied dependency expressed through field 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:596)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:374)

如何解决??? 可能有的小伙伴看到网上有说使用@Lazy注解解决:

1
2
3
@Lazy
@Autowired
private A a;

此处负责任的告诉你这样是解决不了问题的(可能会掩盖问题),@Lazy只是延迟初始化而已,当你真正使用到它(初始化)的时候,依旧会报如上异常。

对于Spring循环依赖的情况总结如下:

  1. 不能解决的情况: 1. 构造器注入循环依赖 2. prototype field属性注入循环依赖
  2. 能解决的情况: 1. field属性注入(setter方法注入)循环依赖

Spring解决循环依赖的原理分析

在这之前需要明白java中所谓的引用传递值传递的区别。

说明:看到这句话可能有小伙伴就想喷我了。java中明明都是传递啊,这是我初学java时背了100遍的面试题,怎么可能有错??? 这就是我做这个申明的必要性:伙计,你的说法是正确的,java中只有值传递。但是本文借用引用传递来辅助讲解,希望小伙伴明白我想表达的意思~

Spring的循环依赖的理论依据基于Java的引用传递,当获得对象的引用时,对象的属性是可以延后设置的。(但是构造器必须是在获取引用之前,毕竟你的引用是靠构造器给你生成的,儿子能先于爹出生?哈哈)

Spring创建Bean的流程

首先需要了解是Spring它创建Bean的流程,我把它的大致调用栈绘图如下:

http://static.cyblogs.com/oepgq3cnb0.png

对Bean的创建最为核心三个方法解释如下:

  • createBeanInstance:例化,其实也就是调用对象的构造方法实例化对象
  • populateBean:填充属性,这一步主要是对bean的依赖属性进行注入(@Autowired)
  • initializeBean:回到一些形如initMethodInitializingBean等方法

从对单例Bean的初始化可以看出,循环依赖主要发生在第二步(populateBean),也就是field属性注入的处理。

Spring容器的'三级缓存'

在Spring容器的整个声明周期中,单例Bean有且仅有一个对象。这很容易让人想到可以用缓存来加速访问。 从源码中也可以看出Spring大量运用了Cache的手段,在循环依赖问题的解决过程中甚至不惜使用了“三级缓存”,这也便是它设计的精妙之处~

三级缓存其实它更像是Spring容器工厂的内的术语,采用三级缓存模式来解决循环依赖问题,这三级缓存分别指:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
...
// 从上至下 分表代表这“三级缓存”
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); //一级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存
...

/** Names of beans that are currently in creation. */
// 这个缓存也十分重要:它表示bean创建过程中都会在里面呆着~
// 它在Bean开始创建时放值,创建完成时会将其移出~
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

/** Names of beans that have already been created at least once. */
// 当这个Bean被创建完成后,会标记为这个 注意:这里是set集合 不会重复
// 至少被创建了一次的 都会放进这里~~~~
private final Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256));
}

注:AbstractBeanFactory继承自DefaultSingletonBeanRegistry~

  1. singletonObjects:用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用
  2. earlySingletonObjects:提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖
  3. singletonFactories:单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖

获取单例Bean的源码如下:

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
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
...
@Override
@Nullable
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
...
public boolean isSingletonCurrentlyInCreation(String beanName) {
return this.singletonsCurrentlyInCreation.contains(beanName);
}
protected boolean isActuallyInCreation(String beanName) {
return isSingletonCurrentlyInCreation(beanName);
}
...
}
  1. 先从一级缓存singletonObjects中去获取。(如果获取到就直接return)
  2. 如果获取不到或者对象正在创建中(isSingletonCurrentlyInCreation()),那就再从二级缓存earlySingletonObjects中获取。(如果获取到就直接return)
  3. 如果还是获取不到,且允许singletonFactories(allowEarlyReference=true)通过getObject()获取。就从三级缓存singletonFactory.getObject()获取。(如果获取到了就从**singletonFactories**中移除,并且放进**earlySingletonObjects**。其实也就是从三级缓存**移动(是剪切、不是复制哦~)**到了二级缓存)

加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决

getSingleton()从缓存里获取单例对象步骤分析可知,Spring解决循环依赖的诀窍:就在于singletonFactories这个三级缓存。这个Cache里面都是ObjectFactory,它是解决问题的关键。

1
2
3
4
5
// 它可以将创建对象的步骤封装到ObjectFactory中 交给自定义的Scope来选择是否需要创建对象来灵活的实现scope。  具体参见Scope接口
@FunctionalInterface
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}

经过ObjectFactory.getObject()后,此时放进了二级缓存earlySingletonObjects内。这个时候对象已经实例化了,虽然还不完美,但是对象的引用已经可以被其它引用了。

此处说一下二级缓存earlySingletonObjects它里面的数据什么时候添加什么移除???

添加:向里面添加数据只有一个地方,就是上面说的getSingleton()里从三级缓存里挪过来 移除addSingleton、addSingletonFactory、removeSingleton从语义中可以看出添加单例、添加单例工厂ObjectFactory的时候都会删除二级缓存里面对应的缓存值,是互斥的

源码解析

Spring容器会将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,而对于创建完毕的Bean将从当前创建Bean池中清除掉。 这个“当前创建Bean池”指的是上面提到的singletonsCurrentlyInCreation那个集合。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
...
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
...
// Eagerly check singleton cache for manually registered singletons.
// 先去获取一次,如果不为null,此处就会走缓存了~~
Object sharedInstance = getSingleton(beanName);
...
// 如果不是只检查类型,那就标记这个Bean被创建了~~添加到缓存里 也就是所谓的 当前创建Bean池
if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}
...
// Create bean instance.
if (mbd.isSingleton()) {

// 这个getSingleton方法不是SingletonBeanRegistry的接口方法 属于实现类DefaultSingletonBeanRegistry的一个public重载方法~~~
// 它的特点是在执行singletonFactory.getObject();前后会执行beforeSingletonCreation(beanName);和afterSingletonCreation(beanName);
// 也就是保证这个Bean在创建过程中,放入正在创建的缓存池里 可以看到它实际创建bean调用的是我们的createBean方法~~~~
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
}
...
}

// 抽象方法createBean所在地 这个接口方法是属于抽象父类AbstractBeanFactory的 实现在这个抽象类里
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
...
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
...
// 创建Bean对象,并且将对象包裹在BeanWrapper 中
instanceWrapper = createBeanInstance(beanName, mbd, args);
// 再从Wrapper中把Bean原始对象(非代理~~~) 这个时候这个Bean就有地址值了,就能被引用了~~~
// 注意:此处是原始对象,这点非常的重要
final Object bean = instanceWrapper.getWrappedInstance();
...
// earlySingletonExposure 用于表示是否”提前暴露“原始对象的引用,用于解决循环依赖。
// 对于单例Bean,该变量一般为 true 但你也可以通过属性allowCircularReferences = false来关闭循环引用
// isSingletonCurrentlyInCreation(beanName) 表示当前bean必须在创建中才行
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName + "' to allow for resolving potential circular references");
}
// 上面讲过调用此方法放进一个ObjectFactory,二级缓存会对应删除的
// getEarlyBeanReference的作用:调用SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference()这个方法 否则啥都不做
// 也就是给调用者个机会,自己去实现暴露这个bean的应用的逻辑~~~
// 比如在getEarlyBeanReference()里可以实现AOP的逻辑~~~ 参考自动代理创建器AbstractAutoProxyCreator 实现了这个方法来创建代理对象
// 若不需要执行AOP的逻辑,直接返回Bean
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
Object exposedObject = bean; //exposedObject 是最终返回的对象
...
// 填充属于,解决@Autowired依赖~
populateBean(beanName, mbd, instanceWrapper);
// 执行初始化回调方法们~~~
exposedObject = initializeBean(beanName, exposedObject, mbd);

// earlySingletonExposure:如果你的bean允许被早期暴露出去 也就是说可以被循环引用 那这里就会进行检查
// 此段代码非常重要~~~~~但大多数人都忽略了它
if (earlySingletonExposure) {
// 此时一级缓存肯定还没数据,但是呢此时候二级缓存earlySingletonObjects也没数据
//注意,注意:第二参数为false 表示不会再去三级缓存里查了~~~

// 此处非常巧妙的一点:::因为上面各式各样的实例化、初始化的后置处理器都执行了,如果你在上面执行了这一句
// ((ConfigurableListableBeanFactory)this.beanFactory).registerSingleton(beanName, bean);
// 那么此处得到的earlySingletonReference 的引用最终会是你手动放进去的Bean最终返回,完美的实现了"偷天换日" 特别适合中间件的设计
// 我们知道,执行完此doCreateBean后执行addSingleton() 其实就是把自己再添加一次 **再一次强调,完美实现偷天换日**
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {

// 这个意思是如果经过了initializeBean()后,exposedObject还是木有变,那就可以大胆放心的返回了
// initializeBean会调用后置处理器,这个时候可以生成一个代理对象,那这个时候它哥俩就不会相等了 走else去判断吧
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}

// allowRawInjectionDespiteWrapping这个值默认是false
// hasDependentBean:若它有依赖的bean 那就需要继续校验了~~~(若没有依赖的 就放过它~)
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
// 拿到它所依赖的Bean们~~~~ 下面会遍历一个一个的去看~~
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);

// 一个个检查它所以Bean
// removeSingletonIfCreatedForTypeCheckOnly这个放见下面 在AbstractBeanFactory里面
// 简单的说,它如果判断到该dependentBean并没有在创建中的了的情况下,那就把它从所有缓存中移除~~~ 并且返回true
// 否则(比如确实在创建中) 那就返回false 进入我们的if里面~ 表示所谓的真正依赖
//(解释:就是真的需要依赖它先实例化,才能实例化自己的依赖)
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}

// 若存在真正依赖,那就报错(不要等到内存移除你才报错,那是非常不友好的)
// 这个异常是BeanCurrentlyInCreationException,报错日志也稍微留意一下,方便定位错误~~~~
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}

return exposedObject;
}

// 虽然是remove方法 但是它的返回值也非常重要
// 该方法唯一调用的地方就是循环依赖的最后检查处~~~~~
protected boolean removeSingletonIfCreatedForTypeCheckOnly(String beanName) {
// 如果这个bean不在创建中 比如是ForTypeCheckOnly的 那就移除掉
if (!this.alreadyCreated.contains(beanName)) {
removeSingleton(beanName);
return true;
}
else {
return false;
}
}

}

这里举例:例如是field属性依赖注入,在populateBean时它就会先去完成它所依赖注入的那个bean的实例化、初始化过程,最终返回到本流程继续处理,因此Spring这样处理是不存在任何问题的。

这里有个小细节:

1
2
3
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}

这一句如果exposedObject == bean表示最终返回的对象就是原始对象,说明在populateBeaninitializeBean没对他代理过,那就啥话都不说了exposedObject = earlySingletonReference,最终把二级缓存里的引用返回即可~

流程总结(非常重要

此处以如上的A、B类的互相依赖注入为例,在这里表达出关键代码的走势:

1、入口处即是实例化、初始化A这个单例BeanAbstractBeanFactory.doGetBean("a")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected <T> T doGetBean(...){
...
// 标记beanName a是已经创建过至少一次的~~~ 它会一直存留在缓存里不会被移除(除非抛出了异常)
// 参见缓存Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256))
if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}

// 此时a不存在任何一级缓存中,且不是在创建中 所以此处返回null
// 此处若不为null,然后从缓存里拿就可以了(主要处理FactoryBean和BeanFactory情况吧)
Object beanInstance = getSingleton(beanName, false);
...
// 这个getSingleton方法非常关键。
//1、标注a正在创建中~
//2、调用singletonObject = singletonFactory.getObject();(实际上调用的是createBean()方法) 因此这一步最为关键
//3、此时实例已经创建完成 会把a移除整整创建的缓存中
//4、执行addSingleton()添加进去。(备注:注册bean的接口方法为registerSingleton,它依赖于addSingleton方法)
sharedInstance = getSingleton(beanName, () -> { ... return createBean(beanName, mbd, args); });
}

2、下面进入到最为复杂的AbstractAutowireCapableBeanFactory.createBean/doCreateBean()环节,创建A的实例

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
protected Object doCreateBean(){
...
// 使用构造器/工厂方法 instanceWrapper是一个BeanWrapper
instanceWrapper = createBeanInstance(beanName, mbd, args);
// 此处bean为"原始Bean" 也就是这里的A实例对象:A@1234
final Object bean = instanceWrapper.getWrappedInstance();
...
// 是否要提前暴露(允许循环依赖) 现在此处A是被允许的
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));

// 允许暴露,就把A绑定在ObjectFactory上,注册到三级缓存`singletonFactories`里面去保存着
// Tips:这里后置处理器的getEarlyBeanReference方法会被促发,自动代理创建器在此处创建代理对象(注意执行时机 为执行三级缓存的时候)
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
...
// exposedObject 为最终返回的对象,此处为原始对象bean也就是A@1234,下面会有用处
Object exposedObject = bean;
// 给A@1234属性完成赋值,@Autowired在此处起作用~
// 因此此处会调用getBean("b"),so 会重复上面步骤创建B类的实例
// 此处我们假设B已经创建好了 为B@5678

// 需要注意的是在populateBean("b")的时候依赖有beanA,所以此时候调用getBean("a")最终会调用getSingleton("a"),
//此时候上面说到的getEarlyBeanReference方法就会被执行。这也解释为何我们@Autowired是个代理对象,而不是普通对象的根本原因

populateBean(beanName, mbd, instanceWrapper);
// 实例化。这里会执行后置处理器BeanPostProcessor的两个方法
// 此处注意:postProcessAfterInitialization()是有可能返回一个代理对象的,这样exposedObject 就不再是原始对象了 特备注意哦~~~
// 比如处理@Aysnc的AsyncAnnotationBeanPostProcessor它就是在这个时间里生成代理对象的(有坑,请小心使用@Aysnc)
exposedObject = initializeBean(beanName, exposedObject, mbd);

... // 至此,相当于A@1234已经实例化完成、初始化完成(属性也全部赋值了~)
// 这一步我把它理解为校验:校验:校验是否有循环引用问题~~~~~

if (earlySingletonExposure) {
// 注意此处第二个参数传的false,表示不去三级缓存里singletonFactories再去调用一次getObject()方法了~~~
// 上面建讲到了由于B在初始化的时候,会触发A的ObjectFactory.getObject() 所以a此处已经在二级缓存earlySingletonObjects里了
// 因此此处返回A的实例:A@1234
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {

// 这个等式表示,exposedObject若没有再被代理过,这里就是相等的
// 显然此处我们的a对象的exposedObject它是没有被代理过的 所以if会进去~
// 这种情况至此,就全部结束了~~~
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}

// 继续以A为例,比如方法标注了@Aysnc注解,exposedObject此时候就是一个代理对象,因此就会进到这里来
//hasDependentBean(beanName)是肯定为true,因为getDependentBeans(beanName)得到的是["b"]这个依赖
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);

// A@1234依赖的是["b"],所以此处去检查b
// 如果最终存在实际依赖的bean:actualDependentBeans不为空 那就抛出异常 证明循环引用了~
for (String dependentBean : dependentBeans) {
// 这个判断原则是:如果此时候b并还没有创建好,this.alreadyCreated.contains(beanName)=true表示此bean已经被创建过,就返回false
// 若该bean没有在alreadyCreated缓存里,就是说没被创建过(其实只有CreatedForTypeCheckOnly才会是此仓库)
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}
}

由于关键代码部分的步骤不太好拆分,为了更具象表达,那么使用下面一副图示帮助小伙伴们理解:

http://static.cyblogs.com/qv40e3auxf.png

最后的最后,由于我太暖心了_,再来个纯文字版的总结。 依旧以上面AB类使用属性field注入循环依赖的例子为例,对整个流程做文字步骤总结如下:

  1. 使用context.getBean(A.class),旨在获取容器内的单例A(若A不存在,就会走A这个Bean的创建流程),显然初次获取A是不存在的,因此走A的创建之路~
  2. 实例化A(注意此处仅仅是实例化),并将它放进缓存(此时A已经实例化完成,已经可以被引用了)
  3. 初始化A:@Autowired依赖注入B(此时需要去容器内获取B)
  4. 为了完成依赖注入B,会通过getBean(B)去容器内找B。但此时B在容器内不存在,就走向B的创建之路~
  5. 实例化B,并将其放入缓存。(此时B也能够被引用了)
  6. 初始化B,@Autowired依赖注入A(此时需要去容器内获取A)
  7. 此处重要:初始化B时会调用getBean(A)去容器内找到A,上面我们已经说过了此时候因为A已经实例化完成了并且放进了缓存里,所以这个时候去看缓存里是已经存在A的引用了的,所以getBean(A)能够正常返回
  8. B初始化成功(此时已经注入A成功了,已成功持有A的引用了),return(注意此处return相当于是返回最上面的getBean(B)这句代码,回到了初始化A的流程中~)。
  9. 因为B实例已经成功返回了,因此最终A也初始化成功
  10. 到此,B持有的已经是初始化完成的A,A持有的也是初始化完成的B,完美~

站的角度高一点,宏观上看Spring处理循环依赖的整个流程就是如此。希望这个宏观层面的总结能更加有助于小伙伴们对Spring解决循环依赖的原理的了解,同时也顺便能解释为何构造器循环依赖就不好使的原因。

循环依赖对AOP代理对象创建流程和结果的影响

我们都知道Spring AOP、事务等都是通过代理对象来实现的,而事务的代理对象是由自动代理创建器来自动完成的。也就是说Spring最终给我们放进容器里面的是一个代理对象,而非原始对象

本文结合循环依赖,回头再看AOP代理对象的创建过程,和最终放进容器内的动作,非常有意思。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class HelloServiceImpl implements HelloService {
@Autowired
private HelloService helloService;

@Transactional
@Override
public Object hello(Integer id) {
return "service hello";
}
}

Service类使用到了事务,所以最终会生成一个JDK动态代理对象Proxy。刚好它又存在自己引用自己的循环依赖。看看这个Bean的创建概要描述如下:

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
protected Object doCreateBean( ... ){
...

// 这段告诉我们:如果允许循环依赖的话,此处会添加一个ObjectFactory到三级缓存里面,以备创建对象并且提前暴露引用~
// 此处Tips:getEarlyBeanReference是后置处理器SmartInstantiationAwareBeanPostProcessor的一个方法,它的功效为:
// 保证自己被循环依赖的时候,即使被别的Bean @Autowire进去的也是代理对象~~~~ AOP自动代理创建器此方法里会创建的代理对象~~~
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) { // 需要提前暴露(支持循环依赖),就注册一个ObjectFactory到三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

// 此处注意:如果此处自己被循环依赖了 那它会走上面的getEarlyBeanReference,从而创建一个代理对象从三级缓存转移到二级缓存里
// 注意此时候对象还在二级缓存里,并没有在一级缓存。并且此时可以知道exposedObject仍旧是原始对象~~~
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);

// 经过这两大步后,exposedObject还是原始对象(注意此处以事务的AOP为例子的,
// 因为事务的AOP自动代理创建器在getEarlyBeanReference创建代理后,initializeBean就不会再重复创建了,二选一的,下面会有描述~~~)

...

// 循环依赖校验(非常重要)~~~~
if (earlySingletonExposure) {
// 前面说了因为自己被循环依赖了,所以此时候代理对象还在二级缓存里~~~(备注:本利讲解的是自己被循环依赖了的情况)
// so,此处getSingleton,就会把里面的对象拿出来,我们知道此时候它已经是个Proxy代理对象~~~
// 最后赋值给exposedObject 然后return出去,进而最终被addSingleton()添加进一级缓存里面去
// 这样就保证了我们容器里**最终实际上是代理对象**,而非原始对象~~~~~
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) { // 这个判断不可少(因为如果initializeBean改变了exposedObject ,就不能这么玩了,否则就是两个对象了~~~)
exposedObject = earlySingletonReference;
}
}
...
}

}

上演示的是代理对象+自己存在循环依赖的case:Spring用三级缓存很巧妙的进行解决了。 若是这种case:代理对象,但是自己并不存在循环依赖,过程稍微有点不一样儿了,如下描述:

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
protected Object doCreateBean( ... ) {
...
// 这些语句依旧会执行,三级缓存里是会加入的 表示它支持被循环引用嘛~~~
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
...

// 此处注意,因为它没有被其它Bean循环引用(注意是循环引用,而不是直接引用~),所以上面getEarlyBeanReference不会执行~
// 也就是说此时二级缓存里并不会存在它~~~ 知晓这点特别的重要
populateBean(beanName, mbd, instanceWrapper);
// 重点在这:AnnotationAwareAspectJAutoProxyCreator自动代理创建器此处的postProcessAfterInitialization方法里,会给创建一个代理对象返回
// 所以此部分执行完成后,exposedObject **已经是个代理对象**而不再是个原始对象了~~~~ 此时二级缓存里依旧无它,更别提一级缓存了
exposedObject = initializeBean(beanName, exposedObject, mbd);

...

// 循环依赖校验
if (earlySingletonExposure) {
// 前面说了一级、二级缓存里都木有它,然后这里传的又是false(表示不看三级缓存~~)
// 所以毋庸置疑earlySingletonReference = null so下面的逻辑就不用看了,直接return出去~~
// 然后执行addSingleton()方法,由此可知 容器里最终存在的也还是代理对象~~~~~~
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) { // 这个判断不可少(因为如果initializeBean改变了exposedObject ,就不能这么玩了,否则就是两个对象了~~~)
exposedObject = earlySingletonReference;
}
}...
...
...
}
}

分析可知,即使自己只需要代理,并不被循环引用,最终存在Spring容器里的仍旧是代理对象。(so此时别人直接@Autowired进去的也是代理对象呀~~~)

终极case:如果我关闭Spring容器的循环依赖能力,也就是把allowCircularReferences设值为false,那么会不会造成什么问题呢?

1
2
3
4
5
6
7
8
// 它用于关闭循环引用(关闭后只要有循环引用现象就直接报错~~)
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(false);
}
}

若关闭了循环依赖后,还存在上面A、B的循环依赖现象,启动便会报错如下:

1
2
3
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)

注意此处异常类型也是BeanCurrentlyInCreationException异常,但是文案内容和上面强调的有所区别~~ 它报错位置在:DefaultSingletonBeanRegistry.beforeSingletonCreation这个位置~

报错浅析:在实例化A后给其属性赋值时,会去实例化B。B实例化完成后会继续给B属性赋值,这时由于此时我们关闭了循环依赖,所以不存在提前暴露引用这么一说来给实用。因此B无法直接拿到A的引用地址,因此只能又去创建A的实例。而此时我们知道A其实已经正在创建中了,不能再创建了。so,就报错了~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class HelloServiceImpl implements HelloService {

// 因为管理了循环依赖,所以此处不能再依赖自己的
// 但是:我们的此bean还是需要AOP代理的~~~
//@Autowired
//private HelloService helloService;

@Transactional
@Override
public Object hello(Integer id) {
return "service hello";
}
}

这样它的大致运行如下:

1
2
3
4
5
6
7
8
9
10
11
12
protected Object doCreateBean( ... ) {
// 毫无疑问此时候earlySingletonExposure = false 也就是Bean都不会提前暴露引用了 显然就不能被循环依赖了~
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
...
populateBean(beanName, mbd, instanceWrapper);
// 若是事务的AOP 在这里会为源生Bean创建代理对象(因为上面没有提前暴露这个代理)
exposedObject = initializeBean(beanName, exposedObject, mbd);

if (earlySingletonExposure) {
... 这里更不用说,因为earlySingletonExposure=false 所以上面的代理对象exposedObject 直接return了~
}
}

可以看到即使把这个开关给关了,最终放进容器了的仍旧是代理对象,显然@Autowired给属性赋值的也一定是代理对象。

最后,以AbstractAutoProxyCreator为例看看自动代理创建器是怎么配合实现:循环依赖+创建代理

AbstractAutoProxyCreator是抽象类,它的三大实现子类InfrastructureAdvisorAutoProxyCreatorAspectJAwareAdvisorAutoProxyCreatorAnnotationAwareAspectJAutoProxyCreator小伙伴们应该会更加的熟悉些

该抽象类实现了创建代理的动作:

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
// @since 13.10.2003  它实现代理创建的方法有如下两个
// 实现了SmartInstantiationAwareBeanPostProcessor 所以有方法getEarlyBeanReference来只能的解决循环引用问题:提前把代理对象暴露出去~
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
...
// 下面两个方法是自动代理创建器创建代理对象的唯二的两个节点~

// 提前暴露代理对象的引用 它肯定在postProcessAfterInitialization之前执行
// 所以它并不需要判断啥的~~~~ 创建好后放进缓存earlyProxyReferences里 注意此处value是原始Bean
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}

// 因为它会在getEarlyBeanReference之后执行,所以此处的重要逻辑是下面的判断
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// remove方法返回被移除的value,上面说了它记录的是原始bean
// 若被循环引用了,那就是执行了上面的`getEarlyBeanReference`方法,所以此时remove返回值肯定是==bean的(注意此时方法入参的bean还是原始对象)
// 若没有被循环引用,getEarlyBeanReference()不执行 所以remove方法返回null,所以就进入if执行此处的创建代理对象方法~~~
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
...
}

由上可知,自动代理创建器它保证了代理对象只会被创建一次,而且支持循环依赖的自动注入的依旧是代理对象。

上面分析了三种case,现给出结论如下: 不管是自己被循环依赖了还是没有,甚至是把Spring容器的循环依赖给关了,它对AOP代理的创建流程有影响,但对结果是无影响的。 也就是说Spring很好的对调用者屏蔽了这些实现细节,使得使用者使用起来完全的无感知~

总结

解决此类问题的关键是要对SpringIOCDI的整个流程做到心中有数,要理解好本文章,建议有【相关阅读】里文章的大量知识的铺垫,同时呢本文又能进一步的帮助小伙伴理解到Spring Bean的实例化、初始化流程。

本文还是花了我一番心思的,个人觉得对Spring这部分的处理流程描述得还是比较详细的,希望我的总结能够给大家带来帮助。 另外为了避免循环依赖导致启动问题而又不会解决,有如下建议:

  1. 业务代码中尽量不要使用构造器注入,即使它有很多优点。
  2. 业务代码中为了简洁,尽量使用field注入而非setter方法注入
  3. 若你注入的同时,立马需要处理一些逻辑(一般见于框架设计中,业务代码中不太可能出现),可以使用setter方法注入辅助完成

参考地址

JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:

  • 强引用(Strong Reference)
  • 软引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。不同的引用在垃圾回收中体现也是不一样~

M

我们先创建一个M对象,后面为了方便的感受GC的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.cyblogs.java.learning.C001_ReferenceType;

/**
* Created with java-learning-demo
*
* @description:
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:30 PM
*/
public class M {

@Override
public void finalize() {
System.out.println("finalize");
}

}

finalize函数是对象在gc的时候,一定会调用该方法。我们重写一下该方法并且打印一行日志。

强引用

只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.cyblogs.java.learning.C001_ReferenceType;

import java.io.IOException;

/**
* Created with java-learning-demo
*
* @description: 正常引用/强引用
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:31 PM
*/
public class C001_01_NormalReference {

public static void main(String[] args) throws IOException {
M m = new M();
// 将对象复制为空
m = null;
// 手动触发GC
System.gc();
// 因为不是触发gc就一定会立马gc,所以让线程阻塞一下
System.in.read();
}
}

控制台日志输出:

1
2
3
Connected to the target VM, address: '127.0.0.1:53621', transport: 'socket'
finalize
Disconnected from the target VM, address: '127.0.0.1:53621', transport: 'socket'

软引用

软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。

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
package com.cyblogs.java.learning.C001_ReferenceType;

import java.lang.ref.SoftReference;

/**
* Created with java-learning-demo
*
* @description: 软引用:当内存不足的时候,gc才会回收。非常适合做缓存
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:31 PM
*/
public class C001_02_SoftReference {

public static void main(String[] args) throws InterruptedException {
// 开辟一个20M的空间
SoftReference<byte[]> m = new SoftReference<byte[]>(new byte[1024 * 1024 * 10]);
System.out.println(m.get());
// 手动GC一下,看是否可以GC掉
System.gc();
// 避免gc不会立马触发,尝试休眠1s
Thread.sleep(1000);
// 然后再尝试获取
System.out.println(m.get());
// 重新开辟一个空间
byte[] b = new byte[1024 * 1024 * 15];
System.out.println(m.get());
}
}

因为它是在内存不足的时候才会触发,所以我们在跑之前需要设置一下最大堆。

1
-Xmx20M

控制台日志输出:

1
2
3
4
5
Connected to the target VM, address: '127.0.0.1:54335', transport: 'socket'
[B@4c3e4790
[B@4c3e4790
null
Disconnected from the target VM, address: '127.0.0.1:54335', transport: 'socket'

你会发现就算我们gc了,后面还是会get得到,因为空间还足够。当后面byte[] b再继续申请空间的时候,发现空间不足了,这个时候就会触发gc动作,把软引用的部分清除掉。

弱引用

弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收

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
package com.cyblogs.java.learning.C001_ReferenceType;

import java.lang.ref.WeakReference;

/**
* Created with java-learning-demo
*
* @description: 弱引用:是为了解决某些地方的内存泄露问题
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:32 PM
*/
public class C001_03_WeakReference {
public static void main(String[] args) {
WeakReference<M> m = new WeakReference<M>(new M());

System.out.println(m.get());
System.gc();
System.out.println(m.get());

ThreadLocal<M> tl = new ThreadLocal<M>();
tl.set(new M());
tl.remove();
}
}

控制台日志输出:

1
2
3
4
5
Connected to the target VM, address: '127.0.0.1:55151', transport: 'socket'
com.cyblogs.java.learning.C001_ReferenceType.M@38cccef
null
finalize
Disconnected from the target VM, address: '127.0.0.1:55151', transport: 'socket'

我们看一下ThreadLocalset方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

http://static.cyblogs.com/QQ20200616-225739@2x.jpg

为什么Entry要使用弱引用?

  • 若是强引用,即使tl=null,但是key的引用还是指向ThreadLocal。所以内存会泄露~而弱引用不会

  • 但是还会有内存泄露的问题,ThreadLocal被回收。key的值变成了null,则导致value的值再也无法被访问到,因此依然存在内存泄露问题。

虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

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
package com.cyblogs.java.learning.C001_ReferenceType;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;

/**
* Created with java-learning-demo
*
* @description: 虚引用:
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:33 PM
*/
public class C001_04_PhantomReference {

private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<M>();
private static final List<Object> LIST = new LinkedList<Object>();


public static void main(String[] args) throws InterruptedException {
PhantomReference<M> m = new PhantomReference<M>(new M(), QUEUE);

// 线程1
new Thread(() -> {
while (true){
LIST.add(new byte[1024 * 1024]);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.get());
}
}).start();

// 线程2
new Thread(()->{
while (true) {
Reference<? extends M> poll = QUEUE.poll();
if (poll != null) {
System.out.println("虚引用对象被JVM回收了" + poll);
}
}
}).start();

Thread.sleep(500);
}
}

在零拷贝中就会使用到虚引用,但我们又无法去操作对外的内存。因为太弱了,我们也无法感知到~ 这里就需要利用到ReferenceQueue

http://static.cyblogs.com/QQ20200616-222909@2x.jpg

参考地址

0%