微笑卡卡西

~黑哈瑞之家~


  • 首页

  • 标签

  • 分类

  • 归档

  • 关于

BACnet 101 - BACnet介绍

发表于 2019-07-16 | 分类于 IoT , BACnet

BACnet 101 - BACnet介绍

Ref: https://www.csimn.com/CSI_pages/BACnet101.html

BACnet概要视图

BACnet代表建筑自动化和控制网络。它由ASHRAE于1996年正式发布,并已被批准为ISO标准16454-5,并正在成为欧盟的标准。

BACnet IP和BACnet MS/TP是标识用于在网络上传输BACnet数据包的物理层(IP或MS/TP)的名称。至少还定义了另外三个物理层,并在一定程度上使用它们。我们将在本网站上讨论IP和MS/TP,因为这是两个最常见的物理层,而且这两个物理层是控制解决方案在其产品线中支持的。

Modbus谈论寄存器,BACnet谈论对象和对象属性。例如,传感器就是一个对象。如果它的属性是它的现值,这个属性在BACnet中被称为,现值属性(The Present Value property)。传感器很可能是两种对象类型之一:模拟输入(温度、湿度等)或二进制输入(感应传感器、开关等)。最常用的属性是现值,但还需要其他属性。其他属性包括状态标志(故障、服务停止)、可靠性,以及其他一些可选的属性,如高限制和低限制。

更多更好的Backnet资料,请参考www.bacnet.org](http://www.bacnet.org/Tutorial/index.html).

BACnet的基础背景知识

BACnet设备在网络上定义为“对象”的集合Oobjecs)。典型的对象包括模拟输入、模拟输出、二进制输入、二进制输出和更复杂的对象,如调度。主要通过网络对对象的属性读写进行消息处理。模拟输入最常引用的属性是“现值(Present Value)”,这通常意味着来自传感器或物理设备的数据。与模拟输入对象相关的其他属性,例如,包括故障状态、可靠性、对象名称、最小和最大限制等。BACnet协议标准为每个对象类型定义了必需的和可选的属性。BACnet设备的制造商文档将列出设备中包含哪些对象类型,以及每个对象中包含哪些可选属性。

除了对象的定义,BACnet协议还定义了“服务(Services)”。这些服务包括对象访问服务、警报和事件服务、文件访问服务等等。对象访问服务是最常用的,因为它们提供对对象属性的基本“读/写”访问。

访问对象属性需要指定以下参数:

  • 设备实例(即,网络上的哪个设备)

  • 对象类型(模拟输入、二进制输入等)

  • 对象实例(即,哪个模拟输入)

  • 属性(当前值、对象名称、状态标志等)

输入对象(Input Objects)很简单,它们只有一个与之关联的“现值”。输出对象(Output Objects)被称为“可指令的(commandable)”,它们更加有趣。因为这个创造了多个算法一起控制一个对象的可能,通过有优先级的指令请求(Command Request)来控制对象,多个指令的时候最高优先级的指令有效;当高优先级指令被取消(Relinquished)时,次一优先级的指令接替生效。对象会维护一个指令优先队列来管理所有的指令(共有16个优先级别)。

因此,对于输出对象的操作需要额外两个参数:

  • 指令优先级
  • 对应优先级上指令的级别

对一个”可指令的”对象去读它的现值(Present Value),则这个读的指令将会是最高优先级的,并且不可被取消。对于其他指令,通过向可执行的对象发送”取消(relinquish)”即可(这个应该是一个特殊的访问方式,应该需要制定之前的指令信息才行)。

BACnet物理层(Physical Layer)

BACnet协议定义了用于物理电信号、寻址、网络访问、错误检查、流控、表示和消息格式等。该协议侧重于构建自动化应用程序。

BACnet IP

BACnet IP使用一个标准的UDP/IP堆栈来发送和接收消息(参见下面UDP/IP的定义)。在大多数情况下,可以在MS/TP链接上找到的包被封装在UDP/IP包中,称为BACnet IP。设备使用IP地址和以太网MAC地址,就像其他UDP/IP网络设备一样。没有主从令牌传递的概念,因为以太网本质上是自动对等的。设备只是随意地将数据传输给它们的目标接收方,让以太网根据需要处理数据包冲突和重试。

BACnet MS/TP

MS/TP代表主从令牌传递。当链路上的每个设备都有令牌时,它被认为是“主设备”。如果不需要立即使用令牌,则需要将令牌传递给下一个设备。这是“令牌传递”部分。链路上当前没有令牌的所有设备都被视为从设备,并被期望侦听当前主设备可能为它提供的任何消息。因为所有的设备轮流成为主设备,所以这个链接实际上是点对点的。

BACnet Application Layer

真正有意思的地方,所有消息处理都在应用层处理,包括设备寻址。IP地址将通过UDP/IP堆栈层获得一个BACnet包。然后应用程序层决定如何处理BACnet消息。可能的消息类型有一个完整的列表,但是最常用的是“读取现值(read present value property)”,另一个“最常用”的是COV通知(值的更改,Change of Value)。


BACnet术语表

UDP/IP - 大家都听说过TCP/IP,与这个一样,UDP/IP也是一个常见的以太网协议栈,不同之处在于TCP是一个”连接(connection)”协议,而UDP是一个”无连接(connectionless)”协议;TCP协议更安全,但为了连接需要额外的开销来确保数据包的安全送达,而UDP速度和效率更高。BACNet IP使用UDP协议,主要原因是BACnet的消息通常都很短,并且彼此独立没有依赖。

Object - 构成BACnet设备的传感器,执行器或其他功能元素的引用概念,属于协议的一部分;常用的对象有模拟输入对象和模拟输出对象。

Object Property - 每个对象都具有BACnet协议所需的几个属性。最常用的属性是现值。其他常见属性包括可靠性和状态标志。对象的可选属性,如模拟输入,包括最小和最大范围,高和低的限制,等等。

COV - Change Of Value - 当设备对象的属性值根据默认参数、或者写指令变化的时候,可以通过向设备订阅COV通知来获取COV通知消息(意译,需要跟进测试确认)。

BBMD - BACnet/IP广播管理设备(BACnet/IP Broadcast Management Device),用于跨大型网络的BACnet实现。BACnet典型的应用场景在一个园区,尽管在协议涉及上支持广域全国范围的网络建设。BACnet设备的运行,需要广播机制来广播消息。标准的IP技术在路由网关规定是不转发广播消息的,BBMD设备在多个不同网络域(IP子网)中进行广播转发到本地网络,来实现跨IP网络的BACnet/IP网络广播实现。IP域(子网中)只有一个设备充当BBMD即可实现广播消息的转发。(目前场景中,如果只在一个子网中建设BACnet,不用考虑BBMD的构建)

MS/TP Mac Address or Station ID - 这是一个8位数字,对于主/从设备来说是0-127。此Mac地址在RS-485链路上本地使用,用于链路上的物理地址设备,不经过路由器。它相当于Modbus RTU从地址。

MS/TP Max Masters - 通过发起”主设备投票(poll for master)的活动来决定MS/TP Link网络中有多少设备可以作为主设备,如果所有的设备都不匹配,会发生不可预期问题。

BACnet IP Addressing - 标准以太网IP地址用于标识IP网络上的设备。IP地址用于物理路由网络上的消息,同时用BACnet设备实例标识BACnet系统中的设备。

Device Instance - 这是与BACnet有关的逻辑地址。无论是在MS/TP链路还是IP网络上,设备实例在所有子网和路由链路上都是惟一的。(补充:访问设备的参数顺序为:设备示例、对象类型、对象示例、属性)

Client/Server versus Master/Slave - 我们倾向于在MS/TP级别考虑主服务器和从服务器,当我们讨论IP网络时,我们开始考虑客户机/服务器。在功能上,客户机和主服务器是同步的,而服务器和从服务器是同步的。

JPA中Model主键使用UUID

发表于 2019-07-07 | 分类于 技术

前言

使用Spring JPA项目进行开发,有关Model的设计,PK如何选择一直是个课题,特别对于集群和微服务的部署架构引入进来之后,一直在找一个平衡的解决方案,最近一个项目中又深入讨论了这个问题,参考国外一个博客https://phauer.com/2016/uuids-hibernate-mysql/的内容,翻译整理如下,供以后参考。

关于数据库PK和UUID选择(MySQL)

  • 参考翻译自:https://phauer.com/2016/uuids-hibernate-mysql/

  • 目的:考量数据库PK的选择和UUID的使用

  • 使用UUID的优势:

    • UUID全局唯一;
    • 跨数据库唯一,合并数据,没有主键冲突;
    • 分布式复制和存储(分布式数据库)方便简单;
    • 可以随时生成UUID,不需要数据库支撑,方便测试和批量数据生成;
    • 自动增量ID容易猜测会导致安全问题。
  • 使用UUID的缺点:

    • 数据库索引的空间大小增大;单个UUID需要16个字节,普通int只需要4个字节;
    • 使用数据库SQL进行特定数据查询时会更加麻烦,下面会详细描述;
    • REST接口中使用UUID会增加payload的大小。
  • 使用UUID创建表:

    • 不建议使用VARCHAR(36)作为UUID数据类型,建议使用BINARY(16)作为字段类型;会降低空间占用,16个字节,没有字符串中的中线字符,数据库索引也会更快;
    • 不过,这样会造成SQL查询稍稍复杂些。
    1
    2
    3
    4
    CREATE TABLE product (
    `id` BINARY(16) NOT NULL primary key
    ,`name` varchar(64)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • JPA映射:

    1
    2
    3
    4
    @Id
    @GeneratedValue(generator = "uuid2")
    @GenericGenerator(name = "uuid2", strategy = "uuid2")
    @Column(columnDefinition = "BINARY(16)")
  • SQL脚本生成UUID

    1
    SELECT uuid(); /*dbe07414-49d1-11e6-b7a7-0242ac140002*/
  • 插入UUID数据

    • uuid()函数返回字符串,包含中线字符,需要使用replace去掉中线字符,然后使用unhex()函数转换十六进制可看字符到UUID数值二进制,最终存入BINARY(16)字段中。
    1
    2
    3
    4
    INSERT INTO product VALUES(
    unhex(replace(uuid(), '-', ''))
    , "car"
    );
  • 查询UUID

    • 使用hex()函数转换UUID到十六进制字符表示。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    SELECT id, name FROM product;
    /* BLOB, 'car' */
    SELECT hex(id), name FROM product;
    /* 'BFF641BA9F3A4584A1BA53824E7AB3B9', 'car' */

    /* 通过特定UUID进行指定Row数据查询 */
    SELECT hex(id), name FROM product
    WHERE id = unhex('BFF641BA9F3A4584A1BA53824E7AB3B9');

    /* or if you have a UUID with dashes: */
    SELECT hex(id), name FROM product
    WHERE id = unhex(replace("2b08e375-275d-473e-910d-32700e34b61a", '-', ''));
  • 小结

    • 在工程的角度,使用UUID作为Model的主键,应该是利大于弊;
    • 使用BINARY(16)存储,性能损耗在可接受范围;
    • 对于SQL层面操作的不便,个人认为可以忍受(不过不同DB操作语言可能会有差异)。

Flutter与vscode分析异常错误处理

发表于 2019-05-31 | 分类于 Flutter , VSCode

Flutter与vscode分析异常错误处理

使用Flutter和VSCode开发一段时间,之前一直使用v1.2.1进行开发,配合XCode调试ios代码,Android Studio调试安卓代码,一直相安无事,最近升级到v1.5.4-hotfix.2之后,遇到奇怪的现象,一周多没有解决,今天总算解决了,记录一下。

1、问题现象:

在v1.5.4-hotfix.2下面,发现pubspec.yaml中定义的dependencies都正常通过flutter packages get能够下载,并且运行flutter run或者直接在vscode中运行APP都没有问题。

但是在vscode中,依赖的一些package在import的时候会出现红色错误提示(Target of URI doesn’t exist: ),对应class引用也会有错误提示(The method ‘xxxx’ isn’t defined for the class)。

实质上上面的import和引用都没有问题。

2、经过观察,和网上调查,都说packages get然后重启vscode可以解决,实际并没有。

但是通过重启vscode,发现刚刚打开工程的时候,在状态栏中提示【Analyzing】很快就结束了,不太正常。

观察vscode状态栏右边的警铃标记位【1】,点击能够看到错误信息:

1
Exception from the Dart analysis server: FileSystemException(path=/Users/user/workspace/ios/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/example/ios/.symlinks/plugins/flutter_test/exampl...

3、对于上面的错误,网上google,各种尝试,都没有解决。

重装了vscode、重装flutter、删除所有的flutter和dart以及pub的缓存,都没有解决。

无奈之下,重新降级flutter版本回到v1.2.1,发现问题解决,但是不少使用的依赖库已经更新到dart的2.2.0版本,必须手工处理太多plugin的版本兼容问题。

最终还是决定在执行flutter upgrade到v1.5.4-hotfix.2,但是升级之后,问题依旧,因此应该跟系统环境没有关系。

4、详细观察了一下警铃标记提示的错误信息,发现是跟ios的pod依赖产生的【.symlinks】目录有关,这个目录是之前通过xcode调试ios代码的时候遗留的,之前没有问题,可能flutter升级之后依赖有问题。

5、尝试删除【.symlinks】之后,重启vscode,发现之前不能正常工作的【Analyzing】恢复了正常。

6、选在IPhone模拟器,运行测试之后,发现在【example/ios】目录中又出现了【.symlinks】目录,xcode打开Runner工程,也能够在Pods依赖中发现对应功能。

这个问题应该只是在对plugin的example功能测试的时候会发生这种情况,也应该是跟v1.5.4-hotfix.2版本问题,不过不影响大局,测试ios之后及时手工删除【.symlinks】目录基本不影响工作了。

7、问题已经报告到flutter的Issue列表: https://github.com/flutter/flutter/issues/33592

Flutter和Dart的异步编程

发表于 2019-04-19 | 分类于 APP编程

Flutter和Dart的异步编程

参考:

  • https://medium.com/@takahirom/how-to-write-flutter-asynchronous-processing-22f845204f30
  • https://www.youtube.com/watch?v=MUDOIAssBDs&feature=youtu.be

使用Flutter和Dart开发APP或者应用时,通常需要访问DB、API、或其他资源,都需要异步处理进行性能提升。

作为Flutter开发组件库,提供了两个组件直接支持异步的UI构建处理(https://flutter.io/widgets/async/)。

  • FutureBuilder: 基于Future的最新数据来构建Widget。
  • StreamBuilder: 基于数据流Stream的数据来构建Widget(可以支持多个数据持续更新构建,BLOC的架构就是依次模式构建)。

Dart Asynchronous 基础

Future表示一个数据还没有存在:

1
2
3
4
5
6
abstract class Future<T> {
Future<R> then<R>(FutureOr<R> onValue(T value), {Function onError});
Future<T> catchError(Function onError, {bool test(Object error)});
Future<T> whenComplete(FutureOr action());
...
}

Stream标识一个异步的迭代对象,有顺序数据、可以操作和过滤数据;迭代是拉(Pull)模式访问数据,Stream是推(Push)模式访问数据。

1
2
3
4
5
6
7
8
abstract class Stream<E>{
listen(void onData(E data), ...);

Stream map(Function f);
Stream<E> where(Function f);
Future<E> get first;
void forEach(void f(E element));
}

Stream & Future例子:

1
2
3
4
5
6
7
8
9
runServer(){
var future = HttpServer.bind('127.0.0.1', 4040);
future.then((HttpServer server){ // a Stream
server.listen((HttpRequest request){
request.reponse.write('Hello, world!');
request.response.close();
});
});
}

问题是使用了太多的Callback级联,通过使用await语法糖可以简化,清晰逻辑。

过渡方案,使用for循环优化例子:

1
2
3
4
5
6
7
runServer(){
var server = HttpServer.bind('127.0.0.1', 4040);
for (HttpRequest request in server.requests) {
request.reponse.write('Hello, world!');
request.response.close();
});
}

await优化的例子:

1
2
3
4
5
6
7
runServer() async{
var server = await HttpServer.bind('127.0.0.1', 4040);
server.listen((HttpRequest request){
request.reponse.write('Hello, world!');
request.response.close();
});
}

await for继续优化:

1
2
3
4
5
6
7
runServer() async{
var server = await HttpServer.bind('127.0.0.1', 4040);
await for (HttpRequest request in server.requests) {
request.reponse.write('Hello, world!');
request.response.close();
});
}

关于Await的用法,对于Future进行等待期执行结果,在代码块(方法)内,await把Future的异步处理变成了同步调用关系(Process);同时await返回Future的泛型结果数据。

1
2
3
4
API:
Future<List<String>> File.readAsLines();
Async use:
List<String> lines = await file.readAsLines();

await也可以使用在非Future对象上的任意变量上,等价于Future的value包装。

1
2
3
await 998;
==
await new Future.value(988);

闭包的使用:

1
2
foo(() async => ...);
foo(() async {...});

Bodywrap与microtask:

1
2
3
Future runServer() async {...}
==
Future runServer() => new Future.miscrotask((){...});

Bodywrap的运行结果和影响(Consequences):

  • 在代码块中的错误会被自行捕获,并返回到Future中;
  • Functions yield at entry (???)
  • 返回的Futures可以被链条处理(chained);
  • 返回结果是Future类型

异步返回多个数据,需要使用Stream,一个数据的时候使用Future。同步的时候使用Iterable返回多个数据。

同步多个数据迭代处理(Iterable & sync*):

1
2
3
4
5
6
Iterable range(int from, int to) sync* {
for (init i = from; i < to; i++) {
yield i;
}
}
range(3, 6).forEach(print); //Prints 3 4 5

yield推送一个数据,yeild*在另外一个迭代之间建立管道,推送数据到上层迭代。

1
2
3
4
5
6
Iterable recRange(int from, int to){
if (from < to){
yield from;
yield* recRange(from + 1, to);
}
}

异步返回多个数据到Streams中,使用async*:

1
2
3
4
5
6
Stream bigFiles(List<String> fileNames) async* {
for (var fileName in fileNames) {
var stat = await new File(fileName).stat();
if (stat.size > 100000) yield fileName;
}
}

Async*的内部细节:

  • 函数直到listen方法调用的时候才会开始执行;
  • cancel方法可以终结async函数;
  • Yield通过event loop返回数据。
1
while (true) { yield 988 }

异步编程的问题:

  • Debug调试困难
    • 处理交错执行
    • 堆栈帧丢失
  • 当Debug时,stacktrace有太多的Noice,解决办法,使用stack_trace包简化调用处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
import 'package:stack_trace/stack_trace.dart';

foo() async{
await 'asynchronous wait';
throw 'error';
}
bar() => foo();
gee() => bar();
main(){
Chain.capture(gee, onError: (e, chain){
print(chain.terse);
});
}

Dart Asynchronous Processing

Dart语言支持异步处理流程,通常的用法有如下7种模式。

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
import 'dart:async';

main() async{
// ① 使用Future和Then级联同步调用
asyncFunc1().then((_) => asyncFunc2()).then((result) {
print(result);
});

// ② 使用await同步调用两个异步方法
var asyncResult1 = await asyncFunc1();
var asyncResult2 = await asyncFunc2();
print(asyncResult2);

// ③ 使用await Future.wait同步等待两个异步调用并发执行结束
var list = await Future.wait([asyncFunc1(), asyncFunc2()]);
print(list[1]);

// ④ 把Future转换成Stream模式调用
asyncFunc1().asStream().asyncMap((_) => asyncFunc2()).listen((result) {
print(result);
});

// ⑤ 把Future转换成Stream,然后使用await for循环Stream的结果同步处理
var stream = asyncFunc1().asStream().asyncMap((_) => asyncFunc2());
await for (var result in stream) {
print(result);
}

// ⑥ 把Future转换成Stram模式调用
asyncFunc1()
.asStream()
.asyncExpand((_) => asyncFunc2().asStream())
.listen((result) {
print(result);
});

// ⑦ 把Future转换成Stream并用yield方式级联
asyncFunc1().asStream().asyncExpand((_) async* {
yield await asyncFunc2();
}).listen((result) {
print(result);
});
}

Future<String> asyncFunc1() async {
return "async return1";
}

Future<String> asyncFunc2() async {
return "async return2";
}

关于什么时候使用Future,什么时候使用Stream。

对于多个数据的异步处理流程,只能使用Stream,对于一般异步处理使用Future和Then更加有利于代码逻辑组织。

1
2
3
4
5
6
7
8
9
10
11
12
Future<String> asyncFunc() async {
return "async return";
}
var asyncResult = await asyncFunc();
print(asyncResult);
// vs
Stream<String> streamFunc() async* {
yield "stream return";
}
streamFunc().listen((result) {
print(result);
});

This allows you to choose whether to use FutureBuilder or StreamBuilder depending on your situation.

“await for” 和 “listen()”

在大多数场景,使用 wait for会应该更清晰表达逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'dart:async';
import 'dart:io';
import 'dart:math';

main() async {
await for (var tick in endlessCountDown()) {
print("coutDown $tick");
}
// endlessCountDown().listen((tick) {
// print("coutDown $tick");
// });
}

Stream<int> endlessCountDown() async* {
var i = pow(2, 30) - 1;
while (true) {
sleep(new Duration(seconds: 1));
yield i--;
}
}

但是使用await和listen还是有区别,一个是同步流程,一个是异步流程。例如下面这个例子,使用await,后一个countUp永远也不会被执行到。

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
import 'dart:async';
import 'dart:io';
import 'dart:math';

main() async {
await for (var tick in endlessCountDown()) {
print("coutDown $tick");
}
await for (var line in endlessCountUp()) {
print("countUp $line");
}
}

Stream<int> endlessCountUp() async* {
var i = 0;
while (true) {
sleep(new Duration(seconds: 1));
yield i++;
}
}

Stream<int> endlessCountDown() async* {
var i = pow(2, 30) - 1;
while (true) {
sleep(new Duration(seconds: 1));
yield i--;
}
}

当然,单独的处理封装成异步方法,再调用,就可以把await的操作又异步化组合一起了,但是有这个必要吗?这个时候也许使用listen更好一下吧,具体看情况而定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'dart:async';
import 'dart:io';
import 'dart:math';

main() {
listenCountDown();
listenCountUp();
}

listenCountDown() async {
await for (var tick in endlessCountDown()) {
print("coutDown $tick");
}
}

listenCountUp() async {
await for (var tick in endlessCountUp()) {
print("coutUp $tick");
}
}
...

这是用listen的例子,是不是在这种状况下会更好一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'dart:async';
import 'dart:io';
import 'dart:math';

main() {
endlessCountDown().listen((tick) {
print("coutDown $tick");
});
endlessCountUp().listen((tick) {
print("coutUp $tick");
});
}
...

注意 在Stream上使用await for的时候,一定要清晰认识到你是否需要等待所有的Stream数据都到达处理完后再执行后续操作。这个在很多场合都是不适用的,例如侦听Dom的事件,如果对Dom两个节点进行侦听事件的处理,就不能适用await for,而应该使用listen异步侦听模式。

简单的说,await for是同步拆解出所有的stream数据并处理,在特定场景需要这样做,另外一些场景缺不能这样做。

“await” 、 “then()” 和 “await Future.wait()”

1
2
3
4
5
6
7
8
9
10
11
12
13
// ① 使用Future和Then级联同步调用
asyncFunc1().then((_) => asyncFunc2()).then((result) {
print(result);
});

// ② 使用await同步调用两个异步方法
var asyncResult1 = await asyncFunc1();
var asyncResult2 = await asyncFunc2();
print(asyncResult2);

// ③ 使用await Future.wait同步等待两个异步调用并发执行结束
var list = await Future.wait([asyncFunc1(), asyncFunc2()]);
print(list[1]);

同上,await是同步等待数据并执行,then是异步侦听模式;而await Future.wait()是多个异步并发执行,并等待所有异步操作执行返回。

下面使用Stopwatch来比较各个处理流程需要的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ②
Stopwatch stopwatch1 = new Stopwatch()..start();
var asyncResult1 = await asyncFunc1();
var asyncResult2 = await asyncFunc2();
print(asyncResult2);
print('await() executed in ${stopwatch1.elapsed}');

// ③
Stopwatch stopwatch2 = new Stopwatch()..start();
var list = await Future.wait([asyncFunc1(), asyncFunc2()]);
print(list[1]);
print('Future.wait() executed in ${stopwatch2.elapsed}');

Future<String> asyncFunc1() async {
return new Future.delayed(const Duration(seconds: 1), () => "async return1");
}

Future<String> asyncFunc2() async {
return new Future.delayed(const Duration(seconds: 1), () => "async return2");
}

根据以上的例子,②用的时间为2秒多些,③用的时间为1秒多些,③更好的并行使用了线程的时间。

因此如果业务场景适合,使用Future.wait会有更好的性能输出。

最后,一个例子展示如何使用FutureBuilder

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
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:github_flutter/access_token.dart';
import 'package:github_flutter/model/event.dart';
import 'package:github_flutter/model/user.dart';
import 'package:http/http.dart' as http;

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.deepPurple,
),
home: new FutureBuilder(future: new Future(() async {
var token = await AccessToken.getInstance().getOrCreate();
var user = await fetchUser(token);
return await fetchEvents(user, token);
}), builder: (BuildContext context, AsyncSnapshot<EventList> feedState) {
if (feedState.error != null) {
// TODO: error handling
}
if (feedState.data == null) {
return new Center(child: new CircularProgressIndicator());
}
return new MyHomePage(title: 'GitHub Events', events: feedState.data);
}),
);
}

Future<User> fetchUser(String token) async {
var userResponse =
await http.get('https://api.github.com/user?access_token=' + token);
return new User.fromJson(json.decode(userResponse.body));
}

Future<EventList> fetchEvents(User user, String token) async {
var response = await http.get('https://api.github.com/users/${user
.login}/events?access_token=' +
token);
print(response.body);
final responseJson = json.decode(response.body);
return new EventList.fromJson(responseJson);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title, this.events}) : super(key: key);

final String title;
final EventList events;

@override
_MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
void _incrementCounter() {
setState(() {});
}

@override
Widget build(BuildContext context) {
var notifiationList = widget.events.events;
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView.builder(
itemCount: notifiationList.length,
itemBuilder: (context, index) {
return new ListTile(
onTap: _launchURL(notifiationList[index]
.url
.replaceFirst("api.github.com/repos", "github.com")),
title: new Text('${notifiationList[index]
.repoFullName} : ${notifiationList[index].type}'),
);
}),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
));
}

_launchURL(String url) {
return () async {
if (await canLaunch(url)) {
await launch(url);
} else {
throw 'Could not launch $url';
}
};
}
}

Flutter的Bloc工程化系列3

发表于 2019-04-18 | 更新于 2019-04-19 | 分类于 APP编程

APP启动页(Splash Page)

说明: 本部分完成部分在分支: https://github.com/dragonetail/flutterpoc/tree/3-splash_page

目标: 涉及Flutter的Router和Splash Page工作原理,全程使用Bloc模式。

思路:

  • 使用Model定义Splash过程所涉及的数据,引导页、广告页、同服务器交互更新图片所有的数据Model。
  • 存储数据Splash所用数据到SP(shared_preferences)中。
  • 提供HTTP API通过后端Rest更新Splash显示所用图片和标题,控制显示动作。
  • 抽象Service提供对SP和HTTP数据的访问封装。
  • 定义Bloc,根据SP中获取的数据进行视图Builder构建,包括默认数据行为设置。
  • 实现Page页面;
  • 通过Common封装实现引导页、广告页的View。
  1. Model数据:

    引导页数据模型。

    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
    class SplashGuideModel {
    bool isUrl;
    List<String> images;
    List<String> textInfos;

    SplashGuideModel({this.isUrl, this.images, this.textInfos});

    SplashGuideModel.fromJson(Map<String, dynamic> json)
    : isUrl = json['isUrl'],
    images = json['images'].cast<String>(),
    textInfos = json['textInfos'].cast<String>();

    Map<String, dynamic> toJson() => {
    'tiisUrltle': isUrl,
    'images': images,
    'textInfos': textInfos,
    };

    @override
    String toString() {
    StringBuffer sb = new StringBuffer('{');
    sb.write("\"isUrl\":\"$isUrl\"");
    sb.write(",\"images\":\"$images\"");
    sb.write(",\"textInfos\":\"$textInfos\"");
    sb.write('}');
    return sb.toString();
    }
    }

    广告页数据模型:

    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
    class SplashAdModel {
    String title;
    String imageUrl;
    String targetUrl;

    SplashAdModel({this.title, this.imageUrl, this.targetUrl});

    SplashAdModel.fromJson(Map<String, dynamic> json)
    : title = json['title'],
    imageUrl = json['imageUrl'],
    targetUrl = json['targetUrl'];

    Map<String, dynamic> toJson() => {
    'title': title,
    'imageUrl': imageUrl,
    'targetUrl': targetUrl,
    };

    @override
    String toString() {
    StringBuffer sb = new StringBuffer('{');
    sb.write("\"title\":\"$title\"");
    sb.write(",\"imageUrl\":\"$imageUrl\"");
    sb.write(",\"targetUrl\":\"$targetUrl\"");
    sb.write('}');
    return sb.toString();
    }
    }

    网络通信用的数据模型:

    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
    import './index.dart';

    class SplashModel {
    String version;
    bool nextShowGuide;
    bool updateGuideInfo;
    bool updateAdInfo;
    SplashGuideModel guideInfo;
    SplashAdModel adInfo;

    SplashModel(
    {this.version,
    this.nextShowGuide,
    this.updateGuideInfo,
    this.updateAdInfo,
    this.guideInfo,
    this.adInfo});

    SplashModel.fromJson(Map<String, dynamic> json)
    : version = json['version'],
    nextShowGuide = json['nextShowGuide'],
    updateGuideInfo = json['updateGuideInfo'],
    updateAdInfo = json['updateAdInfo'],
    guideInfo = json['guideInfo'] == null
    ? null
    : SplashGuideModel.fromJson(json['guideInfo']),
    adInfo = json['adInfo'] == null
    ? null
    : SplashAdModel.fromJson(json['adInfo']);

    Map<String, dynamic> toJson() => {
    'version': version,
    'nextShowGuide': nextShowGuide,
    'updateGuideInfo': updateGuideInfo,
    'updateAdInfo': updateAdInfo,
    'guideInfo': guideInfo?.toJson(),
    'adInfo': adInfo?.toJson(),
    };

    @override
    String toString() {
    StringBuffer sb = new StringBuffer('{');
    sb.write("\"version\":\"$version\"");
    sb.write("\"nextShowGuide\":\"$nextShowGuide\"");
    sb.write(",\"updateGuideInfo\":\"$updateGuideInfo\"");
    sb.write(",\"updateAdInfo\":\"$updateAdInfo\"");
    sb.write(",\"guideInfo\":\"$guideInfo\"");
    sb.write(",\"adInfo\":\"$adInfo\"");
    sb.write('}');
    return sb.toString();
    }
    }
  1. SP访问封装工具类(shared_preferences):

    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
    import 'dart:async';
    import 'dart:convert';
    import 'package:shared_preferences/shared_preferences.dart';

    /// SpUtils spUtils = SpUtils();
    /// or SpUtils spUtils = SpUtils.instance;
    /// await spUtils.initializationDone; //初始化完成
    class SpUtils {
    //单体实例
    static final SpUtils _singleton = new SpUtils._internal();

    //工厂模式 和 静态实例
    factory SpUtils() => _singleton;
    static SpUtils get instance => _singleton;

    Future _doneFuture;
    Future get initializationDone => _doneFuture;

    //私有构造
    SpUtils._internal() {
    _doneFuture = _init();
    }

    SharedPreferences _prefs;
    Future _init() async {
    _prefs = await SharedPreferences.getInstance();
    }

    Future<bool> putObject(String key, Object value) {
    return _prefs.setString(key, value == null ? "" : json.encode(value));
    }

    Map getObject(String key) {
    String _data = _prefs.getString(key);
    return (_data == null || _data.isEmpty) ? null : json.decode(_data);
    }

    Future<bool> putObjectList(String key, List<Object> list) {
    List<String> _dataList = list?.map((value) {
    return json.encode(value);
    })?.toList();
    return _prefs.setStringList(key, _dataList);
    }

    List<Map> getObjectList(String key) {
    List<String> dataLis = _prefs.getStringList(key);
    return dataLis?.map((value) {
    Map _dataMap = json.decode(value);
    return _dataMap;
    })?.toList();
    }

    String getString(String key, {String defaultValue: ''}) {
    return _prefs.getString(key) ?? defaultValue;
    }

    Future<bool> putString(String key, String value) {
    return _prefs.setString(key, value);
    }

    bool getBool(String key, {bool defaultValue: false}) {
    return _prefs.getBool(key) ?? defaultValue;
    }

    Future<bool> putBool(String key, bool value) {
    return _prefs.setBool(key, value);
    }

    int getInt(String key, {int defaultValue: 0}) {
    return _prefs.getInt(key) ?? defaultValue;
    }

    Future<bool> putInt(String key, int value) {
    return _prefs.setInt(key, value);
    }

    double getDouble(String key, {double defaultValue: 0.0}) {
    return _prefs.getDouble(key) ?? defaultValue;
    }

    Future<bool> putDouble(String key, double value) {
    return _prefs.setDouble(key, value);
    }

    List<String> getStringList(String key, {List<String> defaultValue: const []}) {
    return _prefs.getStringList(key) ?? defaultValue;
    }

    Future<bool> putStringList(String key, List<String> value) {
    return _prefs.setStringList(key, value);
    }

    dynamic getDynamic(String key, {Object defaultValue}) {
    return _prefs.get(key) ?? defaultValue;
    }

    bool haveKey(String key) {
    return _prefs.getKeys().contains(key);
    }

    Set<String> getKeys() {
    return _prefs.getKeys();
    }

    Future<bool> remove(String key) {
    return _prefs.remove(key);
    }

    Future<bool> clear() {
    return _prefs.clear();
    }
    }

    实现Dart语言模式的单体类(Singleton)模式,由于shared_preferences的初始化时异步的,需要调用方使用前通过await spUtils.initializationDone;完成初始化工作才能正常后续使用。(注意: 整体体验上这一点在Flutter和Dart语言设计上还是比较复杂的,对一些必须的系统依赖,需要额外关注异步和同步处理流程之间的衔接。)

  1. 封装Http库Dio的实现。

    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
    import 'dart:io';
    import 'dart:convert';

    import 'package:dio/dio.dart';
    export 'package:dio/dio.dart';
    import 'package:cookie_jar/cookie_jar.dart';
    import 'package:flutter/foundation.dart';

    var dio = new MyDio();

    class MyDio extends Dio {
    MyDio([BaseOptions options]) : super(options) {
    super.interceptors
    ..add(CookieManager(CookieJar()))
    ..add(LogInterceptor(responseBody: true));
    (super.transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
    super.options.receiveTimeout = 15000;
    super.options.validateStatus = (int status) {
    return status >= HttpStatus.ok && status < HttpStatus.multipleChoices ||
    status == HttpStatus.notModified;
    //return status >= HttpStatus.ok && status < 300 || status == 304;
    };
    //TODO 认证Token追加到Header的处理
    }
    }

    // Must be top-level function
    _parseAndDecode(String response) {
    return jsonDecode(response);
    }

    parseJson(String text) {
    return compute(_parseAndDecode, text);
    }

    //全局方便约束的变量
    final Options expectHttpOK = Options(
    validateStatus: (int status) {
    return status == HttpStatus.ok; //200
    },
    );

    final Options expectHttpCreated = Options(
    validateStatus: (int status) {
    return status == HttpStatus.created; //201
    },
    );

    构建全局变量dio,实现了继承Dio子类MyDio,封装了通用初始化设置:Cookie处理、日志、JSON转换(reponse中的ContentType是application/json; charset=utf-8的时候,会自动把结果JSON字符串转换成JSON Map)、访问超时、默认返回有效状态校验。

  1. 封装后端数据访问API。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import './http_dio.dart';
    import 'package:flutterpoc/models/index.dart';

    class SplashApi {
    //Online JSON REST mock server, all data is in db.json
    //具有1分钟数据缓存,测试需要等待数据刷新
    static const String splash_base_url =
    'https://my-json-server.typicode.com/dragonetail/flutterpoc';

    static Future<SplashModel> getSplashMode([String version = '']) async {
    {
    Response response = await dio.get(
    splash_base_url + '/splash',
    queryParameters: {"version": version},
    options: expectHttpOK,
    );
    print(response.data);
    return SplashModel.fromJson(response.data);
    }
    }
    }

    封装后端访问数据的API实现。这里使用了my-json-server.typicode.com利用Github上的JSON文件实现模拟REST JSON访问请求。

  1. 封装Model数据的访问Service。

    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
    import 'dart:convert';

    import 'package:flutterpoc/models/index.dart';
    import 'package:flutterpoc/common/index.dart';
    import 'package:flutterpoc/api/index.dart';

    class SplashService {
    static const String splash_version = 'splash.version';
    static const String splash_ad_mode = 'splash.ad.mode';
    static const String splash_ad_model = 'splash.ad.model';
    static const String splash_guide_model = 'splash.guide.model';

    static final SpUtils spUtils = SpUtils.instance;

    static bool isSplashAdMode() {
    return spUtils.getBool(splash_ad_mode);
    }

    static void clearSplashAdMode() {
    spUtils.remove(splash_ad_mode);
    }

    static void setSplashAdMode() {
    spUtils.putBool(splash_ad_mode, true);
    }

    static SplashAdModel getSplashAdModel([SplashAdModel defaultValue]) {
    String _splashModel = spUtils.getString(splash_ad_model);
    if (CommonUtils.isNotEmpty(_splashModel)) {
    Map userMap = json.decode(_splashModel);
    return SplashAdModel.fromJson(userMap);
    }
    return defaultValue;
    }

    static SplashGuideModel getSplashGuideModel([SplashGuideModel defaultValue]) {
    String _splashModel = spUtils.getString(splash_guide_model);
    if (CommonUtils.isNotEmpty(_splashModel)) {
    Map userMap = json.decode(_splashModel);
    return SplashGuideModel.fromJson(userMap);
    }
    return defaultValue;
    }

    static void updateSplashInfo() {
    String version = spUtils.getString(splash_version) ?? '';

    Future<SplashModel> future = SplashApi.getSplashMode(version);
    future.then((SplashModel splash) {
    spUtils.putString(splash_version, splash.version ?? '');

    if (splash.nextShowGuide) {
    clearSplashAdMode();
    }

    if (splash.updateAdInfo && splash.adInfo != null) {
    String _jsonStr = json.encode(splash.adInfo.toJson());
    spUtils.putString(splash_ad_model, _jsonStr);
    }

    if (splash.updateGuideInfo && splash.guideInfo != null) {
    String _jsonStr = json.encode(splash.guideInfo.toJson());
    spUtils.putString(splash_guide_model, _jsonStr);
    }
    }, onError: (error) {
    print(error);
    });
    }
    }

    实现了对SP数据访问的封装,并提供一个对API访问后端数据的更新到SP的封装。

  1. 构建BLOC,组合数据和状态控制。

    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
    import 'dart:async';
    import 'package:flutterpoc/common/index.dart';
    import 'package:flutterpoc/models/index.dart';
    import 'package:flutterpoc/services/index.dart';

    class SplashBloc extends BaseBloc<BaseEvent, BaseState> {
    @override
    BaseState get initialState {
    _init();

    return SplashInitialState();
    }

    void _init() async {
    //等待SP初始化完毕
    await SpUtils.instance.initializationDone;
    if (SplashService.isSplashAdMode()) {
    this.dispatch(SplashAdEvent());
    } else {
    this.dispatch(SplashGuideEvent());
    }

    //3秒之后获取服务器最新的Splash信息,更新本地存储
    Future.delayed(const Duration(seconds: 3), () {
    SplashService.updateSplashInfo();
    });
    }

    @override
    Map<Type, Function(BaseEvent)> get mapper => {
    SplashGuideEvent: _mapSplashGuideEventToState,
    SplashAdEvent: _mapSplashAdEventToState,
    };

    Stream<BaseState> _mapSplashGuideEventToState(BaseEvent event) async* {
    SplashGuideModel splashGuideModel =
    SplashService.getSplashGuideModel(SplashGuideModel(
    isUrl: false,
    images: [
    Utils.getImgPath('guide1', format: 'jpg'),
    Utils.getImgPath('guide2', format: 'jpg'),
    Utils.getImgPath('guide3', format: 'jpg'),
    Utils.getImgPath('guide4', format: 'jpg'),
    ],
    textInfos: [
    "在你需要的每个地方",
    "载你去往每个地方",
    "懂你,更懂你所行",
    "因为在意,所以用心",
    ],
    ));
    yield SplashGuideState(splashGuideModel);
    }

    Stream<BaseState> _mapSplashAdEventToState(BaseEvent event) async* {
    SplashAdModel splashAdModel = SplashService.getSplashAdModel(SplashAdModel(
    title: '带你去旅行',
    imageUrl:
    'https://raw.githubusercontent.com/dragonetail/flutterpoc/master/assets/images/3.0x/ad.jpg',
    targetUrl: 'https://github.com/dragonetail/flutterpoc/',
    ));
    yield SplashAdState(splashAdModel);
    }
    }

    //Event定义
    class SplashGuideEvent extends BaseEvent {}

    class SplashAdEvent extends BaseEvent {}

    //State定义
    class SplashInitialState extends BaseState {
    SplashInitialState() : super();
    }

    class SplashGuideState extends BaseState {
    final SplashGuideModel splashGuideModel;

    SplashGuideState(this.splashGuideModel) : super([splashGuideModel]);
    }

    class SplashAdState extends BaseState {
    final SplashAdModel splashAdModel;

    SplashAdState(this.splashAdModel) : super([splashAdModel]);
    }

    定义了两个状态:SplashGuideEvent、SplashAdEvent;定义了三个状态:SplashInitialState、SplashGuideState、SplashAdState。

    通过Event和状态实现Bloc状态迁移控制。

  1. Page页面构造,实现状态对应Widget的构建和组装。

    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
    import 'package:flutter/material.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'package:flutterpoc/common/index.dart';
    import 'package:flutterpoc/models/index.dart';
    import 'package:flutterpoc/blocs/index.dart';
    import 'package:flutterpoc/services/index.dart';

    class SplashPage extends StatefulWidget {
    SplashPage({Key key}) : super(key: key);

    @override
    _SplashPageState createState() => _SplashPageState();
    }

    class _SplashPageState extends State<SplashPage> {
    final SplashBloc _splashBloc = SplashBloc();

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    body: BlocBuilder<BaseEvent, BaseState>(
    bloc: _splashBloc,
    builder: (BuildContext context, BaseState baseState) {
    if (baseState is SplashGuideState) {
    return buildSplashGuideWidget(baseState);
    } else if (baseState is SplashAdState) {
    return buildSplashAdWidget(baseState);
    } else {
    return Container();
    }
    },
    ),
    );
    }

    Widget buildSplashAdWidget(SplashAdState state) {
    SplashAdModel splashAdModel = state.splashAdModel;
    return SplashADPage(
    adImageUrl: splashAdModel.imageUrl,
    skipActionTitle: '跳过',
    skipAction: _skipAction,
    adAction: _adAction,
    count: 3,
    logoImagePath: Utils.getImgPath('splash_logo'),
    );
    }

    Widget buildSplashGuideWidget(SplashGuideState state) {
    SplashGuideModel splashGuideModel = state.splashGuideModel;
    return SplashGuidePage(
    images: splashGuideModel.images,
    textInfos: splashGuideModel.textInfos,
    nextActionTitle: '立即启程',
    nextAction: _nextActionFromGuide);
    }

    void _nextActionFromGuide() {
    SplashService.setSplashAdMode();
    Navigator.of(context).pushReplacementNamed('/Main');
    }

    void _skipAction() {
    Navigator.of(context).pushReplacementNamed('/Main');
    }

    void _adAction() {
    if (!(_splashBloc.currentState is SplashAdState)) {
    return;
    }
    SplashAdModel splashAdModel =
    (_splashBloc.currentState as SplashAdState).splashAdModel;

    Navigator.of(context).pushReplacementNamed('/Main');
    NavigatorUtil.pushWeb(context,
    title: splashAdModel.title, url: splashAdModel.targetUrl);
    }

    @override
    void dispose() {
    _splashBloc.dispose();
    super.dispose();
    }
    }
  1. APP入口定义SplashPage的使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class MyApp extends StatelessWidget {
    // This widget is the root of your application.
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    routes: {
    '/Main': (ctx) => CounterPage(),
    },
    home: SplashPage(),
    );
    }
    }
  1. Splash共通组件: SplashGuidePage、SplashADPage,构建对应具体画面布局和内容显示。代码请直接查看Github。
  1. 结论: 在整体上,使用Bloc架构、包括SP本地存储、HTTP API远程访问在一起的Splash实现,整体组件隔离还是相当清晰简单的。

Flutter的Bloc工程化系列2

发表于 2019-04-08 | 分类于 APP编程

BLOC重构Counter实现(BLOC逻辑处理工程简化)

说明: 本部分完成部分在分支: https://github.com/dragonetail/flutterpoc/tree/2-counter_refactor_simplication

目标: 根据Flutter缺省计数器例子,遵循Bloc的理念实现增减计数器的逻辑和视图分离,在第一步的基础上,抽取BaseBloc实现,简化Bloc的处理逻辑和工程化。

  1. 在的counter_bloc.dart基础上,抽取BaseBloc、BaseEvent、BaseState三个抽象基类。

    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
    import 'dart:async';
    import 'package:bloc/bloc.dart';
    import 'package:equatable/equatable.dart';
    import 'package:meta/meta.dart';

    abstract class BaseBloc<Event extends BaseEvent, State extends BaseState>
    extends Bloc<Event, State> {
    //Event到State映射函数表
    Map<Type, Function(Event)> get mapper;

    @override
    Stream<State> mapEventToState(
    Event event,
    ) async* {
    if (mapper == null) {
    return;
    }
    Type type = event.runtimeType;
    var function = mapper[type];
    if (function == null) {
    return;
    }
    yield* function(event);
    }
    }

    @immutable
    abstract class BaseEvent extends Equatable {
    BaseEvent([List props = const []]) : super(props);
    }

    @immutable
    abstract class BaseState extends Equatable {
    BaseState([List props = const []]) : super(props);
    }

    对mapEventToState进行了默认实现,根据Map<Type, Function(Event)> get mapper定义的【Event到State映射函数表】进行业务逻辑函数的dispatch操作,简化业务逻辑的if级联判断。

  2. 再看看counter_bloc.dart的实现:

    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
    import 'dart:async';
    import '../blocs.dart';

    class CounterBloc extends BaseBloc<CounterEvent, CounterState> {
    @override
    CounterState get initialState => CountingState(0);

    @override
    Map<Type, Function(CounterEvent)> get mapper => {
    IncrementEvent: _mapIncrementEventToState,
    DecrementEvent: _mapDecrementEventToState,
    };

    Stream<CounterState> _mapIncrementEventToState(CounterEvent event) async* {
    yield CountingState((currentState as CountingState).counter + 1);
    }

    Stream<CounterState> _mapDecrementEventToState(CounterEvent event) async* {
    yield CountingState((currentState as CountingState).counter - 1);
    }
    }

    //Event定义
    abstract class CounterEvent extends BaseEvent{
    CounterEvent([List props = const []]) : super(props);
    }
    class IncrementEvent extends CounterEvent{}
    class DecrementEvent extends CounterEvent{}

    //State定义
    abstract class CounterState extends BaseState{
    CounterState([List props = const []]) : super(props);
    }
    class CountingState extends CounterState {
    final int counter;

    CountingState(this.counter) : super([counter]);

    @override
    String toString() {
    return 'CountingState { counter: $counter }';
    }
    }

    通过重载Map<Type, Function(CounterEvent)> get mapper => {定义了两个Event类到对应处理方法的映射表。

    统一映射方法的处理,接受CounterEvent event,返回Stream<CounterState>。

    同时简化Event和State的定义,整个所有Bloc相关的内容都放到一个文件中,减少文件拆分,简化工程化开发的复杂度。

    【研讨】在这个抽取过程中,通过BaseEvent、BaseState的抽取,技术上应该不需要CounterEvent、CounterState进行过渡,直接在映射方法的参数和返回值上使用BaseEvent、BaseState,是不是会更好?

  1. 结论: 整体上,这样基本对于业务逻辑拆分隔离来说,复杂度已经降低到类似Spring框架的水平附近了。

Flutter的Bloc工程化系列1

发表于 2019-04-07 | 更新于 2019-04-18 | 分类于 APP编程

BLOC重构Counter实现(Flutter的Bloc工程化系列1)

说明: 本部分完成部分在分支: https://github.com/dragonetail/flutterpoc/tree/counter_refactor

目标: 根据Flutter缺省计数器例子,遵循Bloc的理念实现增减计数器的逻辑和视图分离。

  1. 使用fluter命令创建APP,然后在pubspec.yaml中追加依赖

    1
    2
    3
    4
    cupertino_icons: ^0.1.2
    flutter_bloc: ^0.9.1
    meta: ">=1.1.6 <2.0.0"
    equatable: ^0.2.0
  2. 环境准备,使用vscode,安装bloc的插件辅助代码生成。
    参考: https://felangel.github.io/bloc/#/blocvscodeextension>
    直接用这个链接安装: https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc

  3. 创建目录结构:

    1
    2
    3
    4
    lib
    blocs #BLOC逻辑代码
    pages #页面代码
    widgets #组件代码
  4. 在blocs目录下面上右键菜单【Bloc: new bloc】(第二部插件提供功能),输入bloc名字为counter,将创建如下:

    1
    2
    3
    4
    5
    6
    blocs
    bloc # 需要手工更改为counter
    bloc.dart
    counter_bloc.dart
    counter_event.dart
    counter_state.dart

    更改生成的bloc目录为counter

  5. 在blocs下面创建blocs.dart和simple_bloc_delegate.dart,结果文件列表:

    1
    2
    3
    4
    5
    6
    7
    8
    blocs
    counter
    bloc.dart
    counter_bloc.dart
    counter_event.dart
    counter_state.dart
    blocs.dart
    simple_bloc_delegate.dart
  6. counter目录下面生成的文件内容和修改结果:

    • 首先是package的索引,bloc.dart,是当前目录下面内容的export索引文件。内容:
    1
    2
    3
    export 'counter_bloc.dart';
    export 'counter_event.dart';
    export 'counter_state.dart';
    • 事件counter_event.dart文件,为计数器业务逻辑的事物定义。根据Bloc官网资料https://felangel.github.io/bloc/#/fluttercountertutorial的做法看,可以简单使用Enum来实现Event定义,使用int实现state的处理。作为工程化探讨,这里使用标准的Bloc做法进行处理。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      import 'package:equatable/equatable.dart';
      import 'package:meta/meta.dart';

      @immutable
      abstract class CounterEvent extends Equatable {
      CounterEvent([List props = const []]) : super(props);
      }

      class IncrementEvent extends CounterEvent {
      IncrementEvent() : super();

      @override
      String toString() => 'IncrementEvent';
      }

      class DecrementEvent extends CounterEvent {
      DecrementEvent() : super();

      @override
      String toString() => 'DecrementEvent ';
      }

      这个地方使用了增强Equatable组件,Bloc官网没有说明,应该属于增强组件相等性判断的实现,暂不追究原理。

      根据Bloc的模板,首先实现了一个抽象类,这个抽象类声明了【@immutable】,则其所有子类都不可变更,即不能有可变的成员变量(表达某种特定事件实例的含义)。

      上面例子,通过实例化实现了两个Event类型,IncrementEvent和DecrementEvent。

      【最佳实践】查看了Bloc的其他示例,这个地方规约没有统一化,建议所有的Event实例类都以Event结尾。

    • 计数器状态控制counter_state.dart:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      import 'package:equatable/equatable.dart';
      import 'package:meta/meta.dart';

      @immutable
      abstract class CounterState extends Equatable {
      CounterState([List props = const []]) : super(props);
      }

      class CountingState extends CounterState {
      final int counter;

      CountingState(this.counter) : super([counter]);

      @override
      String toString() {
      return 'CountingState { counter: $counter }';
      }
      }

      同Event,state的实现也基本一样,通过【@immutable】约束了所有State类为不可变(表达某种特定状态实例的含义)。

      上面例子,实例化实现了一个状态CountingState,表达了技术过程中的某一时刻的状态,其有一个成员变量counter表示计数器的状态,通过构造函数初始化,不可改变。

      【最佳实践】所有状态实例类以State结尾。

      【其他】同Swift语言一样,Dart语言也依赖变量直接暴露出来进行数据交互,不使用setter和getter方法。

    • 业务逻辑counter_bloc.dart实现:

      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
      import 'dart:async';
      import 'package:bloc/bloc.dart';
      import './bloc.dart';

      class CounterBloc extends Bloc<CounterEvent, CounterState> {
      @override
      CounterState get initialState => CountingState(0);

      @override
      Stream<CounterState> mapEventToState(
      CounterEvent event,
      ) async* {
      if (event is IncrementEvent) {
      yield* _mapIncrementEventToState();
      } else if (event is DecrementEvent) {
      yield* _mapDecrementEventState(event);
      }
      }

      Stream<CountingState> _mapIncrementEventToState() async* {
      yield CountingState((currentState as CountingState).counter + 1);
      }

      Stream<CountingState> _mapDecrementEventState(DecrementEvent event) async* {
      yield CountingState((currentState as CountingState).counter - 1);
      }
      }

      首先需要通过重载实现initialState的实现,Bloc框架会实现当前bloc的状态,通过变量currentState访问。

      业务逻辑主要放到mapEventToState进行处理,根据Bloc的实践例子,这个地方主要是通过event类型进行判断,调用不同的本地私有map函数进行分类处理。

      mapEventToState的返回类型是Stream<State>类型,表达状态流。

  7. blocs下面blocs.dart和simple_bloc_delegate.dart:

    • blocs.dart是export索引。

    • simple_bloc_delegate.dart是根据官网示例对Bloc的默认delegate进行扩展,追加Debug日志功能:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      import 'package:bloc/bloc.dart';

      class SimpleBlocDelegate extends BlocDelegate {
      @override
      void onTransition(Transition transition) {
      super.onTransition(transition);
      print(transition);
      }

      @override
      void onError(Object error, StackTrace stacktrace) {
      super.onError(error, stacktrace);
      print(error);
      }
      }

      定义了这个,需要在APP启动时在main.dart中进行设定使用。

      1
      2
      3
      4
      5
      void main() {
      BlocSupervisor().delegate = SimpleBlocDelegate();

      runApp(MyApp());
      }
  8. 改写MyHomePage:

    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
    import 'package:flutter/material.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'package:flutterpoc/pages/pages.dart';
    import 'package:flutterpoc/blocs/blocs.dart';

    class MyHomePage extends StatefulWidget {
    MyHomePage({Key key, this.title}) : super(key: key);

    final String title;

    @override
    _MyHomePageState createState() => _MyHomePageState();
    }

    class _MyHomePageState extends State<MyHomePage> {
    final CounterBloc _counterBloc = CounterBloc();

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: BlocProvider<CounterBloc>(
    bloc: _counterBloc,
    child: CounterPage(),
    ),
    );
    }
    }

    沿用缺省示例生成的StatefulWidget,并在State中定义了CounterBloc,然后再body处使用BlocProvider把 【bloc: _counterBloc】注入(DI)到Bloc的context中,然后在CounterPage中使用final CounterBloc _counterBloc = BlocProvider.of<CounterBloc>(context);可以获取之前注入的bloc变量。

  9. 页面CounterPage的实现:

    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
    import 'package:flutter/material.dart';
    import 'package:flutter_bloc/flutter_bloc.dart';
    import 'package:flutterpoc/blocs/blocs.dart';
    import 'package:flutterpoc/widgets/widgets.dart';

    class CounterPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    final CounterBloc _counterBloc = BlocProvider.of<CounterBloc>(context);

    return Scaffold(
    body: BlocBuilder<CounterEvent, CounterState>(
    bloc: _counterBloc,
    builder: (BuildContext context, CounterState counterState) {
    return Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    Text(
    'You have pushed the button this many times:',
    ),
    Text(
    '${(counterState as CountingState).counter}',
    style: TextStyle(fontSize: 24.0),
    ),
    ],
    ),
    );
    },
    ),
    floatingActionButton: Column(
    crossAxisAlignment: CrossAxisAlignment.end,
    mainAxisAlignment: MainAxisAlignment.end,
    children: <Widget>[
    PaddingFloatingActionButton(
    icon: Icons.add,
    onPressed: () => _counterBloc.dispatch(IncrementEvent()),
    ),
    PaddingFloatingActionButton(
    icon: Icons.remove,
    onPressed: () => _counterBloc.dispatch(DecrementEvent()),
    )
    ],
    ),
    );
    }
    }

    完成了页面布局和内容组合。使用StatelessWidget实现widget构造。通过使用final CounterBloc _counterBloc = BlocProvider.of<CounterBloc>(context);获取之前父节点注入的bloc变量。

    通过body: BlocBuilder<CounterEvent, CounterState>(构造实际关键bloc业务的UI组件。

    通过'${(counterState as CountingState).counter}'访问State的值,更新显示。

    通过通用封装组件PaddingFloatingActionButton实现浮动按钮的可重用组件封装。

    【综合】在PaddingFloatingActionButton的onPressed事件中,调用bloc发射事件onPressed: () => _counterBloc.dispatch(IncrementEvent()),。在Bloc的实现中,根据mapEventToState进行事件到State的映射转换,转换成State后,通过Stream<CounterState>激活body: BlocBuilder<CounterEvent, CounterState>(的重构(重绘)操作,实现Widget的局部更新。

  10. 最终更改入口main.dart:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import 'package:flutter/material.dart';
    import 'package:bloc/bloc.dart';
    import 'package:flutterpoc/blocs/blocs.dart';
    import 'package:flutterpoc/pages/pages.dart';

    void main() {
    BlocSupervisor().delegate = SimpleBlocDelegate();

    runApp(MyApp());
    }

    class MyApp extends StatelessWidget {
    // This widget is the root of your application.
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
    }
    }
  11. 运行截屏:

    增减计数器运行结果

BLOC参考

参考:

  • https://pub.dartlang.org/packages/bloc
  • https://pub.dartlang.org/packages/flutter_bloc
  • https://felangel.github.io/bloc/
  • https://github.com/felangel/bloc/tree/master/examples/

面试故事-Java的Map篇

发表于 2019-03-12 | 更新于 2019-04-08 | 分类于 面试

前言

在面试与被面之间游荡,把面试作为一项事情来做。

营养早餐

HashMap、Hashtable和TreeMap的区别

  1. 都实现了Map接口,(Key,Value)元组存储逻辑概念,内部采用Hash(rehash)算法提供高效的访问速度。
  2. Hash的计算依赖于Key的hashCode和equals实现。
  3. HashMap允许null作为Key和Value出现,而Hashtable不允许,对于null键值会抛异常。(补充HashMap的null键值对应的value会被存储到内部条目Array的0位,业务层面可以作为默认值的用途来使用,个人意见是不建议使用,使用业务含义清晰的Key常量作为默认值也许会是更好的选择)。
  4. Hashtable的方法是线程安全的,内部方法之间使用Synchronized同步保护;而HashMap和TreeMap需要使用者自己保障线程安全机制。
  5. 因为线程机制的原因,Hashmap的操作会比Hashtable更高效一些(对于Web环境编程的小白来说,正常业务上,建议直接使用Hashtable,除非遇到性能集中爆发的地方,再考虑HashMap吧。补充,Java5提供了一个ConcurrentHashMap,没有太多调查和详细对比,从JDK的API文档来看,是建议使用这个类替代Hashtable的)。
  6. Hashtable和HashMap的iterator、keySet()、values()都是(类)视图方式返回,即fail-fast模式,这种模式下,除非用返回对象直接remove来变更Map,其他方式或进程对Map的修改,都会在下一次hasNext或者next方法的时候(有可能会)触发ConcurrentModificationException。Hashtable提供的keys()和elements()返回的Enumeration不是fail-fast模式的视图化操作模式,是clone模式。
  7. 迭代访问数据的顺序时,HashMap和Hashtable不保证数据的顺序,并且不能保证数据顺序根据时间保持不变;TreeMap能够根据键值的比较器迭代访问整个条目。
  8. 对于TreeMap,如果比较器不允许空值,使用null的Key会引发异常。
  9. 三个详细比较
1
2
3
4
5
6
7
                 | HashMap | Hashtable | TreeMap
-------------------------------------------------------
iteration order | no | no | yes
null key-value | yes-yes | no-no | no-yes
synchronized | no | yes | no
time performance | O(1) | O(1) | O(log n)
implementation | buckets | buckets | red-black tree

fail-fast与fail-safe

  1. 这是对Java的Collections相关类的操作接口的一个实现和使用概念;
  2. 白话来说,fail-fast是以View(视图)的模式对原有Collection数据的访问,这个时候除了直接用View(通常是Iterator接口)进行remove对象,其他方式对原有Collection的增删改都可能会触ConcurrentModificationException](https://docs.oracle.com/javase/8/docs/api/java/util/ConcurrentModificationException.html)。
  3. 而fail-safe则不是以View模式,而是以Clone模式。
  4. fail-fast能够访问到最新的数据内容;fail-safe只能访问到clone时点的数据。
  5. fail-fast通过算法框架控制访问,没有空间需求;fail-safe会clone数据,有空间需求,对于大的数据集合,需要注意。
  6. 简单的来说java.util下面的Collection对象都是使用fail-fast模式(不完全准确,个别特殊接口除外,例如Hashtable的keys()和elements()接口),常见例如:HashMap、Vector、ArrayList、HashSet;java.util.concurrent包下都是用fail-safe模式,例如:CopyOnWriteArrayList、ConcurrentHashMap。

红黑树(Red-black Tree)

红黑树(Red–black tree)同AVL(根据发明者Adelson-Velsky 和 Landis命名)都是一种自平衡二叉查找树,都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。除了O(log n)的时间之外,红黑树的持久版本对每次插入或删除需要 O(log n)的空间。

红黑树的特点:

  • 节点是红色或黑色。
  • 根是黑色。
  • 所有叶子都是黑色(叶子是NIL节点)。
  • 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  • 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

Map相关常用操作

遍历数据

1
2
3
4
5
6
for(Entry entry: map.entrySet()) {
// get key
K key = entry.getKey();
// get value
V value = entry.getValue();
}
1
2
3
4
5
6
7
8
Iterator itr = map.entrySet().iterator();
while(itr.hasNext()) {
Entry entry = itr.next();
// get key
K key = entry.getKey();
// get value
V value = entry.getValue();
}

根据Key排序(需要业务保障Key不能出现null)

1
2
3
4
5
6
7
8
List list = new ArrayList(map.entrySet());
Collections.sort(list, new Comparator() {

@Override
public int compare(Entry e1, Entry e2) {
return e1.getKey().compareTo(e2.getKey());
}
});

使用TreeMap排序

1
2
3
4
5
6
7
8
SortedMap sortedMap = new TreeMap(new Comparator() {

@Override
public int compare(K k1, K k2) {
return k1.compareTo(k2);
}
});
sortedMap.putAll(map);

根据Value进行排序

1
2
3
4
5
6
7
8
9
List list = new ArrayList(map.entrySet());
Collections.sort(list, new Comparator() {

@Override
public int compare(Entry e1, Entry e2) {
return e1.getValue().compareTo(e2.getValue());
}

});

静态Map

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

private static final Map map; //并能真的内容静态,使用的代码仍然能够使用put方法操作Map对象的内容
static {
map = new HashMap();
map.put(1, "one");
map.put(2, "two");
}
}

public class Test {

private static final Map map;
static {
Map aMap = new HashMap();
aMap.put(1, "one");
aMap.put(2, "two");
map = Collections.unmodifiableMap(aMap); //固化保护Map的内容是静态
//如果使用put方法,将会抛出UnsupportedOperationException
}
}

双向映射Map

有时,我们需要一组键对,互相映射,允许通过Key查找,也允许通过Value“反向查找”。JDK不支持双向映射,需要使用两个Map对象来进行保存和处理。

Apache Common Collections 和 Guava 的 BidiMapand BiMap都实现了这类功能。

Map拷贝

多数Map实现类都实现了使用构造方法,传入另外一个Collection对象来实现数据的内容的复制,但是这个复制不是同步的,会出现上面的fail-fast问题(TODO 需要探究或验证)。

正确的用法是使用Collections.synchronizedMap()。

空Map

很少会有需要,但是某个时候调用一些接口需要Map,但是不需要数据的时候创建一个空Map也是需要的。

1
2
3
map = Collections.emptyMap();  //返回的是内容也不可变的Map

map = new HashMap();

LinkedHashMap

  1. 同TreeMap和HashMap一样,特殊的是其迭代顺序是其插入的顺序;并且根据这个博客来看,每次通过Get操作访问其内的元素,会把其访问的元素放到最尾的地方,改变迭代的顺序(如下示例验证)。

    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
    import java.util.LinkedHashMap;
    import java.util.Set;

    import org.springframework.util.Assert;

    public class LinkedHashMapTest {

    public static void main(final String[] args) {
    final LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);

    final Set<Integer> keys = map.keySet();
    Assert.isTrue("[1, 2, 3, 4, 5]".equals(keys.toString()), "");

    map.get(4);
    Assert.isTrue("[1, 2, 3, 5, 4]".equals(keys.toString()), "");

    map.get(1);
    Assert.isTrue("[2, 3, 5, 4, 1]".equals(keys.toString()), "");

    map.get(3);
    Assert.isTrue("[2, 5, 4, 1, 3]".equals(keys.toString()), "");

    System.out.println("OK");
    }
    }
  2. 其时间复杂度,同HashMap一样,内部使用buckets实现,但是其迭代的时间复杂度因为有Link的存在,仅与其元素个数有关,而HashMap的迭代时间复杂度跟其容量有关。

Spring+JWT实现微服务集群

发表于 2019-03-07 | 更新于 2019-04-08 | 分类于 技术

前言

这是一个练习项目,来源于网上一个需求,很感兴趣,就自己做了一个DEMO。

之前这类工作大刘同学做的比较多,就当想念他一下吧。

Spring+JWT实现基础微服务

Spring使用微服务,有完整的Cloud组件协助完成。但是通常Cloud太过庞大,很多小的项目需要一定的业务拆分,但是业务组件之间可能不需要使用Cloud那么庞大的管理。

主要功能

这个Demo就是针对这个目的设立的,主要完成了:

  • 使用JWT实现了无Session化的访问控制,适合前端APP类的接入
  • 使用了一个OAuthServer服务器,完成认证工作(授权的部分Dummy了,很容易扩展)
  • 实现了一个Miscroservcie1作为微服务组件1,模拟获取用户的基本信息
  • 实现了一个Miscroservcie2作为微服务组件2,模拟获取用户的技能信息(多条技能)

业务流程:

  • 用户通过OAuthServer进行Login认证,获取JWT Token
  • 通过JWT Token访问Miscroservcie1,获取用户的基本信息和技能信息
  • Miscroservcie1中,传递JWT Token到Miscroservcie2,获取技能信息,同基本信息一起打包返回给前端

很简单的需求,代码也没有复杂的地方,核心在Miscroservcie1通过ThreadLocal和RestTemplateInterceptor实现了基于RestTemplate的JWT Token的透传,完成的级联认证。

在项目中使用可以优化的内容:

  • JWT Token中如何封装授权信息,还是由Miscroservcie通过接口访问OAuthServer来获取
  • Miscroservcie拿到JWT Token后,是否需要向OAuthServer进行增强验证
  • 结合上面两个需求,理想的方案还是JWT Token尽可能简单,只有Principal的ID(用户ID)和超时等JWT信息,其他附加业务信息通过OAuthServer来提供比较好,当然Miscroservcie可以缓存,或者整体使用缓存集群(例如Hazelcast来实现)会更好

代码

好吧,所有的都在代码中了: MicroservicesJwtOAuth

(本页梳理耗时30分钟)

看HTTP演变论基础科技发展与创新

发表于 2019-03-07 | 更新于 2019-04-08 | 分类于 杂谈

早上通过微信看了这篇【HTTP3 为什么比 HTTP2 靠谱? | 技术头条】,标题有点大,内容却是十分充实细腻的。文章最后总结主要说了三个问题:

  • HTTP/1.x 有连接无法复用、队头阻塞、协议开销大和安全因素等多个缺陷;
  • HTTP/2 通过多路复用、二进制流、Header 压缩等等技术,极大地提高了性能,但是还是存在着问题的;
  • QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议。

但是整篇细读看下来,文章隐藏了进一步的几个要点或问题可以进一步阐述:

  • HTTP/1.x 各种问题,但是是目前网络基石,不过可以简单替换
  • HTTP/2标准于2015年5月以RFC 7540正式发表,根据W3Techs的数据,在2017年5月,在排名前一千万的网站中,有13.7%支持了HTTP/2,尚远远没有普及
  • QUIC也就是HTTP/3,目前貌似只有一个Go开发的Caddy未来服务器支持,客户端应该也只有Chrome一家吧,不过看过一些文章,貌似稳定性还需要一些时间
  • HTTP是这个样子,可以持续发展进化,虽然代价昂贵
  • 但是TCP、UDP这两个基础协议目前尚无持续发展的规划
  • SSH貌似前一段看已过一个替代方案,忘记了名字

细细看来,其实说明了一个基本问题: 越是基础的,越是被广泛应用的,替换更新的代价就越高。

再细看一下,一个产品的规划、上线、推广、升级,一旦到了一定规模,其维护、升级的成本会成级数上涨。但结合商务的同学想法,一个产品要快快下海试水,能不淹死才能继续投入,想想也没有错,商务技术互相商量平衡吧。就怕前面商务铺的大了,技术投入深了,有了基础了,需要在进一步加大投入的时候,商务说看不到岸了,模模糊糊停了,很想学学BigBang中那么直白的说,不过还是算了。

总结一下,上面的文章值得细细看,结束。(回头看看标题,貌似标准的乱水😂)

12
黑哈

黑哈

~黑哈等你回家~
15 日志
11 分类
28 标签
GitHub
© 2019 黑哈
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v7.0.1