码农戏码

新生代农民工的自我修养


  • 首页

  • 归档

  • 标签

  • 关于

  • 在线工具

  • 搜索

traceId在filter还是interceptor中处理

发表于 2022-07-23
字数统计: 624 字数 | 阅读时长 ≈ 2 分钟

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

JVM获取资源的途径和差别

发表于 2022-06-19
字数统计: 1.1k 字数 | 阅读时长 ≈ 4 分钟

JVM加载配置资源文件有两种方式:

1、ClassLoader#getResource

2、Class#getResource

两者之间的区别:

ClassLoader并不关心当前类的包名路径,它永远以classpath为基点来定位资源。需要注意的是在用ClassLoader加载资源时,路径不要以”/“开头,所有以”/“开头的路径都返回null;

Class.getResource如果资源名是绝对路径(以”/“开头),那么会以classpath为基准路径去加载资源,如果不以”/“开头,那么以这个类的Class文件所在的路径为基准路径去加载资源

从源代码层次分析一下,这个结论对不对?

ClassLoader#getResource

1
2
3
4
5
6
7
8
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}

相对路径

1
2
3
4
5
6
//classloader从根节点开始查找
final URL resource = Thread.currentThread().getContextClassLoader().getResource("root.properties");
System.err.println(resource);
Assertions.assertNotNull(resource);

//成功获取

这样子是正常获取到资源

绝对路径

1
2
3
4
5
//以目录作对比,这样写,应该也没问题,但为什么返回是null呢?
//并且很多资料都直接说 classloader加载资源时,不要以 / 开头,以 / 开头都会返回null
final URL resource1 = Thread.currentThread().getContextClassLoader().getResource("/root.properties");
System.err.println(resource1);
Assertions.assertNull(resource1);

很多资料的结论classloader加载资源时,不要以 / 开头,以 / 开头都会返回null,是正确的。

从debug中可以看出来为什么以/开头,获取不到对应的资源。

主要还是对根节点的理解不一样:

classcloader以根节点去查找,是以当前的classpath为起点;

而以 / 开头,就变成类似root下了,自然查找不到

相对路径

1
2
final URL resource2 = Thread.currentThread().getContextClassLoader().getResource("./root.properties");
System.err.println(resource2);

这样是可以成功查找。

Class#getResouce

1
2
3
4
5
6
7
8
9
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}

看着Class#getResource,多加了一步resolveName,其实还是使用了Classloader#getResource方法

其中resolveName()

name不以’/‘开头时,默认是从此类所在的包下取资源;

name以’/‘开头时,则会substring(1),踢掉/,绝对路径变相对数据再从ClassPath根下获取资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) {
Class<?> c = this;
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) {
name = baseName.substring(0, index).replace('.', '/')
+"/"+name;
}
} else {
name = name.substring(1);
}
return name;
}

在与com.zhuxingsheng.lang.GetResourceTest同目录下面创建package.properties

直接写文件名

1
2
3
4
5

final URL resource = GetResourceTest.class.getResource("package.properties");
System.err.println(resource);

//获取资源正常

根据debug信息,可以看出会从当前类目录去查找

绝对路径

1
2
3
4
final URL resource1 = getClass().getResource("/com/zhuxingsheng/lang/package.properties");
System.err.println(resource1);

//获取资源正常

从resolveName方法,可知,写了绝对路径,会被substring(1),也就是手动拼接完事包路径,走了resolveName的第一个不以 / 开头的分支路径。

也就是classloader#getResource不要写绝对路径。

完整相对路径

1
2
3
4
5

final URL resource1 = getClass().getResource("com/zhuxingsheng/lang/package.properties");
System.err.println(resource1);

//获取失败

从debug中可以看出,就是把完整的路径拼接了两次,路径变成了com/zhuxingsheng/lang/com/zhuxingsheng/lang/package.properties

结论

经过源代码的debug,上文的结论是正确的。

ClassLoader并不关心当前类的包名路径,它永远以classpath为基点来定位资源。需要注意的是在用ClassLoader加载资源时,路径不要以”/“开头,所有以”/“开头的路径都返回null;

Class.getResource如果资源名是绝对路径(以”/“开头),那么会以classpath为基准路径去加载资源,如果不以”/“开头,那么以这个类的Class文件所在的路径为基准路径去加载资源

但在springboot中,自定义了classloader,打破了上述规则。下篇再看springboot的加载机制。

DDD之Repository对象生命周期管理

发表于 2022-06-18
字数统计: 2.4k 字数 | 阅读时长 ≈ 8 分钟

在DDD中Repository是一个相当重要的概念。聚合是战略与战术之间的交汇点。而管理聚合的正是Repository。

因为从战略层次,分而治之,我们会按领域、子域、界限上下文、聚合逐步划分降低系统复杂度;从战术层次,我们会从实体、值对象、聚合逐步归并,汇合。

也因此有人解析DDD关键就是两个字:分与合,分是手段,合是目的。

之前写的《DDD之Repository》,大致介绍了Repository作用。

image

一是从“硬件”、“软件”、“固件”阐述了Repository的必要性,相对DAO,具有更高抽象,不再关心数据是从db,cache甚至第三方系统,Repository管理着数据在存档态与活跃态之间的转换

二是Respository与Domain Service之间的调用关系

阅读全文 »

成为首席架构师的打怪升级之路

发表于 2022-06-10
字数统计: 1.9k 字数 | 阅读时长 ≈ 6 分钟

经过多年职场成长,发现架构与管理有很多的相似之处,最大相似之处就是要与个人特性融合实践。也就是纸上得来终觉浅,绝知此事要躬行。

读再多的架构或者管理书籍,那不过是个最基础的起点,你外围触之可及的手段,但离最终的知行合一还存在着很远的距离。

在《架构师的能力模型》中提出架构师的能力模型:

架构师能力模型

程序员只需要专注于“专”,而架构师需要“博而不专”,牺牲技术深度来提高技术广度。

对于一个全能型架构师对架构的决策,需要广度+深度+经验三者兼顾

广度决定能找到的方法+深度决定选择方法的正确性+经验决定找到正确方法的速度

架构师的能力维度从技术+业务+管理升华为技能+影响力+领导力

1.技能是实践架构的基础。它需要知识以及应用知识的能力

2.影响力用来衡量架构师在项目中应用技能后给项目或公司带来多大的效益

3.领导力确保了架构实践的状态能稳步向前推进,同时培养更多的架构师

阅读全文 »

Java并发的问题及应对办法

发表于 2022-06-02
字数统计: 3.1k 字数 | 阅读时长 ≈ 12 分钟

并发问题的源头

并发?为啥需要并发呢?自然是为了性能,增强算力以及协调能力

在现今计算机器体系中,涉及性能的主要有CPU、内存、IO三方面,而这三者的速度也是天壤之别,形象之讲,CPU天上一天,内存是地上一年,IO则要地上十年

怎么应对:

1、CPU增加了多级缓存,均衡与内存的速度差异,并且还从单核发展为多核增加算力

2、操作系统增加线程,分时复用CPU,均衡CPU与IO的速度差异

3、通过即时编译器重排序,处理器乱序执行,以及内存系统重排序优化指令执行次序,更好地利用缓存

但这些措施并不是百利无害的,并发问题就是其中一害。

1、缓存导致的可见性问题

多核时代,每个核都有各自的L1,L2缓存,在各自缓存中修改的数据相互不可见。

在《缓存是个面子工程》提到的硬件缓存,也带来了并发问题。

Java内存模型

2、线程切换带来的原子性问题

这主要有些看似一行的代码,其实需要多条CPU指令才能完成

如count+=1,需要三条指令

指令1:把变量count从内存加载到CPU的寄存器

指令2:在寄存器中执行+1操作

指令3:最后将结果写入内存

当多线程时,线程切换时三条指令就会被错误执行,打破了原子性,导致逻辑的错误。

3、编译优化带来的有序性问题

编译器为了优化性能,有时改变了程序中语句的先后顺序。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}

new Singleton()这句话感觉是

  1. 分配一块内存M
  2. 在内存M上初始化Singleton对象
  3. 然后M的地址赋值给instance变量

实际执行路径却是:

  1. 分配一块内存M
  2. 将M的地址赋值给instance变量
  3. 最后在内存M上初始化Singleton对象

JMM

如何解决上述的三大问题,JSR-133定义了内存模型JMM

A memory model describes, given a program and an execution trace of that program, whether the execution trace is a legal execution of the program. For the Java programming language, the memory model works by examining each read in an execution trace and checking that the write observed by that read is valid according to certain rules.

也就是说一个内存模型描述了一个给定的程序和和它的执行路径是否一个合法的执行路径。对于java序言来说,内存模型通过考察在程序执行路径中每一个读操作,根据特定的规则,检查写操作对应的读操作是否能是有效的。
java内存模型只是定义了一个规范,具体的实现可以是根据实际情况自由实现的。但是实现要满足java内存模型定义的规范。

内存模型的种类大致有两种:

Sequential Consistency Memory Model: 连续一致性模型。这个模型定义了程序执行的顺序和代码执行的顺序是一致的。也就是说 如果两个线程,一个线程T1对共享变量A进行写操作,另外一个线程T2对A进行读操作。如果线程T1在时间上先于T2执行,那么T2就可以看见T1修改之后的值。
这个内存模型比较简单,也比较直观,比较符合现实世界的逻辑。但是这个模型定义比较严格,在多处理器并发执行程序的时候,会严重的影响程序的性能。因为每次对共享变量的修改都要立刻同步会主内存,不能把变量保存到处理器寄存器里面或者处理器缓存里面。导致频繁的读写内存影响性能。

这种模型相当于禁用了缓存。如果再禁止编译器优化,就算是彻底解决上述问题了,但性能将受到严重影响。

Happens-Before Memory Model: 先行发生模型。这个模型理解起来就比较困难。先介绍一个先行发生关系 (Happens-Before Relationship)
  如果有两个操作A和B存在A Happens-Before B,那么操作A对变量的修改对操作B来说是可见的。这个先行并不是代码执行时间上的先后关系,而是保证执行结果是顺序的。

happens-before

happens-before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 happens-before 规则。

happens-before规则:

程序次序规则(program order rule): 在一个线程内,先在前面的代码操作先行。准确的说控制流顺序而不是代码顺序。需要考虑分支,循环等结构。

管程锁定规则(monitor lock rule):同一个资源锁,先unlock,之后才能lock。

Volatile变量规则(volatile variable rule):一个变量被volatile修饰,多线程操作,先执行操作,再执行读操作。(同时写操作只能有一个)

线程启动规则(Thread start rule):Thread对象的start方法,先行发生于此线程的每一个方法。

线程终止规则(Thread Termination rule):该线程的所有方法,先行发生于该线程的终止检测方法。例如:可以通过Thread.join方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行。

线程中断规则(Thread Interruption Rule): 中断方法先行发生于,中断检测方法。中断方法interrupt(),中断检测interrupted()方法。

对象终结规则(finalizer rule): 一个对象的初始化完成(构造函数执行结束)先行发生于它的finalizer方法的开始。

传递性(Transitivity): 如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

as-if-serial

在谈happens-befre常会提到as-if-serial

即时编译器保证程序能够遵守as-if-serial属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。也就是经过重排序的执行结果要与顺序执行的结果保持一致。

而且,如果两个操作之间存在数据依赖时,编译器不能调整它们的顺序,否则将造成程序语义的改变。

1
2
3
4
5
6
7
public class AsIfSerialDemo {
public static void main(String[] args) {
int a = 10;//1
int b = 10;//2
int c = a+ b;//3
}
}

上面示例中:1和3之间存在数据依赖关系,同时2和3之间也存在数据依赖关系。因此在最终执行的指令序列中,3不能被重排序到1和2的前面(3排到1和2的前面,程序的结果将会被改变)。但1和2之间没有数据依赖关系,编译器和处理器可以重排序1和2之间的执行顺序。

VS 时间先行

对于happens-before先行发生,怎么理解,最常与“时间先后发生”搞混淆。

happens-before 关系是用来描述两个操作的内存可见性的。

如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。那么与“时间先后发生”顺序有什么区别?

在《JSR-133: JavaTM Memory Model and Thread Specification》,happens-before是这样定义的:

Two actions can be ordered by a happens-before relationship.
If one action happens-before another, then the first is visible to and ordered before the second.
It should be stressed that a happens-before relationship between two actions does not imply that
those actions must occur in that order in a Java platform implementation. The happens-before
relation mostly stresses orderings between two actions that conflict with each other, and defines
when data races take place.

从定义中可以看出两点:

1、the first is visible to and ordered before the second

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前

2、does not imply that those actions must occur in that order in a Java platform implementation

两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行


由这两条可以得出,JMM是要求当有happens-before关系时,不仅要求了可见性,而且在时间上也得保证有序。然而在不改变语义的前提下,Java平台的实现可以自主决定。这也就表明了happens-before与时间先后没有更大的关联性。

A happens-before B does not imply A happening before B.

A happening before B does not imply A happens-before B.

一个操作 “先行发生” 并不意味着这个操作必定是“时间上的先发生”

1
2
3
// 以下操作在同一个线程中执行
int i = 1;
int j = 2;

根据happens-before规则第一条,“int i = 1” 的操作先行发生(Happens-before)于 “int j = 2”,但在保证语义不改变的前提下,重排序了两条语句,那在时间上,“int j=2”先执行了。

在《The Happens-Before Relation》这篇文章中,作者还举了个示例:

1
2
3
4
5
6
7
8
int A = 0;
int B = 0;

void foo()
{
A = B + 1; // (1)
B = 1; // (2)
}

虽然(1) happens-before (2),而且从上面的as-if-serial判断,(1) 得happen before (2) ,但作者观察并不是。

从图上可看出,A被赋值为0,B被赋值为1,但 (1) 没被执行呢。

关于这个问题,在stackoverflow happens-before 被讨论了。有人指出作者说得不对,而也有人给出解答:

A and B are locations in memory. However the operation B+1 does not happen in memory, it happens in the CPU. Specifically, the author is describing these two operations.

A = B + 1 (1)

  • A1 - The value in memory location B (0) is loaded into a CPU register
  • A2 - The CPU register is incremented by 1
  • A3 - The value in the CPU register (1) is written to memory location A

B = 1 (2)

  • B1 - The value 1 is written to memory location B

Happens-Before requires that the read of B (step A1) happens before the write of B (step B1). However, the rest of the operations have no interdependence and can be reordered without affecting the result. Any of these sequences will produce the same outcome

  • A1, B1, A2, A3
  • A1, A2, B1, A3
  • A1, A2, A3, B1

一个操作 “时间上的先发生” 也不能代表这个操作会是“先行发生”

1
2
3
4
5
6
7
8
9
10
11
private int value = 0;

// 线程 A 调用
pubilc void setValue(int value){
this.value = value;
}

// 线程 B 调用
public int getValue(){
return value;
}

假设存在线程 A 和 B,线程 A 先(时间上的先后)调用了 setValue(1),然后线程 B 调用了同一个对象的 getValue() ,那么线程 B 收到的返回值是什么? 0和1都有可能。因为两个操作之间没有happens-before关系。

volatile

volatile字段的happens-before关系指的是在两个不同线程中,【volatile的写操作】 happens-before之后【对同一字段的读操作】。这里有个关键字“之后”,指的是时间上的先后。
也就是我这边写,你之后再读就一定能读得到我刚刚写的值。普通字段则没有这个保证。也就是上面的setValue()与getValue()示例问题

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

int a=0;
volatile int b=0;

public void method1() {
int r2 = a;
b = 1;
}

public void method2() {
int r1 = b;
a = 2;
}

首先,b加了volatile之后,并不能保证b=1一定先于r1=b,而是保证r1=b始终能够看到b的最新值。比如说b=1;b=2,之后在另一个CPU上执行r1=b,那么r1会被赋值为2。
如果先执行r1=b,然后在另外一个CPU上执行b=1和b=2,那么r1将看到b=1之前的值。

在没有标记volatile的时候,同一线程中,r2=a和b=1存在happens before关系,但因为没有数据依赖可以重排列。一旦标记了volatile,即时编译器和CPU需要考虑到多线程happens-before关系,因此不能自由地重排序。

volatile与synchronized的区别,可以查看《volatile synchronized cas》

总结

本篇总结了Java并发问题的本质:可见性、原子性、有序性;以及应对这些问题,JMM中happens-before模型的规则。以及happens-before与happen before的区别。

Clean Code系列之异常处理

发表于 2022-05-24
字数统计: 1.2k 字数 | 阅读时长 ≈ 4 分钟

先前已经对异常如何设计,如何实践异常都写了几篇阐述了。再一次从Clean Code角度来谈谈异常的使用。

阅读全文 »

怎么做软件设计才美

发表于 2022-05-05
字数统计: 1.2k 字数 | 阅读时长 ≈ 4 分钟

之前学习了极客时间上的一个专栏《软件设计之美》,作者对软件设计、编程范式、设计原则与模式、设计方法进行了讲解,内容全面。

专栏里面的一些内容,我也有些接触,但认知还不够深,比如面向对象。而且专栏把这些内容都串联起来,跟着专栏内容总结梳理一下。

阅读全文 »

实现业务逻辑三种方式:事务脚本、贫血模型、DDD

发表于 2022-04-30
字数统计: 1.9k 字数 | 阅读时长 ≈ 6 分钟

在《领域驱动设计》这本书里面,列举了三种可将业务逻辑建模为软件模型的模式,也就是大家常听说的事务脚本、贫血模型、DDD。

之前我还把这三种模式搞混淆了,too young too simple了。

举个简单的示例:

用户转帐,从一个帐户转到另一个帐户

阅读全文 »

领域服务上抛异常还是返回错误码

发表于 2022-04-30
字数统计: 1.6k 字数 | 阅读时长 ≈ 6 分钟

最近收到这样的问题:

领域服务做业务逻辑校验时应该返回错误码还是抛出业务异常?

这其实不算是领域服务的问题,而是Java异常处理问题。

之前总结过一次如何处理异常

上面的文章基本上就解决异常相关问题了。

这儿再回顾总结一下:

阅读全文 »

CQRS被称为邪教

发表于 2022-04-16
字数统计: 1.5k 字数 | 阅读时长 ≈ 5 分钟

CQRS全称Command Query Responsibility Segregation

在CQRS中,来自客户端的命令通过单独的路径抵达命令模型,而查询操作则采用不同的数据源,这样的好处在于可以优化对查询数据的获取,比如用于展现、用于接口或报告的数据。

CQRS这些年火起来了,常被人挂在嘴边提起。为什么?因为DDD提倡富模型,但从资源库查找所有需要显示的数据是困难的,特别是在需要显示来自不同聚合类型与实例的数据时。领域越复杂,这种困难程度越大。

有没有一种完全不同的方法可以将领域数据映射到界面显示中呢?答案正是CQRS。

在From CRUD to CQRS文章中,作者比对了CRUD模式与CQRS模式

阅读全文 »
123…14
朱兴生

朱兴生

140 日志
3 分类
52 标签
© 2016 — 2022 朱兴生 | Site words total count: 323.5k
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
沪ICP备18040647号-1