wangyuheng's Blog

time for change

项目源码地址: github

背景

利用plantuml绘制架构评审图时,发现数据库ER图手写字段信息成本太大,需要一个把DB表结构转换为plantuml格式的工具。
搜索了一番,没有发现支持工具,所以准备手撸一个,并记录下设计及编码实现的过程。

Read more »

What

产品经理、测试人员、开发人员统一在Gitlab中管理需求bug

Why -> 为什么通过Gitlab issue管理,而不是Jira、Redmine等工具?

  1. 开发团队最终交付物为项目代码,需求bug最终都会转换为一行代码、一次MR。通过issue可以让每一步都可以溯源。
  2. Gitlab issue更轻量,markdown语法让issue更专注于内容本身
Read more »

记录我所经历的微服务架构变更过程。

单体应用

初期最常见的一种架构模式,采用单体架构,服务端和页面在一个war包项目中。

优点

上线最快,用于验证业务模式

缺陷

  1. 页面通过jsp、velocity等服务端渲染,开发成本大,效率低。 e.g. java开发将html修改为jsp时产生布局样式偏差错误
  2. 前后台同时操作同一个DB表
  3. DW和BI订阅业务表,业务端暴露了存储结构并耦合
  4. 发生任何变动,需要整体部署

初探微服务

在业务深耕的过程中,单体应用已经难以应付业务复杂度和数据量。多需求同时迭代,在分支合并时总是带来意想不到的问题。而高耦合的应用导致,前端页面的变更,也需要所有服务发布。架构师为了救我们于水火之中,给我们带来了微服务。概括来讲,就是

  1. 服务端拆分,业务按db结构拆分为不同的微服务,上层产品服务调用微服务,负责聚合编排。
  2. 前后端分离,nodejs替换服务端渲染,负责页面展示、路由及登陆鉴权。

优点

  1. 前后端分离,职责清晰,开发效率提升,服务不需要频繁发布
  2. 通过微服务调用,隐藏内部表结构,拆库拆表
  3. 减小修改范围,快速部署
  4. 基于不同微服务的性能表现,针对性横向扩容

缺陷

  1. 基于表结构建模,导致众多贫血、CRUD的微服务
  2. 产品服务编排底层微服务,多次通信导致耦合
  3. 被BI、DW同步的表,无法拆分
  4. 调用链路增加导致测试以及问题排查成本增加

微服务治理

经过一段时间的积累沉淀,开发人员对微服务、对业务都有了更深层次的理解。为了更加明确业务领域,提升系统的扩展性、应对日益增多的流量以及服务于能力输出的开放平台,有必要对现有微服务进行一次治理。

  1. 通过框架,跟踪记录每次调用链路
  2. 提升CI/CD,节约部署成本
  3. 服务应该基于业务上线文,明确服务需要处理的业务及系统中位置,其次再考虑需要保存什么样的数据。基于业务领域,承担并暴露更具业务含义的接口
  4. 通过协同,替换编排,抽离配置业务流程。
  5. 通过消息中间件,解耦DB和其他部门之间的数据同步

优点

  1. 领域职责明确
  2. 扩展性强

缺陷

  1. 开发成本增加

总结

  1. 技术选型本质上是取舍判断,比较得到的优点以及不得不面临的缺陷
  2. 不存在最牛的架构,只有当前阶段最适合的架构。所以在这里,我列出每次架构变更时的优缺点,希望可以作为参考
  3. 技术架构和人员组织会相互影响
  4. 高内聚、低耦合

收集整理了一些实际业务场景下, 对Java8函数的使用.

预警: 代码较多, 比较无趣

阅读前提

  1. 了解java8的一些新特性, 如: Optional、 Stream
  2. 对函数式编程感兴趣

背景

一切的一切,源于不喜欢在代码中写太多的 ifelse.

scene & solution

1. Optional

Optional应该可以算作对null的一种优雅封装, 比如如下场景 我有一个枚举, 并且提供了一个parse方法, 可以根据key返回对应的枚举值.

1.1 enum parse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum BooleanEnum {    
// 封装boolean
TRUE(1, "是"),
FALSE(0, "否");

private Integer key;
private String label;

BooleanEnum(Integer key, String label) {
this.key = key;
this.label = label;
}

BooleanEnum parse(Integer key) {
...
return ...
}
}

但事实可能没那么美好, 如果这个parse方法传入了一个非法的key, 比如”-999”, 那么应该如何处理?

  1. 返回null
  2. 返回default
  3. 抛出IllegalException

可能性多了, 在调用不同方法时, 就需要考虑这个方法会选择哪种方案.
比如通过方法是否声明了IllegalException异常进行判断、对每个结果进行null!=result判断.
这时, 方法的提供者就应该考虑, 返回一个Optional对象, 方便调用方进行下一步的判断处理.

1
2
3
public static Optional<BooleanEnum> parse(Integer key) {
return Arrays.stream(values()).filter(i -> i.getKey().equals(key)).findAny();
}

1.2 nested isPresent()

基于上述思考, 我们会将方法的返回值通过Optional进行封装, 可能就出现了如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
private Optional<User> login_check_by_if(String username, String password) {
Optional<UserPO> optionalUserPO = findByUsername(username);
if (optionalUserPO.isPresent()) {
UserPO po = optionalUserPO.get();
if (password.equals(po.getPassword())) {
return Optional.of(convert(po));
} else {
return Optional.empty();
}
} else {
return Optional.empty();
}
}

这说明使用者知道了Optional可以方便进行非空判断, 但是远不止于此. Optional已经为我们开启了一个流, 所以基于惰性求值的原则, 我们可以将上述代码简化.

1
2
3
4
5
private Optional<User> login_check_by_map(String username, String password) {
return findByUsername(username)
.filter(po -> Objects.equals(po.getPassword(), password))
.map(this::convert);
}

1.3 .get().get().get()

我们在使用ServerWebExchange获取session值的时候, 为了保证健壮性, 需要针对每一级进行非空判断, 如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private String getTreasureIf(Exchange exchange) {
if (null != exchange) {
Session session = exchange.getSession();
if (null != session) {
Request request = session.getRequest();
if (null != request) {
return request.getTreasure();
} else {
return null;
}
} else {
return null;
}
} else {
return null;
}
}

基于上述原则, 同样可以通过Optional进行简化

1
2
3
4
5
6
private Optional<String> getTreasureOptionalMap(Exchange exchange) {
return Optional.ofNullable(exchange)
.map(Exchange::getSession)
.map(Session::getRequest)
.map(Request::getTreasure);
}

去掉了ifelse之后, 世界是不是清爽了很多?

2. Distinct field

mysql中, 针对field去重, 我们可以直接使用distinct. 那么在java中如何对一个list进行去重呢? 可能是这样

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
public static <T> List<T> removeDuplication(List<T> list, String... keys) {
if (list == null || list.isEmpty()) {
System.err.println("List is empty.");
return list;
}
if (keys == null || keys.length < 1) {
System.err.println("Missing parameters.");
return list;
}
for (String key : keys) {
if (StringUtils.isBlank(key)) {
System.err.println("Key is empty.");
return list;
}
}
List<T> newList = new ArrayList<T>();
Set<String> keySet = new HashSet<String>();
for (T t : list) {
StringBuffer logicKey = new StringBuffer();
for (String keyField : keys) {
try {
logicKey.append(BeanUtils.getProperty(t, keyField));
logicKey.append(SEPARATOR);
} catch (Exception e) {
e.printStackTrace();
return list;
}
}
if (!keySet.contains(logicKey.toString())) {
keySet.add(logicKey.toString());
newList.add(t);
} else {
System.err.println(logicKey + " has duplicated.");
}
}
return newList;
}

其实我们只是借助了Set值不可重复的特性, 为了工具类的通用性, 我们根据 key 和反射进行匹配.

听起来和看起来都很复杂, 我们尝试进行优化, java8已经允许我们使用函数作为入参, 所以我们可以把获取field的方法传入

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void should_return_2_because_distinct_by_age() {
userList = userList.stream()
.filter(distinctByKey(User::getName))
.collect(Collectors.toList());
userList.forEach(System.out::println);
assertEquals(2, userList.size());
}

private static <T, R> Predicate<T> distinctByKey(Function<T, R> keyExtractor) {
Set<R> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}

我们传入一个Function, 返回一个Predicate. 而Predicate正好是Stream#filter的入参.

3. Predicate union predicate

如果需要一个工具类,用来判断url是否符合设定的pattern.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

private boolean includedPathSelector(String antPattern, String urls) {
if (StringUtils.isEmpty(urls) || StringUtils.isEmpty(antPattern)) {
return false;
} else {
for (String url : urls.split(", ")) {
if (Objects.nonNull(url)) {
if (ANT_PATH_MATCHER.match(antPattern, url.trim())) {
return true;
}
}
}
}
return false;
}

如果antPattern也是一个数组集合, 那么就需要进行嵌套循环进行判断. 复杂到不想实现. 所以我们通过reduce&Predicate#or进行判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

private Predicate<String> includedPathSelector(String urls) {
if (StringUtils.isEmpty(urls)) {
return x -> false;
}
return Pattern.compile(", ").splitAsStream(urls)
.filter(Objects::nonNull)
.map(String::trim)
.map(this::ant)
.reduce(p -> false, Predicate::or);
}

private Predicate<String> ant(final String antPattern) {
if (null == antPattern) {
return input -> false;
}
return input -> ANT_PATH_MATCHER.match(antPattern, input);
}

这里希望说明的是返回Predicate对比直接返回boolean有一个好处是可以通过语义更明确的链接进行组合, 如andor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void should_return_true_when_one_of_split_item_match_predicate_or() {
Predicate<String> predicate1 = this.includedPathSelector("/permission");
Predicate<String> predicate2 = this.includedPathSelector("/config");
assertTrue(predicate1.or(predicate2).test("/config"));
assertTrue(predicate1.or(predicate2).test("/permission"));
}

@Test
public void should_return_false_when_one_of_split_item_match_predicate_and() {
Predicate<String> predicate1 = this.includedPathSelector("/permission");
Predicate<String> predicate2 = this.includedPathSelector("/config");
assertFalse(predicate1.and(predicate2).test("/config"));
assertFalse(predicate1.and(predicate2).test("/permission"));
}

4. map & group

Stream提供了丰富的分组、聚合功能. 比如业务中需要把List<Item>转换为一个Map<String,Item>, key为item的id,value为item本身, 或者item中的某个属性. 又或者基于某个属性字段进行分组, 得到一个Map<String,List<Item>>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void map_item_arr_by_uid() {
Map<String, List<Item>> uidMap = new HashMap<>();
for (Item item : list) {
if (uidMap.containsKey(item.getUid())) {
uidMap.get(item.getUid()).add(item);
} else {
List<Item> itemList = new ArrayList<>();
itemList.add(item);
uidMap.put(item.getUid(), itemList);
}
}
uidMap.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v)));
}

直接看如何利用Stream实现

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
@Data
@AllArgsConstructor
private static class Item {
private Long id;
private String uid;
}

private List<Item> list;

@Before
public void setUp() {
long id = 0L;
list = Arrays.asList(new Item(++id, "a"),
new Item(++id, "a"),
new Item(++id, "b"),
new Item(++id, "b"),
new Item(++id, "c"),
new Item(++id, "d")
);
System.out.println("--------------------");
}

@Test
public void map_item_by_id() {
Map<Long, Item> idMap = list.stream().collect(Collectors.toMap(Item::getId, Function.identity()));
idMap.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v)));
}

@Test
public void map_item_arr_by_uid() {
Map<String, List<Item>> uidMap = list.stream().collect(Collectors.groupingBy(Item::getUid, Collectors.toList()));
uidMap.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v)));
}

@Test
public void map_uid_arr_by_uid() {
Map<String, List<Long>> uidMapString = list.stream().collect(Collectors.groupingBy(Item::getUid, Collectors.mapping(Item::getId, Collectors.toList())));
uidMapString.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v)));
}

@Test
public void map_item_count_by_uid() {
Map<String, Long> uidMapCount = list.stream().collect(Collectors.groupingBy(Item::getUid, Collectors.mapping(Function.identity(), Collectors.counting())));
uidMapCount.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v)));
}

5. concurrent & paged

另一个业务中常见的操作是因为原数据比较大, 所以我们将数据拆分为多页, 并设计多个线程并发的进行相应的业务处理. 大致思路应该是

  1. 设定每页period, 计算页数
  2. 设定CountDownLatch
  3. 创建线程池
  4. 遍历list并通过subList拆分
  5. 线程处理对应的subList

java8允许将函数作为入参, 所以第5步的操作, 我们可以把处理逻辑作为入参传递. 但是上面4步, 如何操作呢?

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
private interface DivideHandler {
default int getPeriod() {
return 10;
}

default <T> void divideBatchHandler(List<T> dataList, Consumer<List<T>> consumer) {
Optional.ofNullable(dataList).ifPresent(list ->
IntStream.range(0, list.size())
.mapToObj(i -> new AbstractMap.SimpleImmutableEntry<>(i, list.get(i)))
.collect(Collectors.groupingBy(e -> e.getKey() / getPeriod(), Collectors.mapping(Map.Entry::getValue, Collectors.toList())))
.values()
.parallelStream()
.forEach(consumer)
);
}
}

class PrintDivideHandler implements DivideHandler {
@Override
public int getPeriod() {
return 2;
}

private void batchPrint(List<String> dataList) {
divideBatchHandler(dataList, System.out::println);
}
}

parallelStream会调用Fork/Join框架进行并行处理. 如果需要在程序中显性的指定线程数, 可以通过newForkJoinPool(threadNum).submit(()->{})进行封装

6. chain return notNull & fail-fast

场景如下: 我们需要获取name值, 但是有多个方式可以获取, 也可能获取不到. 所以我们结合业务要求与获取效率进行排序, 并且针对结果进行判断, 非空则返回.

听起来又是一连串的if.

针对这种case如何通过Stream优化呢? 依然是通过惰性求值以及函数入参.

我们先预设一个Stream逻辑, 其中顺序排列多个获取name值的函数, 然后统一执行.

1
2
3
4
5
6
7
8
9
10
11
12
private Optional<String> getOptionalName() {
Stream<Supplier<String>> nameStream = Stream.of(
this::getName1,
this::getName2,
this::getName3,
this::getName4
);

return nameStream.map(Supplier::get)
.filter(Objects::nonNull)
.findAny();
}

如何复杂度再提升, 需要获取多个值构造一个bean, 其中任意一个值为空, 那么就终止stream, 不去进行下一个值的获取操作.

此时可以利用flatMap进行连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Deprecated
private Optional<User> fillBuild() {
Stream<String> nameStreamWrapper = buildStream(Arrays.asList(this::getName1, this::getName2, this::getName3, this::getName4));
Stream<Integer> ageStreamWrapper = buildStream(Arrays.asList(this::getAge1, this::getAge2));
BuildUser buildUser = (name, age) -> Stream.of(new User(name, age));
return ageStreamWrapper.flatMap(age -> nameStreamWrapper.flatMap(name -> buildUser.build(name, age))).findAny();
}

private <T> Stream<T> buildStream(List<Supplier<T>> suppliers) {
return suppliers.stream().map(Supplier::get).filter(Objects::nonNull);
}

@FunctionalInterface
private interface BuildUser {
Stream<User> build(String name, Integer age);
}

加上了@Deprecated标记是因为这么写看起来比较丑, 降低了语义和阅读性.

总结

  1. 函数式编程带来更强的语义, 但是绝不仅是语法糖.
  2. 尝试用函数式思维重构复杂逻辑, 会有意外收获

所有代码及完整测试用例在 https://github.com/wangyuheng/java8-lambda-sample

http log

本文旨在讨论如何利用aop打印springboot http日志,包括访问日志、接口返回response,以及异常堆栈信息。

背景

为什么需要log

有过线上问题排查经历的同学都知道,日志可以给你提供最直白、最明确的信息。此外,通过对日志的收集、审计,可以对BI以及系统健壮性提供建议。尤其是新系统上线之初,系统稳定性不足,更需要日志监控。

Read more »

本文项目已发布到github,后续学习项目也会添加到此工程下,欢迎fork点赞。
https://github.com/wangyuheng/spring-boot-sample

国际化

简单来说,国际化就是让应用(app、web)适应不同的语言和地区的需要,比如根据地区选择页面展示语言。

i18n=internationalization,首末字符i和n,18为中间的字符数

原理

基于传入语言or地区标识进行判断,输出不同内容。伪代码如下:

Read more »

wikipedia的解释

弗兰兹·卡夫卡,生活于奥匈帝国统治下的捷克德语小说家,本职为保险业职员。主要作品有小说《审判》、《城堡》、《变形记》等。

kafka官网的解释

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量分布式 发布订阅 消息系统,它可以处理消费者规模的网站中的所有动作流数据。

两个kafka有一个共同特点: 很会写

消息系统

实现低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。

Read more »

项目源码地址: github

背景

通过plantuml绘制数据库ER图,手写字段信息成本太大。需要一套DDL转换plantuml的工具。

方案

  1. 读取同目录下ddl.sql文件
  2. 基于 ; 分割建表语句
  3. 依赖 druid 进行sql解析
  4. 定义plantuml模版
  5. replaceAll替换模版内容
Read more »
0%