hive sql调优

这里记录一下hive任务调优的三(n)板斧.

map join

对于存在join的sql,首先最简单的就是开启map join:

1
2
set hive.auto.convert.join = true ; -- 开启自动转化成mapjoin
set hive.mapjoin.smalltable.filesize = 2500000 ; -- 设置广播小表size

sql中足够小的表应该放在join操作左边. 由于小表数据会被广播到各个节点,消除了shuffle运算,提高了运算效率.
前提当然是存在足够小的表. 实际业务中一般是各种维度表.

排序消除

注: 是否加速取决于数据集.

排序属于非常耗时的操作(O(nlogn)),所以对于order by,sort by语句,可以从语义上寻找突破口. 例如对于每天最后一次的用户行为,原来的可能是这样写的:

1
2
3
4
5
6
7
SELECT *
(select userid
,url
,row_number(partition by userid order by bigint(timestamp)DESC) as rank
FROM xxxx
)AS a
where rank=1

可以改为先求最大时间戳,再进行join(map join):

1
2
3
4
select * from
(select userid,max(bigint(timestamp)) from xxx group by userid) as a
join xxx on b
on a.timestamp=b.timestamp

这样更改后虽然消除了排序操作,但是引入了shuffle操作(join)(并且对于hive要laod两遍数据),因此是否加速取决于具体的数据集. 对于任务卡死(或者很慢)在reduce阶段的hive任务,可以尝试进行排序消除.
实际经验来看,如果数据量大到导致外排,需要消除order by.

distinct消除(两阶段group by)

回字有4种写法,而distinct一般有2种.

1. 多列或1列去重

1
select distinct a,b,udf(c1) as c2 from xxx

由于hive是通过group by实现distinct,上述sql其实等效于:

1
select a,b,udf(c1) from xxx group by a,b,udf(c1)

可以通过explain查看两者的执行计划是完全一致的.
如果能确定udf是单射变换,也就是c1到c2是一对一,而没有多对一,可以等效改写为:

1
select a,b,udf(c1) from xxx group by a,b,c1

总之,对于这个场景下的distinct使用,如果没有udf,可以不进行消除.

2. 聚合函数中使用(如uv计算)

1
2
3
select dt,count(distinct userid) as uv 
from xxx
group by dt

这种聚合函数中使用distinct属于比较常见的业务查询需求,hive执行时会把所有数据灌到一个reducer中,毫无并行度.
可以使用两阶段group by进行优化,写法:

1
2
3
4
select dt,count(1)
FROM
(select distinct dt,userid from xxx) as t
group by dt

这样去重操作在第一个阶段分担到了多个reducer上,速度提升很多.

实际优化的时候,主要有三种情况阻碍,无法直接改写:

1. 同一列不同条件的count distinct

1
2
3
4
select dt
,count(distinct userid) as seven_uv
,count(distinct if(c1>xxx,userid,NULL)) as new_uv
,count(distinct if(c2>xxx,userid,NULL)) as query_uv

可以通过增加标记列转化:

1
2
3
4
5
6
7
8
9
select dt
,count(userid) as seven_uv
,count(if(is_new=1,userid,NULL)) as new_uv
,count(if(is_query=1,userid,NULL)) as query_uidnum -- query_uv
from
(...
,max(if(c1>xxx,1,0)) is_new
,max(if(c2>xxx,1,0)) as is_query
...) as tt

2. 多维聚合(group by with cube)

可以通过一行变多行,手动维护grouping sets的组合:

1
2
3
4
5
6
lateral view explode(array('全部',platform)) tt1 as platform_t
lateral view explode(array('全部',version)) tt2 as version_t
lateral view explode(array('全部',vendor)) tt3 as vendor_t
lateral view explode(array('全部',phase)) tt4 as phase_t
GROUP BY platform_t,version_t,vendor_t,phase_t
,userid

不同列聚合.

例如:

1
2
count(distinct userid)
count(distinct deviceid)

这种如果确实出现了reduce卡死,可以进行分拆成两个查询分别计算(load两遍数据),最后join到一起. 代码会比较长.

hive之bug汇总

这里总结一下hive的bug,或者说表现与spark sql不同的feature(bug?).
由于hive的distinct实际实现为group by,因此下述的group by相关bug也适用于distinct.

1. 列重命名BUG

导致结果错误.
spark-sql能正常处理.

子查询中重命名列时,如果和原有表中某列名相同,并且where条件中有那一列,取原有表的列值.
构造测试用例:

1
2
3
4
5
SELECT * FROM
(SELECT 123 as paperid
FROM temp.feng_test1
where paperid=70455
)AS a

上述查询的结果是70455,而不是我们想象中的123.
而这个查询:

1
2
3
4
SELECT * FROM
(SELECT 123 as paperid
FROM temp.feng_test1
)AS a

或这个查询:

1
2
3
4
5
SELECT * FROM
(SELECT 123 as paperid
FROM (select 70455 as paperid) as t
where paperid=70455
)AS a

都能正确返回123.

2. GROUP BY+UDF+Serde复合bug

导致抛异常退出.
spark-sql能正常处理.

GROUP BY,自定义UDF和自定义Serde都能正常独立工作.
这是一个多重条件下产生的bug:
1.表定义为string,而自定义的serde类放入了long对象;(bug)
2.自定义UDF使用简便写法(继承UDF,复杂写法为继承GenericUDF);(正常行为)
3.运行如下语句:

1
2
3
select xx
from `1中serde的表`
group by `2中udf`

这个bug可以说是serde写得有问题导致的,严格来说hive没有太大问题.
原因是hive调用简单udf时,是运行时进行反射,填入方法的参数,实参与形参定义不同抛出异常.

3. GROUP BY+重复列 BUG

导致结果错误.
spark-sql能正常处理.

GROUP BY 时,如果有涉及引用的重复列, 如构造用例中的alist[0],由于hive在各种方面都会重用引用,会导致bug.
用例的输出结果为: 1,1,1.
而不是我们想象中的: 1,1,3333.

构造用例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SELECT aid,bid,mistake
FROM
(SELECT 1 as aid
)AS a
JOIN
(SELECT alist[0] as bid
,mistake
FROM (SELECT 2 as courseid) AS a
JOIN (SELECT 2 as courseid
,'3333' as mistake
,array(1) as alist
) AS b
ON a.courseid=b.courseid
GROUP BY alist[0]
,alist[0]
,mistake
)AS b
ON aid=bid

4. row_number + 数据类型溢出 bug

导致结果错误.
spark-sql能正常处理.

假设tp是一个字符串类型,强行转换成int类型时,如果发生数据溢出,比如值是13位时间戳(1514285700375),排序行为将不可预测.既不是降序也不是升序.

构造错误样例如下:

1
2
3
select userid,phaseid,tp
,row_number()over(partition by userid order by int(tp)) as rank
from xxxx

正确样例:

1
2
3
select userid,phaseid,tp
,row_number()over(partition by userid order by bigint(tp)) as rank
from xxxx

5. sort_array BUG(Feature) 函数副作用

导致结果错误.
spark-sql能正确处理.

hive中对数组进行排序后,会改变原有数组.(会在原有数组基础上排序)
spark-sql则会返回一个深拷贝,不改变原有数组.

1
2
3
4
5
select alist
,sort_array(alist) as alist2
FROM
(select array(1,3,2) as alist
)AS t

mysql调优小记

  • 摘要

    mysql表出现慢查询,单表数据量500W700W条. 每天23W条.
    措施:

    1. 优化查询语句;
    2. 更改存储引擎.
    3. 优化索引.

详情

同事去塞班岛玩前留了个大坑,导致网站报表卡死刷不开.

STEP 1

进入后台:

1
show processlist

发现Query很多,show full processlist找出查询语句,发现多表join的时候没有利用到索引.
查询语句模式如下:

1
explain select ...

发现没有用上索引,更改查询语句。

  • explain查询计划中的type:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const(system): 根据PRI或Unique key,只取出确定的(一行)数据,常量优化. 

    eq_ref: JOIN条件包括了所有索引,并且索引是Unique key.
    ref: JOIN条件包括部分索引或不是Unique key.
    ref_or_null: WHERE col=exp or col is null

    index_merge: Where id=xx or userid=xxx. 索引合并优化.

    unique_subquery: where col in( select id from xxx). 这里id唯一.
    index_subquery: 同上,只是id不唯一.

    range: 索引在某个范围内.
    all: 扫全表
  • TODO:
    研究除了子查询以外的方式使用索引.

此外, 索引对于数据类型敏感, 查询中存在字符串和date类型相等的时候, 无法利用索引,
需要将date类型转成字符串.

1
dt = date_format(date_sub(current_date, interval 1 day), '%Y-%m-%d')

STEP 2

检查后台,发现许多连接状态都是:
waiting for table level lock.
进一步发现这几张表的存储引擎是MYISAM,而不是默认的innodb.
由于myisam引擎只有表级锁,不符合我们的使用要求.于是我把涉及到的几张表引擎都改为innodb.

STEP 3

修改后,查询不会卡死(毕竟不扫全表了ORZ),降低到40s,但还是太慢了.
进一步检查explain结果,发现key_len太长了.
于是重新设计索引, 把筛选度高的放前面(利用最左前缀), 并且根据具体业务\语义,尽量缩短索引字段的长度,
实在无法缩短的则取其中一部分. 最后查询缩短到0.04sec,几个报表都是秒出.

sqoop的SQL注入漏洞

早上起来发现公司有一个sqoop导表任务挂了,查看日志错误提示大致如下:

1
2
INFO [main] org.apache.sqoop.mapreduce.db.DBRecordReader: Executing query: SELECT id,xxx FROM taname where ( udid >= 'abc' ) AND ( udid < 'xx' ;select pg_sleep(3) --)
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 1

也就是说sqoop生成的查询语句错了,搜了一下源码没有发现哪里会生成

1
select pg_sleep(3)

这种语句,而且我们用的是mysql,按理说不应当生成pgsql.
而且这个导表任务也不是第一天上线了,此前一直是好好的.

首先按照经验,调整了分隔符配置;查看表结构发现有blob字段,去掉了配置中的direct模式,均没有效果.
其次查job的其他日志,发现其他几个mapper都成功了,只有1个mapper挂了,于是调整并行度,把并行度调成1,导表任务就成功了.

回过头来继续研究这个问题.
也就是说在分割表数据到几个mapper的时候,划分split的时候,查询语句错了.
报错的上一行的日志大致是:

1
org.apache.hadoop.mapred.MapTask: Processing split: udid >= 'xxx' AND udid < 'xxx'); select pg_sleep(3) -- '

sqoop划分split的原理:

  1. --table 按表导: 按主键最大最小取值划分范围,然后按中间值划分split.
  2. --query : 按配置中的-- split by参数对应的列取max,min,然后同上.

因此如果作为划分列的值中如果有脏数据,sqoop就会被sql注入.
因此导致了我们遇到的问题.
平时脏数据没有落在分割点上, 今天可能是由于脏数据的比例逐渐上升,终于落在了分割点上,因此导致了任务失败.
(pg_sleep一般是用于sql注入.)

sort命令笔记

语法:

1
2
sort [OPTION]... [FILE]...
sort [OPTION]... --files0-from=F

参数:

1
2
3
4
5
6
7
8
9
10
11
-b : 忽略行首空格
-c : 检查文件是否已经按照顺序排列
-d : 忽略除了英文字母,数字,空格以外的字符
-f : 将小写字母视为大写字母
-i : 只考虑040到176之间的ASCII字符,其他的忽略
-m : 合并几个排序好的文件
-n : 按数字大小排列
-o : 输出文件
-r : 倒序排列
-t : 排序时的分隔符
+<起始列>-<结束列>: 排序列范围

示例1:

1
2
# 默认按第一列的ascii码排序
sort log.txt

示例2:

1
2
# 查看当前mysql线程中的各种状态的数量,按uv最多输出前10个:
pipe -e 'show processlist \G' | grep State: | uniq -c | sort -rn | head -n10

sed笔记

语法:

1
Usage: sed [OPTION]... {script-only-if-no-other-script} [input-file]...

动作参数:

1
2
3
4
5
6
a: 新增
c: 取代
d: 删除
i: 插入
p: 打印
s: 取代

示例1:

1
2
3
# 在第四行后添加一行(内容为helloWorld),结果打印到console:
sed -e 4a\helloWorld log.txt
# 假如文件根本没有第四行,则不会添加.

示例2: (-e可以省略)

1
2
3
4
5
# 把文件的内容加上行号输出,并且删除第2,3行.
nl log.txt | sed '2,3d'

# 删除第3行及之后的行:
nl log.txt | sed '3,$d'

grep笔记

grep语法:

1
grep [OPTION]... PATTEN [FILE]...

OPTION参数:

1
2
3
4
-i: 忽略大小写
-v: 反转查找
-w: 只显示全字符合的列
-c或--count: 计算符合范本样式的列数.

示例1:

1
2
# 计算处于init状态的mysql线程有多少个.
pipe -e 'show processlist \G' | grep -c 'State: init'

示例2:

1
2
# 查找目录/etc/acpi及其目录下所有文件中包含"update"的文件:
grep -r update /etc/acpi

epoll相关基础知识

背景:

C10K问题. 并发访问量>=10K => 维持TCP连接的进程数超过上限.

  • 远古解决方案:
  1. UDP. (腾讯QQ)
  2. select模式. 每个TCP连接占用一个文件描述符, 所以进程能处理的连接最大不能超过1024.
  3. poll模式. 每次收到数据需要遍历每一个连接查看哪个连接有数据请求。

2002年左右开始的解决方案:

kqueue: FreeBSD
epoll: Linux (2.5.44内核)
IOCP: Windows

  • epoll的编程模型:
    异步非阻塞回调. Reactor,事件驱动,事件轮询(EventLoop).

  • 新的问题
    epoll编程太繁琐,层层回调/嵌套.

  • 解决方案:

  1. 默默忍受: 性能最优.
  2. 协程,coroutine: 需要解决某个协程密集计算带来的问题.
  • 协程
    封装了事件回调,底层库通过保存状态(代码行数,局部变量的值),替程序猿进行回调. 程序猿可以就像一切刚开始的时候那样只写仿佛同步阻塞的代码.

go语言中具体代码:

1
2
go doSomething();
// 类似于 threadPool.submit(new Thread(new Runnable(){run(){doSomething();}}))的效果

会提交一个协程到go语言自带的协程池子里。由于go语言每个函数可以作为一个协程,所以语法显得非常简练方便,而且无需操心线程池。
此外gomain函数也是一个协程,而且不是守护的。(java中main函数是守护的,会等待所有其他线程运行结束)
协程的同步依然需要自己管理,一般是用channel(管道)进行。
语法上,协程提供了一个写非阻塞操作的非常简洁的关键字(go)。
实现上,协程提供了非常方便的协程池,而且由于是用户态线程,因此每个协程的开销足够小,小到可以将每个函数这么小的粒度作为协程。

缺陷:
由于实现上是使用用户态线程/进程,当某一个协程中有密集计算时,其他协程就不运行了. 每个线程上装配的协程的切换是需要在每次协程中发生中断时进行判断的。

解决方案:

  • GoLang: 用户在密集计算的代码中自己yield.
  • ErLang: 无需关心. 由于底层有ErLang自己开发的VM,会自动检测并进行切换.(rabbitmq)

具体实现

  • 问题的本质:
    一个进程处理一个连接, 导致进程开销太大.
    因此解决思路就是一个进程处理多个连接.也就是:
    IO多路复用.

实现1:

每个进程循环处理多个连接.
缺点: 其中一个连接处理卡住会阻塞整个应用.

select模式

实现2: (select模式)

维护一个fd_set状态结构体.
阻塞的原因是IO资源的争用,如某个文件句柄是否可读,可写.(锁)
告诉内核该进程关心的文件句柄, 让内核检查多个文件句柄.

进程发送句柄列表=>内核逐个检查状态是否可用.

缺陷:

  1. 每个进程有关注的文件句柄上限;
  2. 需要重复初始化关心的文件句柄结构体;
  3. 逐个排查连接效率低.

poll模式

实现3:(poll模式)

设计新的数据结构.
通过pollfd数组向内核传递需要关注的事件: 消除文件句柄上限;
不同字段区分关注事件和发生事件: 避免重复初始化.

缺陷:

  1. 逐个排查连接效率低.

epoll模式

实现4: (epoll模式)
内核升级到能对每个FD注册回调,进程能够直接知道是哪个句柄状态变化,而不用轮询,监听回调就行了.
(linux 2.5.44新增api)
事件模型.

epoll具体实现细节

架构:

1
2
3
4
5
// 一堆事情(IO访问),需要切换到内核态访问相应地址空间.例如把数据从内核地址空间拷贝到用户地址空间. 拷贝完以后触发一个读可用事件.
用户进程->内核(
readyList
,红黑树<文件描述符(如Socket)>
)

工作流程:

1
2
3
4
5
6
7
8
9
10
1. 用户进程调用epoll_create;
2. 内核创建readyList,红黑树;
3. 用户进程调用epoll_ctrl,传递监控的句柄(如Socket),以及在上面关注的事件;
4. 内核将句柄插入红黑树;
5. 内核的中断处理程序注册一个回调,如果红黑树中某个句柄的中断到了,
把它对应的事件放到ReadyList.

--- 另一个流程 ---
1. 用户进程调用epoll_wait
2. 内核从ReadyList返回可回调的事件.

LT模式与ET模式
LT模式: 水平触发(JAVA NIO默认),一个句柄上的事件一次没处理完,会再下次调用epoll_wait的时候依然返回它;
ET模式: 边缘触发,仅在第一次返回.

LT模式:

1
2
3
4
1. 用户进程调用epoll_wait;
2. 内核从ReadyList返回数据;// 就绪的Socket拷贝到用户态内存
3. 清空ReadyList;
4. 检查Socket句柄,如果有未处理完的事件(数据没读完),将句柄加入ReadyList.

ET模式没有上述的第四步.

LT模式注意事项

不需要写的话,不要关注写事件.
长期关注写事件会导致CPU100%问题.

ET模式注意事项

每次都要读到返回EWOULDBLOCK为止. (要读干净)

epoll为什么使用红黑树
因为epoll要求快速找到某个句柄,因此首先是一个Map接口,候选实现:

  1. 哈希表 O(1)
  2. 红黑树 O(lgn)
  3. 跳表 近似O(lgn)
    据说老版本的内核和FreeBSD的Kqueue使用的是哈希表.

个人理解现在内核使用红黑树的原因:

  1. 哈希表. 空间因素,可伸缩性.
    (1)频繁增删. 哈希表需要预估空间大小, 这个场景下无法做到.
    间接影响响应时间,假如要resize,原来的数据还得移动.即使用了一致性哈希算法,
    也难以满足非阻塞的timeout时间限制.(时间不稳定)
    (2) 百万级连接,哈希表有镂空的空间,太浪费内存.
  2. 跳表. 慢于红黑树. 空间也高.
  3. 红黑树. 经验判断,内核的其他地方如防火墙也使用红黑树,实践上看性能最优.

数据结构:

1
2
3
4
5
6
7
8
9
10
11
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t; // <=

struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data;/* User data variable */
};

C库的API:

1
2
3
int epoll_create(int size); // size: 最大句柄数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

具体语义:

1
2
3
4
epoll_create: 让内核创建一个epoll item;
epoll_ctl: 修改epoll item: 增加socket句柄,或删除socket句柄,注册回调事件;
epoll_wait: 等待注册的事件发生.
传入的参数epoll_event会从内核那捞回结果.(被回调的事件)

实现5: libevent库

封装epoll. 统一不同平台api.
使用方式:

1
2
3
1. 初始化事件event,定义回调函数f;
2. 将事件加入系统监控事件列表;
3. 监听事件及分发.

NIO的epoll bug
部分Linux的2.6的kernel中,poll和epoll对于突然中断的连接socket会对返回的eventSet事件集合置为POLLHUP,也可能是POLLERR,eventSet事件集合发生了变化,这就可能导致Selector会被唤醒。
进一步导致jdk中NIO实现中某段代码的死循环.

Netty的解决方案:

  1. 将select返回值为0进行计数,直到大于阈值EPOLL_BUG_WORKAROUND;
  2. 重建Selector.

C10M问题

  1. 数据包经过linux内核协议栈导致性能巨大下降.
    解决方案:
    (1)intel的DPDK框架. 直接接管网卡收到的数据包传递到业务代码中进行处理.

  2. 存储访问时间消耗. 切换消耗:
    解决方案:
    (1) 重新设计操作系统, 包括内存池,cache,内存分页,cpu管理等等.

术语/概念

缓存IO

标准IO. 操作系统先将IO数据缓存到文件系统的页缓存(page cache)中,然后再从操作系统内核的缓冲区拷贝到应用程序的地址空间.
缺点:
重复拷贝带来很大的CPU/内存开销.

  • 阻塞/非阻塞:

    内核会不会立即回答;

  • 同步/异步:

    进程会不会等内核回答.

IO模式

  1. 阻塞IO
  2. 非阻塞IO
  3. IO多路复用
  4. 信号驱动IO
  5. 异步IO

阻塞IO

  1. 进程请求IO数据;
  2. 进程阻塞;
  3. 内核准备数据,拷贝到内核空间;
  4. 拷贝到进程地址;
  5. 解除阻塞.

非阻塞IO

  • 轮询
  1. 进程请求IO数据;
  2. 内核回答不OK;
  3. 进程不断轮询;
  4. 内核终于准备好了数据,回答OK.

IO多路复用

包括: select,poll,epoll

一次轮询多个socket,任何一个socket数据搞定的话,内核都会回复一次.

异步非阻塞IO

类似于非阻塞, 取消了轮询.
进程不等待内核的回复.
内核完成准备数据后会通知进程.

库带来的智子攻击

  • 智子攻击:

    <<三体>>中的三体人为了封锁地球科技,使用智子干扰微观粒子的行为,影响人类对于基础科学的实验与研究.

对于程序猿来说,由于现成代码库封装了底层实现,同时引入了很多术语\名词,隐藏了问题的本质, 影响了程序猿真正学会相关技术, 以及超越原有实现.
由于前人对于某一个问题, 往往会根据自己的理解, 创建相关的DSL(领域特定语言),
后人想理解原本一个基础原理,往往不得不先理解这些或简洁或奇怪的术语,越过重重迷雾最后才能接触到真相.

解决方案:

  1. 堆砌时间: 每周研究一次库的底层实现.

spring实战笔记

第一章 主要特性

DI

  • 依赖注入.

    让Spring容器来管理对象的创建和销毁,注入;依赖关系更为明确,耦合程度降低.
    以前是bean工厂,现在是各种ApplicationContext容器.

AOP

  • 切面.

    声明切点,注入before,after事件,降低代码耦合.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <aop:config>
    <aop:aspect ref="minstrel">
    <aop:pointcut id="embark"
    expression="execution(* *.embarkOnQuest(..))"/>

    <aop:before pointcut-ref="embark"
    method="singBeforeQuest"/>

    <aop:after pointcut-ref="embark"
    method="singAfterQuest"/>
    </aop:aspect>
    </aop:config>

模板

1
2
jdbcTemplate+RowMapper, 简化jdbc查询.
// 用JPA的话就不用写RowMapper了.

bean的生命周期

  1. 实例化
  2. 填充属性
  3. setBeanName
  4. setBeanFactory
  5. setApplicationContext
  6. 调用BeanPostProcessor的预初始化方法
  7. 调用InitializingBean的afterProperties set方法
  8. 调用自定义的初始化方法
  9. 调用BeanPostProcessor的初始化后方法
  10. —— bean的使用
  11. —— 容器关闭
  12. 调用DisposableBean的destroy方法
  13. 调用自定义的销毁方法

第二章 DI

讲了一下声明和使用Bean的方法. 与Springboot实战相同,略过.

第三章 条件化配置\高级装配

各种profile切换,条件bean,配置注入等等,与Springboot实战相同,略过.

第四章 AOP

具体实现机制:

Spring 采用jdk动态代理模式来实现Aop机制。Spring AOP采用动态代理过程:1.将切面使用动态代理的方式动态织入到目标对象,形成一个代理对象。2.目标对象如果没有实现代理接口,那么spring会采用CGLib来生成代理对象,该代理对象是目标对象的子类。3.目标对象如果是final类,也没有实现接口,就不能运用AOP

AspectJ是AOP的一个实现,Spring借鉴了AspectJ.(有合作)

术语

  • Advice : 通知(增强)(Before,After,Around)

    描述横切逻辑和方法的具体织入点(方法前、方法后、方法的两端等)

  • PointCut: 切点(例子中的切点是execution表达式规定的方法)

    指定在哪些类的哪些方法上织入横切逻辑.

  • JoinPoint: 连接点 (粒度是方法)

  • Advisor(切面):

    将Pointcut和Advice两者组装起来。有了Advisor的信息,Spring就可以利用JDK或CGLib的动态代理技术采用统一的方式为目标Bean创建织入切面的代理对象了

例子:

1
2
3
4
5
6
7
8
9
10
11
12
<aop:config>
<aop:aspect ref="minstrel">
<aop:pointcut id="embark"
expression="execution(* *.embarkOnQuest(..))"/>

<aop:before pointcut-ref="embark"
method="singBeforeQuest"/>

<aop:after pointcut-ref="embark"
method="singAfterQuest"/>
</aop:aspect>
</aop:config>
  • spring支持的AspectJ切点表达式(挺多的记不住,就不列全了):
    1
    2
    3
    4
    5
    arg()
    this
    target
    execution
    ...

用切点表达式来选取某个方法示例:

1
2
3
4
5
execution(* concert.Performance.perform(..))
// * : 返回任意类型
// 方法所属的类是: concert.Performance
// 具体方法名是: perform
// 方法参数是: 任意参数(两个点..)

简单总结一下步骤(用注解):

  1. 写一个config,把观众和演员都注册为Bean.加上@EnableAspectJAutoProxy.
  2. 观众类上加@Aspect,写各种@Before,@After方法;

代码见:
https://github.com/xiaoyue26/spring-gradle/tree/master/src/main/java/com/xiaoyue/nov/practice/concert

书里还提供了一个思路,通过AOP扩展原来的类,而不改动原来的源代码.

原理总结

  • 动态代理

    JDK动态代理:只能为接口创建动态代理实例,而不能针对类 。
    CGLib :
    (Code GenerationLibrary)动态代理:可以为任何类创建织入横切逻辑代理对象,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final。

  • 原理对比:(尽量用接口,不用final)

JDK动态代理:(只能是接口)

JDK动态代理技术。通过需要代理的目标类的getClass().getInterfaces()方法获取到接口信息(这里实际上是使用了Java反射技术。getClass()和getInterfaces()函数都在Class类中,Class对象描述的是一个正在运行期间的Java对象的类和接口信息),通过读取这些代理接口信息生成一个实现了代理接口的动态代理Class(动态生成代理类的字节码),然后通过反射机制获得动态代理类的构造函数,并利用该构造函数生成该Class的实例对象(InvokeHandler作为构造函数的入参传递进去),在调用具体方法前调用InvokeHandler来处理。

  • CGLib动态代理:(不能是final类)

    字节码技术。利用asm开源包,把代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。采用非常底层的字节码技术,为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,并顺势织入横切逻辑。

  • 两种配置切换:
    1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
    2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
    3、如果目标对象没有实现接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

第二部分 WEB

第五章 构建

第六第七章 SpringMVC

内容,比较繁琐

第八章 Spring Web Flow

stackoverflow上有个帖子详细解释了为啥不能用这个框架.
https://stackoverflow.com/questions/29750720/what-are-the-spring-web-flow-advantages
包括:

  1. xml配置难用;
  2. 不直观;
  3. 与第三方库(如模板引擎)整合复杂;
    … // 看了前三个缺点就没耐心看后几个缺点了…
    核心还是难用,比普通spring的xml配置复杂得多,几乎没有人能坚持看十分钟攻略.

第九章 安全 Spring Security

原理:

  1. 用的filter. (AOP用的interpretor)

antMatchers用的是
ant pattern,和普通的通配符有点不一样,是专门用于url的通配符:

1
2
3
? 匹配任何单字符 
* 匹配0或者任意数量的字符
** 匹配0或者更多的目录

其实就是:

1
2
* 匹配任何字符,不含 “/”
** 匹配任何字符,含 “/”

匹配上路径以后,配置能够做什么.

1
2
3
4
5
6
7
8
9
10
11
12
13
access("hasRole('READER')") 设置SpEL表达式为true时允许访问;
anonymous() 允许匿名访问
authenticated() 允许认证过的用户访问
denyAll() 拒绝所有
fullyAuthenticated() 允许认证过的,但排除remember-me的
hasAnyAuthority(String...) 具备某个权限
hasAnyRole(String ...) 具有某个Role
hasAuthority(String) 某有某个权限
hasRole(String) 具有某个Role
hasIpAddress(String) 具有某个IP
not() 对其他访问方法的结果取反
permitAll() 允许所有
rememberMe() 允许通过Remember-me的.

最后的default条件就是 anyRequest().denyAll().

SpEL: Spring Expression Language:

access方法里头使用,可以代替其他方法,如:
access(“hasRole(‘ROLE_READER’ and hasIpAddress(‘192.168.1.2’))”);

Authentication与Authorization区别

Authentication: 鉴权,用户是谁.
Authorization: 授权,是否可以.

1
2
3
4
// 获得用户是谁: 如果没有鉴权就是null
SecurityContextHolder.getContext().getAuthentication()
// 获得用户的权限:
SecurityContextHolder.getContext().getAuthentication().getAuthorities()

其他概念(自顶向下)

SecurityContextHolder:
为使用者提供全局的SecurityContext//ThreadLocal变量,每个线程唯一.
SecurityContext:
为了hold住Authentication
Authentication:// 鉴权
主要负责两方面信息,一个是当前用户的详细信息(Principal、UserDetails),一个是用户鉴权时需要的信息。
Principal: 安全主体
Authorization: 授权,访问控制.
GrantedAuthority:
提供当前用户(UserDetails)所获得的系统范围内的授权。
UserDetails:
提供了用户的详细信息,主要被用来构建Authentication。
UserDetailsService:
这个接口的实现主要是负责通过用户名查找并提供用户的详细信息(UserDetails)。

密码的比较:
AuthenticationManager以及AuthenticationProvider负责.

鉴权流程

  1. 用户名,密码 => UsernamePasswordAuthenticationToken对象;
    //Authentication的一个实现.
  2. => AuthenticationManager对它进行验证;
  3. => 提取UserDetails,GrantedAuthority => Authentication对象
  4. Authentication=> SecurityContextHolder.getContext().setAuthentication.

其中第一步在:

1
2
3
UsernamePasswordAuthenticationFilter类中:
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

第十章 模板方法设计模式

实现分为: 模板(template,固定部分)和回调(callback,可变部分)
Spring提供的模板包括:

1
2
3
4
5
jdbcTemplate
SimpleJdbcTemplate -- Spring3后已废弃
HibernateTemplate
JdoTemplate
JpaTemplate

第十一章 ORM

  1. DSL
    JPA repository的DSL(黑话):
    1
    2
    3
    4
    5
    readSpitterByFirstnameOrLastnameOrderByLastname();

    动词: read,get,find,count. // 前三个等效
    Spitter: 主题 // 还可以加Distinct
    断言: 还可以有: isAfter,isNull,isLike,IgnoringCase等等.
    只要按这个模式命名方法名,就会自动被实现.

2.@Query自定义查询

1
2
@Query("select username,password,fullname from Reader")
List<Reader> findAllGmailReaders();

3.混合自定义实现
上述情况都是声明一个ReaderRepository接口,
继承JPARepository<Reader,String>
(对象和对象主键类型),
Spring就会自动生成一个Impl,根据定义的方法名自动生成实现.(类似于用iBatis时,工具生成map)
如果有比较复杂的自定义SQL操作,可以创建一个ReaderRepositoryImpl,
和原来的名字相比增加Impl后缀,(这个是默认后缀,可以通过注解配置调整),
然后继承同一个接口(比如都继承/实现ReaderSweeper).
Spring就会混合自动生成的实现和自定义的实现.

//详见github.

第十二章 NoSQL

  1. MongoDb 及其template
  2. note4j 及其template
  3. redis 及其template
    依赖:
1
2
compile "redis.clients:jedis:2.9.0"
compile 'org.springframework.boot:spring-boot-starter-data-redis'

yaml配置:

1
2
3
4
5
spring:
redis:
host: localhost
password: redis123
port: 6379

配置:

1
2
3
4
5
6
@Bean
public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, Product> redis = new RedisTemplate<>();
redis.setConnectionFactory(cf);
return redis;
}

最后一步如果用不着template的话,甚至可以不用配.还蛮简单的.

序列化相关:

Spring Data Redis提供的序列化器:

  1. RedisTemplate会使用JdkSerializationRedisSerializer;
  2. StringRedisTemplate默认会使用StringRedis-Serializer;
  3. 可以在创建RedisTemplate的时候配置其他序列化器:
    1
    2
    3
    4
    5
    6
    7
    8
    @Bean
    public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
    RedisTemplate<String, Product> redis = new RedisTemplate<>();
    redis.setConnectionFactory(cf);
    redis.setStringSerializer(new StringRedisSerializer());
    redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));
    return redis;
    }

第十三章 缓存技术 (切面实现)

  • EhCache 跳过.

1.声明缓存管理器

  • RedisCacheManager :单个缓存管理器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    @EnableCaching
    public class CachingConfig {
    @Bean
    public CacheManager cacheManager(RedisTemplate redisTemplate){
    return new RedisCacheManager(redisTemplate);
    }

    }
  • 多个缓存管理器:CompositeCacheManager
    会迭代查找添加的各个缓存管理器.

2.使用缓存
4个相关注解: // 可用于类或方法级别.

1
2
3
4
@Cacheable : 先找之前缓存的值,没有则调用.
@CachePut : 只是放到缓存里,始终调用. // 一般用于save方法.
@CacheEvict : 清除n个缓存
@Caching : 分组

@CachePut:

1
2
@CachePut(value="spittleCache", key="#result.id")
Spittle save(Spittle spittle);
  • 条件缓存:

    1
    2
    3
    4
    5
    @Cacheable(value="spittleCache"
    unless="#result.message.contains('Nocache')"
    condition="#id >= 10"
    Spittle findOne(Long id);
    )
  • 移除缓存:

    1
    2
    @CacheEvict("spittleCache")
    void remove(long spittleid);
  • 组合缓存操作:

    1
    2
    3
    4
    5
    6
    7
    8
    @Caching(   // 增加id查找,email查找,username查找的缓存
    put = {
    @CachePut(value = "user", key = "#user.id"),
    @CachePut(value = "user", key = "#user.username"),
    @CachePut(value = "user", key = "#user.email")
    }
    )
    public User save(User user) {...};
  • 自定义注解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Caching(  
    put = {
    @CachePut(value = "user", key = "#user.id"),
    @CachePut(value = "user", key = "#user.username"),
    @CachePut(value = "user", key = "#user.email")
    }
    )
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    public @interface UserSaveCache {
    }

第十四章 保护方法应用

  1. 保护方法调用 // SecuredConfig
  2. 使用表达式定义安全规则 //JSR250Config
  3. 创建安全表达式计算器 // ExpressionSecurityConfig
    详见源码chapter_14.
    配置上:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @EnableGlobalMethodSecurity(securedEnabled=true)
    public class SecuredConfig extends GlobalMethodSecurityConfiguration {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
    .inMemoryAuthentication()
    .withUser("user").password("password").roles("USER");
    }
    相关注解:
    1
    2
    3
    4
    @PreAuthorize 方法调用之前,用表达式限制调用
    @PostAuthorize 方法调用之后,检查表达式,抛出异常
    @PostFilter 允许方法调用,按表达式过滤结果
    @PreFilter 允许方法调用,按表达式过滤输入值

service:

1
@Secured({"ROLE_SPITTER", "ROLE_ADMIN"})

第十五章 远程调用

里头的RMI都是java的,不能跨语言.跳过.

第十六章 rest api

  1. 内容协商 Content negotiation:选择一个能用的视图
  2. 消息转化器 Message conversion:转换成客户端使用的形式

协商

一维协商:// 只有视图名

Controller返回 -> 视图名 // 或void,原视图
-> DispatcherServlet -> 视图解析器.

二维协商: // (视图名,内容类型)

  • ContentNegotiatingViewResolver

    内容协商的两个步骤:
    确定请求的媒体类型;
    找到适合媒体类型的最佳视图.

  • 确定请求的媒体类型:

1.查看url的文件后缀名.
2.使用Accept首部类型
3.使用默认类型.

协商优点:
可以同时提供给人看的html和给客户端服务用的json. //重用controller
协商缺点:
客户端不能发送json或xml.
返回值是一个Model,也就是一个kv,比普通的多一层嵌套.

信息转换器

流程:
Accept头信息(application/json)
->处理方法返回的对象交给MappingJacksonHttpMessageConverter.
// 引入 Jackson JSON Processor库.

Spring提供的HTTP信息转化器包括:

1
2
3
4
AtomFeedHttpMessageConverter
BufferedImages
ByteArrayHttpMessageConverter
...
  • @ResponseBody: 发送数据时使用转换器; 配合produces
    1
    2
    3
    4
    5
    6
    @RequestMapping(method=RequestMethod.GET, produces="application/json")
    public List<Spittle> spittles(
    @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
    @RequestParam(value="count", defaultValue="20") int count) {
    return spittleRepository.findSpittles(max, count);
    }
  • @RequestBody: 接受数据时使用转换器. 配合consumes
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RequestMapping(method=RequestMethod.POST, consumes="application/json")
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle, UriComponentsBuilder ucb) {
    Spittle saved = spittleRepository.save(spittle);

    HttpHeaders headers = new HttpHeaders();
    URI locationUri = ucb.path("/spittles/")
    .path(String.valueOf(saved.getId()))
    .build()
    .toUri();
    headers.setLocation(locationUri);

    ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(saved, headers, HttpStatus.CREATED);
    return responseEntity;
    }
  • @RestController注解:
    为所有方法增加@ResponseBody.方法的输入参数增加@RequestBody.
    1
    2
    3
    4
    5
    6
    7
    8
    @PathVariable long id
    // 用路径中的{id}注入id;
    @RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max
    // 用请求参数注入

    @RequestMapping(method=RequestMethod.POST, consumes="application/json")
    @ResponseStatus(HttpStatus.CREATED)
    // 详见chapter_16 spittr-api-message-converters源码项目

第十七章 异步消息

  • 简介
  • JMS java消息服务 略过
  • AMQP 高级消息队列服务 (rabbitMQ)
  • POJO

简介

RMI是同步的,异步的只要投递完就返回.

  1. 发送
  • broker: 消息代理
  • destination: 目的地

dest分为两种:

  • topic 主题,发布/订阅模型
  • queue 队列,点对点模型

JMS

api规范.支持点对点和发布订阅.
//旧

AMQP

高级MQ协议. 不但约束了api,还使不同实现之间可以合作.
加入了Exchange,Binding,解耦了队列,
可以灵活实现除了点对点\发布订阅以外的模型.

Exchange类型:

  • Direct:

    判读消息和binding的key相等.
    把消息路由到key相等的binding上.(binding的队列)

  • Topic:

    判读消息key符合binding的通配符.
    把消息路由到key匹配的binding上.

  • Headers:

    判断消息的headers与bingding参数匹配.

  • Fanout:

    无条件匹配. 广播到所有队列上.

默认会有一个没有名字的Direct Exchange, 所有队列都会绑定到这个Exchange上,并且routing key和队列名相同.

使用rabbitTemplate时,如果不指定exchange
,就会发送到默认的exchange,也就是上面说的默认Direct Exchange.

// 其他内容写入<< rabbitmq in action笔记 >>

第十八章 websocket

与http同级.
持久连接和前端通信(浏览器).用http的话,服务端无法向客户端发消息.

1. 直接使用websocket

前端: 使用ws://协议,或安全的wss://.
后端: 继承AbstractWebSocketHandler接口,引入依赖:

1
compile 'org.springframework.boot:spring-boot-starter-websocket'

为了防止浏览器不兼容的情况,使用sockjs:

1
registry.addHandler(marcoHandler(), "/marco").withSockJS();

相应的,前端也引入sockjs库.

2. 使用封装以后的STOMP

STOMP: Simple Text Oriented Messaging Protocol
简单文本的消息协议.
主要是加了个代理,消息流如下:

1
2
3
4
5
6
两条消息,目的地分别为: /app/marco,/topic/polo
=>请求通道
=>/app: 发送到 AnnotationMethodMessageHandler => 代理通道
/topic: SimpleBrokerMessageHandler
/queue: SimpleBrokerMessageHandler
=>响应通道

如果要二进制的,还有别的协议MQTT.

第十九章 邮件

用qq邮箱代发即可,配置依赖以后,用springboot能自动获得相关bean,调用即可.不使用ssl的话需要配置:

1
2
3
4
5
6
spring.mail.username: xxx@qq.com
spring.mail.password: password
spring.mail.port: 25 # SSL 465
spring.mail.protocol: smtp
spring.mail.host: smtp.exmail.qq.com # smtp.qq.com
spring.mail.properties.mail.smtp.auth: true

第二十章 使用JMX

注册MBean到JMX中.
就试试Jconsole.

第二十一章 Springboot

起步依赖具体引入了什么:

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
spring-boot-starter-actuator=>
spring-boot-starter
,spring-boot-actuator
,spring-core

spring-boot-start-amqp=>
spring-boot-start
,spring-boot-rabbit
,spring-core
,spring-tx

spring-boot-aop=>
spring-boot-starter
,spring-boot-aop
,AspectJ Runtime
,AspectJ Weaver
,spring-core

spring-boot-starter-batch
spring-boot-starter
,HSQLDB
,spring-jdbc
,spring-batch-core
,spring-core
... 太多了,以后有需要再查吧.