开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Vector源码分析 ArrayList和Vector对比:

发表于 2019-08-17

本来今天是想看一下Stack的源码的,但是在看到Stack的父类结构时

1
复制代码public class Stack<E> extends Vector<E>

我想到了我之前还没怎么看过Vector的源码,甚至乎还很少用,我之前对他的了解大概就是停留在跟ArrayList很相似,是线程安全的ArrayList,先总结下ArrayList和Vector的不同之处,然后带着结论去看源码,找原因

ArrayList和Vector对比:

  • 相同之处:
+ 都是基于数组
+ 都支持随机访问
+ 默认容量都是10
+ 都支持动态扩容
+ 都支持fail—fast机制
  • 不同之处:
+ Vector历史比ArrayList久远,Vector是jdk1.0,ArrayList是jdk1.2
+ Vector是线程安全的,ArrayList线程不安全
+ Vector动态扩容默认扩容两倍,ArrayList是1.5倍

底层数据结构

Vector底层是基于数组实现的

1
复制代码protected Object[] elementData;

ArrayList底层数据结构也是数组

1
复制代码private static final Object[]

其他相关属性

1
2
3
4
复制代码    //数组中元素数量
protected int elementCount;
//增长量
protected int capacityIncrement;

构造方法

无参构造,数组容量默认是10

1
2
3
4
复制代码    //无参构造,数组容量默认是10
public Vector() {
this(10);
}

ArrayList默认数组容量也是10

1
2
3
4
5
6
7
复制代码    //默认容量
private static final int DEFAULT_CAPACITY = 10;

//构造指定容量的数组(10)
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

Vcetor其他构造方法:

指定容量和增长量构造

1
2
3
4
5
6
7
8
9
10
11
复制代码    //创建指定容量大小的数组,设置增长量。
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//构造指定容量的数组
this.elementData = new Object[initialCapacity];
//设置增长量
this.capacityIncrement = capacityIncrement;
}

指定容量和增长量为0的构造:

1
2
3
复制代码    public Vector(int initialCapacity) {
this(initialCapacity, 0);
}

传入指定集合构造

1
2
3
4
5
6
7
8
9
复制代码    public Vector(Collection<? extends E> c) {
//转成数组,赋值
elementData = c.toArray();
elementCount = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//如果不是Object[],要重建数组
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}

扩容机制

在了解添加元素之前我们需要理清Vector的扩容机制是怎样的,其实跟ArrayList的扩容机制也很相似

1.计算最小容量:

最小容量 = 当前数组元素数量 + 1,此举的目的就是判断是否需要扩容,最小容量就是相当于成功添加了一个元素后的新的数组元素数量,如果这个新的数组元素数量大于数组长度,那么肯定需要扩容

1
2
3
4
5
复制代码    private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

2.传入最小容量开始扩容:

  • 如果当前数组的增长量 > 0则新数组容量 = 旧数组容量 + 增长量
  • 否则,则新数组容量 = 2 * 旧数组容量
  • 求出新数组容量后,如果新数组容量 < 最小容量,那么新数组容量 = 最小容量
  • 如果新数组容量 > 最大数组容量,则新数组容量 = 整数最大值
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
复制代码    //最小数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

//扩容
private void grow(int minCapacity) {
//旧的数组容量
int oldCapacity = elementData.length;
//新数组容量
//如果当前数组的增长量 > 0则新数组容量 = 旧数组容量 + 增长量
//否则,则新数组容量 = 2 * 旧数组容量
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
//求出新数组容量后,如果新数组容量 < 最小容量,那么新数组容量 = 最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新数组容量 > 最大数组容量,则新数组容量 = 整数最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//真正扩容,实际上就是数组的复制和移动
elementData = Arrays.copyOf(elementData, newCapacity);
}

//判断是取最大数组容量还是整数最大值
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

4.扩容实际:数组复制和移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//params0:original:原数组
//param1:srcPos:原数组开始位置
//param2:copy:新数组
//param3:destPost:新数组开始位置
//param4:copyLength:要copy的数组的长度
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

ArrayList的扩容机制其实和Vector很相似,至少原理是一致的,但是在扩容大小上不一样

因为ArrayList没有增长量这一概念,所以ArrayList默认扩容1.5倍

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
复制代码    private void grow(int minCapacity) {
// 原来的数组容量 = 数组长度
int oldCapacity = elementData.length;
// 新的数组容量 = 原数组容量+原数组容量/2
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断下传进来的最小容量 (最小容量 = 当前数组元素数目 + 1)
// 如果比当前新数组容量小,则使用最容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果判断当前新容量是否超过最大的数组容量 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//开始扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}

//如果判断当前新容量是否超过最大的数组容量 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//如果超多最大数组容量则使用Integer的最大数值,否则还是使用最大数组容量
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

ArrayList扩容流程:

在这里插入图片描述

添加元素

数组尾部添加指定元素

可以看到添加方法上带有synchronized同步关键字,保证了在添加元素时的线程安全,但是也会带来获取锁和释放锁的效率问题

1
2
3
4
5
6
7
复制代码    public synchronized void addElement(E obj) {
modCount++;
//判断是否需要扩容
ensureCapacityHelper(elementCount + 1);
//直接根据下标添加
elementData[elementCount++] = obj;
}

指定位置添加指定元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码    public synchronized void insertElementAt(E obj, int index) {
modCount++;
if (index > elementCount) {
throw new ArrayIndexOutOfBoundsException(index
+ " > " + elementCount);
}
//判断是否需要扩容
ensureCapacityHelper(elementCount + 1);
//数组移动和复制,腾出index位置 index后的元素向后移动一位
System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
//下标添加
elementData[index] = obj;
//元素数量+1
elementCount++;
}

添加指定集合

1
2
3
4
5
6
7
8
9
10
复制代码    public synchronized boolean addAll(Collection<? extends E> c) {
modCount++;
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityHelper(elementCount + numNew);
//扩容,复制到数组后面
System.arraycopy(a, 0, elementData, elementCount, numNew);
elementCount += numNew;
return numNew != 0;
}

删除元素

删除指定下标元素

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
复制代码    public synchronized E remove(int index) {
modCount++;
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
//原来该下标对应的元素值
E oldValue = elementData(index);
//index后面元素的数量
int numMoved = elementCount - index - 1;
//如果该元素不是最后一个元素
if (numMoved > 0)
//该元素后面的元素向前移动一位,覆盖删除
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//数组最后多余的一位为null,gc
elementData[--elementCount] = null; // Let gc do its work

return oldValue;
}

//和上边方法其实思路是一样的
public synchronized void removeElementAt(int index) {
modCount++;
//检查
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
//该元素后面的元素向前移动一位,覆盖删除
System.arraycopy(elementData, index + 1, elementData, index, j);
}
//数组最后多余的一位为null,gc
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}

删除指定元素

1
2
3
4
5
6
7
8
9
10
11
复制代码    public synchronized boolean removeElement(Object obj) {
modCount++;
//找到该元素下标
int i = indexOf(obj);
if (i >= 0) {
//下标正确则根据下标删除
removeElementAt(i);
return true;
}
return false;
}

删除所有元素

1
2
3
4
5
6
7
8
复制代码    public synchronized void removeAllElements() {
modCount++;
//循环删除每一个元素,gc
for (int i = 0; i < elementCount; i++)
elementData[i] = null;

elementCount = 0;
}

删除指定范围的元素

1
2
3
4
5
6
7
8
9
10
11
12
复制代码    protected synchronized void removeRange(int fromIndex, int toIndex) {
modCount++;
//原理还是数组的移动,将toIndex后的元素向前移动 toIndex - fromIndex
int numMoved = elementCount - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);

// Let gc do its work
int newElementCount = elementCount - (toIndex-fromIndex);
while (elementCount != newElementCount)
elementData[--elementCount] = null;
}

查询

从指定下标开始找到指定元素第一次出现的下标

从前往后找

1
2
3
4
5
6
7
8
9
10
11
12
复制代码    public synchronized int indexOf(Object o, int index) {
if (o == null) {
for (int i = index ; i < elementCount ; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = index ; i < elementCount ; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

从后往前找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码    public synchronized int lastIndexOf(Object o, int index) {
if (index >= elementCount)
throw new IndexOutOfBoundsException(index + " >= "+ elementCount);

if (o == null) {
for (int i = index; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = index; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

返回指定下标的元素

1
2
3
4
5
6
7
8
9
10
复制代码    E elementData(int index) {
return (E) elementData[index];
}

public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);

return elementData(index);
}

是否包含指定元素

1
2
3
4
复制代码    //看没有该下标
public boolean contains(Object o) {
return indexOf(o, 0) >= 0;
}

迭代器

迭代器和ArrayList相比是差不多的,包括实现也是,可以参考ArrayList源码分析

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

关于 adb 命令你所需要知道的

发表于 2019-08-17

概述

Android Debug Bridge (adb) 是一个通用命令行工具,是 Android 开发/测试人员必备工具,关于adb的详细介绍可以查看谷歌 adb 官方中文文档 Android Debug Bridge, 这里只是总结了一下常用的命令

文章中涉及的命令说明

  • 文章中的命令只针对于连接一个设备的情况,如果连接多个设备,需要在 adb shell -s 后面指定设备序列号, 格式如下所示:
1
2
3
4
5
复制代码格式:
adb -s 225278f8 shell

获取设备序列号:
adb devices
  • <serial number> : 设备序列号
  • <package_name> : Apk 的包名
  • <local> : pc 端路径
  • <remote> : Android 设备上的路径
  • <filepath_in_device> : Android 设备的文件路径
  • <local_apk_path> : pc 端 apk 的路径
  • <package_name>/<main_class> : Apk包名 / 启动类,格式如下所示
1
2
3
4
5
复制代码格式:
google.architecture.universal/.ActivityMain

打开Apk,然后输入下面命令查看:
adb shell dumpsys window | grep mCurrentFocus

命令总结

以下所有命令参考谷歌 adb 官方文档 Android Debug Bridge,列出了经常用命令

常用命令

备注 命令
查看连接设备 adb devices
查看连接设备列表信息 adb devices -l
指定设备 adb -s <serial number> shell

上传文件和下载文件

备注 命令
将电脑上的文件上传的设备 adb push <local> <remote>
设备中的文件下载到电脑 adb pull <remote> <local>

安装和卸载应用

备注 命令
安装Apk adb install <local_apk_path>
安装Apk 并且授予Apk所有权限 adb install -g <local_apk_path>
覆盖安装Apk且保留数据 adb install <local_apk_path>
降级安装Apk adb install -d <local_apk_path>
卸载Apk adb uninstall <package_name>
卸载Apk且保留数据 adb shell pm uninstall -k <package_name>
静默安装应用 adb shell pm install -t -r <local_apk_path>
卸载所有应用(包括系统应用) adb shell pm uninstall -k --user 0 <package_name>

pm

备注 命令
显示第三放应用 adb shell pm list package -3
显示系统应用 adb shell pm list packages -s
显示apk文件路径和包名列表 adb shell pm list packages -f
查看apk路径 adb shell pm path <package_name>
清除数据和缓存 adb shell pm clear <package_name>

dumpsys

备注 命令
查看运行Apk的包名 `adb shell dumpsys window
查看Activity任务栈 adb shell dumpsys activity activities
查看堆的分配情况 adb shell dumpsys meminfo <package_name>
查看应用信息 adb shell dumpsys package <package_name>
获取设备显示屏参数信息 adb shell dumpsys window displays
清除bugreport信息 adb shell dumpsys batterystats --reset
从bugreport中过滤关键字信息 `adb shell dumpsys batterystats
进入dozeModel deep状态 adb shell dumpsys deviceidle force-idle
进入dozeModel light状态 adb shell dumpsys deviceidle force-idle light
退出dozeModel adb shell dumpsys deviceidle unforce
重新激活设备 adb shell dumpsys battery reset
退出充电状态 adb shell dumpsys battery unplug
禁用doze mode adb shell dumpsys deviceidle disable adb shell dumpsys deviceidle whitelist

getprop

备注 命令
获取设备型号 adb shell getprop ro.product.model
获取设备的Android系统版本 adb shell getprop ro.build.version.release

wm

备注 命令
获取设备屏幕分辨率 adb shell wm size
获取设备屏幕密度(单位:dpi) adb shell wm density

调试命令

备注 命令
查看应用的进程 `adb shell ps -ef
查看内存占用情况 `adb shell ps
查看Activity的启动时间 adb shell am start -W <package_name>/<main_class>
强制关闭应用 adb shell am force-stop <package_name>
实时查看设备CPU、内存占用等信息 adb shell top
实时查看内存占用排名前number的应用 adb shell top -m <number>
为Apk跑number次monkey测试 adb shell monkey -v -p <package_name> <number>
获取设备的MAC地址 adb shell cat /sys/class/net/wlan0/address
获取设备的内存占用信息 adb shell cat /proc/meminfo

其他命令

备注 命令
查看日志 `adb shell logcat
输出日志到log.txt文件 `adb shell logcat
截取屏幕 adb shell screencap -p <filepath_in_device>
录制屏幕 adb shell screenrecord -p <filepath_in_device>

常见问题汇总

列举一些常见问题,后续会不断完善

1. ubuntu 下使用 adb 出现 no permissions 问题解决方案

运行 sudo adb devices 出现下列情况

1
2
复制代码List of devices attached 
2aca417d no permissions

如何解决 no permissions?

在未连接 Android 设备的情况下,运行命令 lsusb, 查看一下 ubuntu 下的 usb

1
2
3
4
复制代码Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 003: ID 413c:2113 Dell Computer Corp.
Bus 001 Device 002: ID 413c:301a Dell Computer Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

连接 Android 设备, 打开 usb 调试模式,运行命令 lsusb, 查看一下 ubuntu 下的 usb

1
2
3
4
5
复制代码Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 003: ID 413c:2113 Dell Computer Corp.
Bus 001 Device 002: ID 413c:301a Dell Computer Corp.
Bus 001 Device 055: ID 18d1:4ee7 Google Inc.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

通过对比两个结果,可以查看到新连接的 Android 设备信息,注意其 ID 号,这里是18d1:4ee7

然后进入到 /etc/udev/rules.d/ 目录下,查看是否有 .rules 文件.没有则可以自己新建一个(名字可以随意取,不能有中文),添加端口信息到文件中

1
2
3
4
5
复制代码#&emsp;打开一个文件
sudo vim 51-android.rules

# 添加端口信息到51-android.rules
SUBSYSTEM=="usb",ATTRS{idVendor}=="18d1",ATTRS{idProduct}=="4ee7",MODE="0666"

这里 18d1 和 4ee7 则分别是上一步中查看到的 android 设备的信息,MODE 表示权限,完成之后执行下面命令

1
2
复制代码sudo chmod a+rx /etc/udev/rules.d/51-android.rules
sudo service udev restart

最后拔掉 usb 重新连接,然后在运行下面命令重启 adb 服务

1
2
3
复制代码sudo adb kill-server
sudo adb start-server
sudo adb devices

如果上述步骤都操作正确,运行 sudo adb devices 如下所示,就可以使用 adb 操作设备了

1
2
复制代码List of devices attached 
2aca417d device

参考文献

  • 谷歌adb官方中文文档
  • adb常用命令整理
  • awesome-adb
  • ADB-常用命令
  • 针对低电耗模式和应用待机模式进行优化

结语

致力于分享一系列 Android 系统源码、逆向分析、算法相关的文章,每篇文章都会反复推敲,结合新的技术,带来一些新的思考,如果你同我一样喜欢 coding,一起来学习,期待与你一起成长

文章列表

Android 10 源码系列

  • 0xA01 Android 10 源码分析:Apk 是如何生成的
  • 0xA02 Android 10 源码分析:Apk 的安装流程
  • 0xA03 Android 10 源码分析:Apk 加载流程之资源加载
  • 0xA04 Android 10 源码分析:Apk 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 如何高效获取视频截图
  • 10分钟入门 Shell 脚本编程
  • 如何在项目中封装 Kotlin + Android Databinding

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

DataBinding,再学不会你砍我

发表于 2019-08-16

首先,这不是一篇关于DataBinding使用介绍,同时也不是一篇源码分析,不涉及最底层的实现逻辑。更多的是实际开发过程中面对的问题及其涉及的原理,透过它们可以更快的帮助我们排查问题,提升开发效率。

开始之前咱们先考虑下面的问题,如果你觉得有些了解但又似是而非,那这篇文章能让你对databinding有一个全面的认识(蜜汁自信);如果你觉得这些问题都太简单了,那你是不是…也应该复习一下(强行挽留)。

  1. android:text=@{user.name}这种常见的binding方式当user对象为空时,会引起空指针问题吗?为什么?
  2. View的全部属性都支持binding吗?比如padding?margin?为什么?
  3. 双向绑定是如何处理无限循环调用的?

灵魂拷问.jpeg

配置

Android Gradle 插件(版本>=3.1.0-alpha06)支持自动生成binding类。

1
2
3
4
5
6
复制代码android {
...
dataBinding {
enabled = true
}
}

官方文档戳这里,文内示例来自官方sample,更多内容参考官方博客。

基本使用

为属性赋值使用表达式@{},内部可使用java代码,但并不完全是java代码。
来看下面的例子:

1
2
3
4
5
6
复制代码<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="@{user.likes &gt; 9 ? @color/star : @android:color/black}"
app:srcCompat="@{user.likes &lt; 4 ? R.drawable.ic_person_black_96dp : R.drawable.ic_whatshot_black_96dp }" />

其中user对象类型为实体类TestProfile:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class TestProfile {
private String name;
private String lastName;
private int likes;

public TestProfile(String name, String lastName, int likes) {
this.name = name;
this.lastName = lastName;
this.likes = likes;
}
//省略getter/setter...
}

在databinding表达式中使用的是关于user.likes的三目运算,但TestProfile中likes属性被private修饰,那为什么不会编译报错呢?事实上,databinding编译器为我们做一次映射,它会在user对象的类中依次寻找xxx属性的getXxx()方法和xxx()方法,如果二者都没有找到才会报出编译问题。

另外,databinding表达式还支持,直接访问ObservableInt等用于单向绑定的Obserable对象的具体值,举个栗子,我们把TestProfile稍微改动一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class TestProfile {
private String name;
private String lastName;
//改为支持单向绑定的ObservableInt
private ObservableInt likes;

public TestProfile(String name, String lastName, ObservableInt likes) {
this.name = name;
this.lastName = lastName;
this.likes = likes;
}
//省略getter/setter...
}

这样改完后,binding表达式不用做任何修改,也就是编译器帮我们做了转换,即user.likes == user.getLikes().get()。

需要验空吗?

到这里我们回头看看文章开头提的第一个问题:android:text=@{user.name}这种常见的binding方式当user对象为空时,会引起空指针问题吗?在我们这个例子中假设传入的user对象为空会怎样呢?

事实上,DataBinding编译器早就想到了这个问题,如果不对基本的验空问题做处理,你可以想象xml布局中便会遍布各种验空语句,一次验空你还能接受,类似这样的验空a.b().c()你将如何处理呢?所以DataBinding框架在使用variable之前会对其值进行验空操作,如果有级联调用会依次验空后再使用。

goon.gif

结合上面绑定likes的例子,我们看一下DataBinding编译器到底是如何实现验空操作的。这里需要补充一下知识,控件真正和数据完成绑定发生在bindingImpl类(这个类也是自动生成的)的executeBindings方法中。

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
复制代码# ObservableFieldProfileBindingImpl
protected void executeBindings() {
...
//TestProfile中的ObservableInt对象
androidx.databinding.ObservableInt userLikes = null;
//mUser为实际binding的对象
com.example.android.databinding.basicsample.data.TestProfile user = mUser;
//ObservableInt属性值
int userLikesGet = 0;

if ((dirtyFlags & 0x7L) != 0) {

//第一步验空 user验空
if (user != null) {
// read user.likes
userLikes = user.getLikes();
}
updateRegistration(0, userLikes);

//第二步验空 ObservableInt验空
if (userLikes != null) {
// read user.likes.get()
userLikesGet = userLikes.get();
}
...
}
}

bind是如何完成的?

在回答第二个问题之前,我们先考虑一下这个问题:日常工作中经常需要开发自定义View,那对于自定义View的自定义属性如何才能支持DataBinding呢?

来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码class MyCustomView : View {
var mDrawable: Drawable? = null

constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
val typedArray = context!!.theme.obtainStyledAttributes(attrs,
R.styleable.MyCustomView, 0, 0)
mDrawable = typedArray.getDrawable(R.styleable.MyCustomView_img)
typedArray.recycle()
}

override fun onDraw(canvas: Canvas?) {
mDrawable?.run {
setBounds(0, 0, this.intrinsicWidth, this.intrinsicHeight)
draw(canvas!!)
}
}
}

MyCustomView是一个自定义view,其内部定义了一个自定义属性img,类型为Drawable,用于绘制在画布上。

使用的test_custom_view.xml布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="myDrawable"
type="android.graphics.drawable.Drawable" />
</data>
<com.example.android.databinding.basicsample.ui.MyCustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:img="@{myDrawable}" />
</layout>

activity中的绑定代码:

1
2
复制代码val binding: TestCustomViewBinding = DataBindingUtil.setContentView(this, R.layout.test_custom_view)
binding.myDrawable = resources.getDrawable(R.mipmap.ic_launcher)

此时如果运行就会报错。

1
2
复制代码****/ data binding error ****msg:Cannot find the setter for attribute 'app:img' with parameter type android.graphics.drawable.Drawable on com.example.android.databinding.basicsample.ui.MyCustomView.
file:/android-databinding/BasicSample/app/src/main/res/layout/test_custom_view.xml loc:14:19 - 14:28 ****\ data binding error ****

通过错误信息我们已经能推断出绑定是通过app:img属性的setter方法完成的,事实上这个setter方法匹配原则和上述绑定实体的setter方法是一致的,即img对应的setter方法,包括img()和setImg()方法,若在自定义view中没有找到此setter方法就会报错。
我们为MyCustomView添加setImg解决了问题。

1
2
3
4
5
6
复制代码class MyCustomView : View {
...
fun setImg(d: Drawable) {
mDrawable = d
}
}

但往往实际情况是这个自定义view不是我们自己写的,且源码内没有控件属性对应的setter方法,我们又无法直接修改它的源码,此时该如何处理呢?

@BindAdapter注解

@BindAdapter注解可以帮我们完成这项工作,它位于databinding-common库中,这个库定义了databinding支持的全部注解。此时我们假设MyCustomView内img的set方法签名为setMyImg,我们只需在任意的类中添加一个声明此注解的静态方法。

1
2
3
4
5
6
7
复制代码# BindingAdapters.kt
...
@BindingAdapter("img")
@JvmStatic
fun setImg(view: MyCustomView, drawable: Drawable) {
view.setMyImg(drawable)
}

此静态方法名可随便起,注解中img属性的命名空间为可选,不写则xml中声明的任意命名空间通配。重要的是参数签名,第一个参数需为绑定控件的名称,第二个参数则是绑定img属性对应的类型,只有二者与xml中的绑定声明一致方可生效。如果想获得绑定前的值可以这样声明函数。

1
2
3
4
5
6
复制代码@BindingAdapter("img")
@JvmStatic
//old为绑定发生前img的属性值
fun yyy(view: MyCustomView, old: Drawable?, drawable: Drawable) {
view.setMyImg(drawable)
}

多个属性支持同时使用一个方法完成绑定,写法这酱紫的:

1
复制代码@BindingAdapter(value = ["property1", "property2"], requireAll = false)

requireAll表示是否value数组中声明的所有属性都完成绑定才执行此静态方法,默认true,此细节问题这里不做展开。

@BindingMethod注解

注意到我们在上面的例子中绑定实现也仅仅只是调用了一下view.setMyImg(drawable)方法。像这种情况我们可以用一个更简洁的形式实现,@BindingMethod注解,它本质上用于描述一种映射关系。在我们的例子中就是将img属性映射到setMyImg方法即可。

1
2
3
4
复制代码@BindingMethods(BindingMethod(type = MyCustomView::class, attribute = "img", method = "setMyImg"))
class MyCustomView : View {
...
}

如果我们并不能修改MyCustomView源码,可在创建任意类并为其声明BindingMethods注解完成绑定。

讲到这我们可以解答开篇的第二个问题:View的全部属性都支持binding吗?比如padding?margin?为什么?

有了上面的分析,这个问题的回答可以转换成下面三个子问题:

  1. View源码中是否有签名符合要求的setPadding、setMargin方法。
  2. 在databinding框架内是否有声明@BindAdapter(“android:padding”)注解且签名符合要求的静态方法。
  3. 在databinding框架内是否有声明@BindingMethod注解且该注解在View源码中有映射关系实现setPadding/setMargin。

查看View源码你会发现其中没有setMargin方法,有setPadding方法但是多参的setPadding(int left, int top, int right, int bottom)不符合方法签名要求,所以1二者都不满足。

在databinding-adapters库中定义了常用组件的扩展绑定关系,比如View对应其ViewBindingAdapter,TextView对应其TextViewBindingAdapter等等。

其中ViewBindingAdapter完成了padding属性的绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@BindingMethods({
@BindingMethod(type = View.class, attribute = "android:backgroundTint", method = "setBackgroundTintList"),
@BindingMethod(type = View.class, attribute = "android:nextFocusLeft", method = "setNextFocusLeftId"),
...
@BindingMethod(type = View.class, attribute = "android:onLongClick", method = "setOnLongClickListener"),
@BindingMethod(type = View.class, attribute = "android:onTouch", method = "setOnTouchListener"),
})
public class ViewBindingAdapter {
@BindingAdapter({"android:padding"})
public static void setPadding(View view, float paddingFloat) {
final int padding = pixelsToDimensionPixelSize(paddingFloat);
view.setPadding(padding, padding, padding, padding);
}
...
}

但并没有margin属性,且在其@BindingMethod声明中也没有映射实现,因此得到结论:android:padding属性支持绑定,而margin不支持。以layout_marginBottom为例,下面是一个简单的实现。

1
2
3
4
5
6
7
复制代码@BindingAdapter("android:layout_marginBottom")
public static void setBottomMargin(View view, float bottomMargin) {
MarginLayoutParams layoutParams = (MarginLayoutParams) view.getLayoutParams();
layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin,
layoutParams.rightMargin, Math.round(bottomMargin));
view.setLayoutParams(layoutParams);
}

640.jpeg

单向绑定

上面部分解释了控件和数据之间是如何进行绑定的,但实际场景中数据持续发生着变化,那么怎么将数据的变化及时反映到UI上呢?这就是本节要讲的单向绑定。

单向绑定可以通过两种方式实现

  • 使用ObservableXxx类型替代原有类型
  • 实体继承BaseObservable类,并用@Bindable注解声明属性的getter方法。

ObservableXxx

这里的xxx包含基本类型和集合类型,比如int对应ObservableInt,boolean对应ObservableBoolean等,ArrayList对应ObservableArrayList,引用类型统一使用ObservableField,比如String对应ObservableField。为方便起见,下文将ObservableXxx统一用ObservableField表示。

举个栗子,假设我们的实体如下:

1
2
3
复制代码data class ObservableFieldProfile(
var likes: Int
)

observable_field_profile.xml布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码<layout

<data>
<variable
name="user"
type="com.example.android.databinding.basicsample.data.ObservableFieldProfile" />
</data>

<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
...
android:src="@{user.likes &lt; 4 ? R.drawable.ic1 : R.drawable.ic2 }"/>
</layout>

绑定代码

1
2
3
4
复制代码val binding: ObservableFieldProfileBinding =
DataBindingUtil.setContentView(this, R.layout.observable_field_profile)
val user = ObservableFieldProfile(0)
binding.user = user

当我们改变其中的属性时user.likes=10,并不会触发UI的刷新,要想达到同步刷新的效果,只需将likes的类型由Int换为ObservableInt。

1
2
3
复制代码data class ObservableFieldProfile(
val likes: ObservableInt
)

同时数据变更使用user.likes.set(10)。

BaseObservable + @Bindable注解

上面的方式有一个的问题,就是实体的类型发生了变化,那在真实项目中但凡使用到此属性的地方都要进行修改。
我们可以换一种写法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码class ObservableFieldProfile(var name: String, val lastName: String) : BaseObservable() {

constructor(name: String, lastName: String, likes: Int) : this(name, lastName) {
this.likes = likes
}

@get:Bindable
var likes: Int = 0
set(value) {
field = value
notifyPropertyChanged(BR.likes)
}
}
  1. 让实体类继承BaseObservable类。
  2. 为属性的set方法添加Bindable注解。
  3. 自定义set方法调用notifyPropertyChanged触发刷新。

notifyPropertyChanged方法继承自BaseObservable,其参数为BR.likes。这里需要解释一下BR类,它是databinding编译器自动生成的,其内部为所有可变对象定义了一个int类型的唯一标识,这里的可变对象包含:

  • 在xml布局中声明的variable变量
  • 使用@Bindable注解标识的方法对应的属性名(属性不必真的存在,含有getter即可)。

这也就是为什么一定要声明Bindable注解,这样就能指定刷新特定的属性了;同时由于getter内可以写任意代码,因此可操作性更强。

相比于ObservableField而言虽然不必改变属性的类型了,但对应实体类来说面临了更多的问题:

  1. 使用了继承而不是实现,如果实体类本身有继承关系此方案不可行(如果一定要这么做只能实现Observable接口,并把BaseObservable的内部实现拷贝到现有实体中)。
  2. 需重写set方法,但在Kotlin中对data类型内部的属性使用set太不友好,看看一个简单的data类改成啥样了…。

二者的关系

ObservableField是基于BaseObservable实现的。

databinding_uml.png

讲到这里需要插播一个问题,数据驱动UI变化时,databinding框架怎么知道要更新哪些UI控件,换言之,如何将可变化的数据和UI控件进行关联的?

绑定原理

之所以要提这个问题,是因为开发中经常遇到的这样的场景。比如,一个TextView需要与一个ObservableInt绑定,但在绑定之前需要做一系列判断、转换工作,如果统统都将这些逻辑写在bind表达式中可读性会变得很差,所以通常的做法是将判断、转换封装成一个方法,在bind表达式中直接调用方法即可。看起来合理的方案在实际使用中稍有不慎就会出bug。

看下面的例子,ViewModel如下:

1
2
3
复制代码class ProfileLiveDataViewModel : ViewModel() {
val likes: MutableLiveData<Int> = MutableLiveData(0)
}

布局文件是酱紫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewmodel"
type="com.example.android.databinding.basicsample.data.ProfileLiveDataViewModel"/>
</data>

<TextView
android:id="@+id/likes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Integer.toString(viewmodel.likes+1)}"
.../>

通过bind表达式内容可以看到我们的需求是将likes加1在转成一个String类型付给text属性。看到这里你可能想对这个表达式做一下优化改成这样android:text="@{viewmodel.testConvert(viewmodel.likes)}",其中在viewmodel中新增testConvert方法统一封装转换动作。

1
2
3
4
复制代码# viewmodel
fun testConvert(likes: Int): String {
return Integer.toString(likes + 1)
}

到这呢,你发现表达式还是没怎么简化,你可能需要进一步优化(挖坑),改成这样android:text="@{viewmodel.testConvert2()}"

1
2
3
4
复制代码# viewmodel
fun testConvert2(): String {
return Integer.toString(likes.value?.plus(1) ?: 0)
}

看似简单的改动其实已经引发了一个bug————此时如果我们更新likes的值,将不会同步更新到这个view上。

wtf.jpg

为什么会这样呢?我们简单解释一下,这与bind表达式的解析有关,解析过程中会查找表达式中是否有可变的部分,可变部分包含variable、ObservableField、标有@Bindable注解的getter、LiveData部分,重复项将合并,最终会在生成的ViewBindingImpl类中打印注释。

1
2
3
4
5
6
7
8
9
10
11
复制代码// dirty flag
private long mDirtyFlags = 0xffffffffffffffffL;
/* flag mapping
flag 0 (0x1L): viewmodel.likes
flag 1 (0x2L): viewmodel.lastName
flag 2 (0x3L): viewmodel.popularity
flag 3 (0x4L): viewmodel.name
flag 4 (0x5L): viewmodel
flag 5 (0x6L): null
flag mapping end*/
//end

mDirtyFlags是一个刷新标记,用于判断数据的变化对应刷新哪些view,flag mapping是每个刷新项的映射标记,所有可变化部分都将在这里展示,我们的例子中android:text="@{viewmodel.testConvert2()}"仅映射到variable–viewmodel,即只有当viewmodel变化时才会刷新此view,调用的方法testConvert2内部逻辑将忽略,而android:text="@{viewmodel.testConvert(viewmodel.likes)}"将会映射到viewmodel和viewmodel.likes,只要二者之一有变化都会刷新此view。

interesting.png

双向绑定

如果说单向绑定是数据驱动,那么双向绑定就是数据驱动+事件驱动,用@={}表达式标识双向绑定。

双向绑定图.png

所以要实现双向绑定也就是在单向绑定的基础上加上事件驱动逻辑即可。事件驱动最终要表现在数据变化上,因此可以总结完成事件驱动的三个步骤。

  1. 监听事件变化并抛给框架
  2. 获取控件当前属性值
  3. 用当前属性值更新数据

官方已经将常用控件的部分属性实现了双向绑定。

双向绑定官方实现.png

因为EditText继承TextView,我们拿常用的EditText的text属性结合TextViewBindingAdapter源码来说明反向绑定的过程。

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
复制代码# TextViewBindingAdapter
① //event可缺省
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}

②
@BindingAdapter(value = { ..., "android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, ...,
final InverseBindingListener textAttrChanged) {
final TextWatcher newValue;
...
newValue = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
...
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
...
//收到事件 通知框架发生变化
if (textAttrChanged != null) {
textAttrChanged.onChange();
}
}
@Override
public void afterTextChanged(Editable s) {
...
}
};
...
if (newValue != null) {
view.addTextChangedListener(newValue);
}
}

对于EditText来说可驱动数据变化的事件显然就是TextChangedListener,每当输入字符变化时会回调onTextChanged方法,dataBinding框架使用了一个InverseBindingListener接口以供用户将事件通知抛出。与InverseBindingListener接口对应需要向框架提供一个绑定属性textAttrChanged,默认它是由属性名+AttrChanged后缀组成(上面②处代码)。InverseBindingListener接口具体实现代码由框架生成,总得来说就是获取控件属性的当前值,然后用此值更新数据。

获取当前值的方法我们需要告诉框架,通常方案:

  • 使用@InverseBindingAdapter注解一个静态方法返回控件属性当前值比如上面①处代码。
  • 使用@InverseBindingMethod注解一个类,声明反向绑定针对的控件、属性、inverse事件名(可缺省)、控件内获取属性当前值(可缺省)。例如:
1
2
3
4
5
6
复制代码@InverseBindingMethods({@InverseBindingMethod(
type = android.widget.TextView.class,
attribute = "android:text",
event = "android:textAttrChanged",
method = "getText")})
public class MyTextViewBindingAdapters

避免无限循环

通过图例我们可以发现,如果事件驱动反向绑定成功后,数据会发生变化,按正常逻辑来讲,将继续触发单向绑定,如此一来将陷入无限循环中。

为中断这种循环,通常的做法是在更新UI前,校验新旧数据是否相同,如果相同则不进行刷新动作。比如TextView,其内部的setText方法并不会检验新旧数据的一致性问题,所以在TextViewBindingAdapter内重新绑定了android:text属性,添加校验逻辑。

1
2
3
4
5
6
7
8
9
10
复制代码@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
//校验
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
...
view.setText(text);
}

常用技巧

  1. 默认值 当绑定数据还未赋值时可指定默认值android:text='@{user.firstName, default="Placeholder text"}'。
  2. 验空的三目运算可换为”??”操作符,比如:android:text="@{user.displayName ?? user.lastName}"等价于android:text="@{user.displayName != null ? user.displayName : user.lastName}"。
  3. String placeholder可以这么用,android:text="@{@string/nameFormat(firstName, lastName)}"。
  4. binding表达式转义字符
    • “<”符号 用&lt;代替
    • “&”符号 用&amp;代替

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用Gradle对Java代码进行开发规范检查

发表于 2019-08-15

PMD是一种开源分析源代码错误的工具,它会发现一些常见的编程缺陷,比如未使用的变量,空的catch块,不必要的对象创建等。它支持Java,JavaScript等。
此外,用户还可以自己定义规则,检查Java代码是否符合某些特定的编码规范。
基于PMD,阿里巴巴基于自己的Java编码规范实现了P3C-PMD

设置检查规则

检查规则为xml格式,注意配置中指定的配置文件在jra包中,需要p3c包编译到项目中才行正确引入:

1
2
3
复制代码dependencies {
pmd "com.alibaba.p3c:p3c-pmd:2.0.0"
}

xml配置文件如下:

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
复制代码<?xml version="1.0"?>
<ruleset name="Custom ruleset"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
<description>
自定义Rule set
</description>
<!-- 引入PMD制定的Rule, 来源于https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java -->
<rule ref="rulesets/java/android.xml">
<exclude name="CallSuperLast"/>
</rule>
<rule ref="rulesets/java/basic.xml">
<exclude name="CollapsibleIfStatements"/>
</rule>
<rule ref="rulesets/java/clone.xml"/>
<rule ref="rulesets/java/finalizers.xml"/>
<rule ref="rulesets/java/imports.xml"/>
<rule ref="rulesets/java/javabeans.xml"/>

<rule ref="rulesets/java/optimizations.xml">
<exclude name="LocalVariableCouldBeFinal"/>
<exclude name="MethodArgumentCouldBeFinal"/>
</rule>
<rule ref="rulesets/java/sunsecure.xml"/>

<rule ref="rulesets/java/unnecessary.xml">
<exclude name="UselessParentheses"/>
</rule>

<!-- 引入阿里的Rule, 来源于 https://github.com/alibaba/p3c/tree/master/p3c-pmd/src/main/resources/rulesets/java -->
<rule ref="rulesets/java/ali-comment.xml">
</rule>

<rule ref="rulesets/java/ali-concurrent.xml">
</rule>

<rule ref="rulesets/java/ali-constant.xml">
</rule>

<rule ref="rulesets/java/ali-exception.xml">
</rule>

<rule ref="rulesets/java/ali-flowcontrol.xml">
</rule>

<rule ref="rulesets/java/ali-naming.xml">
</rule>

<rule ref="rulesets/java/ali-oop.xml">
</rule>

<rule ref="rulesets/java/ali-orm.xml">
</rule>

<rule ref="rulesets/java/ali-other.xml">
</rule>

<rule ref="rulesets/java/ali-set.xml">
</rule>

</ruleset>

该配置文件放在etc/pmd/relest.xml下

配置Gradle

1
2
3
4
5
6
7
8
9
10
11
12
复制代码apply plugin: "pmd"

pmd {
toolVersion = '6.17.0'
ignoreFailures = true
ruleSetConfig = resources.text.fromFile("etc/pmd/ruleset.xml")
}

dependencies {
pmd "com.alibaba.p3c:p3c-pmd:2.0.0"
...
}

其中ignoreFailures如果设置为true表示规范检查即使是不通过gradle check也不会报错,设置为false时,代码规范检查必须通过才check的时候才不会报错。

运行PMD

可以通过命令:

gradle check

运行pmdMain,它会检查__src/main/java__下的代码,还会运行pmdTest,它会检查__src/main/test__下的代码。
也可以分别运行这两个命令:

1
2
复制代码gradle pmdMain
gradle pmdTest

运行之后的结果在目录build/reports/pmd中的main.html,test.html文件中

参考

基于Gradle使用阿里巴巴Java开发规约进行代码检查

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

正则表达式之Matcher类中group方法

发表于 2019-08-15

前言

同事把一个excel表给我,里面的数据大概有几千的样子吧。自己需要把里面的数据一个一个拿出来做一个http请求,对得到的结果进行过滤,然后再写到上面去。这是就涉及到用脚本来进行操作了,于是自己搞了一个Java的脚本出来,里面涉及到一些正则表达式,自己虽然说会,但是一直对 Matcher类中的group方法 不太了解。网上的博客也没有写的特别清楚,于是有了此文。

Pattern 和 Matcher

在java.util.regex 包下

  1. Pattern(模式类) : 用来表达和陈述所要搜索模式的对象。Pattern.compile(pattern) pattern 也就是你写的正则表达式
  2. Matcher(匹配器类):真正影响搜索的对象。上面Pattern.compile(pattern)得到一个Pattern对象 为 r。 r.matcher(line) line也即是你需要进行匹配的字符串 。这样会得到一个 Matcher 的对象。
  3. PatternSyntaxException: 当遇到不合法的搜索模式时,会抛出例外。

正则表达式语法

在有的语言中,一个反斜杠 \ 就足以具有转义的作用,但是Java中需要两个 \\ 反斜杠。表示转义的作用。一些字符在正则表达式中的说明,意义。详情可查看 runoob

上代码

现在我的正则表达式为 (//d+)([a-z]+)(//d+)

  1. //d+ 表示最少匹配一个数字
  2. [a-z]+ 表示最少匹配一个字符
  3. 需要指定的字符串为 "123ra9040 123123aj234 adf12322ad 222jsk22"
  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
复制代码public static void main( String args[] ){

// 按指定模式在字符串查找
String line = "123ra9040 123123aj234 adf12322ad 222jsk22";
String pattern = "(\\d+)([a-z]+)(\\d+)";
// String pattern1 = "([\\u4E00-\\u9FA5]+|\\w+)";

// 创建 Pattern 对象
Pattern r = Pattern.compile(pattern);

// 现在创建 matcher 对象
Matcher m = r.matcher(line);
int i = 0;
// m.find 是否找到正则表达式中符合条件的字符串
while (m.find( )) {
// 拿到上面匹配到的数据
System.out.println("----i="+i);
System.out.println("Found value: " + m.group(0) );
System.out.println("Found value: " + m.group(1) );
System.out.println("Found value: " + m.group(2) );
System.out.println("Found value: " + m.group(3) );
i++;
System.out.println("|||||||");
System.out.println("");
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码----i=0
Found value: 123ra9040
Found value: 123
Found value: ra
Found value: 9040
|||||||

----i=1
Found value: 123123aj234
Found value: 123123
Found value: aj
Found value: 234
|||||||

----i=2
Found value: 222jsk22
Found value: 222
Found value: jsk
Found value: 22
|||||||
  1. group(0) 对应着 ((//d+)([a-z]+)(//d+)) 123ra9040
  2. group(2) 输出的数据 是 group(0)中所匹配的数据 也就是([a-z]+) 匹配到是数据 ra
  3. group(3) 输出的数据 是 group(0)中所匹配的数据 也就是(//d+) 匹配到是数据 9040

总结

  1. Matcher 类中group(0) 表示正则表达式中符合条件的字符串。
  2. Matcher 类中 group(1) 表示正则表达式中符合条件的字符串中的第一个() 中的字符串。
  3. Matcher 类中 group(2) 表示正则表达式中符合条件的字符串中的第二个() 中的字符串。
  4. Matcher 类中 group(3) 表示正则表达式中符合条件的字符串中的第三个() 中的字符串。
  5. 如果不明白,我相信看代码会很明白的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我是如何理解Java8 Stream

发表于 2019-08-14

之前看了许多介绍Java8 Stream的文章,但是初次接触真的是难以理解(我悟性比较低),没办法只能”死记硬背”,但是昨天我打王者荣耀(那一局我赢了,牛魔全场MVP)的时候,突然迸发了灵感,感觉之前没有理解透彻的一下子就理解透彻了。所以决定用简单的方式来回忆下我认为的java8 Stream.

lambda表达式

语法

lambda表达式是Stream API的基石,所以想要学会Stream API的使用,必须先要理解lambda表达式,这里对lambda做一个简单回顾。

我们常常会看到这样的代码

1
2
3
4
5
6
复制代码Arrays.sort(new Integer[]{1, 8, 7, 4}, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});

上面这种写法就是使用了匿名类,我们经常会使用匿名类的方式,因为我们只运行一次,不想它一直存在。虽然说lambda表达式是为了什么所谓的函数式编程,也是大家在社区千呼万唤才出来的,但是在我看来就是为了方(偷)便(懒)。

上面的代码写着麻烦,但是转换成下面这样的呢?

1
2
复制代码Arrays.sort(new Integer[]{1, 8, 7, 4},
(first,second) -> first.compareTo(second));

这样看着多清爽,而且把一些不必要的细节都屏蔽了。对于这种只包含一个抽象方法的接口,你可以通过lambda接口来创建该接口的对象,这种接口被称为函数式接口。

lambda表达式引入了一个新的操作符:->,它把lambda表达式分为了2部分

1
复制代码(n) -> n*n

左侧指定表达式所需的参数,如果不需要参数,也可以为空。右侧是lambda代码块,它指定lambda表达式的动作。

需要注意的是如果方法中只有一个返回的时候不用声明,默认会返回。如果有分支返回的时候需要都进行声明。

1
2
3
4
5
复制代码(n) -> {
if( n <= 10)
return n*n;
return n * 10;
}

方法引用以及构造器引用

方法引用

有些时候,先要传递给其他代码的操作已经有实现的方法了。比如GUI中先要在按钮被点击时打印event对象,那么可以这样调用

1
复制代码button.setOnAction(event -> System.out.println(event));

这个时候我想偷懒,我不想写event参数,因为只有一个参数,jvm不能帮帮我吗?下面是修改好的代码

1
2
复制代码
button.setOnAction(System.out::println);

表达式System.out::println是一个方法引用,等同于lambda表达式x -> System.out.println(x)。**::**操作符将方法名和对象或类的名字分割开来,以下是三种主要的使用情况:

  1. 对象::实例方法
  2. 类::静态方法
  3. 类::实例方法

前两种情况,方法引用等同于提供方法参数的lambda表达式。比如Math::pow ==== (x,y) -> Math.pow(x,y)。

第三种情况,第一个参数会称为执行方法的对象。比如String::compareToIgnoreCase ==== (x,y) -> x.compareToIgnoreCase(y)。

还有this::equals ==== x -> this.equals(x),super::equals ==== super.equals(x)。

构造器引用

1
2
复制代码List<String> strList = Arrays.asList("1","2","3");
Stream<Integer> stream = strList.stream().map(Integer::new);

上面代码的Integer::new就是构造器引用,不同的是在构造器引用中方法名是new。如果存在多个构造器,编译器会从上下文推断并找出合适的那一个。

StreamAPI

Stream这个单词翻译过来就是流的意思,溪流的流,水流的流。

Stream

在我看来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
复制代码Map<String,Map<String,Integer>> resultMap = new HashMap<>();
Map<String,Integer> maleMap = new HashMap<>();
Map<String,Integer> femaleMap = new HashMap<>();

resultMap.put("male", maleMap);
resultMap.put("female",femaleMap);

for(int i = 0; i < list.size(); i++) {
Person person = list.get(i);
String gender = person.getGender();
String level = person.getLevel();
switch (gender) {
case "male":
Integer maleCount;
if("gold".equals(level)) {
maleCount = maleMap.get("gold");
maleMap.put("gold", null != maleCount ? maleCount + 1 : 1);
} else if("soliver".equals(level)){
maleCount = maleMap.get("soliver");
maleMap.put("soliver", null != maleCount ? maleCount + 1 : 1);
}
break;

case "female":
Integer femaleCount;
if("gold".equals(level)) {
femaleCount = femaleMap.get("gold");
femaleMap.put("gold", null != femaleCount ? femaleCount + 1 : 1);
} else if("soliver".equals(level)){
femaleCount = femaleMap.get("soliver");
femaleMap.put("soliver", null != femaleCount ? femaleCount + 1 : 1);
}
break;

}
}

上面的代码作用是统计不同性别的工程师职级的人数,在Java StreamAPI出来之前,这样类似的业务代码在系统中应该是随处可见的,手打上面的代码我大概花了两分钟,有了Stream之后,我偷了个懒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码Map<String,Map<String,Integer>> result = list.stream().collect(
Collectors.toMap(
person -> person.getGender(),
person -> Collections.singletonMap(person.getLevel(), 1),
(existValue,newValue) -> {
HashMap<String,Integer> newMap = new HashMap<>(existValue);
newValue.forEach((key,value) ->{
if(newMap.containsKey(key)) {
newMap.put(key, newMap.get(key) + 1);
} else {
newMap.put(key, value);
}
});
return newMap;
})
);

或者改成这样的代码

1
2
3
4
5
6
7
8
9
10
复制代码Map<String,Map<String,Integer>> result =  stream.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.toMap(
person->person.getLevel(),
person -> 1,
(existValue,newValue) -> existValue + newValue
)
)
);

不仅代码块减少了许多,甚至逻辑也更清晰了。真的是用stream一时爽,一直用一直爽呀。

Stream作为流,它可以是有限的可以是无限的,当然我们用得最多的还是有限的流(for循环就是有限的流),如上面那张图一样,我们可以对流中的元素做各种各样常见的处理。比如求和,过滤,分组,最大值,最小值等常见处理,所以现在就开始使用Stream吧

Stream的特性

  1. Stream自己不会存储元素,元素可能被存储在底层集合中,或者被生产出来。
  2. Stream操作符不会改变源对象,相反,他们会返回一个持有新对象的stream
  3. Stream操作符是延迟执行的,可能会等到需要结果的时候才去执行。

Stream API

函数式接口 参数类型 返回类型 抽象方法名 描述 其他方法
Runnable 无 void run 执行一个没有参数和返回值的操作 无
Supplier 无 T get 提供一个T类型的值
Counsumer T void accept 处理一个T类型的值 chain
BiConsumer<T,U> T,U void accept 处理T类型和U类型的值 chain
Function<T,R> T R apply 一个参数类型为T的函数 compose,andThen,identity
BiFunction<T,U,R> T,U R apply 一个参数类型为T和U的函数 andThen
UnaryOperator T T apply 对类型T进行的一元操作 compose,andThen,identity
BinaryOperator T,T T apply 对类型T进行二元操作 andThen
Predicate T boolean test 一个计算boolean值的函数 And,or,negate,isEqual
BiPredicate<T,U> T,U boolean test 一个含有两个参数,计算boolean值的函数 and,or,negate

map()和flatMap()的区别

使用map方法的时候,相当于对每个元素应用一个函数,并将返回的值收集到新的Stream中。

1
2
3
4
5
6
复制代码Stream<String[]>	-> flatMap ->	Stream<String>
Stream<Set<String>> -> flatMap -> Stream<String>
Stream<List<String>> -> flatMap -> Stream<String>
Stream<List<Object>> -> flatMap -> Stream<Object>

{{1,2}, {3,4}, {5,6} } -> flatMap -> {1,2,3,4,5,6}

中间操作以及结束操作

Stream上的所有操作分为两类:中间操作和结束操作,中间操作只是一种标记(调用到这类方法,并没有真正开始流的遍历。),只有结束操作才会触发实际计算。简单的说就是API返回值仍然是Stream的就是中间操作,否则就是结束操作。

如何debug

  1. 请使用代码段,比如IntStream.of(1,2,3,4,5).fiter(i -> {return i%2 == 0;})将断点打在代码段上即可。
  2. 引用方法也可以进行调试,在isDouble中打上断点比如IntStream.of(1,2,3,4,5).fiter(MyMath::isDouble)

那些不好理解的API

  1. reduce()
    我们以前做累加是如何完成的呢?
1
2
3
4
5
复制代码
int sum = 0;
for(int value in values) {
sum = sum + value;
}

现在改成stream的方式来实现

1
复制代码values.stream().reduce(Integer::sum);

这个reduce()方法就是一个二元函数:从流的前两个元素开始,不断将它应用到流中的其他元素上。

如何写好Stream代码

stream API就是为了方便而设计的,在sql层面并不方便处理的数据可以通过stream来实现分组,聚合,最大值,最小值,排序,求和等等操作。所以不要把它想得太复杂,只管写就好了。总有那么一天你熟练了就可以写出简洁得代码。或者从现在开始把你项目中的大量for循环改造成stream方式。

代码示例

本来想写大段代码来样式到stream API的转换,但是想了想完全没有必要,github上找了hutool工具类的部分代码来完成转换示例。(可以通过这种方式来提高stream api的能力)

  1. 计算每个元素出现的次数(请先想象下jdk7怎么实现)
1
2
3
复制代码代码效果:[a,b,c,c,c]  -> a:1,b:1,c:3

Arrays.asList("a","b","c","c","c").stream().collect(Collectors.groupingBy(str->str, Collectors.counting()));
  1. 以特定分隔符将集合转换为字符串,并添加前缀和后缀(请先想象下jdk7怎么实现)
1
2
复制代码List<String> myList = Arrays.asList("a","b","c","c","c");
myList.stream().collect(Collectors.joining(",","{","}"));
  1. 判断列表不全为空(请先想象下jdk7怎么实现)
1
复制代码myList.stream().anyMatch(s -> !s.isEmpty());

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

浅谈阿里 Node 框架 Midway 在企业产品中的应用实

发表于 2019-08-09

什么是 Midway

Midway(中途岛)品牌是淘宝技术部(前淘宝 UED)前端部门研发的一款基于 Node.js 的全栈开发解决方案。它将搭配团队的其他产品,Pandora 和 Sandbox,将 Node.js 的开发体验朝着全新的场景发展,让用户在开发过程中享受到前所未有的愉悦感。

Midway 基于 阿里 Egg.js 框架二开,将 IoC 引入到框架中,借鉴 Nest.js,引入丰富的装饰器方法,提升开发中的用户体验。

midway 的一些特性如下。

依赖注入( IoC )

首先想说说 控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称 DI),还有一种方式叫“依赖查找”(Dependency Lookup)。

通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。通俗地来说,有点像上京东或者淘宝购买商品,你只需要在搜索框中输入你要购买的商品,可以选择它的分类、品牌、价格等参数,然后软件就会给你提供你心仪的商品,你只需要下单、购买、等着收货即可。简单明了,如果软件给你推荐的结果,你并不满意,我们就会抛出异常,整个过程无需你自己控制,而是电商平台类似容器的结构来控制。

所有的商品都会在电商平台中注册,你只需要告诉 Ioc 你是什么东西,你要买什么东西,然后 Ioc 就会在系统运行到你需要的时候,将你要的东西传递给你。同时也将你售卖的东西,交付给其他需要的地方,所有商品的创建、配送、销毁,全部由 IoC 控制。这就是控制反转。

在这里插入图片描述

Midway 框架采用 injection 这个 Npm 包做 IoC 控制,这个包本身也是淘宝中途岛团队自主开发的,实现了依赖注入。

基于 Egg.js

Midway 是基于阿里开源的另一款 Node 框架 Egg.js 为基础开发的。

Egg.js 号称为企业级框架和应用而生,Egg 采用“约定优于配置”,规划了一套统一的约定,进行开发,我司现有产品采用 Egg.js 进行开发迭代,Egg.js 约束了一套目录规范和开发规范和插件规范,减少因为人的差异导致项目规范混乱,Egg.js 有很高的可扩展性,使用 Egg.js 提供的 loader 机制可以让框架根据开发者自己的规划,定义默认配置,亦可覆盖 Egg.js 的默认配置。

因为 Egg.js 的这种高度可扩展性,给开发者提供了基于 Egg.js 封装上层框架的能力,并且 Egg.js 插件高度可扩展,并且现有生态较为完备,并且兼容 koa 的插件,生态环境非常优异。Egg.js 通过 Node 官方的 Cluster 模块,实现多进程模型,充分利用多核心 CPU 性能。另外 Egg.js 在淘宝双十一,和阿里绝大部分的 Web 系统中的表现,充分的说明了,Egg.js 优异的稳定性。

个人觉得 Midway 选择 Egg.js 的原因有以下几点:

  1. 底层基于 koa,提过封装上层框架能力;
  2. 双十一的并发量都可以支撑住(不要给我说,你们平台的流量能够比淘宝天猫双十一还要高);
  3. 兼容 koa 插件,Egg.js 插件也有几百个,一些使用率很高的框架由官方维护,例如,egg-mongoose、egg-sequelize 等等;
  4. 阿里维护的项目,并且阿里自身也在深度使用;
  5. 问题回答的效率,首先是国内的框架,你可以用中文提交 issue,另外就是回复效率很高(本人提过几个 issue,基本上一小时内就回复了,不知道是不是我问题简单的原因)。

采用 Typescript

TypeScript是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。安德斯·海尔斯伯格,C# 的首席架构师,已工作于 TypeScript 的开发。

  • TypeScript 扩展了 JavaScript 的语法,所以任何现有的JavaScript 程序可以不加改变的在 TypeScript 下工作。
  • TypeScript 是为大型应用之开发而设计,而编译时它产生 JavaScript 以确保兼容性。
  • TypeScript 支持为已存在的 JavaScript 库添加类型信息的头文件,扩展了它对于流行的库如 jQuery、MongoDB、Node.js 和 D3.js 的好处。并且我们经常使用的宇宙第一编辑器 Visual studio code 也是 Typescript 开发的。
  • Typescript 可以使用 Javascript 中的所有代码和概念,Typescript 是为了 JavaScript 开发更加容易而诞生的,Typescript 只从语义核心方面对 JavaScript 原有模型进行扩展,所以 JavaScript 可以无需修改,或少量修改即可和 Typescript 同时工作,也可以使用编译器将 Typescript 转换为 JavaScript 代码。
  • Typescript 通过类型注解,提供编译时的静态检查,并且为函数提供了缺省参数,并且引入了模块的概念,可以把声明、数据、函数和类封装在模块中。使用 Typescript 相较于 JavaScript 有以下显著优势:
  1. 静态检查
  2. 大型项目的规范,使用 Typescript 更容易重构
  3. 协作能力
  4. 生产力,干净的 ES6 代码,自动完成和动态输入提高了开发者的工作效率

提供多种装饰器

因为 Midway 使用 Typescript 开发,所以支持装饰器方法。在 Java 开发中,装饰器模式非常常见,通过装饰器方法可以向一个现有的对象添加新的功能,并且不会改变对象的结构,装饰器和被装饰的对象可以独立,不会相互耦合。

举例说明,例如 Midway 提供了路由装饰器,可以直接通过装饰器方法声明路由。代码示例如下(代码来源于官方示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码import { provide, controller, inject, get } from 'midway';

@provide()
@controller('/user')
export class UserController {

@inject('userService')
service: IUserService;

@get('/:id')
async getUser(ctx): Promise<void> {
const id: number = ctx.params.id;
const user: IUserResult = await this.service.getUser({id});
ctx.body = {success: true, message: 'OK', data: user};
}
}

如上代码使用 @controller 声明这个类为控制器类,同时通过标注请求,声明了请求的方法,除了 @get 以外,Midway 还基于 koa-router 封装了 @post、@del、 @put、@patch、@options、@head、 @all,其他装饰器方法的示例可以通过 Midway 官方文档查看,本文只做简要介绍,后续本人可能会写一系列的 Midway 实战教程,敬请期待。

浅谈我司现有产品的技术痛点

我司基于 Egg.js 开发了一套通用 OA 产品(持续迭代中),在实际开发过程中,体验还是不错,但是也有一些开发体验不好的地方,因为使用 JavaScript 所以自动补齐,智能提示基本上是无法使用,虽然官方或个人提供了插件但是使用起来,总是有些不尽如人意,另外我们的路由声明、多文件
多文件夹,维护起来很麻烦:比如说我要写一个新的接口,我需要先去 router 文件夹里面创建一份路由文件,然后在 /app/router.js 文件里引用这个文件。

在这里插入图片描述

然后再去控制器里面创建文件,编写方法,好几个文件,来回切换。所以我觉得使用装饰器方法声明路由还是很方便的。最开始尝试过使用第三方插件实现装饰器注册路由,但是体验不是很好,后续就没有继续使用了。

另外就是 JavaScript 语言本身的痛点,代码规范、接口声明、类型效验问题、多人同时开发,每个人的开发习惯都不相同,所以长期迭代维护的项目可能会因为每个人的习惯不同,可维护性逐渐降低,我们现在的私有化部署项目,很多低级 Bug 都是因为类型不对,数据结构不对等之类的引起。

如果真的要维护这个项目几年,JavaScript 灵活度很高,所以项目前期可以使用 JavaScript 构建,以换取开发效率高,但是我觉得如果不进行规范约束,后期几乎不可维护。从企业产品角度出发,因为 Typescript 面向对象编程语言的结构,保持了代码的整洁度,代码规范的一致性,因此我个人觉得,在企业项目中使用 Typescript 更加适合。如果只是小型项目或个人项目,JavaScript 更适合灵活开发,开发效率更高。不过一切技术的选择都要从实际场景出发。任何不从实际场景出发的技术选型都是耍流氓。

使用 Midway 的意义

根据之前讲的 Midway 的特性,我简单总结了一下,使用 Midway 带来的好处,有以下几点。

  1. 使用 Midway 学习成本低,如果你之前使用过阿里的 Egg.js 框架,基本上不需要怎么学习,即可用于实际工作中
  2. 各种装饰器方法提升开发效率
  3. 使用 Typescript 使用
  4. 使用 Ioc,优化项目依赖管理
  5. 底层基于 Egg.js 兼容 Egg.js 的所有生态
  6. 采用 Typescript,强类型,面向接口编程
  7. 提供装饰器方法,简化开发

如何从 Egg 平稳迁移到 Midway

将项目从一个框架,迁移到另一个框架,并不是一件简单的事,不过把 Egg 项目重构为 Midway 还算是没有什么特别的困难,首先因为 Midway 基于 Egg.js 所以之前项目中使用的 egg 插件或 koa 插件,可以无需修改,或者少量修改,即可在 Midway 中使用,另外因为 Midway 的很多方法与 Egg.js 保持一致,所以大部分你在 Egg.js 中使用的方法,亦可在 Midway 中继续使用,目录结构这部分,与 Egg.js 大致相同,不过 Midway 在其基础上,重新对项目结构分层,将项目分为 Web 层和业务逻辑层:

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
复制代码├── README.md
├── README.zh-CN.md
├── dist ---- 编译后目录
├── logs ---- 本地日志目录
│ └── midway6-test ---- 日志应用名开头
│ ├── common-error.log ---- 错误日志
│ ├── midway-agent.log ---- agent 输出的日志
│ ├── midway-core.log ---- 框架输出的日志
│ ├── midway-web.log ---- koa 输出的日志
│ └── midway6-test-web.log
├── package.json
├── src ---- 源码目录
│ ├── app ---- web 层目录
│ │ ├── controller ---- web 层 controller 目录
│ │ │ ├── home.ts
│ │ │ └── user.ts
│ │ ├── middleware (可选) ---- web 层中间件目录
│ │ │ └── trace.ts
│ │ ├── public (可选) ---- web 层静态文件目录,可以配置
│ │ ├── view (可选)
│ │ | └── home.tpl ---- web 层模板
│ ├── config
│ │ ├── config.default.ts
│ │ ├── config.local.ts
│ │ ├── config.prod.ts
│ │ ├── config.unittest.ts
│ │ └── plugin.ts
│ └── lib ---- 业务逻辑层目录,自由定义
│ │ └── service ---- 业务逻辑层,自由定义
│ │ └── user.ts
│ ├── interface.ts ---- 接口定义文件,自由定义
│ ├── app.ts ---- 应用扩展文件,可选
│ └── agent.ts ---- agent 扩展文件,可选
├── test
│ └── app
│ └── controller
│ └── home.test.ts
├── tsconfig.json
└── tslint.json

另外就是在使用 Typescript 后,开发者需要编写接口定义,和声明文件,不过我相信,大家学习一些 Typescript 知识以后,这些都不是问题。另外,是否需要重构项目也要从实际情况出发,应先实际调研,当前项目是否规范混乱,难以维护,另外就是重构的意义是否大于重构的成本。如果你的项目还没有开始实施,或刚刚开始实施,如果你想使用 Typescript 作为产品语言,我觉得 Midway 可以作为你的框架选型之一,另外可能有些人会说,如果我都要写 Typescript 了,为什么我不用 Nest.js 呢?我觉得框架选型还是要从实际出发,你就直接使用,而是要看,这个框架的性能,稳定性,维护团队,未来规划等等方面出发,我选择 Egg.js,选择 Midway 首先是因为它的维护团队是阿里巴巴,性能稳定,另外就是有 IoC 机制,优化了开发体验。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

大厂面试Kafka,一定会问到的幂等性

发表于 2019-08-09

01 幂等性如此重要

Kafka作为分布式MQ,大量用于分布式系统中,如消息推送系统、业务平台系统(如结算平台),就拿结算来说,业务方作为上游把数据打到结算平台,如果一份数据被计算、处理了多次,产生的后果将会特别严重。
02 哪些因素影响幂等性


使用Kafka时,需要保证exactly-once语义。要知道在分布式系统中,出现网络分区是不可避免的,如果kafka broker 在回复ack时,出现网络故障或者是full gc导致ack timeout,producer将会重发,如何保证producer重试时不造成重复or乱序?又或者producer 挂了,新的producer并没有old producer的状态数据,这个时候如何保证幂等?即使Kafka 发送消息满足了幂等,consumer拉取到消息后,把消息交给线程池workers,workers线程对message的处理可能包含异步操作,又会出现以下情况:

  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失
    • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行
    • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失

本文将针对以上问题作出讨论

03 Kafka保证发送幂等性

针对以上的问题,kafka在0.11版新增了幂等型producer和事务型producer。前者解决了单会话幂等性等问题,后者解决了多会话幂等性。

单会话幂等性

为解决producer重试引起的乱序和重复。Kafka增加了pid和seq。Producer中每个RecordBatch都有一个单调递增的seq; Broker上每个tp也会维护pid-seq的映射,并且每Commit都会更新lastSeq。这样recordBatch到来时,broker会先检查RecordBatch再保存数据:如果batch中 baseSeq(第一条消息的seq)比Broker维护的序号(lastSeq)大1,则保存数据,否则不保存(inSequence方法)。
ProducerStateManager.scala

1
复制代码private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {    validationType match {      case ValidationType.None =>      case ValidationType.EpochOnly =>        checkProducerEpoch(producerEpoch, offset)      case ValidationType.Full =>        checkProducerEpoch(producerEpoch, offset)        checkSequence(producerEpoch, firstSeq, offset)    }}private def checkSequence(producerEpoch: Short, appendFirstSeq: Int, offset: Long): Unit = {  if (producerEpoch != updatedEntry.producerEpoch) {    if (appendFirstSeq != 0) {      if (updatedEntry.producerEpoch != RecordBatch.NO_PRODUCER_EPOCH) {        throw new OutOfOrderSequenceException(s"Invalid sequence number for new epoch at offset $offset in " +          s"partition $topicPartition: $producerEpoch (request epoch), $appendFirstSeq (seq. number)")      } else {        throw new UnknownProducerIdException(s"Found no record of producerId=$producerId on the broker at offset $offset" +          s"in partition $topicPartition. It is possible that the last message with the producerId=$producerId has " +          "been removed due to hitting the retention limit.")      }    }  } else {    val currentLastSeq = if (!updatedEntry.isEmpty)      updatedEntry.lastSeq    else if (producerEpoch == currentEntry.producerEpoch)      currentEntry.lastSeq    else      RecordBatch.NO_SEQUENCE    if (currentLastSeq == RecordBatch.NO_SEQUENCE && appendFirstSeq != 0) {ne throw mew UnknownProducerIdException(s"Local producer state matches expected epoch $producerEpoch " +        s"for producerId=$producerId at offset $offset in partition $topicPartition, but the next expected " +        "sequence number is not known.")    } else if (!inSequence(currentLastSeq, appendFirstSeq)) {      throw new OutOfOrderSequenceException(s"Out of order sequence number for producerId $producerId at " +        s"offset $offset in partition $topicPartition: $appendFirstSeq (incoming seq. number), " +        s"$currentLastSeq (current end sequence number)")    }  }}  private def inSequence(lastSeq: Int, nextSeq: Int): Boolean = {    nextSeq == lastSeq + 1L || (nextSeq == 0 && lastSeq == Int.MaxValue)  }

引申:Kafka producer 对有序性做了哪些处理

假设我们有5个请求,batch1、batch2、batch3、batch4、batch5;如果只有batch2 ack failed,3、4、5都保存了,那2将会随下次batch重发而造成重复。我们可以设置max.in.flight.requests.per.connection=1(客户端在单个连接上能够发送的未响应请求的个数)来解决乱序,但降低了系统吞吐。
新版本kafka设置enable.idempotence=true后能够动态调整max-in-flight-request。正常情况下max.in.flight.requests.per.connection 大于1。当重试请求到来且时,batch 会根据 seq重新添加到队列的合适位置,并把max.in.flight.requests.per.connection设为1, 这样它 前面的 batch序号都比它小,只有前面的都发完了,它才能发。

1
复制代码    private void insertInSequenceOrder(Deque<ProducerBatch> deque, ProducerBatch batch) {        // When we are requeing and have enabled idempotence, the reenqueued batch must always have a sequence.        if (batch.baseSequence() == RecordBatch.NO_SEQUENCE)            throw new IllegalStateException("Trying to re-enqueue a batch which doesn't have a sequence even " +                "though idempotency is enabled.");        if (transactionManager.nextBatchBySequence(batch.topicPartition) == null)            throw new IllegalStateException("We are re-enqueueing a batch which is not tracked as part of the in flight " +                "requests. batch.topicPartition: " + batch.topicPartition + "; batch.baseSequence: " + batch.baseSequence());        ProducerBatch firstBatchInQueue = deque.peekFirst();        if (firstBatchInQueue != null && firstBatchInQueue.hasSequence() && firstBatchInQueue.baseSequence() < batch.baseSequence()) {            List<ProducerBatch> orderedBatches = new ArrayList<>();            while (deque.peekFirst() != null && deque.peekFirst().hasSequence() && deque.peekFirst().baseSequence() < batch.baseSequence())                orderedBatches.add(deque.pollFirst());            log.debug("Reordered incoming batch with sequence {} for partition {}. It was placed in the queue at " +                "position {}", batch.baseSequence(), batch.topicPartition, orderedBatches.size())            deque.addFirst(batch);            // Now we have to re insert the previously queued batches in the right order.            for (int i = orderedBatches.size() - 1; i >= 0; --i) {                deque.addFirst(orderedBatches.get(i));            }            // At this point, the incoming batch has been queued in the correct place according to its sequence.        } else {            deque.addFirst(batch);        }    }

多会话幂等性

在单会话幂等性中介绍,kafka通过引入pid和seq来实现单会话幂等性,但正是引入了pid,当应用重启时,新的producer并没有old producer的状态数据。可能重复保存。
Kafka事务通过隔离机制来实现多会话幂等性

kafka事务引入了transactionId 和Epoch,设置transactional.id后,一个transactionId只对应一个pid, 且Server 端会记录最新的 Epoch 值。这样有新的producer初始化时,会向TransactionCoordinator发送InitPIDRequest请求, TransactionCoordinator 已经有了这个 transactionId对应的 meta,会返回之前分配的 PID,并把 Epoch 自增 1 返回,这样当old
producer恢复过来请求操作时,将被认为是无效producer抛出异常。 如果没有开启事务,TransactionCoordinator会为新的producer返回new pid,这样就起不到隔离效果,因此无法实现多会话幂等。

1
复制代码private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int, offset: Long): Unit = {    validationType match {      case ValidationType.None =>      case ValidationType.EpochOnly =>        checkProducerEpoch(producerEpoch, offset)      case ValidationType.Full => //开始事务,执行这个判断        checkProducerEpoch(producerEpoch, offset)        checkSequence(producerEpoch, firstSeq, offset)    }}private def checkProducerEpoch(producerEpoch: Short, offset: Long): Unit = {    if (producerEpoch < updatedEntry.producerEpoch) {      throw new ProducerFencedException(s"Producer's epoch at offset $offset is no longer valid in " +        s"partition $topicPartition: $producerEpoch (request epoch), ${updatedEntry.producerEpoch} (current epoch)")    }  }

04 Consumer端幂等性

如上所述,consumer拉取到消息后,把消息交给线程池workers,workers对message的handle可能包含异步操作,又会出现以下情况:

  • 先commit,再执行业务逻辑:提交成功,处理失败 。造成丢失
    • 先执行业务逻辑,再commit:提交失败,执行成功。造成重复执行
    • 先执行业务逻辑,再commit:提交成功,异步执行fail。造成丢失

对此我们常用的方法时,works取到消息后先执行如下code:

1
复制代码if(cache.contain(msgId)){  // cache中包含msgId,已经处理过        continue;}else {  lock.lock();  cache.put(msgId,timeout);  commitSync();  lock.unLock();}// 后续完成所有操作后,删除cache中的msgId,只要msgId存在cache中,就认为已经处理过。Note:需要给cache设置有消息

如果喜欢我的文章,请长按二维码,关注靳刚同学, 同时您的转发也是对我最大的支持,谢谢!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习 underscore 源码整体架构,打造属于自己的函数

发表于 2019-08-08

前言

你好,我是若川。这是学习源码整体架构第二篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库

2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库

3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库

4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

5.学习 vuex 源码整体架构,打造属于自己的状态管理库

6.学习 axios 源码整体架构,打造属于自己的请求库

7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

8.学习 redux 源码整体架构,深入理解 redux 及其中间件原理

感兴趣的读者可以点击阅读。

虽然看过挺多underscore.js分析类的文章,但总感觉少点什么。这也许就是纸上得来终觉浅,绝知此事要躬行吧。于是决定自己写一篇学习underscore.js整体架构的文章。

本文章学习的版本是v1.9.1。
unpkg.com源码地址:https://unpkg.com/underscore@1.9.1/underscore.js

虽然很多人都没用过underscore.js,但看下官方文档都应该知道如何使用。

从一个官方文档_.chain简单例子看起:

1
2
复制代码_.chain([1, 2, 3]).reverse().value();
// => [3, 2, 1]

看例子中可以看出,这是支持链式调用。

读者也可以顺着文章思路,自行打开下载源码进行调试,这样印象更加深刻。

链式调用

_.chain 函数源码:

1
2
3
4
5
复制代码_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};

这个函数比较简单,就是传递obj调用_()。但返回值变量竟然是instance实例对象。添加属性_chain赋值为true,并返回intance对象。但再看例子,实例对象竟然可以调用reverse方法,再调用value方法。猜测支持OOP(面向对象)调用。

带着问题,笔者看了下定义 _ 函数对象的代码。

_ 函数对象 支持OOP

1
2
3
4
5
复制代码var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

如果参数obj已经是_的实例了,则返回obj。
如果this不是_的实例,则手动 new _(obj);
再次new调用时,把obj对象赋值给_wrapped这个属性。
也就是说最后得到的实例对象是这样的结构
{ _wrapped: '参数obj', }
它的原型_(obj).__proto__ 是 _.prototype;

如果对这块不熟悉的读者,可以看下以下这张图(之前写面试官问:JS的继承画的图)。
构造函数、原型对象和实例关系图

继续分析官方的_.chain例子。这个例子拆开,写成三步。

1
2
3
4
5
6
7
8
9
10
复制代码var part1 = _.chain([1, 2, 3]);
var part2 = part1.reverse();
var part3 = part2.value();

// 没有后续part1.reverse()操作的情况下
console.log(part1); // {__wrapped: [1, 2, 3], _chain: true}

console.log(part2); // {__wrapped: [3, 2, 1], _chain: true}

console.log(part3); // [3, 2, 1]

思考问题:reverse本是Array.prototype上的方法呀。为啥支持链式调用呢。
搜索reverse,可以看到如下这段代码:

并将例子代入这段代码可得(怎么有种高中做数学题的既视感^_^):

1
复制代码_.chain([1,2,3]).reverse().value()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码var ArrayProto = Array.prototype;
// 遍历 数组 Array.prototype 的这些方法,赋值到 _.prototype 上
_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
// 这里的`method`是 reverse 函数
var method = ArrayProto[name];
_.prototype[name] = function() {
// 这里的obj 就是数组 [1, 2, 3]
var obj = this._wrapped;
// arguments 是参数集合,指定reverse 的this指向为obj,参数为arguments, 并执行这个函数函数。执行后 obj 则是 [3, 2, 1]
method.apply(obj, arguments);
if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
// 重点在于这里 chainResult 函数。
return chainResult(this, obj);
};
});
1
2
3
4
5
复制代码// Helper function to continue chaining intermediate results.
var chainResult = function(instance, obj) {
// 如果实例中有_chain 为 true 这个属性,则返回实例 支持链式调用的实例对象 { _chain: true, this._wrapped: [3, 2, 1] },否则直接返回这个对象[3, 2, 1]。
return instance._chain ? _(obj).chain() : obj;
};

if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
提一下上面源码中的这一句,看到这句是百思不得其解。于是赶紧在github中搜索这句加上""双引号。表示全部搜索。

搜索到两个在官方库中的ISSUE,大概意思就是兼容IE低版本的写法。有兴趣的可以点击去看看。

I don’t understand the meaning of this sentence.

why delete obj[0]

基于流的编程

至此就算是分析完了链式调用_.chain()和_ 函数对象。这种把数据存储在实例对象{_wrapped: '', _chain: true} 中,_chain判断是否支持链式调用,来传递给下一个函数处理。这种做法叫做 基于流的编程。

最后数据处理完,要返回这个数据怎么办呢。underscore提供了一个value的方法。

1
2
3
复制代码_.prototype.value = function(){
return this._wrapped;
}

顺便提供了几个别名。toJSON、valueOf。
_.prototype.valueOf = _.prototype.toJSON = _.prototype.value;

还提供了 toString的方法。

1
2
3
复制代码_.prototype.toString = function() {
return String(this._wrapped);
};

这里的String() 和new String() 效果是一样的。
可以猜测内部实现和 _函数对象类似。

1
2
3
复制代码var String = function(){
if(!(this instanceOf String)) return new String(obj);
}
1
2
3
复制代码var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};

细心的读者会发现chainResult函数中的_(obj).chain(),是怎么实现实现链式调用的呢。

而_(obj)是返回的实例对象{_wrapped: obj}呀。怎么会有chain()方法,肯定有地方挂载了这个方法到_.prototype上或者其他操作,这就是_.mixin()。

_.mixin 挂载所有的静态方法到 _.prototype, 也可以挂载自定义的方法

_.mixin 混入。但侵入性太强,经常容易出现覆盖之类的问题。记得之前React有mixin功能,Vue也有mixin功能。但版本迭代更新后基本都是慢慢的都不推荐或者不支持mixin。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码_.mixin = function(obj) {
// 遍历对象上的所有方法
_.each(_.functions(obj), function(name) {
// 比如 chain, obj['chain'] 函数,自定义的,则赋值到_[name] 上,func 就是该函数。也就是说自定义的方法,不仅_函数对象上有,而且`_.prototype`上也有
var func = _[name] = obj[name];
_.prototype[name] = function() {
// 处理的数据对象
var args = [this._wrapped];
// 处理的数据对象 和 arguments 结合
push.apply(args, arguments);
// 链式调用 chain.apply(_, args) 参数又被加上了 _chain属性,支持链式调用。
// _.chain = function(obj) {
// var instance = _(obj);
// instance._chain = true;
// return instance;
};
return chainResult(this, func.apply(_, args));
};
});
// 最终返回 _ 函数对象。
return _;
};

_.mixin(_);

_mixin(_) 把静态方法挂载到了_.prototype上,也就是_.prototype.chain方法 也就是 _.chain方法。

所以_.chain(obj)和_(obj).chain()效果一样,都能实现链式调用。

关于上述的链式调用,笔者画了一张图,所谓一图胜千言。

underscore.js 链式调用图解

underscore.js 链式调用图解

_.mixin 挂载自定义方法

挂载自定义方法:
举个例子:

1
2
3
4
5
6
7
复制代码_.mixin({
log: function(){
console.log('哎呀,我被调用了');
}
})
_.log() // 哎呀,我被调用了
_().log() // 哎呀,我被调用了

_.functions(obj)

1
2
3
4
5
6
7
复制代码_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};

_.functions 和 _.methods 两个方法,遍历对象上的方法,放入一个数组,并且排序。返回排序后的数组。

underscore.js 究竟在_和_.prototype挂载了多少方法和属性

再来看下underscore.js究竟挂载在_函数对象上有多少静态方法和属性,和挂载_.prototype上有多少方法和属性。

使用for in循环一试便知。看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码var staticMethods = [];
var staticProperty = [];
for(var name in _){
if(typeof _[name] === 'function'){
staticMethods.push(name);
}
else{
staticProperty.push(name);
}
}
console.log(staticProperty); // ["VERSION", "templateSettings"] 两个
console.log(staticMethods); // ["after", "all", "allKeys", "any", "assign", ...] 138个
1
2
3
4
5
6
7
8
9
10
11
12
复制代码var prototypeMethods = [];
var prototypeProperty = [];
for(var name in _.prototype){
if(typeof _.prototype[name] === 'function'){
prototypeMethods.push(name);
}
else{
prototypeProperty.push(name);
}
}
console.log(prototypeProperty); // []
console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 152个

根据这些,笔者又画了一张图underscore.js 原型关系图,毕竟一图胜千言。

原型关系图

uunderscore.js 原型关系图

整体架构概览

匿名函数自执行

1
2
3
复制代码(function(){

}());

这样保证不污染外界环境,同时隔离外界环境,不是外界影响内部环境。

外界访问不到里面的变量和函数,里面可以访问到外界的变量,但里面定义了自己的变量,则不会访问外界的变量。
匿名函数将代码包裹在里面,防止与其他代码冲突和污染全局环境。
关于自执行函数不是很了解的读者可以参看这篇文章。
[译] JavaScript:立即执行函数表达式(IIFE)

root 处理

1
2
3
4
复制代码var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this ||
{};

支持浏览器环境、node、Web Worker、node vm、微信小程序。

导出

1
2
3
4
5
6
7
8
复制代码if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}

关于root处理和导出的这两段代码的解释,推荐看这篇文章冴羽:underscore 系列之如何写自己的 underscore,讲得真的太好了。笔者在此就不赘述了。
总之,underscore.js作者对这些处理也不是一蹴而就的,也是慢慢积累,和其他人提ISSUE之后不断改进的。

支持 amd 模块化规范

1
2
3
4
5
复制代码if (typeof define == 'function' && define.amd) {
define('underscore', [], function() {
return _;
});
}

_.noConflict 防冲突函数

源码:

1
2
3
4
5
6
复制代码// 暂存在 root 上, 执行noConflict时再赋值回来
var previousUnderscore = root._;
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};

使用:

1
2
3
4
5
6
7
8
9
10
复制代码<script>
var _ = '我就是我,不一样的烟火,其他可不要覆盖我呀';
</script>
<script src="https://unpkg.com/underscore@1.9.1/underscore.js">
</script>
<script>
var underscore = _.noConflict();
console.log(_); // '我就是我,不一样的烟火,其他可不要覆盖我呀'
underscore.isArray([]) // true
</script>

总结

全文根据官网提供的链式调用的例子, _.chain([1, 2, 3]).reverse().value();较为深入的调试和追踪代码,分析链式调用(_.chain() 和 _(obj).chain())、OOP、基于流式编程、和_.mixin(_)在_.prototype挂载方法,最后整体架构分析。学习underscore.js整体架构,利于打造属于自己的函数式编程类库。

文章分析的源码整体结构。

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
复制代码(function() {
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this ||
{};
var previousUnderscore = root._;

var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};

if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
_.VERSION = '1.9.1';

_.chain = function(obj) {
var instance = _(obj);
instance._chain = true;
return instance;
};

var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() : obj;
};

_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return chainResult(this, func.apply(_, args));
};
});
return _;
};

_.mixin(_);

_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
_.prototype[name] = function() {
var obj = this._wrapped;
method.apply(obj, arguments);
if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
return chainResult(this, obj);
};
});

_.each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
_.prototype[name] = function() {
return chainResult(this, method.apply(this._wrapped, arguments));
};
});

_.prototype.value = function() {
return this._wrapped;
};

_.prototype.valueOf = _.prototype.toJSON = _.prototype.value;

_.prototype.toString = function() {
return String(this._wrapped);
};

if (typeof define == 'function' && define.amd) {
define('underscore', [], function() {
return _;
});
}
}());

下一篇文章是学习lodash的源码整体架构。学习 lodash 源码整体架构,打造属于自己的函数式编程类库

读者发现有不妥或可改善之处,欢迎评论指出。另外觉得写得不错,可以点赞、评论、转发,也是对笔者的一种支持。

推荐阅读

underscorejs.org 官网

undersercore-analysis

underscore 系列之如何写自己的 underscore

笔者往期文章

面试官问:JS的继承

面试官问:JS的this指向

面试官问:能否模拟实现JS的call和apply方法

面试官问:能否模拟实现JS的bind方法

面试官问:能否模拟实现JS的new操作符

前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。

若川的博客,用vuepress重构了,阅读体验可能好些

掘金专栏,欢迎关注~

segmentfault前端视野专栏,开通了前端视野专栏,欢迎关注~

知乎前端视野专栏,开通了前端视野专栏,欢迎关注~

语雀前端视野专栏,新增语雀专栏,欢迎关注~

github blog,相关源码和资源都放在这里,求个star^_^~

微信公众号 若川视野

可能比较有趣的微信公众号,长按扫码关注。也可以加微信 ruochuan12,注明来源,拉您进【前端视野交流群】。
若川视野

本文使用 mdnice 排版

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

订单模块数据库表解析(三)

发表于 2019-08-06

SpringBoot实战电商项目mall(18k+star)地址:github.com/macrozheng/…

摘要

本文主要对订单退货及订单退货原因设置功能相关表进行解析,采用数据库表与功能对照的形式。

订单退货

相关表结构

订单退货申请表

主要用于存储会员退货申请信息,需要注意的是订单退货申请表的四种状态:0->待处理;1->退货中;2->已完成;3->已拒绝。

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
复制代码create table oms_order_return_apply
(
id bigint not null auto_increment,
order_id bigint comment '订单id',
company_address_id bigint comment '收货地址表id',
product_id bigint comment '退货商品id',
order_sn varchar(64) comment '订单编号',
create_time datetime comment '申请时间',
member_username varchar(64) comment '会员用户名',
return_amount decimal(10,2) comment '退款金额',
return_name varchar(100) comment '退货人姓名',
return_phone varchar(100) comment '退货人电话',
status int(1) comment '申请状态:0->待处理;1->退货中;2->已完成;3->已拒绝',
handle_time datetime comment '处理时间',
product_pic varchar(500) comment '商品图片',
product_name varchar(200) comment '商品名称',
product_brand varchar(200) comment '商品品牌',
product_attr varchar(500) comment '商品销售属性:颜色:红色;尺码:xl;',
product_count int comment '退货数量',
product_price decimal(10,2) comment '商品单价',
product_real_price decimal(10,2) comment '商品实际支付单价',
reason varchar(200) comment '原因',
description varchar(500) comment '描述',
proof_pics varchar(1000) comment '凭证图片,以逗号隔开',
handle_note varchar(500) comment '处理备注',
handle_man varchar(100) comment '处理人员',
receive_man varchar(100) comment '收货人',
receive_time datetime comment '收货时间',
receive_note varchar(500) comment '收货备注',
primary key (id)
);

公司收货地址表

用于处理退货申请时选择收货地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码create table oms_company_address
(
id bigint not null auto_increment,
address_name varchar(200) comment '地址名称',
send_status int(1) comment '默认发货地址:0->否;1->是',
receive_status int(1) comment '是否默认收货地址:0->否;1->是',
name varchar(64) comment '收发货人姓名',
phone varchar(64) comment '收货人电话',
province varchar(64) comment '省/直辖市',
city varchar(64) comment '市',
region varchar(64) comment '区',
detail_address varchar(200) comment '详细地址',
primary key (id)
);

管理端展现

  • 退货申请列表
    展示图片
  • 待处理状态的详情
    展示图片

展示图片

  • 退货中状态的详情
    展示图片

展示图片

  • 已完成状态的详情
    展示图片

展示图片

  • 已拒绝状态的详情
    展示图片

展示图片

移动端展现

  • 在我的中打开售后服务

展示图片

  • 点击申请退货进行退货申请

展示图片

  • 提交退货申请

展示图片

  • 在申请记录中查看退货申请记录

展示图片

  • 查看退货申请进度详情

展示图片

订单退货原因设置

订单退货原因表

用于会员退货时选择退货原因。

1
2
3
4
5
6
7
8
9
复制代码create table oms_order_return_reason
(
id bigint not null auto_increment,
name varchar(100) comment '退货类型',
sort int,
status int(1) comment '状态:0->不启用;1->启用',
create_time datetime comment '添加时间',
primary key (id)
);

管理端展现

  • 退货原因列表

展示图片

  • 添加退货原因

展示图片

移动端展现

  • 退货申请时选择退货原因

展示图片

公众号

mall项目全套学习教程连载中,关注公众号第一时间获取。

公众号图片

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…861862863…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%