码农戏码

新生代农民工的自我修养

0%

最近项目中使用了高版本的springboot-2.6.4,以及swagger3

1
2
3
4
5
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

结果启动应用程序失败,报错:

1
Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException

这个问题,网上资料不少,主要原因是因为springboot2.6.x后,把pathMatcher默认值修改了,springfox年久失修,与springboot出现了兼容性问题。

找到一个Spring Boot下的Issue:https://github.com/spring-projects/spring-boot/issues/28794,但这个issue已经关闭了,目前这个问题的主要讨论在springfox,具体issue是这个:https://github.com/springfox/springfox/issues/3462

主要项目中还需要使用springboot-actuator,所以简单的修改一下配置spring.mvc.pathmatch.matching-strategy=ant-path-matcher还不行。可参考:Spring Boot 2.6.x 集成swagger3.0.0报错解决方案Swagger is not working with Spring Boot 2.6.X

在此问题追踪过程中,第一个就是原先的Ant方式与当前的PathPattern有什么区别:

AntPathMatcher vs PathPattern

诞生时间

AntPathMatcher是一个早在2003年(Spring的第一个版本)就已存在的路径匹配器,

而PathPattern是Spring 5新增的,旨在用于替换掉较为“古老”的AntPathMatcher。

性能

PathPattern性能比AntPathMatcher优秀。

理论上pattern越复杂,PathPattern的优势越明显

功能

1、PathPattern只适用于web环境,AntPathMatcher可用于非web环境。

2、PathPattern去掉了Ant字样,但保持了很好的向下兼容性。

3、除了不支持将 ** 写在path中间之外(以消除歧义),其它的匹配规则从行为上均保持和AntPathMatcher一致

4、并且还新增了强大的{*pathVariable}的支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test() {
System.out.println("======={*pathVariable}语法======");
PathPattern pattern = PathPatternParser.defaultInstance.parse("/api/com/zhuxingsheng/{*pathVariable}");

// 提取匹配到的的变量值
System.out.println("是否匹配:" + pattern.matches(PathContainer.parsePath("/api/com/zhuxingsheng/a/b/c")));
PathPattern.PathMatchInfo pathMatchInfo = pattern.matchAndExtract(PathContainer.parsePath("/api/com/zhuxingsheng/a/b/c"));
System.out.println("匹配到的值情况:" + pathMatchInfo.getUriVariables());
}

======={*pathVariable}语法======
是否匹配:true
匹配到的值情况:{pathVariable=/a/b/c}

在没有PathPattern之前,虽然也可以通过/**来匹配成功,但却无法得到匹配到的值,现在可以了!

5、整体上可认为后者兼容了前者的功能

具体介绍可查看:
《Spring5新宠PathPattern,AntPathMatcher:那我走?》

在源码中也有详细说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
org.springframework.web.util.pattern.PathPattern

? matches one character
* matches zero or more characters within a path segment
** matches zero or more path segments until the end of the path
{spring} matches a path segment and captures it as a variable named "spring"
{spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring"
{*spring} matches zero or more path segments until the end of the path and captures it as a variable named "spring"


Note: In contrast to org.springframework.util.AntPathMatcher,

** is supported only at the end of a pattern. For example /pages/{**} is valid but /pages/{**}/details is not. The same applies also to the capturing variant {*spring}.

The aim is to eliminate ambiguity when comparing patterns for specificity

在springboot 2.6后,Spring MVC处理程序映射匹配请求路径的默认策略已从AntPathMatcher更改为PathPatternParser

Actuator端点现在也使用基于 PathPattern 的 URL 匹配。需要注意的是,Actuator端点的路径匹配策略无法通过配置属性进行配置。

如果需要将默认切换回 AntPathMatcher,可以将 spring.mvc.pathmatch.matching-strategy 设置为 ant-path-matcher

1
spring.mvc.pathmatch.matching-strategy=ant-path-matcher

springboot2.6.X与swagger3兼容

为什么改变了下pathmatch方式,就会影响到swagger,没想明白,毕竟swagger的路径,PathPattern也是可以正常解析的。

debug了下代码:

不配置spring.mvc.pathmatch.matching-strategy

应用在启动时,会自动设置PatternPaser

可以看到默认值就是PATH_PATTERN_PARSER,也正是springboot2.6后的默认方式:

不配置时spring.mvc.pathmatch.matching-strategy,pathPatterns是被赋值的:

springfox.documentation.spring.web.WebMvcRequestHandler#getPatternsCondition时,就是null。

这样也就出现了文章开头的兼容问题。

在配置ant-path-matcher后,RequestMappingInfo中的pathPatterns和patterns的赋值变化,pathPatterns是无值,patterns是有值。

1
spring.mvc.pathmatch.matching-strategy=ant-path-matcher

解决方案

解决springboot2.6和swagger冲突的问题这篇文章算是列举方案比较全的。

如果只是通过BeanProcessor修改了HandleMapping,但不修改pathmatch,会访问不了swagger,会出现以下错误:

1
2
3
o.s.web.servlet.PageNotFound             : No mapping for GET /webjars/js/chunk-vendors.90e8ba20.js
o.s.web.servlet.PageNotFound : No mapping for GET /webjars/js/chunk-735c675c.be4e3cfe.js
o.s.web.servlet.PageNotFound : No mapping for GET /webjars/css/app.f802fc13.css

可以追加一下swagger资源的映射,最终出的方案:https://www.jianshu.com/p/1ea987c75073;

在整合actuator时,SpringBoot 2.6.* 整合springfox 3.0报错中也指出了,并且解释了原理。但没有理解作者表达的springfox.documentation.spring.web.WebMvcRequestHandler#getPatternsCondition时为null的过滤掉。

代码里面反而是把为null的提取出来了呀。

springdoc

既然swagger3.0更新不及时,就不用再纠结,直接使用springdoc也是很好的方案。

使用springdoc来替换swagger3.0,《从springfox迁移到springdoc》

总结

虽然解决了问题,但原理尚需追踪。不如用springdoc来得简单些。

DDD统一语言

统一语言,最早听到这个概念是在学习DDD的时候。

统一语言在DDD中,是一个很重要的概念。

DDD中的几个大词:问题域,解决域,战略,战术,统一语言,界限上下文

Read more »

map

map() method -> Data Transformation

map() takes Stream as input and return Stream

Stream map(Stream input){}

1
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

It’s mapper function produces single value for each input value.hence it is also called One-To-One mapping.

这个方法比较好理解,把一个事物映射为另一个事物,是一对一的关系。

在没有stream.map()时,就在使用apache和guava的类似api

apache中的ListUtils

1
public static <E> List<E> transformedList(final List<E> list,final Transformer<? super E, ? extends E> transformer) 

guava中的Lists

1
public static <F, T> List<T> transform(List<F> fromList, Function<? super F, ? extends T> function)

flatMap

flatMap() -> map() + Flattering

flatMap() takes Stream<Stream> as input and return Stream

Stream map(Stream<Stream> input){}

1
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

It’s mapper function produces multiple value for each input value.hence it is also called One-To-Many mapping.

flattering

flatMap()其实是两个方法的合并,map()好理解,主要是flattering。

Before Flattening: [[t,u], [v,w,x], [y,x]]

After Flattening: [t,u,v,w,x,y,x]

其实就是把两层数组打平了。

实例

在stackoverflow上找的一个示例:

What’s the difference between map() and flatMap() methods in Java 8?

flatMap helps to flatten a Collection<Collection> into a Collection. In the same way, it will also flatten an Optional<Optional> into Optional.

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 Parcel {

String name;
List<String> items;

public Parcel(final String name, final String... items) {
this.name = name;
this.items = Arrays.asList(items);
}

public List<String> getItems() {
return items;
}

public static void main(final String[] args) {
final Parcel amazon = new Parcel("amazon", "Laptop", "Phone");
final Parcel ebay = new Parcel("ebay", "Mouse", "Keyboard");
final List<Parcel> parcels = Arrays.asList(amazon, ebay);

System.out.println("-------- Without flatMap() ---------------------------");
final List<List<String>> mapReturn = parcels.stream()
.map(Parcel::getItems)
.collect(Collectors.toList());
System.out.println("\t collect() returns: " + mapReturn);

System.out.println("\n-------- With flatMap() ------------------------------");
final List<String> flatMapReturn = parcels.stream()
.map(Parcel::getItems)
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println("\t collect() returns: " + flatMapReturn);
}

}

结果输出:

1
2
3
4
5
-------- Without flatMap() ---------------------------
collect() returns: [[Laptop, Phone], [Mouse, Keyboard]]

-------- With flatMap() ------------------------------
collect() returns: [Laptop, Phone, Mouse, Keyboard]

As you can see, with map() only:

  • The intermediate type is Stream<List>
  • The return type is List<List>

and with flatMap():

  • The intermediate type is Stream
  • The return type is List

参考

flatMap() Method in Java 8

新晋管理者都有一些相同的烦恼和忧愁。

“管理事太杂了,没时间写代码,越来越虚”

“如何平稳技术与管理?”

“管理事太琐碎,离技术越来越远,未来职业怎么规划?”

“做管理最大的挑战,就是舍弃技术,特别难”

这些问题的本源是因为新晋管理者正在进入一个全新的领域,离开以往的舒适区。

以往的舒适区是技术范围,而且主要是技术实现。接受一个功能需求,通过技术实现出来。

而成为管理后,不再是技术实现,而是任务分配,团队建设,资源协调等等脱离技术的事项。

走出舒适区是相当痛苦的,它会让人产生不安全感,让人容易自我质疑,以及自我否定,进而退回舒适区

新晋管理者都有类似这种前怕狼后怕虎的焦虑。

前怕狼后怕虎,狼是啥?管理之路不好走,事很杂,心里虚。虎是啥?离技术越来越远,失去了技术优势。

从自身安身立命安全感角度讲,是青黄不接的时候,管理还没有摸出门道,不知道怎么搞定,倍感焦虑;而熟练掌握的技术,却投入的时间越来越少。

这种纠结的状态,归纳为一个词叫:患得患失。还没有得到时,担心得不到;得到后,又担心失去。

Read more »

为什么学了一堆DDD理论,但就是无法落地呢?很多人认为它只是个理论。

最近又看了一遍《IDDD》第十章聚合,结合已有的理论知识,来反思下这个问题。

DDD聚合是什么?

最容易与DDD聚合混淆的就是OO聚合关系。

由上图可以看出,OO聚合表示了一种关联关系;而DDD聚合表示了一种边界。

OO聚合关系(Aggregation) 表示一个整体与部分的关系。通常在定义一个整体类后,再去分析这个整体类的组成结构,从而找出一些成员类,该整体类和成员类之间就形成了聚合关系。

如上图中Question与Answer的关系。一个问题与很多的回答构成一个完整的问答关系。

在OO中还有一种比聚合关系更强的关联关系:

组合关系(Composition)也表示类之间整体和部分的关系,但是组合关系中部分和整体具有统一的生存周期。一旦整体对象不存在,部分对象也将不存在,部分对象与整体对象之间具有相同的生命周期

继续以上图为例,Answer都是因Question存在而存在,当Question消亡时,Answer也自然消亡。

但像知乎,就算Question没了,Answer也会留存。

OO聚合与DDD聚合是什么样的关系呢?

因为聚合有隐含的构建关系和级联生命周期,通常会把OO组合关系构建成DDD聚合,其实组合关系只是聚合的必要条件,而非充分条件

如果是聚合,那么聚合根与实体自然是组合,存在级联生命周期,但不代表存在级连生命周期都是聚合。

特别是,混淆了数据生命周期和对象生命周期,

例如在“获取客户订单”这一业务场景下,Customer 与 Order 之间也存在整体/部分的组合关系,但它们却不应该放在同一个 DDD 聚合内。

从数据生命周期看,一般如果数据库中顾客数据删除了,那么他对应的订单也会删除。

但不适合建模成聚合。

因为这两个类并没有共同体现一个完整的领域概念;同时,这两个类也不存在不变量的约束关系。

而且聚合后,订单就不再具有独立身份,每次查询订单时候,必须提供客户身份和订单身份,构成级连身份才可以。类似于当仅仅使用订单号查询订单时,是不行的,必须带上身份证和订单号一起,才能查询到订单。

聚合困境

看似把一堆实体和值对象放一起就组成聚合,在《IDDD》中提供了一个示例。使用过JIRA的人应该很容易理解。我简单按书中的思路叙述下。

第一次尝试

这次尝试,应该是完全按产品需求直接建模。

Product,BacklogItem,Release,Sprint的确是一组组合关系。

而且Product是聚合根。

1
2
3
4
5
6
7
8
public class Product {
private Set<BacklogItem> backlogItems;

private Set<Release> releases;

private Set<Sprint> sprints;

}

虽然这完整表达了模型,但实现很残酷。不得不考虑的问题:

1、持久化时,乐观并发,增加了操作失败概率

2、影响系统性能和可伸缩性

第二次尝试

将一个大的Product聚合拆分成了4个相对较小聚合。

虽然解决了事务问题,多个用户请求可以同时创建任何数量的BacklogItem、Release和Sprint实例。

但对客户端来说,这4个较小的聚合却多少会带来一些不便。

设计小聚合

一个完整的聚合

如果要加载一个完整的聚合,需要把所有这些实体与值对象都加载出来。那系统性能和可伸缩性大受影响。

为了解决这些问题,所有提出要设计小聚合。

小聚合不仅有性能和可伸缩性上的好处,它还有助于事务的成功执行,即它可以减少事务提交冲突。这样一来,系统的可用性也得到增强。在你的领域中,迫使你设计大聚合的不变条件约束并不多。当你遇到这样的情况时,可以考虑添加实体或者是集合,但无论如何,我们都应该将聚合设计得尽量小。

聚合之间不能直接加载到同一个边界之内,得通过唯一标识引用其他聚合。

通过标识引用并不意味着完全丧失了对象导航性。有时在聚合中使用Repository来定位其他聚合。这种作法也被称为失联领域模型(Disconnected Domain Model)。

这就是矛盾体,一方面希望保障模型的完整性,我们需要完整的聚合;另一方面又有各种实现限制条件。

这些原因正是《实现业务逻辑的本种方式》中提到的单体架构演变为分层架构的局限性。

总结越来,聚合有三点特性:

1、Global entities(aggregation root and entity) is the boundary of consistency

2、Global entities(aggregation root and entity) is the boundary of lifecycle

3、Object model assumes same lifecycle boundary within the global entity

DDD困境

由聚合的困境,管窥一斑,DDD落地的困境何尝不是类似原因:

1、Domain Driven Design,but technology may have a say

2、Using fundamentalism DDD,it only works for simple cases,so does transaction script,procedure of oriented programming

3、Smaller aggregation works as fine as a EJB2-refurbished architecture

由上面的聚合示例观察,第一点的确是现实。

我们使用DDD,就是想Domain Driven,可考虑了很多技术落地因素,打破了完整的模型。选择模型还是选择性能,是放在我们面前的第一道选择题。

而第二点在现如今多运行时分布式架构中,肯定不可能像在一个单休中加载完整的聚合对象。因此当要使用原味的DDD时,只能在简单的项目中,而DDD却说要在复杂场景下再去使用。不要简单问题复杂化。这又是个矛盾体。

这些问题怎么解决?

当前能想到的解决方案似乎只有在《DDD对象生命周期管理》提到的关联对象模式。既能保证模型的完整,又能兼顾性能。

总结

聚合设计时,尽量使用小聚合。这对吗?解决设计困境了吗?

如果使用小聚合,会造成一种现象。会出现很多的service。

只有使用service,才能在聚合间跨越对象生命周期,维持一致性。

这会慢慢演化成贫血模型,因为一部分逻辑在对象中,另一部分会放到service中。

所以我们得重新审视一些指导原则。或者时时提醒是不是过多的考虑了实现细节,破坏了模型。

怎么才能更好地保证模型完整性,而兼顾当前的技术限制。

技术人员的未来是什么?

我想作为一名技术人,在夜深人静时候,都会想想这个直击灵魂的问题。

作为一名搬砖人,哪里需要哪里搬?

不甘于作为工具人,但出路在何方?

新技术层出不穷,学到何年何月?怎么保持活到老学到老的状态?

在之前的一系列文章中,写过《程序员成长职级》《最好的职业建议》《首席架构师的打怪之路》等,其实也都是来源于对上述问题的思考。

怎么突破呢?其实市场也给了部分答案,比如市场要求从几年前的T型人才π型人才,这也是作为一名技术人,需要三条腿:技术、业务、管理。

尤其业务,对于技术人员的重要性,很多技术人不理解,甚至工作多年的人也不理解。技术只是工具,达到目标的手段。而业务才是最终目标。不懂业务,最后必然会成为工具人。

很多技术人被沦为随时可替代的工具人也不自知,甚至对自身技术自鸣得意,孤芳自赏。

也许会有人反驳,我再深入解释一下。技术人想干什么?技术牛啊,怎么体现技术牛呢?写个数据库,写个操作系统,写个docker,或者写个开源框架等等。

为什么是这些东东呢?因为这些技术通用,与技术紧密关联,但其实这些只是更具技术属性而已,它们对外的统一特征是产品,首先是个产品,其次是个技术属性偏强的产品。

如果你对自己要达到的目标都不了解,再牛的技术也会有种无力感,报国无门空自怨。

回归到主题,技术人员的三个阶段。最近看到陶勇医生著作的《自造》,他讲了学医的三个阶段,我常见得对技术人是通用的。

Read more »

架构师,程序员追求的最高title。

那么想成为一名架构师,就会出现一系列的疑问:

什么是架构师?

怎么才能成为架构师?

优秀架构师需要哪些素质?

之前根据《郭东白的架构课》总结了《成为首席架构师的打怪升级之路》。这也是郭东白对架构师的思考之路。

其实在2021年5月28日的Qcon软件大会上郭东白就做了一场主题是《如何成为一名优秀的架构师》演讲。相当精彩。特地找到PPT,学习一下。可惜是没有找到完整的演讲视频。

什么是架构师

架构师:

1)a person engaged in the design of certain large constructions. – dictionary.com

设计人员从事某些大型建筑物设计的人

2)a person who designs and guides a plan or undertaking. – Merriam-Webster

设计和指导计划或事业的人。

在之前的总结文章中,其实都没有给出架构师的定义,只是说比普通程序员更牛的程序员,以前提出了“技术+影响力+领导力”的能力模型。

而郭东白给出了一个更确切的定义:

具备架构能力的人。

架构能力是指为相对复杂的场景设计并引导一个或多个研发团队,来实施结构化软件系统的能力。

兼职架构师与全职架构师

对于任何一家公司,架构设计永远是必要的。

其实在很多公司,架构师职位是有职无岗的,就是有架构师职责的需要,但不会独立设置架构师的岗位。很多时候都是由TL兼任。

架构师的生存

必要条件一:目标正确

一个优秀的架构师应该具备对软件做出正确的设计和决策的基本能力,在做这些决策时往往需要考虑很多限制条件,如:时间成本、人力成本、资金成本、范围要求、质量要求、组织结构等,架构师从来不是独行侠,他是上承需求,下接实现的一种角色。

总结一下就是要有工程化思维。

必要条件二:能力满足

这些能力,从硬实力再到软件实力,一条也不能落下。

能力范围从普通研发到兼职架构师,再到全职架构师,层次递增。

对于能力攻关,是先宽度再深度,还是先深度再宽度?

建议先深度再宽度,这样可以一通百通,没有深度是谈不上宽度的。

架构师的素质

第一是:有眼光,什么叫有眼光?就是要有深度的业务理解,能看到好的机会;

第二是:善于思考,有足够的技术视野能找到正确的技术和组织设计,亦即独立思考能力;

第三是,有良知,这是一个架构师随着时间的流逝,沉淀在身上最重要的品质。什么是有良知?为人正直,选择做正确的事情。很多人是非常聪明的,业务理解能力强,技术实践丰富,但他不一定为公司或为组织做最正确的事情。有良知是非常重要的一个事情,如果架构师没有素质,他会让一家公司的损失很惨重。

第四是:能感召,能感招是什么概念?他要能够引导组织作出正确的决策,大多数架构师不是决策者,但他是决策的建议者,因为他是个决策的建议者,所以他的沟通能力,表达能力可以引导一个组织最终走向正确的选择,这也是架构师必须具备的素质。

这几点,可以对应上前面的能力模型:技术、影响力、领导力。但这四点总结得更完善些

1、眼光其实就是业务,有了业务理解深度,才能知道价值点,进而通过影响力发挥价值

2、思考是根本,不管是对技术,还是业务。这是第一源能力

3、良知是德

4、感召力,对应上领导力,能引导他人一起走向正确的选择

对于思考能力,再补充点,业务理解是什么?什么是业务理解不够?

架构师的价值创造来自于独立、理性的、有深度的思考。从技术视角看业务,从业务中发现技术机会。通过复盘发现思考漏洞,提升思考质量。

讲一个技术人做业务理解,是在技术视角往上看到业务,从技术视角理解业务叫业务理解。

并不是一个做业务的,把业务怎么做说的很详细,没用,要从技术视角看到对业务的理解,同样,技术洞察,从业务视角反过来看技术,找到洞察,这才是要的洞察。

技术做技术的洞察,业务做业务的思考,这两个完全是割裂的。

总结

架构以及架构师,是程序员奋斗的事业和目标。所以虽然已经总结了很多了篇文章,但还是不时从高手那儿不停地吸取和反复思考。这是一个持续过程。现在有,未来还有。

通过不断地思考架构的本源和架构师的价值,成为一名德才兼备的优秀架构师。

基于”如何成为一名优秀的架构师”的分享总结

QCon大会官方PPT下载(源PPT下载)

InfoQ官方视频合辑(可能有视频上传)

马斯洛需求层次理论是管理心理学中人际关系理论、群体动力理论、权威理论、需要层次理论、社会测量理论的五大理论支柱之一。

以往对马斯洛需求层次理论的理解很肤浅:人对需求是有层次的,层次间都有依赖关系。当低层次的需求被满足后,才会去考虑更高一层次的需求。

而且好像也没什么用处。知道之后既不能提升硬技能,也不能升职加薪。

马斯洛理论定义

通过《郭东白的架构课》的学习,了解到马斯洛理论的本意是:我们可能同时并行存在着多个需求,这些需求之间并不存在依赖或层次关系。

包含两个重点:

第一点:不是需求有层次,而是动机有优先级

如果这些需求得不到满足,那么它们各自会诱发动机。但动机有优先级,且具备抢占性质。所以任何时候,只有一个动机在主导着整个人的意识和行为。

第二点:动机跃迁模型

人有且只有一个主导动机。这个动机由人的内在需求所驱动,并独占且主导你当前的一切意识和行为,你整个人,包括你的视觉、听觉、嗅觉,你的思考、记忆、行为等。直到这个动机背后的需求被完全满足之后,更高层次的动机才可能进入主导位置。

学习了理论本意后,由于理论抽象度高,需要更具象的事才能更好地理解,经过反复思考,找到了一些日常工作中的常见的场景,可以用为进一步理解此理论的抓手。

指导架构

郭东白讲架构活动需要尊重和顺应人性,架构师必须洞察研发人员的人性,在方案设计和架构活动组织中充分考虑研发的人性,才能保障方案的正确性,以及方案的高效实施。

什么样的架构才算是尊重和顺应人性呢?不得不再次搬出当下两个流行的事物:中台和微服务

中台

对于中台概念,可以温习之前写的中台是什么。定义中台为企业级能力复用平台。

在做企业级业务架构时,做出来的模型,需要考虑两点:

1、凡是公用的部分,应该照顾到所有利益相关方的需求;

2、凡是已实现的功能都应该对新的需求方开放并支持必要的扩展。

部门利益是做企业级的最大障碍,跨越这个障碍是对业务架构师设计能力的最高挑战。

中台建设虽然需要兼顾各方的利益,但更多主要还是解决企业管理层对于公司长期生存与可持续发展的恐惧与焦虑问题

中台建设的这两个考虑点,似乎是郭东白架构课程中重要价值输出的浓缩版本。

前半段结合马斯洛理论,中台的建设通常都会伴随企业内的组织重构以及利益和职责的再分配,中台必然会触碰到前端业务团队利益,这自然威胁到了业务团队。

尤其很多公司建设中台是跟风式的,本身就没有明确的战略意图,相当于架构活动没有正确目标,甚至没有目标,更没有清晰的构建路径,冒然简单粗暴地下沉前端业务功能,让原本根错综复杂的前端业务团队缺失了安全感,按马斯洛理论,业务团队满足安全需求的动机占据主导位置,可以想象,整个中台的推进速度。

后半段则是任何企业活动的目标,都是为企业带来长期生存的优势。管理层、架构师都得考虑当下的活动是否真的带来生存优势。生存优势我简单理解为降本增效,ROI越高越有优势。

微服务

之前谈到微服务常会提到《康威定律与反康威定律》,随着近些年互联网行业的不景色,从以前的拆掉中台到现在的《拆掉微服务》

康威定律有个常用示例:如果有四个小组合作开发一个编译器,那么你将得到一款具有四个步骤的编辑器。

结合马斯洛理论,每个小组都有了自己的一亩三分地,也就有了归属感和安全感,从人性角度,团队会更有活力、进取心和责任感。

技术管理

作为TL,在《码而优则仕》中提出三件重要事情。尤其激活团队。

TL如何激励团队?

都说现在00后是来整顿职场的,所以不能再像过去,员工以生存为目的,无条件接受资本主义压榨。

现在生活条件大大改善,对应马斯洛最底层需求已经完全满足。管理者怎么办呢?会通过内部激励机制,如晋升职级;和红线制度,明确惩罚事项。

有没有更高阶的方式方法呢?脱离上述的靠外部驱动。

马斯洛理论给出指导方针:让员工有更多的归属和尊重。激发他们内部需求。

总结

一个看似无用的理论,经过高人的讲解,可以感知到原来这个理论贯穿在每日生活的细节中。

马斯洛理论就是这样,不管是做架构还是做管理,只要有人参与的活动,都渗透着它的威力。

上篇《ClassLoader#getResource与Class#getResource的差别》了解原生java获取资源方式以及方式之间的区别。

这篇介绍一下springboot的加载方式。

要想调试springboot加载方式,不能直接在idea中运行主程序,要使用真实场景下的java -jar方式运行,需要做两件事:

1、需要打包springboot应用程序

2、在IDEA中用java -jar springboot.jar来运行才能debug

springboot使用maven plugin打包成可运行的jar文件

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

SpringBoot打出的jar包,可以直接通过解压的方式查看内部的构造。一般情况下有三个目录。

  1. BOOT-INF:这个文件夹下有两个文件夹classes用来存放用户类,也就是原始jar.original里的类;还有一个是lib,就是这个原始jar.original引用的依赖。
  2. META-INF:这里是通过java -jar启动的入口信息,记录了入口类的位置等信息。
  3. org:Springboot loader的代码,通过它来启动。

MANIFEST.MF文件的内容:

1
2
3
4
5
6
7
8
9
10
Manifest-Version: 1.0
Implementation-Title: springboot-test
Implementation-Version: 1.0-SNAPSHOT
Start-Class: com.zhuxingsheng.App
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.1.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

里面有两个重要的参数:

1
2
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.zhuxingsheng.App

Main-Class:记录了java -jar的启动入口,当使用该命令启动时就会调用这个入口类的main方法,显然可以看出,Springboot转移了启动的入口,不是应用自身的com.zhuxingsheng.App入口类。

Start-Class:应用自身的com.zhuxingsheng.App入口类,当内嵌的jar包加载完成之后,会使用LaunchedURLClassLoader线程加载类来加载这个用户编写的入口类。

在IDEA中正常启动应用程序,整个类加载体系与直接使用java -jar springboot.jar是不一样的,想
要在IDEA里面debug springboot应用程序

先引入loader依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>

再对应用程序通过maven package打包成jar

再在IDEA中设置:

并指定Path to JAR.

启动之后,先进入JarLauncher:

debug进入后,会使用springboot自定义的LaunchedURLClassLoader加载应用程序,LaunchedURLClassLoader类体系:

加载资源的过程如在《Classloader加载资源的方式》中提到的一样。

与之前的做个小实验,但这次做点小变动,在依赖的jar中也放一个META-INF/app.properties文件。

并在工程本身的resources里面也放一个META-INF/app.properties

此时系统中有两个META-INF/app.properties,通过下面的四种情况来加载资源文件,会获取到哪一个文件?

1
2
3
4
5
6
7
8
9
10
11
//第一种场景
final URL resource = Thread.currentThread().getContextClassLoader().getResource("/META-INF/app.properties");

//第二种场景
final URL resource = Thread.currentThread().getContextClassLoader().getResource("META-INF/app.properties");

//第三种场景
final URL resource1 = App.class.getResource("/META-INF/app.properties");

//第四种场景
final URL resource1 = App.class.getResource("META-INF/app.properties");

第一种 ClassLoader绝对路径

按《Classloader加载资源的方式》结论,应该会返回null。

然而实事并非无此:

这不得不提到在URLClassPath里面有两个内部Loader:

FileLoader 是加载文件夹中的文件

JarLoader 是加载jar中的文件

在《Classloader加载资源的方式》中的结论是基于FileLoader加载的,而现在的方式是使用JarLoader。

使用ClassLoader.getResource时,都是基于根节点查找,这点是没错的,只是根节点是BOOT-INF下的lib和classes:

去加载每一个jar中的文件,判断是不是存在:

可以看出,因为根节点不同,所以文件没有加载到,项目根目录里面的META-INF/app.properties,是在整体工程根目录的META-INF/app.properties中。

此时,找到的文件目录是在:

jar:file:/Users/zhuxingsheng/workspace/springboot-demo/springboot-test/target/app.jar!/BOOT-INF/lib/general-tool-utils-1.1.0-SNAPSHOT.jar!/META-INF/app.properties

第二种 ClassLoader 相对路径

可以看出使用的是AppClassLoader,加载的路径为

jar:file:/Users/zhuxingsheng/workspace/springboot-demo/springboot-test/target/app.jar!/META-INF/app.properties

可以看出 相对路径 与绝对路径的区别,以及与FileLoader的区别:

1、绝对路径是LaunchedURLClassLoader从classpath根节点查找;相对路径是AppClassLoader从当前jar为根目录查找

2、FileLoader绝对路径是:file:/META-INF/app.properties,而JarLoader的绝对路径则不同了,会带上整个classpath

第三种 Class 绝对路径

《Classloader加载资源的方式》知道,这种方式与第二种场景效果一致:

1
2
//第二种场景
final URL resource = Thread.currentThread().getContextClassLoader().getResource("META-INF/app.properties");

路径地址在:

jar:file:/Users/zhuxingsheng/workspace/springboot-demo/springboot-test/target/app.jar!/META-INF/app.properties

第三种 Class 相对路径

类似于:

1
final URL resource = Thread.currentThread().getContextClassLoader().getResource("com/zhuxingsheng/META-INF/app.properties");

总结

此篇一是介绍了怎么在IDEA中debug出运行java -jar springboot.jar的效果。二是介绍springboot类加载机制,以及绝对路径与相对路径的区别。

当依赖jar包中有与工程目录下有同路径同名资源文件时,为了不必要的冲突,在classloader#getResource时,不要使用绝对路径。

如在apollo的源码中:

也会特意使用substring处理掉绝对路径。保证加载资源的正确性。

traceId在链路监控中是很重要的组成部分,在实际项目中,traceId处理可以在filter中处理,也可以在interceptor中处理。

本文重点说明下他们的区别是什么?以及spring对fitler的增强。

1、filter与interceptor的执行顺序

2、spring的OncePerRequestFilter

执行顺序

注册了两个fitler和一个interceptor

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
@Override
public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(new SelfInterceptor());
}

@Bean
public FilterRegistrationBean registerSecondFilter() {
FilterRegistrationBean<SecondFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SecondFilter());
registration.addUrlPatterns("/*");
registration.setName("SecondFilter");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
return registration;
}

@Bean
public FilterRegistrationBean registerTraceIdFilter() {
FilterRegistrationBean<TraceIdFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new TraceIdFilter());
registration.addUrlPatterns("/*");
registration.setName("traceIdFilter");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}

1、Filter:与Spring 无关,基于Servlet, 可以获取request 和 response,但是具体处理方法及相关参数获取不到;

2、Interceptor:与Spring相关,可以获取request 和 response 以及具体处理方法,但是获取不到具体方法参数的值;

3、Aspect: 与Spring相关,不可获取request和response , 可以获取具体方法及具体方法参数的值;

OncePerRequestFilter

Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal method with HttpServletRequest and HttpServletResponse arguments.

As of Servlet 3.0, a filter may be invoked as part of a REQUEST or ASYNC dispatches that occur in separate threads. A filter can be configured in web.xml whether it should be involved in async dispatches. However, in some cases servlet containers assume different default configuration. Therefore sub-classes can override the method shouldNotFilterAsyncDispatch() to declare statically if they should indeed be invoked, once, during both types of dispatches in order to provide thread initialization, logging, security, and so on. This mechanism complements and does not replace the need to configure a filter in web.xml with dispatcher types.

简单的说就是去适配了不同的web容器,以及对异步请求,也只过滤一次的需求。另外打个比方:如:servlet2.3与servlet2.4也有一定差异:

在servlet2.3中,Filter会经过一切请求,包括服务器内部使用的forward转发请求和<%@ include file=”/login.jsp”%>的情况

servlet2.4中的Filter默认情况下只过滤外部提交的请求,forward和include这些内部转发都不会被过滤,

因此此处我有个建议:我们若是在Spring环境下使用Filter的话,个人建议继承OncePerRequestFilter吧,而不是直接实现Filter接口。这是一个比较稳妥的选择

Ways to add Servlet Filters in Spring Boot