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

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


  • 首页

  • 归档

  • 搜索

填坑!完结娱乐圈明星关系图谱

发表于 2019-06-22

填坑填坑!

娱乐圈明星关系图谱体验地址,建议先体验后阅读本文,或者先打开链接,等阅读完没准页面也加载好了(太卡警告!或者去B站看下录制的28s超短视频:超酷炫的娱乐圈明星关系图谱,初次录,戛然而止勿喷):
desertsx.github.io/yulequan-re…

在 InteractiveGraph 实现酷炫关系图谱之前瞻 一文里边扯皮边介绍了娱乐圈明星关系图谱的相关内容,并讲解了项目的关键步骤,但因为一直没有将代码上传到 GitHub,未免有些夸夸其谈,毕竟 talk is cheap, show me the code 才是正事。

)在重新修改过各文件名称(因而会和前瞻一文有些出入,某些平台上可编辑的话会进行修改以确保和本文一致)、去掉无用冗余内容、加好注释说明后,代码已经开源在:DesertsX/YuLeQuan_Stars_Relation_Graph_By_InteractiveGraph。

文件目录结构如下所示,用的是可在线生成的工具:Dir Tree Noter,以下对各文件作用进行简单说明。

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
复制代码YuLeQuan_Stars_Relation_Graph_By_InteractiveGraph
├─ stock_csv2json.py
├─ stock_data.csv
├─ apachecn_csv2json.py
├─ apachecn_data.csv
├─ ylq_all_star_relations.csv
└─ ylq_star_infos.csv
├─ ylq_star_images_spider.py
├─ Infos_and_Data2Json.ipynb
├─ webapp
│ ├─ __init__.py
│ ├─ app_run.py
│ ├─ static
│ │ ├─ honglou.json
│ │ ├─ stock_graph.json
│ │ ├─ apachecn_graph.json
│ │ └─ ylq_star_relation_graph_v2.json
│ │ ├─ images
│ │ │ ├─ person
│ │ │ └─ star
│ │ ├─ lib
│ │ │ ├─ font-awesome-4.7.0
│ │ │ ├─ interactive-graph-0.1.0
│ │ │ └─ jquery-3.2.1
│ ├─ templates
│ │ ├─ apachecn.html
│ │ ├─ stock_relation.html
│ │ └─ yulequan-relations-graph.html
│ └─ views
│ ├─ __init__.py
│ └─ graph_view.py

项目里其实包含三个小项目:stock / apachecn / ylq_star_relation_graph,且分别保留了原始 csv 数据及转换成所需 JSON 数据的 python 代码;处理后的数据位于 webapp/static 下;对应的 html 文件位于 web/templates/ 下;如果图谱里的节点用到了图像,一律在 webapp/static/images 下,而这里的 person 对应 apachecn项目,star 对应 ylq_star_relation_graph 项目,其中后者由 ylq_star_images_spider.py 爬取获得所需的千余张明星图像;webapp/static/lib里用到的是 InteractiveGraph 的资源,未做修改,直接使用即可。

项目用到了 Flask,需自行安装;运行 app_run.py 启动内建的服务器,浏览器里打开http://127.0.0.1:5000,再结合 graph_view.py 里定义过的路由,就能看到三个小项目的对应展示情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码# graph_view.py
from flask import Blueprint, request, render_template
graph = Blueprint('graph', __name__)

# http://127.0.0.1:5000/graph/relation?stock1=%E5%B9%B3%E5%AE%89%E9%93%B6%E8%A1%8C&stock2=%E6%8B%9B%E5%95%86%E9%93%B6%E8%A1%8C
@graph.route('/graph/relation', methods=['GET'])
def get_relation():
stock1 = request.args.get('stock1')
stock2 = request.args.get('stock2')
return render_template('stock_relation.html', stock1=stock1, stock2=stock2)

# http://127.0.0.1:5000/apachecn
@graph.route('/apachecn')
def apachecn():
return render_template('apachecn.html')

# http://127.0.0.1:5000/yulequan-relations-graph
@graph.route('/yulequan-relations-graph') # 不要写成 /graph/yulequan-relations-graph 否则加载头像图片时无法显示
def ylq():
return render_template('yulequan-relations-graph.html')

三个小项目分别对应的链接:

1
2
3
复制代码http://127.0.0.1:5000/graph/relation?stock1=%E5%B9%B3%E5%AE%89%E9%93%B6%E8%A1%8C&stock2=%E6%8B%9B%E5%95%86%E9%93%B6%E8%A1%8C
http://127.0.0.1:5000/apachecn
http://127.0.0.1:5000/yulequan-relations-graph

以上,如果你只想运行本项目,了解这么多即可。

如果你想构建自己的关系图谱,但对数据处理和转换没有头绪,接下来的内容或许能帮助到你。本次娱乐圈明星关系图谱的数据处理和转换在 Infos_and_Data2Json.ipynb 里有详细代码和必要的说明,本文仅简述下要点。

最终想构建出怎样的关系图谱,就需要预先准备好怎样的数据。

)例如当点击明星节点时,想呈现哪些详细介绍内容,就需要在爬取数据时解析和存储对应的数据,本次仅为练手,所以只用到了明星个人主页里很少的数据,以刘烨为例(刘烨个人主页),其 infos 就是 ['中国吉林', '75 KG', '1978-03-23']。

明星关系图谱里涉及明星类和地区类两类节点,而查看爬取完的数据,发现地区数据比较杂乱,还需进行处理。

这里是古柳的一种处理方式,大家可自行DIY:海外的地区一律用对应的国家名;中国的地区有细分的则一律用对应的省份名,无细分的则统一用“中国”;剩下的用“不详”。其中,area_list 和 area_map 是根据实际数据整理出来的,更详细代码见:DesertsX/YuLeQuan_Stars_Relation_Graph_By_InteractiveGraph。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码area_list = ['美国', '以色列', '澳大利亚', '英国', '加拿大', '文莱', '新加坡', '西班牙', '越南', '罗马尼亚', '马来西亚', '菲律宾', '新西兰', 
'韩国', '日本', '北京', '天津', '上海', '重庆', '河北', '山西', '辽宁', '吉林', '江苏', '浙江', '安徽', '福建', '江西', '山东',
'河南', '湖北', '湖南', '广东', '海南', '四川', '贵州', '云南', '陕西', '甘肃', '青海', '台湾', '广西', '西藏', '宁夏', '新疆',
'香港', '澳门', '内蒙古', '黑龙江']

area_map = {'纽约': '美国', '美籍': '美国', '俄克拉荷马': '美国', '加州': '美国', '伦敦': '英国',
'东京': '日本', '京畿道高阳市': '韩国', '大邱广域市': '韩国', '台北':'台湾', '遵义': '贵州',
'南京': '江苏', '青岛': '山东', '深圳': '广东', '杭州': '浙江', '成都': '四川', '衡水': '河北',
'大连': '山东', '齐齐哈尔': '黑龙江', '淮安': '江苏', '温州': '浙江', '唐山': '河北', '福州': '福建',
'营口': '辽东', '武汉': '湖北', '广州': '广东'}

def get_city(address):
for area in area_list:
if area in address:
return area
for area in area_map.keys():
if area in address:
return area_map[area]
if '中国' in address: return '中国'
else: return '不详'
ylq_star_infos['area'] = ylq_star_infos['address'].apply(get_city)
ylq_star_infos.head()

接下来是将爬取的 CSV 数据转换成 InteractiveGraph 所需的 JSON 数据,可以参考Github/InteractiveGraph项目里给出的红楼梦数据:dist/examples/honglou.json。古柳对该数据集曾简单介绍和分析过,可见:安利一个惊艳的红楼梦可视化作品和左手读红楼梦,右手写BUG,闲快活。详细代码一展开讲又会又臭又长,大家还是去 GitHub看吧,有疑问可去“Python交友娱乐会所”(QQ群:613176398),最终 JSON 数据最终格式大致如下,InteractiveGraph 0.1.1 版本还需加一段 JavaScript 代码,InteractiveGraph 0.2.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
40
41
42
43
44
45
46
47
复制代码{
"categories": {
"Star": "明星",
"Area": "地区"
},
"data": {
"nodes": [
{
"label": "中国",
"value": 211,
"id": 117971764772430883811744432104367026350,
"categories": [
"Area"
],
"info": ""
},
...
{
"label": "新垣结衣",
"value": 1,
"id": 71,
"image": "static/images/star/新垣结衣.jpg",
"categories": [
"Star"
],
"info": "日本 / 1988-06-11"
},
...
],
"edges": [
{
"id": 221862466013404320763033294366140362926,
"label": "姐弟",
"from": 801,
"to": 1255
},
...
{
"id": 300862216428897559752162867914678825134,
"label": "出生于",
"from": 1,
"to": 117971766594678621641213275765944971438
},
...
]
}
}

处理完数据,回过头添加对应 yulequan-relations-graph.html 文件;在graph_view.py 里定义路由,渲染 html 文件,运行 app_run.py ,打开 http://127.0.0.1:5000/yulequan-relations-graph 就成功啦!欢迎大家在自己感兴趣的数据集上动手实现酷炫的关系图谱。

以上,完结撒花:欢迎关注公众号“牛衣古柳”哈!

本文转载自: 掘金

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

Jetpack Data Binding入门指南

发表于 2019-06-21

又到周末好时光,开始嗨之前再抽点时间看看本文,能看到最后的都是大佬,收下我的膜拜。本文技术内容讲的是关于Data Binding Library的那点事,有的同学可能解过了,有的娃可能都不知道是什么东东…。为了不落伍,和大家一样优秀,决定写Jetpack方面的文章。与别人不一样的是,会加入自己的理解和栗子,而不是简单的翻译(我英文水平也不行)。如果大家发现有误的地方,希望多加指点,在此谢过。

About Jetpack

一年前有缘看了一下Jetpack,但并没有过多的去关注,最近在看Google IO 2019相关资料,看到了Jetpack的身影,不得不陷入深思,无法自拔。

JetPack的官方说法:

Jetpack 是 Android 软件组件的集合,使您可以更轻松地开发出色的 Android 应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。

总结性

  • 加速开发:以组件的形式供我们依赖使用。
  • 消除样板代码:还记得在Activity中一大堆findViewById么?能做的不止这么多。
  • 构建高质量应用:现在化设计、避开bug、向后兼容。

Android Jetpack 组件是库的集合,这些库是为协同工作而构建的,不过也可以单独采用,同时利用 Kotlin 语言功能帮助提高工作效率。可全部使用,也可混合搭配!

以上是对官网的摘录。作为开山之篇,先从架构方向的数据绑定库入门开始,让同学感受它的魅力。

Data Binding Library(数据绑定库)

借助数据绑定库(Data Binding Library),可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。数据绑定库要求在Android 4.0以上,Gradle 1.5.0以上。实践证明Android SDK和Gradle版本越高,对Data Binding的支持越好,越简单,速度越快。

举个栗子,这个栗子不重,两只手指可以举起来:

1
2
3
复制代码findViewById<TextView>(R.id.sample_text).apply {
text = viewModel.userName
}

栗子中通过findViewById找到TextView组件,并将其绑定到 viewModel 变量的 userName 属性。而下面在布局文件中使用数据绑定库将文本直接分配到TextView组件上,这样就无需调用上述任何 Java 代码。

1
复制代码<TextView  android:text="@{viewmodel.userName}" />

竟然这么好用,为啥不了解看看呢?

配置

在我们的项目build.gradle文件下配置如下代码。

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

如果Gradle插件版本在3.1.0-alpha06以上,可以使用新的Data Binding编译器,有利于加速绑定数据文件的生成。在项目的gradle.properties文件添加如下配置。

1
复制代码android.databinding.enableV2=true

同步一下,没什么问题的话,配置已经成功了~

入门

  • 定义一个数据对象
1
复制代码data class User(var name: String, var age: Int)
  • 布局绑定

我们创建名为activity_main.xml的布局文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.gitcode.jetpack.User"/>
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
//在TextView中使用
<TextView android:layout_width="match_parent"
android:gravity="center"
android:text="@{user.name}"
android:layout_height="match_parent"/>
</LinearLayout>
</layout>

布局文件的根元素不再是以往的LinearLayout、RelativeLayout等等,而是layout。在data元素内添加variable,其属性表示声明一个com.gitcode.jetpack.User类型的变量user。如果多个变量的话,可在data元素下添加多个varialbe元素,格式是一致的。

1
2
3
4
复制代码<data>
<variable name="user" type="com.gitcode.jetpack.User"/>
<variable name="time" type="com.gitcode.jetpack.Time"/>
</data>

在@{}语法中使用表达式将变量赋值给view的属性。例如:这里将user变量的firstName属性赋值给TextView的text属性。

1
复制代码android:text="@{user.firstName}"
  • 绑定数据

此时布局声明的user变量值还是初始值,我们需要为其绑定数据。

默认情况下,会根据目前布局文件名称来生成一个绑定类(binding class),例如当前布局文件名是activity_main,那么生成的类名就是ActivityMainBinding。

绑定类会拥有当前布局声明变量,并声明getter或者setter方法,也就是说ActivityMainBinding类会带有user属性和getUser、setUser方法,变量的默认初始化与Java一致:引用类型为null,int为0,bool为false。

在MainActivity的onCreate()方法中添加如下代码,将数据绑定到布局上。

1
2
3
复制代码val binding: ActivityMainBinding 
= DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.user = User("GitCode", 3)

经典代码是这样的:

1
2
3
4
复制代码setContentView(R.layout.activity_main)
val user=User("GitCode",3)
val tvName=findViewById<TextView>(R.id.tvName)
tvName.text = user.name

可有看出,使用数据绑定库会使代码简洁很多,可读性也很高。
运行一下项目,既可以考到效果了~

运行效果

如果是在Fragment、Adapter中使用,那就要换个姿势了。

1
2
3
4
5
复制代码val listItemBinding = ListItemBinding
.inflate(layoutInflater, viewGroup, false)
//或者
val listItemBinding = DataBindingUtil
.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

恭喜,你已经入门了

可以选择继续学习,

看下文

也可以当做了解

点个赞

看看其他文章了~

布局与绑定表达式

在一开始介绍Data Binding Libaray时,就使用了@{}语法,花括号里面的内容称为绑定表达式,绑定表达式其实并不复杂,跟我们正常使用Java和Kotlin语言的表达式没多大区别。那我们可以在表达式中使用什么类型的运算符或者关键字呢?

常用运算符

运算符 符号
算术 加、减、乘、除、求余(+ 、 - 、* 、/、 %)
逻辑 与、或(&&、
一元 + 、-、 !、 ~
移位 >>、 >>>、 <<
关系 == 、> 、<、 >= 、<=(使用符号<时,要换成<)

其他常用的

同时也支持字符拼接+,instanceof,分组、属性访问、数组访问、?:、转型、访问调用,基本类型等等等。
也就是说,绑定表达式语言大多数跟宿主代码(Java or Kotlin)的表达式差不多。为什么说是大多数,因为不能使用this、super、new和Explicit generic invocation(明确的泛型调用)等。

丢个栗子:

1
2
3
复制代码android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

再举丢个栗子:

1
复制代码android:text="@{user.displayName ?? user.lastName}"

如果user.displayName不为null则使用,否则使用user.lastName.在这里也看得出,可以通过表达式访问类的属性。绑定类会自动检查当前变量是否为null,以避免发生空指针异常。栗子:如果user变量为null,那么user.lastName也会是null。

集合

像数组,链表,Maps等常见的集合,都可以采用下标[]访问它们的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List&lt;String>"/>
<variable name="sparse" type="SparseArray&lt;String>"/>
<variable name="map" type="Map&lt;String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
//或者
android:text="@{map.key}"

注意在data元素内添加了import元素,表示导入该类型的定义,这样表达式中引用属性可读性高点,使用也方便。

来个容易掰的栗子:

1
2
3
4
5
6
7
8
9
复制代码<data>
<import type="android.view.View"/>
</data>

<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

通过导入View类型,就可以使用相关属性,例如这里的View.VISIBLE。

有时导入的类全名太长了或者存在相同类型的类名,我们就可以给它取个别名,然后就可用别名进行coding~

1
2
3
4
复制代码<import type="android.view.View"/>

<import type="com.gitcode.jetpack.View"
alias="JView"/>

使用资源

使用下面语法:

1
复制代码android:padding="@{@dimen/largePadding}"

相关资源的的表达式引用,贴张官网截图:

事件处理

数据绑定库允许我们在事件到View时候通过表达式去处理它。
在数据绑定库中支持两种机制:方法调用和监听器绑定。

1
复制代码好想一笔带过,因为原文看不明白~~~~(>_<)~~~~
方法调用

点击事件会直接绑定到处理方法上,当一个事件发生,会直接传给绑定的方法。类似我们在布局上使用android:onclick与Activity 的方法绑定。在编译的时候已经绑定,在@{}表达式中的方法如果在Activity找不到或者方法名错误,就会在编译时期报错,方法签名(返回类型和参数相同)一致。

丢个栗子:

定义一个接口,用于处理事件。

1
2
3
4
复制代码//定义一个处理点击事件的类
interface MethodHandler {
fun onClick(view: View)
}

在布局声明了methodHandler变量,并在Button的onClick方法使用表达式@{methodHandler::onClick},onClick方法需要与上面接口一致,不然编译器期报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
...
<variable name="methodHandler"
type="com.gitcode.jetpack.MethodHandler"/>
</data>

<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_height="match_parent">
...
<Button android:layout_width="wrap_content"
android:text="Method references"
android:layout_marginTop="10dp"
android:onClick="@{methodHandler::onClick}"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>

然后在Activity中实现MethodHandler,并赋值给绑定类的变量。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码class MainActivity : AppCompatActivity(), MethodHandler{
lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
...
binding.methodHandler = this
}

override fun onClick(view: View) {
Log.i(TAG, "Method handling")
}
}

因此,当我们点击Button的时候,Activity的onClick方法就会被回调。

监听器绑定

监听器绑定与方法调用不同的是,监听器不再编译器与处理方法绑定,而是在点击事件传递到当前view时,才与处理方法绑定,而且监听器并不要表达式方法名与处理方法同名,只要返回类型一致即可,如果有返回值得话。

来个栗子:

  • 定义接口用于处理事件
1
2
3
复制代码interface  ListenerHandler {
fun onClickListener(view: View)
}
  • 在布局中定义变量和表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="listener" type="com.gitcode.jetpack.ListenerHandler"/>
</data>

<Button android:layout_width="wrap_content"
android:text="Listener"
android:layout_marginTop="10dp"
android:onClick="@{(view)->listener.onClickListener(view)}"
android:layout_height="wrap_content"/>
</LinearLayout>
<layout>

注意到使用lambda表达式,因此可以在@{}内做更多操作,如预处理数据等。

  • 处理方法
    同样在Activity实现ListenerHandler方法,并赋值给绑定类的变量。
1
2
3
4
5
6
7
8
9
10
11
12
复制代码class MainActivity : AppCompatActivity(), ListenerHandler {
lateinit var binding: ActivityMainBinding

override fun onClickListener(view: View) {
Log.i(TAG, "Listener handling")
}

override fun onCreate(savedInstanceState: Bundle?) {
...
binding.listener=this
}
}

点击Button,就能看到onClickListener回调了~

不过瘾的,看官网吧

好了,讲到这里,大家喝杯奶茶续命,休息会吧~

吃瓜啦

吃完瓜了没?吃完了就该继续撸文了,毕竟革命尚未成功~

绑定类

前面讲的大多数是在布局中去使用表达式,从这开始,讲点代码中的操作。在一开始入门时候,讲到会根据当前布局生成绑定类,绑定类类名由布局名称根据Pascal规则和添加Binding后缀生成。举个栗子就明白了,当前布局名称:activity_shared.xml。生成绑定类名称:ActivitySharedBinding。

那么绑定类的作用是什么?

绑定类是数据绑定库为让我们可以访问布局中的变量和视图而生成的类。

如何创建或者定制绑定类呢?

创建绑定类

  • 使用静态inflate()方法
1
复制代码ActivityMainBinding.inflate(LayoutInflater.from(this))

重载版本

1
复制代码ActivityMainBinding.inflate(getLayoutInflater(), viewGroup, false)
  • 使用静态bind()方法
1
2
复制代码//一般这种情况是布局有作其他用途
ActivityMainBinding.bind(viewRoot)
  • 在Fragment,ListView,或RecyclerView的adapter使用
1
2
3
4
5
6
复制代码val listItemBinding = ListItemBinding.inflate(layoutInflater,
viewGroup, false)
// 或者
val listItemBinding = DataBindingUtil
.inflate(layoutInflater, R.layout.list_item,
viewGroup, false)

定制绑定类

通过修改data元素的class属于达到定制不同名称的绑定类,和其所存储位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码//生成绑定类名为:ContactItem,存放在当前组件的绑定类包中
<data class="ContactItem">
…
</data>

//生成绑定类名为:ContactItem,存放在当前组件包中
<data class=".ContactItem">
…
</data>
//生成绑定类名为:ContactItem,存放在com.gitcode包中
<data class="com.gitcode.ContactItem">
…
</data>

访问Views

如果需要访问布局中Views,需要给Views添加id,数据绑定库会尽快通过findViewById去绑定。并在Activity中通过绑定类使用。例如:

1
复制代码binding.tvName.text="GitCode"

访问变量

数据绑定库会为在布局中声明的变量在绑定类中生成setter和getter。例如:

1
复制代码binding.user=User("GitCode",3)

绑定类官网

绑定适配器

每个布局表达式都对应着一个绑定适配器,用于进行设置相应属性或监听器所需的框架调用.通俗点说,我们通过调用什么方法去给属性赋值?我们在代码通过setText()方法给view的text属性赋值。讲的就是下面的代码:

1
复制代码binding.tvAge.text="20" //通过tvAge的setText()给TextView的android:text属性赋值

好像跟我们平常调用的没什么区别:

1
复制代码tvAge.text="20"

这里讲的就是这个,当数据变化时,我们调用合适的方法(例如setText方法),去给view的属性赋值(例如android:text的text属性)。还不懂的话,继续看~

给View的属性赋值

数据绑定库提供三种方式让我们去给View的属性赋值:库自己决定选择调用方法;明确指定调用方法;自定义调用逻辑方法。

库自动选择

假如View有个属性color,库会尝试去查找setColor(args)方法,参数args的类型需要和表达式的返回类型一致。例如android:color=@{"black"},因为"black"是字符串类型,所以args的参数类型就是String。命名空间android并没有作强制要求,也可以是gitcode:color=@{"black"}。库查找方法的标准是setXXX()方法名和参数类型,这里的XXX是指属性名。

明确指定

虽然库自动选择已经很智能了,但有时view的属性和方法名并不一致,这是就需要我们明确指定,避免库自动选择找不到。例如ImageView的android:tint属性是关联到setImageTintList(ColorStateList)方法,而不是setTint(),这时,就需要明确指定了。

1
2
3
4
5
复制代码@BindingMethods(value = [
BindingMethod(
type = android.widget.ImageView::class,
attribute = "android:tint",
method = "setImageTintList")])

BindingMethods是注解在类上的,例如Activity。可以包含一个到多个BindingMethod注解。BindingMethod中type表示当前方法(method)匹配到到哪个View的属性(attribute)上。

定制逻辑方法

虽然上面两者已经满足了大多数情况,但一些特殊情况还是需要自己处理逻辑的。例如,view的android:paddingLeft属性,没有setPaddingLeft(int)方法,但提供了setPadding(left, top, right, bottom)方法。这时候就需要我们自定义逻辑了。

1
2
3
4
5
6
7
复制代码@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom())
}

BindingAdapter注解允许定制属性的setter逻辑。setPaddingLeft方法的第一个参数必须是我们要处理属性的逻辑的View,后面的参数是根据BindingAdapter注解的属性来定位的。例如这里BindingAdapter注解只声明了android:paddingLeft属性,那么参数padding就是paddigLeft对应的值。设置多个属性是这样子的:

1
2
3
4
5
6
7
复制代码@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
Picasso.get().load(url).error(error).into(view)
}

<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}" />

从这里可以看出,库对命名空间并没有作要求。注解的值imageUrl和error类型必须对应方法参数url和error的类型String和Drawable,只有ImageView同时匹配到两个属性,上述方法才会生效。为此,可以通过设置requireAll = false,匹配一个值也会生效。

1
2
3
4
5
6
7
8
复制代码@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String, placeHolder: Drawable) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}

类型转换

在绑定表达式返回一个对象时,库会选择一个方法来设置属性的值,而该对象会转型为方法参数的类型。这种机制可以方便使用ObservableMap来存储数据。

1
2
3
4
复制代码<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

绑定表达式的userMap["lastName"]会返回值,该值会查找setText(CharSequence) 方法中自动转型为字符串并设置给TextView的text属性。但参数类型不确定的时候,就需要进行强制类型转换了,以表明类型。

有时候,绑定表达式返回的类型与设置属性方法的参数类型并不一致。例如:android:background属性期待的是Drawable(setBackground(drawable),但设置color值时确实一个Int。

1
2
3
4
复制代码<View
android:background="@{@color/red}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

这时候我们需要使用BindingConversion注解将返回值类型Int转换成期待的类型Drawable。

1
2
复制代码@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

总结

写本文的时候,参考官网,看英文文档,对一个英语刚过四级的人…词我都认识,但组成句子,我就一脸懵逼了…

写到一半的时候,想放弃,或者想一笔带过…但,说过,要打造高质量文章,和对读者负责,所以熬了几个夜…夜太黑,没人担心明天会不会后悔~

看了一下别人的文章,基本都是支持参考官网翻译的,并没有加入个人理解和筛选。而本文是在多次参考阅读官网文章之下加入个人理解,让本文更加通俗易懂,更清晰表达官网的意图。

能看到结尾的同学也是很牛逼,需要很大的耐心,给你点个👍。那能不能举个爪,让我看看你们的👐。

Data Binding还有其他知识点,我发现的英语水平已经不够用,大家可以看看原汁原味的官网,或者等到后面我再把它写完…

坚持初心,写优质好文章
开文有益,点赞支持好文

本文是Jetpack系列文章第一篇

第二篇:
Jetpack:你如何管理Activity和Fragment的生命周期?

第三篇:
Jetpack:在数据变化时如何优雅更新Views数据

本文转载自: 掘金

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

Java Serializable:明明就一个空的接口嘛

发表于 2019-06-21

对于 Java 的序列化,我一直停留在最浅显的认知上——把那个要序列化的类实现 Serializbale 接口就可以了。我不愿意做更深入的研究,因为会用就行了嘛。

但随着时间的推移,见到 Serializbale 的次数越来越多,我便对它产生了浓厚的兴趣。是时候花点时间研究研究了。

01、先来点理论

Java 序列化是 JDK 1.1 时引入的一组开创性的特性,用于将 Java 对象转换为字节数组,便于存储或传输。此后,仍然可以将字节数组转换回 Java 对象原有的状态。

序列化的思想是“冻结”对象状态,然后写到磁盘或者在网络中传输;反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。

再来看看序列化 Serializbale 接口的定义:

1
2
复制代码public interface Serializable {
}

明明就一个空的接口嘛,竟然能够保证实现了它的“类的对象”被序列化和反序列化?

02、再来点实战

在回答上述问题之前,我们先来创建一个类(只有两个字段,和对应的 getter/setter),用于序列化和反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码class Wanger {
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

再来创建一个测试类,通过 ObjectOutputStream 将“18 岁的王二”写入到文件当中,实际上就是一种序列化的过程;再通过 ObjectInputStream 将“18 岁的王二”从文件中读出来,实际上就是一种反序列化的过程。

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 class Test {

public static void main(String[] args) {
// 初始化
Wanger wanger = new Wanger();
wanger.setName("王二");
wanger.setAge(18);
System.out.println(wanger);

// 把对象写到文件中
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));){
oos.writeObject(wanger);
} catch (IOException e) {
e.printStackTrace();
}

// 从文件中读出对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));){
Wanger wanger1 = (Wanger) ois.readObject();
System.out.println(wanger1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}

}

不过,由于 Wanger 没有实现 Serializbale 接口,所以在运行测试类的时候会抛出异常,堆栈信息如下:

1
2
3
4
复制代码java.io.NotSerializableException: com.cmower.java_demo.xuliehua.Wanger
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.cmower.java_demo.xuliehua.Test.main(Test.java:21)

顺着堆栈信息,我们来看一下 ObjectOutputStream 的 writeObject0() 方法。其部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}

也就是说,ObjectOutputStream 在序列化的时候,会判断被序列化的对象是哪一种类型,字符串?数组?枚举?还是 Serializable,如果全都不是的话,抛出 NotSerializableException。

假如 Wanger 实现了 Serializable 接口,就可以序列化和反序列化了。

1
2
3
4
5
6
复制代码class Wanger implements Serializable{
private static final long serialVersionUID = -2095916884810199532L;

private String name;
private int age;
}

具体怎么序列化呢?

以 ObjectOutputStream 为例吧,它在序列化的时候会依次调用 writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
desc.checkDefaultSerialize();

int primDataSize = desc.getPrimDataSize();
desc.getPrimFieldValues(obj, primVals);
bout.write(primVals, 0, primDataSize, false);

ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
desc.getObjFieldValues(obj, objVals);
for (int i = 0; i < objVals.length; i++) {

try {
writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
}
}
}

那怎么反序列化呢?

以 ObjectInputStream 为例,它在反序列化的时候会依次调用 readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
desc.checkDefaultSerialize();

int primDataSize = desc.getPrimDataSize();
desc.getPrimFieldValues(obj, primVals);
bout.write(primVals, 0, primDataSize, false);

ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
desc.getObjFieldValues(obj, objVals);
for (int i = 0; i < objVals.length; i++) {

try {
writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
}
}
}

我想看到这,你应该会恍然大悟的“哦”一声了。Serializable 接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成。

03、再来点注意事项

开门见山的说吧,static 和 transient 修饰的字段是不会被序列化的。

为什么呢?我们先来证明,再来解释原因。

首先,在 Wanger 类中增加两个字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码class Wanger implements Serializable {
private static final long serialVersionUID = -2095916884810199532L;

private String name;
private int age;

public static String pre = "沉默";
transient String meizi = "王三";

@Override
public String toString() {
return "Wanger{" + "name=" + name + ",age=" + age + ",pre=" + pre + ",meizi=" + meizi + "}";
}
}

其次,在测试类中打印序列化前和反序列化后的对象,并在序列化后和反序列化前改变 static 字段的值。具体代码如下:

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
复制代码// 初始化
Wanger wanger = new Wanger();
wanger.setName("王二");
wanger.setAge(18);
System.out.println(wanger);

// 把对象写到文件中
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));){
oos.writeObject(wanger);
} catch (IOException e) {
e.printStackTrace();
}

// 改变 static 字段的值
Wanger.pre ="不沉默";

// 从文件中读出对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));){
Wanger wanger1 = (Wanger) ois.readObject();
System.out.println(wanger1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
// Wanger{name=王二,age=18,pre=沉默,meizi=王三}
// Wanger{name=王二,age=18,pre=不沉默,meizi=null}

从结果的对比当中,我们可以发现:

1)序列化前,pre 的值为“沉默”,序列化后,pre 的值修改为“不沉默”,反序列化后,pre 的值为“不沉默”,而不是序列化前的状态“沉默”。

为什么呢?因为序列化保存的是对象的状态,而 static 修饰的字段属于类的状态,因此可以证明序列化并不保存 static 修饰的字段。

2)序列化前,meizi 的值为“王三”,反序列化后,meizi 的值为 null,而不是序列化前的状态“王三”。

为什么呢?transient 的中文字义为“临时的”(论英语的重要性),它可以阻止字段被序列化到文件中,在被反序列化后,transient 字段的值被设为初始值,比如 int 型的初始值为 0,对象型的初始值为 null。

如果想要深究源码的话,你可以在 ObjectStreamClass 中发现下面这样的代码:

1
2
3
4
5
6
7
8
9
复制代码private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT;

int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}

看到 Modifier.STATIC | Modifier.TRANSIENT,是不是感觉更好了呢?

04、再来点干货

除了 Serializable 之外,Java 还提供了一个序列化接口 Externalizable(念起来有点拗口)。

两个接口有什么不一样的吗?试一试就知道了。

首先,把 Wanger 类实现的接口 Serializable 替换为 Externalizable。

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
复制代码class Wanger implements Externalizable {
private String name;
private int age;

public Wanger() {

}

public String getName() {
return name;
}


@Override
public String toString() {
return "Wanger{" + "name=" + name + ",age=" + age + "}";
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {

}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

}

}

实现 Externalizable 接口的 Wanger 类和实现 Serializable 接口的 Wanger 类有一些不同:

1)新增了一个无参的构造方法。

使用 Externalizable 进行反序列化的时候,会调用被序列化类的无参构造方法去创建一个新的对象,然后再将被保存对象的字段值复制过去。否则的话,会抛出以下异常:

1
2
3
4
5
6
7
复制代码java.io.InvalidClassException: com.cmower.java_demo.xuliehua1.Wanger; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150)
at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:790)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1782)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27)

2)新增了两个方法 writeExternal() 和 readExternal(),实现 Externalizable 接口所必须的。

然后,我们再在测试类中打印序列化前和反序列化后的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码// 初始化
Wanger wanger = new Wanger();
wanger.setName("王二");
wanger.setAge(18);
System.out.println(wanger);

// 把对象写到文件中
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {
oos.writeObject(wanger);
} catch (IOException e) {
e.printStackTrace();
}

// 从文件中读出对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {
Wanger wanger1 = (Wanger) ois.readObject();
System.out.println(wanger1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
// Wanger{name=王二,age=18}
// Wanger{name=null,age=0}

从输出的结果看,反序列化后得到的对象字段都变成了默认值,也就是说,序列化之前的对象状态没有被“冻结”下来。

为什么呢?因为我们没有为 Wanger 类重写具体的 writeExternal() 和 readExternal() 方法。那该怎么重写呢?

1
2
3
4
5
6
7
8
9
10
11
复制代码@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}

1)调用 ObjectOutput 的 writeObject() 方法将字符串类型的 name 写入到输出流中;

2)调用 ObjectOutput 的 writeInt() 方法将整型的 age 写入到输出流中;

3)调用 ObjectInput 的 readObject() 方法将字符串类型的 name 读入到输入流中;

4)调用 ObjectInput 的 readInt() 方法将字符串类型的 age 读入到输入流中;

再运行一次测试了类,你会发现对象可以正常地序列化和反序列化了。

序列化前:Wanger{name=王二,age=18}
序列化后:Wanger{name=王二,age=18}

05、再来点甜点

让我先问问你吧,你知道 private static final long serialVersionUID = -2095916884810199532L; 这段代码的作用吗?

嗯……

serialVersionUID 被称为序列化 ID,它是决定 Java 对象能否反序列化成功的重要因子。在反序列化时,Java 虚拟机会把字节流中的 serialVersionUID 与被序列化类中的 serialVersionUID 进行比较,如果相同则可以进行反序列化,否则就会抛出序列化版本不一致的异常。

当一个类实现了 Serializable 接口后,IDE 就会提醒该类最好产生一个序列化 ID,就像下面这样:

1)添加一个默认版本的序列化 ID:

1
复制代码private static final long serialVersionUID = 1L。

2)添加一个随机生成的不重复的序列化 ID。

1
复制代码private static final long serialVersionUID = -2095916884810199532L;

3)添加 @SuppressWarnings 注解。

1
复制代码@SuppressWarnings("serial")

怎么选择呢?

首先,我们采用第二种办法,在被序列化类中添加一个随机生成的序列化 ID。

1
2
3
4
5
6
7
8
复制代码class Wanger implements Serializable {
private static final long serialVersionUID = -2095916884810199532L;

private String name;
private int age;

// 其他代码忽略
}

然后,序列化一个 Wanger 对象到文件中。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// 初始化
Wanger wanger = new Wanger();
wanger.setName("王二");
wanger.setAge(18);
System.out.println(wanger);

// 把对象写到文件中
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {
oos.writeObject(wanger);
} catch (IOException e) {
e.printStackTrace();
}

这时候,我们悄悄地把 Wanger 类的序列化 ID 偷梁换柱一下,嘿嘿。

1
2
复制代码// private static final long serialVersionUID = -2095916884810199532L;
private static final long serialVersionUID = -2095916884810199533L;

好了,准备反序列化吧。

1
2
3
4
5
6
复制代码try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {
Wanger wanger = (Wanger) ois.readObject();
System.out.println(wanger);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}

哎呀,出错了。

1
2
3
4
5
复制代码java.io.InvalidClassException:  local class incompatible: stream classdesc 
serialVersionUID = -2095916884810199532,
local class serialVersionUID = -2095916884810199533
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27)

异常堆栈信息里面告诉我们,从持久化文件里面读取到的序列化 ID 和本地的序列化 ID 不一致,无法反序列化。

那假如我们采用第三种方法,为 Wanger 类添加个 @SuppressWarnings("serial") 注解呢?

1
2
3
4
复制代码@SuppressWarnings("serial")
class Wanger3 implements Serializable {
// 省略其他代码
}

好了,再来一次反序列化吧。可惜依然报错。

1
2
3
4
5
复制代码java.io.InvalidClassException:  local class incompatible: stream classdesc 
serialVersionUID = -2095916884810199532,
local class serialVersionUID = -3818877437117647968
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27)

异常堆栈信息里面告诉我们,本地的序列化 ID 为 -3818877437117647968,和持久化文件里面读取到的序列化 ID 仍然不一致,无法反序列化。这说明什么呢?使用 @SuppressWarnings("serial") 注解时,该注解会为被序列化类自动生成一个随机的序列化 ID。

由此可以证明,Java 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,还有一个非常重要的因素就是序列化 ID 是否一致。

也就是说,如果没有特殊需求,采用默认的序列化 ID(1L)就可以,这样可以确保代码一致时反序列化成功。

1
2
3
4
复制代码class Wanger implements Serializable {
private static final long serialVersionUID = 1L;
// 省略其他代码
}

06、再来点总结

写这篇文章之前,我真没想到:“空空其身”的Serializable 竟然有这么多可以研究的内容!

写完这篇文章之后,我不由得想起理科状元曹林菁说说过的一句话:“在学习中再小的问题也不放过,每个知识点都要总结”——说得真真真真的对啊!


本文转载自: 掘金

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

Configuration与Component作为配置类

发表于 2019-06-18

@Configuration注解的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码/**
* @Description 测试用的配置类
* @Author 弟中弟
* @CreateTime 2019/6/18 14:35
*/
@Configuration
public class MyBeanConfig {
@Bean
public Country country(){
return new Country();
}
@Bean
public UserInfo userInfo(){
return new UserInfo(country());
}
}

@Component注解的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码/**
* @Description 测试用的配置类
* @Author 弟中弟
* @CreateTime 2019/6/18 14:36
*/
@Component
public class MyBeanConfig {
@Bean
public Country country(){
return new Country();
}
@Bean
public UserInfo userInfo(){
return new UserInfo(country());
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoTest {

@Autowired
private Country country;

@Autowired
private UserInfo userInfo;

@Test
public void myTest() {
boolean result = userInfo.getCountry() == country;
System.out.println(result ? "同一个country" : "不同的country");
}

}

如果是@Configuration打印出来的则是同一个country,@Component则是不同的country,这是为什么呢?

1
2
3
4
5
6
7
8
9
10
复制代码@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";
}

你点开@Configuration会发现其实他也是被@Component修饰的,因此context:component-scan/ 或者 @ComponentScan都能处理@Configuration注解的类。

@Configuration标记的类必须符合下面的要求:

配置类必须以类的形式提供(不能是工厂方法返回的实例),允许通过生成子类在运行时增强(cglib 动态代理)。

配置类不能是 final 类(没法动态代理)。

配置注解通常为了通过 @Bean 注解生成 Spring 容器管理的类,

配置类必须是非本地的(即不能在方法中声明,不能是 private)。

任何嵌套配置类都必须声明为static。

@Bean 方法可能不会反过来创建进一步的配置类(也就是返回的 bean 如果带有

@Configuration,也不会被特殊处理,只会作为普通的 bean)。

但是spring容器在启动时有个专门处理@Configuration的类,会对@Configuration修饰的类cglib动态代理进行增强,这也是@Configuration为什么需要符合上面的要求中的部分原因,那具体会增强什么呢?
这里是个人整理的思路 如果有错请指点

userInfo()中调用了country(),因为是方法那必然country()生成新的new contry(),所以动态代理增加就会对其进行判断如果userInfo中调用的方法还有@Bean修饰,那就会直接调用spring容器中的country实例,不再调用country(),那必然是一个对象了,因为spring容器中的bean默认是单例。不理解比如xml配置的bean

1
复制代码<bean id="country" class="com.hhh.demo.Country" scope="singleton"/>

这里scope默认是单例。

以上是个人理解,详情源码的分析请看https://www.jb51.net/article/153430.htm

但是如果我就想用@Component,那没有@Component的类没有动态代理咋办呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码/**
* @Description 测试用的配置类
* @Author 弟中弟
* @CreateTime 2019/6/18 14:36
*/
@Component
public class MyBeanConfig {
@Autowired
private Country country;
@Bean
public Country country(){
return new Country();
}
@Bean
public UserInfo userInfo(){
return new UserInfo(country);
}
}

这样就保证是同一个Country实例了


如果有错请大佬们指点 谢谢 0.0

本文转载自: 掘金

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

读源码 redigo为什么多线程不安全

发表于 2019-06-18

redigo是golang的一个操作redis的第三方库,之所以选择这个库,是因为它的文档十分丰富,操作起来也比较简单。一个典型的redigo的使用如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码package main

import (
"github.com/gomodule/redigo/redis"
"log"
)

func main() {
conn, err := redis.Dial("tcp", "192.168.1.2:6379")
if err != nil {
log.Fatalf("dial redis failed :%v\n", err)
}

result, err := redis.String(conn.Do("SET", "hello", "world"))
if err != nil {
log.Fatalln(err)
}

log.Println(result)
}

这里需要注意的一点是,redis 默认是只能本机访问的,可以通过修改 /etc/redis/redis.conf 中的 bind 来实现远程访问,这里我将 bind 改为了服务所在机器的 IP 。

虽然,redigo 的使用十分简单,但是,在它的文档中也指出了一点需要我们特别注意,我们可以在 godoc 中看到原文:

Connections support one concurrent caller to the Receive method and one concurrent caller to the Send and Flush methods. No other concurrency is supported including concurrent calls to the Do and Close methods.

翻译过来就是:

连接支持同时运行单个执行体调用 Receive 和 单个执行体调用 Send 和 Flush 方法。不支持并发调用 Do 和 Close 方法。

本着程序员追根究底的好奇心,我看了一下 redigo 实现 Do 方法的源码,大致弄清楚了为什么 Do 函数是并发不安全的了。它的部分源码如下所示:

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
复制代码func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
return c.DoWithTimeout(c.readTimeout, cmd, args...)
}

func (c *conn) DoWithTimeout(readTimeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
c.mu.Lock()
pending := c.pending
c.pending = 0
c.mu.Unlock()

if cmd == "" && pending == 0 {
return nil, nil
}

if c.writeTimeout != 0 {
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
}

if cmd != "" {
if err := c.writeCommand(cmd, args); err != nil {
return nil, c.fatal(err)
}
}

if err := c.bw.Flush(); err != nil {
return nil, c.fatal(err)
}

var deadline time.Time
if readTimeout != 0 {
deadline = time.Now().Add(readTimeout)
}
c.conn.SetReadDeadline(deadline)

if cmd == "" {
reply := make([]interface{}, pending)
for i := range reply {
r, e := c.readReply()
if e != nil {
return nil, c.fatal(e)
}
reply[i] = r
}
return reply, nil
}

var err error
var reply interface{}
for i := 0; i <= pending; i++ {
var e error
if reply, e = c.readReply(); e != nil {
return nil, c.fatal(e)
}
if e, ok := reply.(Error); ok && err == nil {
err = e
}
}
return reply, err
}

func (c *conn) writeCommand(cmd string, args []interface{}) error {
c.writeLen('*', 1+len(args))
if err := c.writeString(cmd); err != nil {
return err
}
for _, arg := range args {
if err := c.writeArg(arg, true); err != nil {
return err
}
}
return nil
}

上面三个函数实现在 redigo 的 redis 包的 conn.go 文件中,在 DoWithTimeout 方法中,我们可以看到它是顺序执行数据的发送和相应的接收的,而且,函数中还是没有加锁的。虽然,golang 的 TCP 发送底层实现是有加锁的,可以保证一次写操作的数据中,不会有另一次写操作的数据插入,但是,在这个 DoWithTimeout 的实现中,我们还是能隐约闻到一种不安全的味道。

我们把焦点锁定在 writeCommand 这个方法上。从它的实现,我们可以了解到,它的作用主要是在 for … range 中将 redis 的命令发送到 redis-server 执行。这时,我们可能会注意到,这个函数是没有加锁的,如果 for … range 是往一个全局的缓冲去中写数据,那么,并发时很有可能会导致数据的交叉。为了证实这个假设,我们继续看 writeArg 的实现:

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
复制代码func (c *conn) writeArg(arg interface{}, argumentTypeOK bool) (err error) {
switch arg := arg.(type) {
case string:
return c.writeString(arg)
case []byte:
return c.writeBytes(arg)
case int:
return c.writeInt64(int64(arg))
case int64:
return c.writeInt64(arg)
case float64:
return c.writeFloat64(arg)
case bool:
if arg {
return c.writeString("1")
} else {
return c.writeString("0")
}
case nil:
return c.writeString("")
case Argument:
if argumentTypeOK {
return c.writeArg(arg.RedisArg(), false)
}
// See comment in default clause below.
var buf bytes.Buffer
fmt.Fprint(&buf, arg)
return c.writeBytes(buf.Bytes())
default:
// This default clause is intended to handle builtin numeric types.
// The function should return an error for other types, but this is not
// done for compatibility with previous versions of the package.
var buf bytes.Buffer
fmt.Fprint(&buf, arg)
return c.writeBytes(buf.Bytes())
}
}

func (c *conn) writeString(s string) error {
c.writeLen('$', len(s))
c.bw.WriteString(s)
_, err := c.bw.WriteString("\r\n")
return err
}

writeArg 方法是通过判断传入参数的不同来调用不同的方法来写数据的,不过这几个方法的底层其实都是调用了 writeString 这个方法。在 writeString 这个方法的实现中,我们看到 redigo 是把数据都写到 bw 的。bw 是 conn 一个 net.Conn 的 writter,也就是说,如果并发执行 Do 方法的话,这几个并发的执行体都是往同一个 net.Conn的 writter 中写数据的,这基本证实了我上面的假设。

我们回过来看 DoWithTimeout 函数执行了 writeCommand 之后,调用的 bw 的 Flush 方法,这个方法将缓冲区中的数据都发送出去,我们看一下它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码// Flush writes any buffered data to the underlying io.Writer.
func (b *Writer) Flush() error {
if b.err != nil {
return b.err
}
if b.n == 0 {
return nil
}
n, err := b.wr.Write(b.buf[0:b.n])
if n < b.n && err == nil {
err = io.ErrShortWrite
}
if err != nil {
if n > 0 && n < b.n {
copy(b.buf[0:b.n-n], b.buf[n:b.n])
}
b.n -= n
b.err = err
return err
}
b.n = 0
return nil
}

从代码中,我们可以看到,在调用了 b.wr.Write 方法后,有一个判断已写的数据长度是否和缓冲区的数据长度相等的操作。从上面的分析我们可以知道,redigo 在调用 Do 的整个过程中都是没有加锁的,那么,在并发时,一个执行体的 Flush 过程中,很有可能会有别的执行体往 writer 的缓冲区中写数据,出现在调用完 b.wr.Write 之后对已写数据长度小于缓冲区数据长度的现象,从而导致 short write 的错误。

我们可以写一个程序测试一下:

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
复制代码package main

import (
"github.com/gomodule/redigo/redis"
"log"
"sync"
)

func main() {
conn, err := redis.Dial("tcp", "192.168.1.2:6379")
if err != nil {
log.Fatalf("dial redis failed :%v\n", err)
}

wg := sync.WaitGroup{}
wg.Add(2)

go func() {
defer wg.Done()
result, err := redis.String(conn.Do("SET", "hello", "world"))
if err != nil {
log.Fatalln(err)
}
log.Println(result)
}()

go func() {
defer wg.Done()
result, err := redis.String(conn.Do("SET", "hello", "world"))
if err != nil {
log.Fatalln(err)
}
log.Println(result)
}()

wg.Wait()
}

执行之后,果然出现了 short write 的错误:

redigo 的作者推荐我们在并发时使用连接池来保证安全,redigo 的连接池的实现将会在下次一起阅读。

读源码可以了解到开源作者实现开源作品的思路,还可以开拓视野,认识到一些更好的编程技巧,这个习惯可是要好好坚持啊。

本文转载自: 掘金

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

【快学springboot】4接口参数校验 前言 新建一个

发表于 2019-06-16

前言

在开发接口的时候,参数校验是必不可少的。参数的类型,长度等规则,在开发初期都应该由产品经理或者技术负责人等来约定。如果不对入参做校验,很有可能会因为一些不合法的参数而导致系统出现异常。

上一篇文章讲了多种接受参数的方法【快学springboot】3.多种接受参数的方式。因为目前json类型的参数是最常用的,所以这里只讲解json类型的参数校验。其他类型的大同小异。

新建一个Param.java

1
2
3
4
5
6
7
8
9
10
复制代码public class Param {

private String username;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

目前这个类只有一个username属性。

使用Validated注解开启参数校验

1
2
3
4
复制代码@PostMapping(value = "/validate/notblank")
public Object validateNotBlank(@RequestBody @Validated Param param) {
return param;
}

NotBlank 非空判断

修改Param.java,在username属性上添加NotBlank注解

1
2
复制代码@NotBlank
private String username;

测试

【快学springboot】4.接口参数校验

【快学springboot】4.接口参数校验

通过控制台,我们可以看到抛出了一个MethodArgumentNotValidException异常,上面的返回结果其实是springboot默认的异常捕获器返回的。为了统一接口的数据返回格式,我们也需要自定义一个全局异常拦截器,这个将会在下一讲中讲解。

我们还可以通过NotBlank注解的message属性设置异常信息:

1
2
复制代码@NotBlank(message = "username不可为空")
private String username;

【快学springboot】4.接口参数校验

Length字符串长度判断

还是那个Param.java,我们给username加上Length注解,如下:

1
2
3
复制代码@Length(min = 2,max = 3)
@NotBlank(message = "username不可为空")
private String username;

【快学springboot】4.接口参数校验

同样,我们也可以使用message属性来设置返回错误信息。这里就不演示了。

NotNull 限制属性不可谓null

虽然前面有NotBlank注解了,但是一些Integer,Long等包装类型,还是需要用到NotNull注解来判断。

范围判断Min和Max

在Param.java上新增一个age属性,并且使用Min和Max注解限定大小

1
2
3
复制代码@Min(1)
@Max(100)
private Integer age;

这样写的问题是,如果age为空,则不会校验。如下:

【快学springboot】4.接口参数校验

所以我们还需要加上NotNull注解

1
2
3
4
复制代码@Min(1)
@Max(100)
@NotNull
private Integer age;

【快学springboot】4.接口参数校验

【快学springboot】4.接口参数校验

也可使用@Range注解来限定范围

1
复制代码@Range(min = 1, max = 100)

Email 注解校验邮件

1
2
复制代码@Email
private String email;

【快学springboot】4.接口参数校验

Pattern 注解校验正则表达式

1
2
复制代码@Pattern(regexp = "\d{4}-\d{2}-\d{2}")
private String birthday;

【快学springboot】4.接口参数校验

总结

以上总结了部分常用的参数校验的方法,当然还有很详细的一些没有列举处理,有需要的,可以自行搜索使用方式即可。参数校验,往往需要和全局的异常拦截器来配套使用,使得返回的数据结构永远是保持一致的,这样才能方便客户端解释。

下一节会对统一返回格式和全局异常拦截器做一个讲解。

本文转载自: 掘金

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

用 Docker Compose 搭建 Nodejs 应用

发表于 2019-06-15

在本地电脑上开发 Node.js 应用,需要安装一个 Node.js,还有就是准备一个应用需要的数据库管理系统。在本地安装 Node.js 你可以直接下载使用安装包,这样你的系统里只会包含一个版本的 Node.js。如果想同时拥有多个版本的 Node.js,可以使用 NVM 这个工具来管理安装在电脑上的多个版本的 Node.js,你可以在不同版本之间来回切换。

另外还有一种搭建 Node.js 应用开发环境的方法,就是使用 Docker。下面我介绍一下在本地电脑上使用 Docker 与 Docker Compose 搭建一个 Node.js 的开发环境。我会用 Nest.js 这个应用框架作为演示。

需求

在本地使用 Docker 可以去安装一个 Docker 桌面版,比如 Docker for Windows 或 Docker for Mac。在 Mac 电脑上使用 Docker 桌面版不会有太大问题,但是在 Windows 上运行 Docker 桌面版会有一些需求。

系统必须是 Windows 10 专业版或企业版,普通的 Windows 10 不行,而且需要在系统里启用 Hyper-V。

准备

下载安装 Docker 桌面版,体积挺大,需要下一会儿,下载之前你需要使用 Docker Hub 帐号登录一下才行。

  • Windows:https://docs.docker.com/docker-for-windows/install/
  • macOS:https://docs.docker.com/docker-for-mac/install/

Docker 的大部分操作需要在命令行界面下完成,所以你需要准备一个命令行界面,macOS 用户可以使用系统自带的终端(Terminal),Windows 用户建议下载安装完整版的 Cmder,然后新建一个 Bash as admin 的命令行。

配置

Docker 在创建容器的时候,需要用到一些镜像,也就是如果你的系统上还没有这些镜像的话,Docker 会自动到一个地方去下载这个镜像,保存在你的电脑上,然后基于这个镜像去创建你需要的容器。

下载这些镜像的地方默认在国外,所以我们在国内有时会比较慢,解决的方法是配置一下 Docker 让它使用国内的镜像加速地址。比如阿里云暂时就提供了这个服务,你可以使用自己的阿里云帐号登录到阿里云的容器镜像服务,在里面你会找到一个镜像加速地址,看起来像这样:https://wgaccbzr.mirror.aliyuncs.com

把你在阿里云容器镜像服务上找到的加速地址,配置到你的 Docker 的 Registry Mirrors 里面。

Nest.js 开发环境

下面我们用 Docker Compose 搭建一个在本地可以运行 Nest.js 应用的开发环境。

docker-compose.yml

Docker Compose 允许我们在一个文件里描述应用需要的服务(容器),为你要开发的项目新建一个目录,然后在根目录下创建一个 docker-compose.yml 文件。里面先添加两行代码:

1
2
3
复制代码version: '3'

services:

用 version 设置了一下要使用 Docker Compose 版本,一会儿开发环境需要的几个服务会在 services 下面定义。

.env

在 docker-compose.yml 文件里定义的服务可以使用一些环境变量,这些环境变量还有对应的值可以单独放在一个叫 .env 的文件里面,这个文件就相当于是一个配置文件。在项目根目录下面创建一个空白的文件叫:.env 。

准备 Nest.js 应用的命令行工具

你打算开发基于 Nest.js 框架的 Node 应用,可以先去安装这个框架提供的命令行工具(@nestjs/cli),它可以让我们使用命令去创建全新的应用还有应用里需要的一些组件。不过因为我们打算用 Docker 的方式搭建应用的开发环境,所以就不直接在电脑上去安装这个工具了,因为这需要你在电脑上安装 Node.js。

在 docker-compose.yml 文件里定义一个服务,它的作用就是让我们可以使用 Nest.js 框架里提供的命令行工具,打开 docker-compose.yml 文件,在 services 下面添加一个命令行工具服务:

1
2
3
4
5
6
7
8
复制代码version: '3'

services:
cli:
image: nestjs/cli
volumes:
- ./app:/workspace
tty: true

上面定义了一个叫 cli 的服务,这个名字你可以随便定义,这个服务用的 image,也就是镜像是 nestjs/cli,volumes 设置了一下数据卷的功能,意思就是让当前目录下的 app 这个目录,对应容器里的 /workspace 这个位置。把 tty 设置成 true 是为了让这个容器一直运行。

打开系统的终端(Terminal),Windows 用户推荐使用 Cmder。进入到 docker-compose.yml 文件所在的目录,然后运行服务:

1
2
复制代码cd ~/desktop/ninghao-nestjs
docker-compose up -d cli

上面执行了两条命令,第一行是进入到了 docker-compose.yml 文件所在的目录,第二行命令是在后台运行了在 docker-compose.yml 文件里定义的一个叫 cli 的服务。验证一下服务是否运行:

1
2
3
4
复制代码docker-compose ps
Name Command State Ports
-------------------------------------------------
ninghao-nestjs_cli_1 /bin/sh Up 3000/tcp

注意服务的 State 是 Up 表示正在运行,下面可以登入这个 cli 服务:

1
复制代码docker-compose exec cli /bin/sh

进来以后你的命令提示符会像这样:

1
复制代码/workspace #

在这个容器里我们可以使用 Nest.js 应用里的命令行工具,执行:

1
复制代码nest

会出现一些帮助信息:

1
2
3
4
5
6
7
8
9
10
复制代码Usage: nest [options] [command]

Options:
-V, --version output the version number
-h, --help output usage information

Commands:
new|n [options] [name] Generate Nest application
generate|g [options] [name] [path] Generate a Nest element
Available schematics:

用 Nest 命令行工具创建应用

进入到创建的 cli 这个容器里面以后,可以执行 nest 命令,下面我们用这个命令去创建一个 Nest.js 项目。执行:

1
复制代码nest new app

会出现类似的东西:

1
2
3
4
5
6
复制代码⚡  We will scaffold your app in a few seconds..

CREATE /app/.prettierrc (51 bytes)
CREATE /app/README.md (3370 bytes)
CREATE /app/nest-cli.json (84 bytes)
...

上面就是用了 nest new 命令创建了一个项目,放在 app 目录的下面,虽然是在 cli 容器里创建的这个项目,但是我们配置了这个服务的数据卷,所以创建的项目文件也会在本地电脑上看到。也就是你在本地电脑上这个 docker-compose.yml 文件所在的目录的下面,会看到一个 app 目录,这里的东西就是创建的 Nest.js 项目。

在开发应用的时候,如果你要使用 nest 命令行工具生成项目需要的文件,你就可以进入到这个 cli 服务容器里面,然后使用 nest 命令去创建你需要的东西。

创建的项目的时候可能会提示:

1
2
复制代码Failed to execute command: git init
Git repository has not been initialized

这是因为创建完项目之后,nest 命令会去初始化一个代码仓库,但是在这个容器里并没有安装 git ,所以执行相关命令的时候就会出现问题。你可以在本地用 Git 对项目做源代码管理。

注意如果你觉得创建项目的时候速度慢,可以在进入 cli 服务里面以后,执行一下:

1
复制代码npm config set registry https://registry.npm.taobao.org

定义应用服务

在 docker-compose.yml 文件里,再定义一个运行 Nest.js 应用的服务:

1
2
3
4
5
6
7
8
9
复制代码  nest:
image: node:${NODE_VERSION}
working_dir: /home/node/app
command: npm config set registry https://registry.npm.taobao.org
command: npm run start:dev
volumes:
- ./app:/home/node/app
ports:
- ${APP_PORT}:3000

上面定义了一个叫 nest 的服务,因为我们创建的应用是基于 Nest.js 框架的,所以这个服务的名字叫 nest,你也可以换成自己喜欢的名字。

nest 这个服务用的 image 是 node,具体的版本用了一个环境变量,NODE_VERSION,这个环境变量还有对应的值要在 .env 文件里设置一下。

working_dir 进入到工作目录,然后执行了两个 command,一个是设置了一下 npm 的安装源,这样以后安装包的时候会快一些,第二个 command 是运行了项目的开发服务。

volumes 设置了数据卷,让当前目录下的 app 这个目录,对应 nest 这个服务容器里的 /home/node/app ,我们在这个服务的这个目录的下面,执行了 npm run start:dev,这也就会运行这个 Nest.js 项目的开发服务。

ports 设置了公开的端口,就是设置一个主机(本地电脑)上的端口,让这个端口对应这个容器里的某个端口。运行了 Nest 应用的开发服务以后,会使用 3000 这个端口提供服务。${APP_PORT} 这里用了一个叫 APP_PORT 的环境变量,具体的值要在 .env 文件里设置一下:

1
2
复制代码NODE_VERSION=11.13
APP_PORT=3000

注意在 .env 文件里,我们让 APP_PORT 这个环境变量的值等于 3000,也就是公开的端口应该就是 3000:3000,也就是本地电脑上的 3000 端口对应的是这个服务里的 3000 端口。

有了这个新的 nest 服务,要再去运行一下:

1
复制代码docker-compose up -d nest

如果一切正常,打开浏览器,访问 http://localhost:3000,你应该会看到一个 “Hello World”。

以后你需要用 npm install 给项目安装一些 Package 的时候,可以进入到这个 nest 服务里面,使用 npm。进入这个服务可以执行:

1
复制代码docker-compose exec nest bash

定义数据服务

开发 Nest.js 应用支持使用多种不同类型的数据库,需要哪种数据库系统,你就去创建一个对应的服务就行了。比如我要在应用里使用 MySQL 这种数据库,所以可以在 docker-compose.yml 里面,再去定义一个 mysql 服务:

1
2
3
4
5
6
7
8
9
10
11
复制代码  mysql:
image: mysql:${MYSQL_VERSION}
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
restart: always
ports:
- ${MYSQL_PORT}:3306
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}

服务里面用到了一些环境变量,打开 .env ,在文件里去定义这些环境变量还有对应的值:

1
2
3
4
5
6
复制代码MYSQL_VERSION=5.7
MYSQL_PORT=3306
MYSQL_DATABASE=nest
MYSQL_USER=nest
MYSQL_ROOT_PASSWORD=root
MYSQL_PASSWORD=password

主要就是设置一下要使用的 MySQL 系统的版本,在本地主机上访问这个数据服务用的端口是什么,还有创建的数据库的名字、用户还有密码是什么。你可以修改 .env 文件里的这些环境变量的值,来改变数据库的配置。

上面会创建一个 5.7 版本的 MySQL,在本地主机上使用这个数据服务用的端口是 3306,数据库系统里会创建一个叫 nest 的数据库,操作这个数据库可以使用 nest 用户,对应的密码是 password,另外设置了一下数据库系统的 root 用户的密码为 root。

定义好这个数据服务,需要去运行一下:

1
复制代码docker-compose up -d mysql

查看日志

查看容器里的输出的日志,可以执行:

1
复制代码docker-compose logs --follow

附录

docker-compose.yml:

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
复制代码version: '3'

services:
cli:
image: nestjs/cli
volumes:
- ./:/workspace
tty: true
nest:
image: node:${NODE_VERSION}
working_dir: /home/node/app
command: npm config set registry https://registry.npm.taobao.org
command: npm run start:dev
volumes:
- ./app:/home/node/app
ports:
- ${APP_PORT}:3000
mysql:
image: mysql:${MYSQL_VERSION}
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
restart: always
ports:
- ${MYSQL_PORT}:3306
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}

.env:

1
2
3
4
5
6
7
8
9
复制代码NODE_VERSION=11.13
APP_PORT=3000

MYSQL_VERSION=5.7
MYSQL_PORT=3306
MYSQL_DATABASE=nest
MYSQL_USER=nest
MYSQL_ROOT_PASSWORD=root
MYSQL_PASSWORD=password

本文转载自: 掘金

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

消息点击率翻倍的背后——闲鱼无侵入可扩展IFTTT系统 一、

发表于 2019-06-14

作者:闲鱼技术-剑辛

一、面临问题

在闲鱼生态里,用户之间会有很多种关系。其中大部分关系是由买家触发,联系到卖家,比如买家通过搜索、收藏、聊天等动作与卖家产生联系;另外一部分是平台与用户之间的关系。对这些关系分析之后我们发现这些关系中存在两个问题:

  • 用户产生关系的层次不够丰富;
    现有系统只维护了一部分用户关系,包括收藏、点赞等,用户关系的层次还不够丰富。
  • 用户之间关系是单向且不够实时;
    在现有的玩法中,买家可以通过多种行为与卖家产生联系,但卖家不能主动与买家发生关系和互动;而且平台计算的关系都是离线的,对用户的吸引力不足。

上面提到的场景经过抽象归纳之后都是同一个范式:当某个条件被满足之后,就会触发相对应的动作。这个范式是IFTTT的基本理念,而闲鱼IFTTT就是对这些问题的解决方案。

二、IFTTT概念

IFTTT是一个被称为 “网络自动化神器” 的创新型互联网服务理念,它很实用而且概念很简单。IFTTT全称是 *If this then that *,意思是如果满足“this”条件,则触发执行“that”动作。IFTTT由三部分构成,分别为Trigger、Action和Recipe。

undefined

可以看出IFTTT本身概念并不复杂,它的真正魔力在于“由简单组成的复杂”,也就是由众多简单的IFTTT流程相互衔接成跨越整个互联网、跨越多平台、跨越多设备的状态机。

2.1、闲鱼IFTTT

闲鱼IFTTT是基于闲鱼的业务场景与IFTTT理念结合后产生的,提供IFTTT标准协议封装,对业务无侵入可扩展的服务编排系统。

undefined

闲鱼IFTTT的两个特性对应上述两个问题,分别是:

  • 多维用户关系感知

多维指的是覆盖面,闲鱼IFTTT通过更多维度的挖掘,抽象并维护了更丰富的用户关系。基于用户关系数据,我们可以产出用户画像,并通过更有效的方式触达用户。

  • 实时用户双向互动

闲鱼IFTTT底层具有对用户关系大数据的高效存储和处理能力,以支持上层业务中用户关系实时处理;闲鱼IFTTT不仅支持买家到卖家关系,而且通过设计天生支持卖家到买家关系。

闲鱼IFTTT把之前平台与用户的互动、买家到卖家的联系,切换称闲鱼用户之间天然的关系互动,对用户骚扰更少且激活拉回的效果更好,我们基于这个场景设计闲鱼IFTTT的技术方案。

三、技术方案

undefined

首先按照IFTTT规范对业务进行建模,分为Channel、Trigger和Action层,其中Channel层是数据底层,将Trigger和Action关联后组成标准Recipe。

  • Channel
    Channel层在闲鱼IFTTT的作用是保存和管理用户关系数据,Channel层定义了用户关系的元数据结构,包括关系类型、源账户和目标账户。Channel层是闲鱼IFTTT的基石,Trigger和Action均基于用户关系数据进一步抽象业务逻辑。
  • Trigger
    Trigger是业务上自定义的触发事件,与业务息息相关,可能是关注的人上新、浏览宝贝降价或者是参加的百币夺宝活动开奖等。当Trigger触发后,闲鱼IFTTT会根据Trigger类型和配置的关系类型计算用户名单,并调用Action层进行处理。
  • Action
    Action层处理对象是Trigger触发后计算的用户名单,可以给名单里的用户发Push,发权益或者其他定制逻辑。Action本身是标准化、可插拔的组件,业务上可以利用Action组件对用户名单做AB测试,快速实验不同Action策略。

接下来我们说一下闲鱼IFTTT详细技术方案,方案如下:

undefined

整体技术方案按照业务建模的结构图细化,补充依赖的技术组件。整体流程不再细述,针对流程中重点模块详细说明。
3.1、场景快速接入


设计场景快速接入的目的是让业务对接入闲鱼IFTTT无感知,因为在最开始的设计中,场景接入是准备通过在业务逻辑里增加AOP切面,将业务数据和场景上报。但因为这种方式对业务本身有一定侵入,增加业务执行的RT而且不够灵活,最终被否决。

而现在的场景快速接入方案解决了这些问题,通过SLS接入所有应用的海量网络请求日志,记录请求的URL、参数和响应;将SLS作为Blink流计算任务的数据源;根据diamond动态下发的规则实时筛选网络请求URL和参数,把数据按照指定格式组装后上报给Channel层。

场景快速接入方案将业务逻辑与场景接入解耦,支持快速接入,灵活变更且延迟低,是针对大数据场景接入的高性能解决方案。

3.2、计算用户名单

计算用户名单模块采用责任链模式设计,因为在不同Trigger场景中,业务对用户名单的计算和筛选逻辑都是不同的。通过责任链模式,将主流程与业务筛选逻辑解耦,并支持各业务灵活定制筛选逻辑,互不干扰。

3.3、PushAction

Action层是闲鱼IFTTT中最重要的一环,会直接触达到用户,Action的逻辑会直接影响用户对平台的直观感受和活跃率。消息Push是Action中最常见的逻辑,更要防止用户被骚扰,PushAction逻辑如下:

  • 敏感人群过滤;
  • 疲劳度校验;
  • 对发送人群进行AB实验;
  • 组装消息;
  • 将Action各节点日志同步到SLS,方便检索和排查问题;
  • 统计消息发送数据及点击数据,为业务后续决策提供依据;

3.3.1、疲劳度

疲劳度是防止用户被骚扰的关键,我们针对疲劳度进行了分层设计,分为三层,第一层为用户级别疲劳度,控制一个用户在一个周期内收到消息数量;第二层是业务维度,控制用户在一个周期内收到某个业务的消息数量;第三层是目标级别,控制用户在一个周期内收到同一个发送者消息数量。

在业务维度层面,支持灵活控制多个业务联合疲劳度,保证用户不会被消息过度骚扰。

undefined

3.4、用户关系存储

用户关系数据是闲鱼IFTTT的基石,它的特点是存储量级大,达到TB级别;而且对存储和查询的性能要求高,TPS和QPS的峰值都在一万以上。经过调研,我们发现集团内部开发的Lindorm可以满足需求。

Lindorm是阿里内部基于Hbase自研的高性能KV存储数据库,对Hbase的性能和稳定性均有一定优化。闲鱼IFTTT采用Lindorm作为用户关系数据存储,经性能测试验证数据读取QPS达到7万,数据存储TPS在10万以上。Lindorm本身性能优异,为闲鱼IFTTT高性能奠定基础。

四、效果验证

闲鱼IFTTT自上线以来,已支持关注上新、浏览宝贝降价和租房小区上新等多个业务场景,提供买卖双方实时双向互动能力,平均每天处理关系数据数亿条,处理Trigger量达到上千万,处理Action量达到亿级别,消息点击率较离线push提高1倍以上。

闲鱼IFTTT目前支持的是用户互动场景,后续我们将结合闲鱼自身业务特点,对IFTTT进行更高维度抽象,封装标准Recipe接口,将闲鱼IFTTT打造成提供流程编排、管理能力的服务平台。

在我看来,IFTTT从2010年推出以来,在国外有很大的热度,在互联网和物联网领域都有专门的公司和团队在研发,IFTTT的概念虽然简单,却通过标准化协议满足用户的强需求-让各种互联网产品为用户服务。这其实也给我们互联网从业者一些思考:在新机遇面前,究竟是快速投入比较重要还是抽象标准协议解决一类问题更加有效?

五、名词注解

  • SLS:cn.aliyun.com/product/sls
  • Diamond:阿里内部研发的持久配置管理中间件;
  • Blink:data.aliyun.com/product/sc?…
  • MetaQ:阿里内部研发的分布式、队列模型的消息中间件;
  • Lindorm:阿里内部基于HBase研发的新一代分布式NoSQL数据库,阿里云类似产品:www.aliyun.com/product/ots…
  • Tair:阿里内部研发的高性能、分布式、可扩展、高可靠的Key-Value结构存储系统;

更多前沿技术,欢迎订阅公众号“闲鱼技术”

本文转载自: 掘金

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

ArrayList 与 LinkedList

发表于 2019-06-12

摘要

  • 与vector(线程安全) 一起都是list的子类
  • ArrayList基于数组的数据结构 LinkedList基于双向链表的数据结构
  • ArrayList 查询时直接返回数据,LinkedList需要遍历链表,所以在随机查询中ArrayList效率比较高
  • ArrayList 在增删时需要移动元素的位置,需要进行数组的copy,LinkedList需要根据位置查到插入位置的元素,然后再进行添加指针,在数据量比较小时(10000以下)时性能差不多,但是数据量较大时LinkedList效率比较高

1.与vector(线程安全) 一起都是list的子类

List JDK文档描述

2.ArrayList基于数组的数据结构 LinkedList基于双向链表的数据结构

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
复制代码   // ArrayList
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//LinkedList
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);

Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;

Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}

for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}

if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}

size += numNew;
modCount++;
return true;
}

3.ArrayList 查询时直接返回数据,LinkedList需要遍历链表,所以在随机查询中ArrayList效率比较高

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
复制代码    //ArrayList
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

return (E) elementData[index];
}
//LinkedList
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}


Node<E> node(int index) {
// assert isElementIndex(index);

if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

4.ArrayList 在增删时需要移动元素的位置,需要进行数组的copy,LinkedList需要根据位置查到插入位置的元素,然后再进行添加指针,在数据量比较小时(10000以下)时性能差不多,但是数据量较大时LinkedList效率比较高

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
复制代码    //ArrayList
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

//LinkedList
public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码public class Test1 {
static List<Integer> arrayData =new ArrayList<Integer>();
static List<Integer> linkedData =new LinkedList<Integer>();
public static int maxCount = 30000;

public static void main(String[] args) {

for(int i=0;i<maxCount;i++){
arrayData.add(i);
linkedData.add(i);
}
System.out.println("maxCount:"+maxCount);
//获得两者随机访问的时间
System.out.println("arrayData get time:"+getTime(arrayData));
System.out.println("linkedData get time:"+getTime(linkedData));
//获得两者插入数据的时间
System.out.println("arrayData insert time:"+insertTime(arrayData));
System.out.println("linkedData insert time:"+insertTime(linkedData));

}
//获取数据
public static long getTime(List<Integer> list){
long time=System.currentTimeMillis();
for(int i = 0; i < maxCount; i++){
int getData = list.get(i);
}
return System.currentTimeMillis()-time;
}

//插入数据
public static long insertTime(List<Integer> list){
long num = maxCount;
int index = 1000;
long time=System.currentTimeMillis();
for(int i = 1; i < num; i++){
list.add(index, i);
}
return System.currentTimeMillis()-time;
}

}

6.运行结果

1
2
3
4
5
6
7
8
9
10
11
复制代码maxCount:10000
arrayData get time:1
linkedData get time:82
arrayData insert time:24
linkedData insert time:27

maxCount:30000
arrayData get time:2
linkedData get time:658
arrayData insert time:127
linkedData insert time:58

本文转载自: 掘金

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

开发者必备Mysql命令

发表于 2019-06-12

摘要

开发者必备Mysql常用命令,涵盖了数据定义语句、数据操纵语句及数据控制语句,基于Mysql5.7。

数据定义语句(DDL)

数据库操作

  • 登录数据库:
1
复制代码mysql -uroot -proot
  • 创建数据库:
1
复制代码create database test
  • 查看所有数据库:
1
复制代码show databases

展示图片

  • 选择数据库并使用:
1
复制代码use test
  • 查看所有数据表:
1
复制代码show tables
  • 删除数据库:
1
复制代码drop database test

表操作

  • 创建表:
1
复制代码create table emp(ename varchar(10),hiredate date,sal decimal(10,2),deptno int(2))
1
复制代码create table dept(deptno int(2),deptname varchar(10))

展示图片

  • 查看表的定义:
1
复制代码desc emp

展示图片

  • 查看表定义(详细):
1
复制代码show create table emp \G

展示图片

  • 删除表:
1
复制代码drop table emp
  • 修改表字段:
1
复制代码alter table emp modify ename varchar(20)
  • 添加表字段:
1
复制代码alter table emp add column age int(3)
  • 删除表字段:
1
复制代码alter table emp drop column age
  • 字段改名;
1
复制代码alter table emp change age age1 int(4)
  • 修改表名:
1
复制代码alter table emp rename emp1

数据操纵语句(DML)

插入记录

  • 指定名称插入:
1
复制代码insert into emp (ename,hiredate,sal,deptno) values ('zhangsan','2018-01-01','2000',1)
  • 不指定名称插入:
1
复制代码insert into emp values ('lisi','2018-01-01','2000',1)
  • 批量插入数据:
1
复制代码insert into dept values(1,'dept1'),(2,'dept2')

修改记录

1
复制代码update emp set sal='4000',deptno=2 where ename='zhangsan'

删除记录

1
复制代码delete from emp where ename='zhangsan'

查询记录

  • 查询所有记录:
1
复制代码select * from emp
  • 查询不重复的记录:
1
复制代码select distinct deptno from emp
  • 条件查询:
1
复制代码select * from emp where deptno=1 and sal<3000
  • 排序和限制:
1
复制代码select * from emp order by deptno desc limit 2
  • 分页查询(查询从第0条记录开始10条):
1
复制代码select * from emp order by deptno desc limit 0,10
  • 聚合(查询部门人数大于1的部门编号):
1
复制代码select deptno,count(1) from emp group by deptno having count(1) > 1
  • 连接查询:
1
复制代码select * from emp e left join dept d on e.deptno=d.deptno
  • 子查询:
1
复制代码select * from emp where deptno in (select deptno from dept)
  • 记录联合:
1
复制代码select deptno from emp union select deptno from dept

数据控制语句(DCL)

权限相关

  • 授予操作权限(将test数据库中所有表的select和insert权限授予test用户):
1
复制代码grant select,insert on test.* to 'test'@'localhost' identified by '123'
  • 查看账号权限:
1
复制代码show grants for 'test'@'localhost'

展示图片

  • 收回操作权限:
1
复制代码revoke insert on test.* from 'test'@'localhost'

展示图片

  • 授予所有数据库的所有权限:
1
复制代码grant all privileges on *.* to 'test'@'localhost'
  • 授予所有数据库的所有权限(包括grant):
1
复制代码grant all privileges on *.* to 'test'@'localhost' with grant option
  • 授予SUPER PROCESS FILE权限(系统权限不能指定数据库):
1
复制代码grant super,process,file on *.* to 'test'@'localhost'
  • 只授予登录权限:
1
复制代码grant usage on *.* to 'test'@'localhost'

帐号相关

  • 删除账号:
1
复制代码drop user 'test'@'localhost'
  • 修改自己的密码:
1
复制代码set password = password('123')
  • 管理员修改他人密码:
1
复制代码set password for 'test'@'localhost' = password('123')

其他

字符集相关

  • 查看字符集:
1
复制代码show variables like 'character%'

展示图片

  • 创建数据库时指定字符集:
1
复制代码create database test2 character set utf8

展示图片

时区相关

  • 查看当前时区(UTC为世界统一时间,中国为UTC+8):
1
复制代码show variables like "%time_zone%"

展示图片

  • 修改mysql全局时区为北京时间,即我们所在的东8区:
1
复制代码set global time_zone = '+8:00';
  • 修改当前会话时区:
1
复制代码set time_zone = '+8:00'

展示图片

  • 立即生效:
1
复制代码flush privileges

公众号

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

公众号图片

本文转载自: 掘金

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

1…868869870…956

开发者博客

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