0%

统一的异常管理

  1. DAO 模式通过接口编程隔离的 DB 实现
  2. 实现的时候,由于 JDBC 规范将异常下方给供应商,导致接口代码会随着实现的改变而改变
  3. DAO 模式描述的场景实在是诱人,我们通过统一异常来 apply DAO 模式
  4. 不能直接在实现层吞了异常,所以把他们包装成 unchecked exception 抛出,这样客户端就不需要强制检查了
  5. 各供应商的异常包装形式不同,比如错误信息存放位置,错误代码规定等。所以 Spring 提供了异常转译功能做统一处理

JDBC API 最佳实践

  • 通过模版方法封装重复代码
  • 对 SQL Exception 进行转译

  • Join Point: In Spring AOP, a join point always represents a method execution. 事件触发点
  • Advice: Action taken by an aspect at a particular join point. 遇到 join point 时会执行的动作,行为
  • Pointcut: A predicate that matches join points. 匹配 method 的表达式
  • Advisor: an Advisor is an aspect that contains only a single advice object associated with a pointcut expression. 只包含一个 advice 的 aspect
  • Advised: However you create AOP proxies, you can manipulate them BY using the advised interface. 代理对象的配置信息

AOP 模型

AOP 有两种注册方式 Xml 或者 Annotation, 做这两种设置时,底层其实就是注册了一个特殊的 BeanPostProcessor(DefaultAdvisorAutoProxyCreator/AnnotationAwareAspectJAutoProxyCreator),剩下的气势和普通的 getBean() 流程一样,到处理 BeanPostProcessor 时,上面的说的两个 processor 会判断当前 bean 是不是需要代理,如果要的话,通过 ProxyFactory 创建代理并返回。

Spring支持 event 模型,自带了几种 event,比如 ContextRefreshedEvent,ContextStartedEvent 等,会在 application context setup 的时候发出来,还可以自定义 event,参考官方文档中的 BlockedListEvent 例子。

自带的 event 是在 AbstractApplicationContext::finishRefresh::publishEvent 中处理的。如果是自定义的 event,我们还需要定义出对应的 Listener,可以通过实现 ApplicationListener 接口,在 context setup 的时候自动注入到 context 中。这个 listener 其实就是一段如何处理 event 的逻辑。

目标和受体都准备好了,接下来就是如何触发的问题了。我们可以让 service 实现 ApplicationEventPublisherAware 接口,然后调用 publish() 触发事件。publish 即 context,实现在 AbstractApplicationContext::publishEvent 中。他会根据 event type 等条件,删选出匹配的 listener 并执行,逻辑还是很直观的。

这整个 event 处理模型,使用了观察者的设计模式,实现的时候很简单,就是将所有的观察者放到一个 list 中,当事件发生时,通过 for 或者 iterator 遍历所有的观察者执行目标函数即可。

IoC容器的两个主要阶段:容器启动阶段 + Bean实例化阶段

  1. bean def load
  2. post process bean definition - BeanFactoryPostProcessor
  3. instantiate beans
  4. populate property
  5. before bean post processor
  6. initializer
  7. init-method
  8. after bean post processor
  9. ready to use bean
  10. destory

如何记忆:

  • 1-2 属于 def 部分
  • 3-4 属于创建 bean
  • 5-8 属于修改 bean, 根据扩展点不同所以有这么多接口,主旨都是一样的
  • 9 容器中的状态
  • 10 销毁对象的时候会执行,很少接触

BeanFactoryPostProcessor - bfpp

在加载 def 的时候更具配置来修改 def 属性的值。比如可以在 xml 中通过使用 ${} 的语法来指代配置文件中的内容。典型应用如 PropertyOverrideConfigurer 和 PropertySourcesPlaceholderConfigurer。如果是想改变 bean 实例属性,使用 BeanPostProcessor。

在 Spring doc 的这个章节出现了 eagerly 的描述,目的是说,这种 processor 是用来对 def, bean 做操作的,所以 container 找到后会立即加载,lazy-load 也不生效的。这是从 processor 的定义决定的。

如果使用 BeanFactory + bfpp 的方式,需要显示的注册 bfpp cfg.postProcessBeanFactory(factory); 不是很方便,所以一般都直接用 ApplicationContext 来操作,自动实现 processor 的注册

ApplicationContext 中的 refresh() 方法中的

  • obtainFreshBeanFactory() 方法包含加载 bean definition 的步骤
  • invokeBeanFactoryPostProcessors() 方法就是处理 bfpp 的地方

对象实例化

refresh() 的 finishBeanFactoryInitialization() 方法负责实现 bean 的实例化,具体逻辑托管给 doGetBean() 方法。

  • AbstractAutowireCapableBeanFactory::createBeanInstance 负责创建空壳 bean
  • AbstractAutowireCapableBeanFactory::populateBean 填充属性
  • AbstractAutowireCapableBeanFactory::initializeBean 初始化,包括处理 processor, init, init-method
  • AbstractAutowireCapableBeanFactory::registerDisposableBeanIfNecessary 注册 disposable 方法备用
  • initializeBean::invokeAwareMethods, 部分 aware 接口注入(BeanName, ClassLoader, BeanFactory)
  • initializeBean::applyBeanPostProcessorsBeforeInitialization 处理 processor before 部分
  • initializeBean::invokeInitMethod,分两块
    • 第一部分对应 InitializingBean 接口的方法
    • 第二部分对应 init-method 的方法, 使得 bean 执行 init-method 方法作为初始化的一步,可以通过 xml 和 @Bean 配置
  • initializeBean::applyBeanPostProcessorsAfterInitialization

进度:最终只是草草的过了一遍官方文档,知道了他只用法,以后要用到的时候再深入看看吧

最近用到 SAP 的 scimono 这个 repo, 里面用到了 JUnit5,使用中发现和之前用的 TestNG 相比增加了很多有意思的特性,特意看下官方文档顺便写一些 demo 实践一下。

官方文档:JUnit5

PS: JUnit5 有一个相关的子项目 junit-platform-console-standalone,提供一个叫 ConsoleLauncher 的东西,可以通过命令行运行测试,给出的结果 UI 还挺好看

2.8.1 Operating System and Architecture Conditions

JUnit 中竟然还自带操作系统的删选,有点意思。下面的例子中,只有 MAC 相关的 case 能够执行。

PS: 标签的 compose 使用还是挺有意思的,就是在新的标签上添加已有标签实现功能继承,有机会可以深入看看他是怎么实现这个特性的。

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
class QuickGuide {
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
System.out.println("onlyOnMacOs");
}

@TestOnMac
void testOnMac() {
System.out.println("testOnMac");
}

@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
System.out.println("onLinuxOrMac");
}

@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
System.out.println("notOnWindows");
}

@Test
@EnabledOnOs(WINDOWS)
void onWindows() {
System.out.println("notOnWindows");
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs(MAC)
@interface TestOnMac {
}
}

// Disabled on operating system: Mac OS X
// notOnWindows
// testOnMac
// onlyOnMacOs
// onLinuxOrMac

除了操作系统,还有很多其他的筛选条件可供选择

  • 根据芯片架构,5.7 可配 @DisabledOnOs(architectures = "x86_64")
  • 根据JRE版本 @DisabledForJreRange(max = JAVA_11)
  • 根据JVM删选 @EnabledInNativeImage
  • 根据系统的 property 配置删选 @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
  • 根据环境变量 @EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
  • 自定义条件, 通过自己制定方法返回结果判断是否执行 UT, 示例如下,效果上和 ExecutionCondition 很像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class QuickGuide3 {
@Test
@EnabledIf("customCondition")
void enabled() {
System.out.println("enabled");
}

@Test
@DisabledIf("customCondition")
void disabled() {
System.out.println("disabled");
}

boolean customCondition() {
return true;
}
}
// enabled
// @DisabledIf("customCondition") evaluated to true

2.11 Test Instance Lifecycle

JUnit 会在每个 test case 执行的时候新建一个 test instance 来达到隔离的目的,如果想要每个 class 共用一个 test instance, 可以在 class 上添加 @TestInstance(Lifecycle.PER_CLASS) 注释

2.12. Nested Tests

通过 @Nested 注解可以将测试以一种更结构化的形式组织起来

2.13. Dependency Injection for Constructors and Methods

可以通过 TestInfo 作为测试或这 class 的参数,提供一些环境信息,比如 case 名字,method 名字,annotation 信息等。

2.14. Test Interfaces and Default Methods

介绍如何在不使用 mock lib 的情况下测试 interface,不是很明白,用到再看

2.16. Parameterized Tests

通过 @ParameterizedTest + @ValueSource 实现 TestNG 中 dataProvider 的效果

2.18. Dynamic Tests

动态生成测试用例

在测试 jackson API 的时候遇到一个很奇怪的问题,ObjectMapper 调用 print 之后再设置就不起作用了。下面的代码,最后的打印语句不能输出 protected 的 field,如果将第一个打印语句注释掉就可以。谷歌了半天没看到有人解答这个问题,汗,看来得自己看源码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SetVisibilityTest {
public String attr1 = "public attr";
protected String attr5 = "protected str";

public static void main(String[] args) throws Exception {
SetVisibilityTest bean = new SetVisibilityTest();

ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(bean));
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(bean));
}
}

呃。。。下了源码,读了 ObjectMapper 的文档,至少从使用方面找到了这个限制。

Mapper instances are fully thread-safe provided that ALL configuration of the
instance occurs before ANY read or write calls. If configuration of a mapper instance
is modified after first usage, changes may or may not take effect, and configuration
calls themselves may fail.

解决方案有两种,一个是使用 ObjectReader/Writer 代替,另一个是新建一个 mapper 或者使用 mapper.copy() 效果一样。不过我对为什么会有这种 behavior 还是很好奇,可以继续看看源码

Jackson 基本信息

官网 Github - jackson

主要由三部分组成

  • Streaming, jackson-core 底层实现
  • Annotation, jackson-annotation 注解相关
  • Databind, jackson-databind 我们直接使用的层,做 POJO 到 Json 的 map

貌似它里面还有关于对象树的处理,我对这种数据结构挺感兴趣,刚好看看他是怎么实现的。

今天在做 feature 的时候需要用到 SCIMono 这个项目里的 compliance test。看了一下它的执行逻辑,他是通过 jar 运行的,之前没有做过这种类型的东西,特别记录一下怎么创建类似的 jar 包。项目地址

idea 创建创建可执行 jar

  1. 创建测试 project,添加测试类 jk.Main 并添加测试方法 psvm 类型的即可
  2. 右键 project -> Open Module Settings -> 选中 Artifacts
  3. 在界面中间部分点击 ‘+’ 号,选择 JAR -> From modules with dependencies
  4. ‘Main Class:’ 中填入入口 class 的全路径, 也可以点文件夹 icon 选择,更方便
  5. 点击 OK, idea 自动为你生成 MANIFEST.MF 文件。借助 idea 很多配置自动生成了,避免了很多错误
  6. 在同一个界面的最右侧,勾选 ‘include in project build’,这个 jar 就会在 build 的时候自动创建,生产路径在同一界面给出了
  7. 选择窗口顶部的 Build, 然后选择 rebuild project 或者 build artifacts 都行,可以看到目录树那边有生产 out 文件夹,里面就有对应的jar
  8. 展开 out, 选中 jar 所在文件夹,右键 open in -> Terminal, 输入 java -jar executable-jar.jar jackjava -jar executable-jar.jar 测试

测试类代码如下

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
if(args.length > 0) {
System.out.println("Hello " + args[0]);
} else {
System.out.println("Hello world");
}
}
}
1
2
Manifest-Version: 1.0
Main-Class: jk.Main

通过 maven shade 插件简化创建过程

官方文档有挺详细的说明文档,还包括使用例子。基本流程可以概括为,在 pom 文件中添加配置,然后在项目中运行 maven package 即可, 不过我看好多文章都写的 maven clean package 也不知道哪里抄的。运行结束后可以在 target 下看到两个 jar 包,不以 origin 开头的就是我们需要的那个 jar 包。使用这个 plugin 省去了很多配置的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<!-- 指定入口程序 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>jk.Main</Main-Class>
</manifestEntries>
</transformer>
<!-- 整合 MF -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

其他问题

在 SCIMono 项目中,他将控制台收到的参数存到 System.properties 中,case 中从系统配置中那这些变量来使用。本地测试的时候可以在 UT 的 configuration 中添加 -Dkey=value 的方式添加这些配置

集合优化了对象存储,Streams则是关于一组对象的处理。Steams 是与任何特定存储机制无关的元素序列,流没有存储。

流可以在不使用赋值或可变数据的情况下,对有状态的系统建模,这非常有用

Java 8 通过在即可中添加 default 方法的方式,使流式编程整合到集合类中,流操作有三类:创建流,修改流元素,消费流元素。

OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为。

纯粹的函数是编程有额外约束,数据必须不可变:设置一次,永不改变。这种方式解决了多处理器时数据竞争的问题

Lambda 表达式是使用最小可能语法编写的函数定义

  • Lambda表达式产生函数,而不是类
  • Lambda语法尽可能少,这正是为了使Lambda易于编写和使用

Lambda表达式基本语法:

  1. 参数
  2. -> 可视为产出
  3. -> 之后的内容都是方法体

Lambda 表达式通常比匿名内部类产生更容易读懂的代码

方法引用:类名或对象名::方法名

函数式接口,只能包含一个方法,如果出现多个,会抛 exception(这个结论有问题 Function 这个接口就有多个方法)。这个接口可以添加 @FunctionalInterface 注释,但是可选项,不加也没问题。

java.util.function 包旨在创建一组完整的目标接口,使得我们一般情况下不需要再定义自己的接口。他们的命名规则如下:

  1. 如果处理对象而非基本类型,名称则为 Function, Consumer, Predicate 等。参数类型通过泛型添加。
  2. 如果接受基本类型,则由名称的第一部分表示,如 LongConsumer, DoubleFunction,IntPredicate 等,返回基本类型的 Supplier 例外
  3. 如果返回值为基本类型,则用 To 表示,如 ToLongFunction 和 IntToLongFunction
  4. 返回类型与参数类型一致,则是一个运算符,单个参数用UnaryOperator,两个参数用BinaryOperator
  5. 接收两个参数,返回布尔值用 Predicate
  6. 加收的两个参数类型不同,则名字中有一个 Bi

使用函数接口时,方法名无关紧要,只要参数类型和返回值相同即可。

高阶函数:指一个消费或产生函数的函数。

闭包:讲了一写变量作用域的问题,不过不太看的懂。大概是函数中的变量都相当于 final 吧,大概这个意思。

函数组合:多个函数组成新的函数,是函数式编程的基本组成部分。

柯里化:将一个多参的函数转化为一系列单参函数, 套娃表达式,理解上有点难读懂

纯函数式编程:Java支持并发,但是如果你的业务核心部分都是并发的,应该考虑使用 Scala或者 Clojure, 他们在一开始就为保持不变性而设计的。

章节摘抄

  • 新写的代码中有泛型支持的要加上检测,别去掉。e.g. List 而不是直接 List list=…
  • 消除非受检警告
  • 列表优先于数组
  • 优先考虑泛型 - 这个建议中给出的 Stack 例子,属实没有 get 到他要讲的点

泛型那段读不下去了,很多概念性的东西读起来很吃力,需要再重新看看泛型相关的知识点了,应该可以结合 Collection 来看效果会比较好。

请不要在新代码中使用原生态类型

原生态类型指的是原本可以使用泛型的地方,没有指定,比如 List 写出 List list =… 的情况

泛型给了你在编译器检测类型匹配的能力

List 是 List 的子类,而不是 List 的子类,语法上是这么规定的,挺神奇。

现在还能使用原生态类型是为了向下兼容,Java 1.5 才出的泛型

无限制通配符类型,比如 Set 的无限制形式 Set<?> 读作 ‘某个类型的集合’

Java 1.5 之后,只有两种情况允许使用原生态类型,一种是类 class 操作的时候,比如 List.class, String[].class, int.class。另一种是与 instanceof 相关的操作,比如 if(o instance of Set)

消除非受检警告

使用泛型是会碰到很多 unchecked warning,把他们都修复,这个可以保证代码运行时不会出现 ClassCastException.

列表优先于数组

数组是协变的(covariant),如果 sub 是 super 的子类,那么 sub[] 就是 super[] 的子类。

泛型是不可变的(invariant), 如果 Type1 是 Type2 的子类,List 和 List 没有继承关系。

作则认为上面的这种特性是数组的缺点。举个例子,下面两种变量都不能将 String 放入变量中,第一种要运行时才能发现,第二种编译期就抛错了。

1
2
3
4
5
Object[] a = new Long[1];
a[0]="I don't fit it"; // 可以添加,运行时出错

List<Object> b = new ArrayList<Long>(); // 编译时就会抛错
b.add("I don't fit it");

数组是具体化的,泛型则通过擦除机制保证代码的通用性。应为数组的类型不安全的,所以不能创建泛型数组,这种做法和泛型的定义相违背。

像 E, List, List 这样的类型称作不可具体化的(non-reifiable)类型,是运行时表示法包含的信息比编译时表示法包含的信息更好的类型。唯一可以具体化的参数化类型是无限制通配符,但是不常用

当遇到泛型数组创建错误时,最好的方案是用集合类型List 代替数组类型 E[], 这样可能会损失一些性能或者简洁性,但是却有更高的安全性和通用性。

一般来说,数组和泛型不能混用,如果混合使用时出现编译错误或警告,第一反应因该是用列表代替数组。