简

人生短暂,学海无边,而大道至简。


  • 首页

  • 归档

  • 分类

  • 标签

MySQL的EXPLAIN 命令详解

发表于 2017-12-20 | 分类于 mysql

EXPLAIN命令是查看优化器如何决定执行查询的主要方法。可以帮助我们深入了解MySQL的基于开销的优化器,还可以获得很多可能被优化器考虑到的访问策略的细节,以及当运行SQL语句时哪种策略预计会被优化器采用。

执行计划包含的信息:

+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+

id

包含一组数字,表示查询中执行select子句或操作表的顺序

id相同,执行顺序由上至下,id值越大,优先级越高,越先执行

select_type

每个select子句的类型(简单OR复杂)

SIMPLE:查询中不包含子查询或者UNION
PRIMARY:查询中若包含任何复杂的子部分,最外层查询则被标记为:PRIMARY
SUBQUERY:在SELECT或WHERE列表中包含了子查询,该子查询被标记为:SUBQUERY
DERIVED:在FROM列表中包含的子查询被标记为:DERIVED(衍生)用来表示包含在from子句中的子查询的select,mysql会递归执行并将结果放到一个临时表中。服务器内部称为"派生表",因为该临时表是从子查询中派生出来的
UNION:若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中,外层SELECT将被标记为:DERIVED
UNION RESULT:从UNION表获取结果的SELECT被标记为:UNION RESULT

SUBQUERY和UNION还可以被标记为DEPENDENT和UNCACHEABLE。DEPENDENT意味着select依赖于外层查询中发现的数据。UNCACHEABLE意味着select中的某些 特性阻止结果被缓存于一个item_cache中。

type

表示MySQL在表中找到所需行的方式:ALL, index, range, ref, eq_ref, const, system, NULL 从左到右,性能从最差到最好。

ALL:Full Table Scan, MySQL将遍历全表以找到匹配的行
index:Full Index Scan,index与ALL区别为index类型只遍历索引树
range:索引范围扫描,对索引的扫描开始于某一点,返回匹配值域的行。显而易见的索引范围扫描是带有between或者where子句里带有<, >查询。当mysql使用索引去查找一系列值时,例如IN()和OR列表,也会显示range(范围扫描),当然性能上面是有差异的。
ref:使用非唯一索引扫描或者唯一索引的前缀扫描,返回匹配某个单独值的记录行
eq_ref:类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件
const、system:当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system。
NULL:MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。

possible_keys

指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用

key

显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL

key_len

表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度)

ref

表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值

rows

表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数

Extra

包含不适合在其他列中显示但十分重要的额外信息

Using index

该值表示相应的select操作中使用了覆盖索引(Covering Index)

覆盖索引(Covering Index)MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件包含所有满足查询需要的数据的索引称为覆盖索引(Covering Index)

注意:如果要使用覆盖索引,一定要注意select列表中只取出需要的列,不可select *,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降

Using where

表示mysql服务器将在存储引擎检索行后再进行过滤。许多where条件里涉及索引中的列,当(并且如果)它读取索引时,就能被存储引擎检验,因此不是所有带where字句的查询都会显示”Using where”。有时”Using where”的出现就是一个暗示:查询可受益与不同的索引。

Using temporary

表示MySQL需要使用临时表来存储结果集,常见于排序和分组查询。

这个值表示使用了内部临时(基于内存的)表。一个查询可能用到多个临时表。有很多原因都会导致MySQL在执行查询期间创建临时表。两个常见的原因是在来自不同表的上使用了DISTINCT,或者使用了不同的ORDER BY和GROUP BY列。可以强制指定一个临时表使用基于磁盘的MyISAM存储引擎。这样做的原因主要有两个:

内部临时表占用的空间超过min(tmp_table_size,max_heap_table_size)系统变量的限制
使用了TEXT/BLOB 列

Using filesort

MySQL中无法利用索引完成的排序操作称为”文件排序”

Using join buffer

改值强调了在获取连接条件时没有使用索引,并且需要连接缓冲区来存储中间结果。如果出现了这个值,那应该注意,根据查询的具体情况可能需要添加索引来改进能。

Impossible where

这个值强调了where语句会导致没有符合条件的行。

Select tables optimized away

这个值意味着仅通过使用索引,优化器可能仅从聚合函数结果中返回一行.

Index merges

当MySQL 决定要在一个给定的表上使用超过一个索引的时候,就会出现以下格式中的一个,详细说明使用的索引以及合并的类型。

Using sort_union(...)
Using union(...)
Using intersect(...)

总结:

  • EXPLAIN不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况
  • EXPLAIN不考虑各种Cache
  • EXPLAIN不能显示MySQL在执行查询时所作的优化工作
  • 部分统计信息是估算的,并非精确值
  • EXPALIN只能解释SELECT操作,其他操作要重写为SELECT后查看执行计划。

使用mysql-proxy实现读写分离

发表于 2017-12-20 | 分类于 mysql

一、Mysql-Proxy原理

Mysql-Proxy是一个处于你的client端和Mysql Server端之间的一个简单程序,它可以监测、分析和改变他们的通信。它使用灵活没有限制,常见的用途包括:负载平衡,故障、查询分析,查询过滤和修改等等。它一个中间层代理,简单的说,Mysql-Proxy就是一个连接池,负责将前台应用的请求转发给后台数据库,并且通过使用lua脚本,可以实现复杂的连接控制和过滤,从而实现读写分离和负载平衡。对于应用来说,MySQL Proxy是完全透明的,应用则只需要连接到MySQL Proxy的监听端口即可。当然,这样proxy机器可能成为单点失效,但完全可以使用多个proxy机器做为冗余,在应用服务器的连接池配置中配置到多个proxy的连接参数即可。<摘自百度百科>

二、MySQL-Proxy安装配置

实现读写分离是有lua脚本实现的,现在mysql-proxy里面已经集成,无需再安装lua。

1、解压到/usr/local/mysql-proxy,开始配置mysql-proxy

创建脚本存放目录
[root@localhost mysql-proxy]# mkdir lua
[root@localhost mysql-proxy]# mkdir logs

2、复制读写分离配置文件和管理脚本。

[root@localhost mysql-proxy]# cp share/doc/mysql-proxy/rw-splitting.lua ./lua
[root@localhost mysql-proxy]# cp share/doc/mysql-proxy/admin-sql.lua ./lua

3、创建配置文件

[root@localhost mysql-proxy]# vim /etc/mysql-proxy.cnf
配置内容如下

[mysql-proxy]
daemon=true#守护进程方式
user=root#运行mysql-proxy用户
admin-username=mysqlproxy #主从mysql共有的用户
admin-password=123456 #用户的密码
proxy-address=192.168.1.105:4040#mysql-proxy运行ip和端口,不加端口,默认4040
proxy-read-only-backend-addresses=192.168.1.104#指定从slave读取数据
proxy-backend-addresses=192.168.1.103#指定主master写入数据
proxy-lua-script=/usr/local/mysql-proxy/lua/rw-splitting.lua#指定读写分离配置文件位置
admin-lua-script=/usr/local/mysql-proxy/lua/admin-sql.lua#指定管理脚本
log-file=/usr/local/mysql-proxy/logs/mysql-proxy.log#日志位置
log-level=info#定义log日志级别,由高到低分别有(error|warning|info|message|debug)
keepalive=true#mysql-proxy崩溃时,尝试重启

注意配置的时候不能有注释,坑爹的。
由于安全要求,必须将配置文件权限设为660(创建人可读写,同组人可读),否则不允许启动。

4、配置读写分离

[root@localhost mysql-proxy]# vim lua/rw-splitting.lua

在这段代码做修改如下

1
2
3
4
5
6
7
8
9
-- connection pool
if not proxy.global.config.rwsplit then
proxy.global.config.rwsplit = {
min_idle_connections = 1,--4, --默认是超过4个连接数才开始读写分离
max_idle_connections = 1,--8,默认为8,改为1

is_debug = false
}
end

三、启动测试

/usr/local/mysql-proxy/bin/mysql-proxy --defaults-file=/etc/mysql-proxy.cnf
netstat -tupln | grep 4040 #已经启动
killall -9 mysql-proxy #关闭mysql-proxy使用

在配置文件中配置访问主从的用户,主从数据库中还没有,在主库创建用户名密码并赋予权限,从库也会相应创建。

mysql> grant all privileges on *.* to 'mysqlproxy'@'192.168.1.105' identified by '123456' with grant option;
mysql> flush privileges;
mysql> select user,host from user;
| mysqlproxy | 192.168.1.103         |
| slave      | 192.168.1.104         |

使用客户端连接MySQL-Proxy,进行测试,这里使用win端

C:\Users\Administrator>net start mysql
MySQL 服务正在启动 ........
MySQL 服务已经启动成功。
C:\Users\Administrator> mysql -u mysqlproxy -p123456 -h 192.168.1.105 -P 4040
mysql>

使用两个远程客户端连接MySQL-Proxy。

在主节点查看3306端口,总的有三个客户端连接主库:分别是slave,两个代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@localhost mysql-proxy]# lsof -i :3306
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
mysqld 16087 mysql 15u IPv6 105964 0t0 TCP *:mysql (LISTEN)
mysqld 16087 mysql 42u IPv6 106004 0t0 TCP 192.168.1.103:mysql->192.168.1.104:56687 (ESTABLISHED)
mysqld 16087 mysql 44u IPv6 118888 0t0 TCP 192.168.1.103:mysql->192.168.1.105:60578 (ESTABLISHED)
mysqld 16087 mysql 76u IPv6 119880 0t0 TCP 192.168.1.103:mysql->192.168.1.105:60592 (ESTABLISHED)
在slave节点查看3306端口,总的有二个客户端连接主库:分别是代理和主节点
[root@localhost ~]# lsof -i :3306
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
mysqld 7775 mysql 13u IPv6 25595 0t0 TCP *:mysql (LISTEN)
mysqld 7775 mysql 34u IPv4 49633 0t0 TCP 192.168.1.104:56687->192.168.1.103:mysql (ESTABLISHED)
mysqld 7775 mysql 38u IPv6 55458 0t0 TCP 192.168.1.104:mysql->192.168.1.105:50689 (ESTABLISHED)
在代理节点查看4040端口,总的有二个客户端连接主库:分别是远程连接代理的两个客户端
[root@localhost lua]# lsof -i :4040
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
mysql-pro 3853 root 10u IPv4 31717 0t0 TCP 192.168.1.105:yo-main (LISTEN)
mysql-pro 3853 root 11u IPv4 31753 0t0 TCP 192.168.1.105:yo-main->192.168.1.1:56767 (ESTABLISHED)
mysql-pro 3853 root 13u IPv4 33954 0t0 TCP 192.168.1.105:yo-main->192.168.1.1:57368 (ESTABLISHED)
在master和slave上开启日志实时监测sql执行测试。

查看日志配置是否打开

mysql> SHOW VARIABLES LIKE "general_log%"; SET GLOBAL general_log = 'ON';
+------------------+--------------------------------------------+
| Variable_name    | Value                                      |
+------------------+--------------------------------------------+
| general_log      | OFF                                        |
| general_log_file | /usr/local/mysql-5.6.31/data/localhost.log |
+------------------+--------------------------------------------+

打开日志

SET GLOBAL general_log = 'ON';

查看执行的sql日志
[root@localhost mysql-proxy]# tail -f /usr/local/mysql-5.6.31/data/localhost.log
/usr/local/mysql-5.6.31/bin/mysqld, Version: 5.6.31-log (MySQL Community Server (GPL)). started with:
Tcp port: 3306 Unix socket: /tmp/mysql.sock

关闭从机上的slave功能

当远程执行mysql> select * from demo.user;时:
master  日志无变化
slave  变化:
190226  3:18:47           12 Query        select * from demo.user
当执行   insert into demo.user values(default,'欧阳询')
        master 日志变化:7 Query        insert into demo.user values(default,'欧阳询')
        slave无变化  并且在slave上也查不到这一条数据。
注意:测试读写分离需要在slave上关闭slave功能,如果开启的话slave日志会增加一条信息COMMIT /* implicit, from Xid_log_event */这个因为是主从开启的,主库新增,从库也跟着变化。

MySQL主从配置

发表于 2017-12-20 | 分类于 mysql

主从配置

MySQL数据库自身提供的主从复制功能可以方便的实现数据的多处自动备份,实现数据库的拓展。多个数据备份不仅可以加强数据的安全性,通过实现读写分离还能进一步提升数据库的负载性能。

20200420183514

20200420183530

MySQL之间数据复制的基础是二进制日志文件(binary log file)。一台MySQL数据库一旦启用二进制日志后,其作为master,它的数据库中所有操作都会以”事件”的方式记录在二进制日志中,其他数据库作为slave通过一个I/O线程与主服务器保持通信,并监控master的二进制日志文件的变化,根据变化把变化复制到自己的中继日志中,然后slave的一个SQL线程会把相关的”事件”执行到自己的数据库中,以此实现从数据库和主数据库的一致性,也就实现了主从复制。

实现MySQL主从复制需要进行的配置

主服务器:

开启二进制日志log_bin 不是执行日志/状态日志,而是操作命令日志。一般配置主从才需要配置
配置唯一的server-id
获得master二进制日志文件名及位置
创建一个用于slave和master通信的用户账号

从服务器:

配置唯一的server-id
使用master分配的用户账号读取master二进制日志
启用slave服务

主从配置开始,环境如下:

2台Linux虚拟机,MySQL5.6,确保主从数据库一模一样。
192.168.1.103 master
192.168.1.104 slave

1、master库修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
修改配置文件my.cnf,插入配置
[root@localhost ~]# vim /etc/my.cnf
[mysqld]
log_bin=master_bin #开启二进制日志
server_id=1 #设置server_id
重启MySQL,创建用于slave连接master的用户账号
[root@localhost ~]# service mysql start
Starting MySQL. SUCCESS!
[root@localhost ~]# mysql -u root -p123456
创建用户
mysql> grant all privileges on *.* to 'slave'@'192.168.1.104' identified by 'slave' with grant option;
刷新权限
mysql> flush privileges;
mysql> select user,host from user;
...
| slave | 192.168.1.104 |

查看master状态
mysql> SHOW MASTER STATUS;
+-------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+-------------------+----------+--------------+------------------+-------------------+
| master_bin.000001 | 425 | | | |
+-------------------+----------+--------------+------------------+-------------------+

2、从库修改

同样修改my.cnf,只需要添加添加server_id

server_id=2 #设置server-id,必须唯一

3、关于auto.cnf

MySQL数据目录中通常存在一个名为auto.cnf文件,存储了server-uuid的值,MySQL启动时,会自动从data_dir/auto.cnf 文件中获取server-uuid值,并将这个值存储在全局变量server_uuid中。如果这个值或者这个文件不存在,那么将会生成一个新的uuid值,并将这个值保存在auto.cnf文件中。server-uuid与server-id一样,用于标识MySQL实例在集群中的唯一性,这两个参数在主从复制中具有重要作用,默认情况下,如果主、从库的server-uuid或者server-id的值一样,将会导致主从复制报错中断。
由于我的虚拟机是安装好MySQL后直接克隆的,所以他们的这个uuid值是一样的,所以需要做一步,把其中一个值改了。如如果是分别单独安装的MySQL,则不用改。
[root@localhost ~]# cat /usr/local/mysql-5.6.31/data/auto.cnf
 [auto]
 server-uuid=6c73ecbe-3138-11e9-a3d0-000c29e525bc

4、从库配置slave

分别启动主从的MySQL,MySQL的slave会默认启动,但是空配置。
在从库上配置slave
配置的依据是master的信息,ip为master的IP,用户名密码是master提供给slave访问的用户密码,日志文件是在master中查看主库保存的命令的日志信息文件。
mysql> stop slave;
mysql> CHANGE MASTER TO MASTER_HOST='192.168.1.103',MASTER_USER='slave',MASTER_PASSWORD='slave',MASTER_LOG_FILE='master_bin.000001';
mysql> start slave;

查看slave

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
mysql> show slave status\G;
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.1.103
Master_User: slave
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: master_bin.000001
Read_Master_Log_Pos: 120
Relay_Log_File: localhost-relay-bin.000001
Relay_Log_Pos: 284
Relay_Master_Log_File: master_bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0 最后一次错误的信息描述
Last_Error:
Skip_Counter: 0 中断了多少次
Exec_Master_Log_Pos: 120
Relay_Log_Space: 930
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0 最后一次错误的IO请求编号
Last_IO_Error:
Last_SQL_Errno: 0 最后一次错误执行sql命令编号
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1
Master_UUID: 6c73ecbe-3138-11e9-a3d0-000c29e525bc
Master_Info_File: /usr/local/mysql-5.6.31/data/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
1 row in set (0.00 sec)

5、测试

在主库中创建demo库,并且创建一个表,并添加一条数据,在从库中查看有没有对应有相应数据。
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
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| demo |
| mysql |
| performance_schema |
| test |
+--------------------+
5 rows in set (0.00 sec)

mysql> use demo;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+----------------+
| Tables_in_demo |
+----------------+
| user |
+----------------+
1 row in set (0.00 sec)

mysql> select * from user;
+----+-----------+
| id | name |
+----+-----------+
| 1 | 张三丰 |
| 2 | 江小白 |
+----+-----------+
2 rows in set (0.01 sec)

mysql>
执行,没问题,当在主库删除一条数据,从库中也对应消失一条数据。
主库执行:
mysql> select * from user;
+----+-----------+
| id | name |
+----+-----------+
| 1 | 张三丰 |
| 2 | 江小白 |
+----+-----------+
2 rows in set (0.03 sec)
mysql> delete from user where id=1;
Query OK, 1 row affected (0.14 sec)
从库执行:
mysql> mysql> select * from user;
+----+-----------+
| id | name |
+----+-----------+
| 2 | 江小白 |
+----+-----------+
1 row in set (0.00 sec)

完全OK!

还可以用到的其他相关参数:

master开启二进制日志后默认记录所有库所有表的操作,可以通过配置来指定只记录指定的数据库甚至指定的表的操作,具体在mysql配置文件的[mysqld]可添加修改如下选项:

1
2
3
4
5
6
# 不同步哪些数据库  
binlog-ignore-db = mysql
binlog-ignore-db = test
binlog-ignore-db = information_schema
# 只同步哪些数据库,除此之外,其他不同步
binlog-do-db = game

MySQL主从之change master语法

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
CHANGE MASTER TO option [, option] ...  

option:
MASTER_BIND = 'interface_name'
| MASTER_HOST = 'host_name'
| MASTER_USER = 'user_name'
| MASTER_PASSWORD = 'password'
| MASTER_PORT = port_num
| MASTER_CONNECT_RETRY = interval
| MASTER_RETRY_COUNT = count
| MASTER_DELAY = interval
| MASTER_HEARTBEAT_PERIOD = interval
| MASTER_LOG_FILE = 'master_log_name'
| MASTER_LOG_POS = master_log_pos
| MASTER_AUTO_POSITION = {0|1}
| RELAY_LOG_FILE = 'relay_log_name'
| RELAY_LOG_POS = relay_log_pos
| MASTER_SSL = {0|1}
| MASTER_SSL_CA = 'ca_file_name'
| MASTER_SSL_CAPATH = 'ca_directory_name'
| MASTER_SSL_CERT = 'cert_file_name'
| MASTER_SSL_CRL = 'crl_file_name'
| MASTER_SSL_CRLPATH = 'crl_directory_name'
| MASTER_SSL_KEY = 'key_file_name'
| MASTER_SSL_CIPHER = 'cipher_list'
| MASTER_SSL_VERIFY_SERVER_CERT = {0|1}
| IGNORE_SERVER_IDS = (server_id_list)

server_id_list:
[server_id [, server_id] ... ]

执行change master语句前如果从机上slave io及sql线程已经启动,需要先停止(执行stop slave)。change master to后面不指定某个参数的话,该参数保留原值或默认值。

MySQL同步故障:” Slave_SQL_Running:No” 两种解决办法

解决办法一

Slave_SQL_Running: No
1.程序可能在slave上进行了写操作
2.也可能是slave机器重起后,事务回滚造成的.
一般是事务回滚造成的:
解决办法:
mysql> stop slave ;
mysql> set GLOBAL SQL_SLAVE_SKIP_COUNTER=1;
mysql> start slave ;

解决办法二

首先停掉Slave服务:slave stop
到主服务器上查看主机状态:记录File和Position对应的值
进入master
mysql> show master status;
+----------------------+----------+--------------+------------------+
| File                 | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+----------------------+----------+--------------+------------------+
| localhost-bin.000094 | 33622483 |              |                  |
+----------------------+----------+--------------+------------------+
1 row in set (0.00 sec)


然后到slave服务器上执行手动同步:
mysql> change master to
......
> master_log_file=localhost-bin.000094',
> master_log_pos=33622483 ;
1 row in set (0.00 sec)
mysql> start slave ;
1 row in set (0.00 sec)
手动同步需要停止master的写操作!

网络通信介绍和Socket

发表于 2017-12-13 | 分类于 网络

网络通信

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。

建立网络通信连接至少要一个端口号。socket 本质是编程接口(API),对 TCP/IP 的封装,TCP/IP 也要提供可供程序员做网络开发所用的接口,这就是 Socket 编程接口。

套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。【如果包含数据交互+断开连接,那么一共是五个步骤】

1
2
3
(1)服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
(2)客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。
(3)连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

在 java.net 包是网络编程的基础类库。其中 ServerSocket 和 Socket 是网络编程的基础类型。ServerSocket 是服务端应用类型。Socket 是建立连接的类型。当连接建立成功后,服务器和客户端都会有一个 Socket 对象示例,可以通过这个 Socket 对象示例,完成会话的所有操作。

同步与异步

同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。

异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。

阻塞和非阻塞

阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。

非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在那里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(同步非阻塞)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(异步非阻塞)。

BIO (Blocking I/O)

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

传统 BIO

BIO通信(一请求一应答)

20200413181131

采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。

采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。

我们再设想一下当客户端并发访问量增加后这种模型会出现什么问题?

在 Java 虚拟机中,线程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。

伪异步 IO

为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

20200413181242

采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。

代码示例:

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
public class BIOClient {
public static void main(String[] args) {
// 创建多个线程,模拟多个客户端连接服务端
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 3333);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello ").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
}
}
} catch (IOException e) {
}
}).start();
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 3333);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello two").getBytes());
Thread.sleep(2000);
} catch (Exception e) {
}
}
} catch (IOException e) {
}
}).start();
}
}

服务端:

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
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(3333);
// 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理
while (true) {
try {
// 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
}
}).start();

} catch (IOException e) {
}

}

}
}

直接使用线程池:

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 class BIOServer1 {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(100);
//线程池
ServerSocket serverSocket = new ServerSocket(1111);
//主线程死循环等待新连接到来
while (!Thread.currentThread().isInterrupted()) {
Socket socket = serverSocket.accept();
//为新的连接创建新的线程
executor.submit(new ConnectIOnHandler(socket));
}
}
}
class ConnectIOnHandler extends Thread {
private Socket socket;

public ConnectIOnHandler(Socket socket) {
this.socket = socket;
}

public void run() {
//死循环处理读写事件
while (!Thread.currentThread().isInterrupted() && !socket.isClosed()) {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
String body = null;
while ((len = inputStream.read(data)) != -1) {
body = new String(data, 0, len);
System.out.println("服务端接收到: " + body);
}
} catch (Exception e) {

}
}
}
}

或:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class BIOServer {
/*public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(3333);
// 接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理
while (true) {
try {
// 阻塞方法获取新的连接
Socket socket = serverSocket.accept();
// 每一个新的连接都创建一个线程,负责读取数据
new Thread(() -> {
try {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
} catch (IOException e) {
}
}).start();

} catch (IOException e) {
}

}

}*/
public static void main(String[] args){
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(5645);
Socket socket = null;
ServerThreadExcutorPool singleExcutor = new ServerThreadExcutorPool(50, 1000);
while (true) {
socket = serverSocket.accept();
singleExcutor.execute(new ServerHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != serverSocket) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}


}
class ServerThreadExcutorPool {
private ExecutorService executorService;

public ServerThreadExcutorPool(int maxPoolSize, int queneSize) {
executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queneSize));
}
public void execute(Runnable task) {
executorService.execute(task);
}
}
class ServerHandler implements Runnable {

private Socket socket;

public ServerHandler(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
try {
while (true) {
int len;
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
// 按字节流方式读取数据
String body = null;
while ((len = inputStream.read(data)) != -1) {
body = new String(data, 0, len);
}
System.out.println("服务端接收到: " + body);
socket.getOutputStream().write(body.getBytes());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO (New I/O)

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO中的N可以理解为Non-blocking,它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket相对应的 SocketChannel和 ServerSocketChannel 两种不同的套接字通道实现,支持阻塞和非阻塞两种模式。

阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;

非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

NIO与IO区别

NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 说起。然后,可以从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。

1)Non-blocking IO(非阻塞IO)

IO流是阻塞的,NIO流是不阻塞的。

Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

Java IO的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干其他任何任务。

2)Buffer(缓冲区)

IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。

Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。

在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。

3)Channel (通道)

NIO 通过Channel(通道) 进行读写。

通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。

4)Selectors(选择器)

NIO有选择器,而IO没有。

选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。

20200413181631

NIO 读数据和写数据方式

通常来说NIO中的所有IO都是从 Channel(通道) 开始的。

  • 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
  • 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。

20200413181711

NIO核心组件简单介绍

NIO 包含下面几个核心的组件:

Channel(通道)
Buffer(缓冲区)
Selector(选择器)

代码示例

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
public class NIOServer {
public static void main(String[] args) throws IOException {
// serverSelector负责轮询是否有新的连接,服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 while 循环死等
Selector serverSelector = Selector.open();
// clientSelector负责轮询连接是否有数据可读
Selector clientSelector = Selector.open();

new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(1111));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

while (true) {
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();

if (key.isAcceptable()) {
try {
// 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}

}
}
}
} catch (IOException ignored) {
}
}).start();

new Thread(() -> {
try {
while (true) {
// 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();

if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}

}
}
}
} catch (IOException ignored) {
}
}).start();

}
}

JDK 原生 NIO 编程复杂、编程模型难:

JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%

项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高。

Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。

Java NIO浅析 https://zhuanlan.zhihu.com/p/23488863

AIO (Asynchronous I/O)

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。(除了 AIO 其他的 IO 类型都是同步的,这一点可以从底层IO线程模型解释,推荐一篇文章:https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41#wechat_redirect

四、Spring集成之5-使用JMX管理Spring Bean

发表于 2017-09-30 | 分类于 Spring实战4版
  • 将Spring bean暴露为MBean
  • 远程管理Spring Bean
  • 处理JMX通知
Spring对DI的支持是通过在应用中配置bean属性, 但是,一旦应用已经部署并且正在运行, 单独使用DI并不能改变应用的配置。假设希望深入了解正在运行的应用并要在运行时改变应用的配置, 可以使用Java管理扩展(Java Management Extensions, JMX) 。
JMX这项技术能够让我们管理、 监视和配置应用。 这项技术最初作为Java的独立扩展, 从Java 5开始, JMX已经成为标准的组件。

使用JMX管理应用的核心组件是托管bean(managed bean, MBean) 。 所谓的MBean就是暴露特定方法的JavaBean, 这些方法定义了管理接口。 JMX规范定义了如下4种类型的MBean:
  • 标准MBean: 标准MBean的管理接口是通过在固定的接口上执行反射确定的, bean类会实现这个接口;
  • 动态MBean: 动态MBean的管理接口是在运行时通过调用DynamicMBean接口的方法来确定的。 因为管理接口不是通过静态接口定义的, 因此可以在运行时改变;
  • 开放MBean: 开放MBean是一种特殊的动态MBean, 其属性和方法只限定于原始类型、 原始类型的包装类以及可以分解为原始类型或原始类型包装类的任意类型;
  • 模型MBean: 模型MBean也是一种特殊的动态MBean, 用于充当管理接口与受管资源的中介。 模型Bean并不像它们所声明的那样来编写。 它们通常通过工厂生成, 工厂会使用元信息来组装管理接口。
Spring的JMX模块可以让我们将Spring bean导出为模型MBean, 这样我们就可以查看应用程序的内部情况并且能够更改配置——应用的运行期。

1、将Spring bean导出为MBean
这里有几种方式可以让我们通过使用JMX来管理Spittr应用中的bean。 为了让事情尽量保持简单。SpittleController只做适度的改变, 增加一个新的spittlesPerPage属性:
之前, 当我们调用SpitterService的getRecentSpittles()方法时, SpittleController传入20作为第二个参数, 以硬编码方式。现在通过使用JMX在运行时进行配置。

spittlesPerPage属性本身并不能实现通过外部配置来改变页面上所显示Spittle的数量。 它只是bean的一个属性。下一步把SpittleControllerbean暴露为MBean, 而spittlePerPage属性将成为MBean的托管属性(managedattribute),就可以在运行时改变该属性的值。

Spring的 MBeanExporter可以把一个或多个Spring bean导出为MBean服务器(MBean server) 内的模型 MBean。 MBean服务器(有时候也被称为MBean代理) 是MBean生存的容器。 对MBean的访问, 也是通过MBean服务器来实现的。
如图,将Spring bean导出为JMX MBean之后, 可以使用基于JMX的管理工具(例如JConsole或者VisualVM) 查看正在运行的应用程序, 显示bean的属性并调用bean的方法。

下面的@Bean方法在Spring中声明了一个MBeanExporter, 它会将spittleControllerbean导出为一个模型MBean:  
SpittleController所有的public成员都被导出为MBean的操作或属性。 这可能并不是我们所希望看到的结果, 我们真正需要的只是可以配置spittlesPerPage属性。 我们不需要调用spittles()方法或SpittleController中的其他方法或属性。 因此, 我们需要一个方式来筛选所需要的属性或方法。
为了对
MBean的属性和操作获得更细粒度的控制, Spring提供了几种选择, 包括:
  • 通过名称来声明需要暴露或忽略的bean方法;
  • 通过为bean增加接口来选择要暴露的方法;
  • 通过注解标注bean来标识托管的属性和操作。
我们会尝试每一种方式来决定哪一种最适合SpittleControllerMBean。 我们首先通过名称来选择bean的哪些方法需要暴露。  

2、远程MBean  
虽然最初的JMX规范提及了通过MBean进行应用的远程管理, 但是它并没有定义实际的远程访问协议或API。 因此, 会由JMX供应商定义自己的JMX远程访问解决方案, 但这通常又是专有的。
为了满足以标准方式进行远程访问JMX的需求, JCP(Java Community Process) 制订了JSR-160: Java管理扩展远程访问API规范(JavaManagement Extensions Remote API Specification) 。 该规范定义了JMX远程访问的标准, 该标准至少需要绑定RMI和可选的JMX消息协议(JMX Messaging Protocol , JMXMP) 。

本小节中, 我们将看到Spring如何远程访问MBean。 我们首先从配置Spring把SpittleController导出为远程MBean开始, 然后我们再了解如何使用Spring远程操纵MBean。
暴露远程MBean  
使MBean成为远程对象的最简单方式是配置Spring的ConnectorServer-FactoryBean:  
ConnectorServerFactoryBean会创建和启动JSR-160 JMXConnectorServer。 默认情况下, 服务器使用JMXMP协议并监听端口9875——因此, 它将绑定“service:jmx:jmxmp://localhost:9875”。 但是我们导出MBean的可选方案并不局限于JMXMP。
根据不同
JMX的实现, 我们有多种远程访问协议可供选择, 包括远程方法调用(Remote Method Invocation, RMI) 、 SOAP、 Hessian/Burlap和IIOP(Internet InterORB Protocol) 。 为MBean绑定不同的远程访问协议, 我们仅需要设置ConnectorServerFactoryBean的serviceUrl属性。 例如, 如果我们想使用RMI远程访问MBean, 我们可以像下面示例这样配置:  
将ConnectorServerFactoryBean绑定到了一个RMI注册表, 该注册表监听本机的1099端口。 这意味着我们需要一个RMI注册表运行时, 并监听该端口。 
本示例中不使用RmiServiceExporter, 而是通过在Spring中声明RmiRegistryFactoryBean来启动一个RMI注册表, 如下面的@Bean方法所示:  
现在我们的MBean可以通过RMI进行远程访问了。 
访问远程MBean  
要想访问远程MBean服务器, 我们需要在Spring上下文中配置MbeanServer-ConnectionFactoryBean。 
MBeanServerConnectionFactoryBean是一个可用于创建MbeanServer-Connection的工厂bean。由MBeanServerConnectionFactoryBean所生成的MBeanServerConnection实际上是作为远程MBean服务器的本地代理。 它能够以MBeanServerConnection的形式注入到其他bean的属性中:  
MBeanServerConnection提供了多种方法, 我们可以使用这些方法查询远程MBean服务器并调用MBean服务器内所注册的MBean的方法。 例如, 如果我们希望知道在远程MBean服务器中有多少已注册的MBean, 可以用如下的代码片段打印这些信息:  
传递给queryNames()方法的两个参数用于过滤查询结果。 如果将两个参数都设置为null, 输出结果为所有已注册的MBean的名称。  
为了访问MBean属性, 我们可以使用getAttribute()和setAttribute()方法。 
调用MBean的操作, 那我们需要使用invoke()方法。 

代理MBean  
Spring的MBeanProxyFactoryBean是一个代理工厂bean,可以让我们可以直接访问远程的MBean(就如同配置在本地的其他bean一样) 。 图展示了它的工作原理。  
objectName属性指定了远程MBean的对象名称。 在这里是引用我们之前导出的SpittleControllerMBean。
server属性引用了MBeanServerConnection, 通过它实现MBean所有通信的路由。 在这里, 我们注入了之前配置的MBeanServerConnectionFactoryBean。
proxyInterface属性指定了代理需要实现的接口。
对于上面声明的remoteSpittleControllerMBean, 我们现在可以把它注入到类型为SpittleControllerManagedOperations的bean属性中, 并使用它来访问远程的MBean。 这样, 我们就可以调用setSpittlesPerPage()和getSpittlesPerPage()方法了。

3、处理通知  
通过查询MBean获得信息只是查看应用状态的一种方法。 当有重要问题发生时候, 更好的方式是当这类事件发生时让MBean通知我们。 JMX通知(JMX notification)是MBean与外部世界主动通信的一种方法, 而不是等待外部应用对MBean进行查询以获得信息 :
Spring通过NotificationPublisherAware接口提供了发送通知的支持。 任何希望发送通知的MBean都必须实现这个接口。 
SpittleNotifierImpl实现了NotificationPublisherAware接口。 这并不是一个要求苛刻的接口, 它仅要求实现一个方法: setNotificationPublisher。
SpittleNotificationImpl也实现了SpittleNotifier接口的方法: millionthSpittlePosted()。 这个方法使用了setNotificationPublisher()方法所注入的NotificationPublisher来发送通知: 我们的Spittle数量又到了一个新的百万级别。
一旦
sendNotification()方法被调用, 就会发出通知。  

监听通知  
接收MBean通知的标准方法是实现javax.management.NotificationListener接口。 
PagingNotificationListener是一个典型的JMX通知监听器。 当接收到通知时, 将会调用handleNotification()方法处理通知。  
剩下的工作只需要使用MBeanExporter注册PagingNotificationListener:  
MBeanExporter的notificationListenerMappings属性用于在监听器和监听器所希望监听的MBean之间建立映射。 在本示例中, 我们建立了PagingNotificationListener来监听由SpittleNotifier MBean所发布的通知  




  

四、Spring集成之4-使用WebSocket和STOMP实现消息功能

发表于 2017-09-29 | 分类于 Spring实战4版
  • 在浏览器和服务器之间发送消息
  • 在Spring MVC控制器中处理消息
  • 为目标用户发送消息
异步消息是应用程序之间通用的交流方式。 但是, 如果某一应用是运行在Web浏览器中,那就不同了。
WebSocket协议提供了通过一个套接字实现全双工通信的功能。 除了其他的功能之外, 它能够实现Web浏览器和服务器之间的异步通信。 全双工意味着服务器可以发送消息给浏览器, 浏览器也可以发送消息给服务器。

Spring 4.0为WebSocket通信提供了支持, 包括:
  • 发送和接收消息的低层级API;
  • 发送和接收消息的高级API;
  • 用来发送消息的模板;
  • 支持SockJS, 用来解决浏览器端、 服务器以及代理不支持WebSocket的问题。
WebSocket功能实现了
基于服务器端和浏览器的应用之间实现异步通信。 
1、使用Spring的低层级WebSocket API  
按照其最简单的形式, WebSocket只是两个应用之间通信的通道。 位于WebSocket一端的应用发送消息, 另外一端处理消息。 因为它是全双工的, 所以每一端都可以发送和处理消息。  
WebSocket通信可以应用于任何类型的应用中, 但是WebSocket最常见的应用场景是实现服务器和基于浏览器的应用之间的通信。 浏览器中的JavaScript客户端开启一个到服务器的连接, 服务器通过这个连接发送更新给浏览器。 相比历史上轮询服务端以查找更新的方案, 这种技术更加高效和自然。

为了阐述
Spring低层级的WebSocket API, 让我们编写一个简单的WebSocket示例, 基于JavaScript的客户端与服务器玩一个无休止的“MarcoPolo”游戏。 服务器端的应用会处理文本消息(“Marco!”) , 然后在相同的连接上往回发送文本消息(“Polo!”) 。 为了在Spring使用较低层级的API来处理消息, 我们必须编写一个实现WebSocketHandler的类:  
WebSocketHandler需要我们实现五个方法。 相比直接实现WebSocketHandler, 更为简单的方法是扩展AbstractWebSocketHandler, 这是WebSocketHandler的一个抽象实现。 如下的程序清单展现了MarcoHandler, 它是AbstractWebSocketHandler的一个子类, 会在服务器端处理消息。  
AbstractWebSocketHandler是一个抽象类, 但是并不要求必须重载任何特定的方法。 相反, 它让我们来决定该重载哪一个方法。 除了重载WebSocketHandler中所定义的五个方法以外, 我们还可以重载AbstractWebSocketHandler中所定义的三个方法:
  • handleBinaryMessage()
  • handlePongMessage()
  • handleTextMessage()
这三个方法只是handleMessage()方法的具体化, 每个方法对应于某一种特定类型的消息。

因为
MarcoHandler将会处理文本类型的“Marco!”消息, 因此我们应该重载handleTextMessage()方法。 当有文本消息抵达的时候, 日志会记录消息内容, 在两秒钟的模拟延迟之后, 在同一个连接上返回另外一条文本消息。
MarcoHandler所没有重载的方法都由AbstractWebSocketHandler以空操作的方式(no-op) 进行了实现。 这意味着MarcoHandler也能处理二进制和pong消息, 只是对这些消息不进行任何操作而已。
另外一种方案, 我们可以扩展
TextWebSocketHandler, 不再扩展Abstract-WebSocketHandler:  
TextWebSocketHandler是AbstractWebSocketHandler的子类, 它会拒绝处理二进制消息。 它重载了handleBinaryMessage()方法, 如果收到二进制消息的时候, 将会关闭WebSocket连接。 与之类似, BinaryWebSocketHandler也是AbstractWeb-SocketHandler的子类, 它重载了handleTextMessage()方法, 如果接收到文本消息的话, 将会关闭连接。  
可以重载afterConnectionEstablished()和afterConnectionClosed()  方法,他们记录了连接信息。 当新连接建立的时候, 会调用afterConnectionEstablished()方法, 类似地, 当连接关闭时, 会调用afterConnectionClosed()方法。如果我们想在连接的生命周期上建立或销毁资源时, 这些方法会很有用。

注意, 这些方法都是以“after”开头。 这意味着, 这些事件只能在事件发生后才产生响应, 因此并不能改变结果。

现在, 已经有了消息处理器类, 我们必须要对其进行配置, 这样
Spring才能将消息转发给它。 在Spring的Java配置中, 这需要在一个配置类上使用@EnableWebSocket, 并实现WebSocketConfigurer接口, 如下面的程序清单所示。  
registerWebSocketHandlers()方法是注册消息处理器的关键。 通过重载该方法, 我们得到了一个WebSocketHandlerRegistry对象, 通过该对象可以调用addHandler()来注册信息处理器。 
另外, 如果你更喜欢使用
XML来配置Spring的话, 那么可以使用websocket命名空间:  

连接到“marco” WebSocket的JavaScript客户端  

2、应对不支持WebSocket的场景  

由于客户端浏览器对WebSocket支持情况不统一,服务端也是如此。WebSocket是很好的技术解决方案,但是如果不支持的情况下,需要备用方案。
SockJS是WebSocket技术的一种模拟, 在表面上, 它尽可能对应WebSocketAPI, 但是在底层如果WebSocket技术不可用的话, 就会选择另外的通信方式。 SockJS会优先选WebSocket, 但是如果WebSocket不可用的话, 它将会从如下的方案中挑选最优的可行方案:
  • XHR流。
  • XDR流。
  • iFrame事件源。
  • iFrame HTML文件。
  • XHR轮询。
  • XDR轮询。
  • iFrame XHR轮询。
  • JSONP轮询。
当然没有必要全部了解这些方案。 SockJS让我们能够使用统一的编程模型, 上层使用SockJS都一样,只是SockJS在底层会提供备用方案。
为了在服务端启用SockJS通信, 我们在Spring配置中可以很简单地要求添加该功能。 
或
addHandler()方法会返回WebSocketHandlerRegistration, 调用其withSockJS()方法就能声明我们想要使用SockJS功能, 如果WebSocket不可用的话, SockJS的备用方案就会发挥作用。  

要在客户端使用SockJS, 需要确保加载了SockJS客户端库。 依赖于使用JavaScript模块加载器(如require.js或curl.js) 加载JavaScript库。 
用WebJars解析Web资源  
使用了WebJars来解析JavaScript库, 作为项目Maven或Gradle构建的一部分, 就像其他的依赖一样。 为了支持该功能, 在Spring MVC配置中搭建了一个资源处理器, 让它负责解析路径以“/webjars/**”开头的请求, 这也是WebJars的标准路径:
在这个资源处理器准备就绪后, 我们可以在Web页面中使用如下的<script>标签加载SockJS库:  
<script>标签来源于一个Thymeleaf模板, 并使用“@{...}”表达式来为JavaScript文件计算完整的相对于上下文的URL路径。

这里最核心的变化是创建SockJS实例来代替WebSocket。 SockJS尽可能地模拟了WebSocket,很多其他代码并不需要变化。 相同的onopen、 onmessage和onclose事件处理函数用来响应对应的事件, 相同的send()方法用来发送“Marco!”到服务器端。
但是客户端
-服务器之间通信的运行方式却有了很大的变化。 我们可以完全相信客户端和服务器之间能够进行类似于WebSocket这样的通信, 即便浏览器、 服务器或位于中间的代理不支持WebSocket, 我们也无需再担心了。

不管哪种场景,这种通信形式都显得层级过低。 让我们看一下如何在WebSocket之上使用STOMP(Simple Text Oriented MessagingProtocol) , 为浏览器-服务器之间的通信增加恰当的消息语义。  

3、使用STOMP消息  
假设HTTP协议并不存在, 只能使用TCP套接字来编写Web应用,这就很复杂,需要自行设计客户端和服务器端都认可的协议, 从而实现有效的通信。  幸好有HTTP, 它解决了Web浏览器发起请求以及Web服务器响应请求的细节,开发人员并不需要编写低层级TCP套接字通信相关的代码。
直接使用WebSocket(或SockJS) 就很类似于使用TCP套接字来编写Web应用。 因为没有高层级的线路协议(wire protocol) , 因此就需要我们定义应用之间所发送消息的语义, 还需要确保连接的两端都能遵循这些语义。
不过, 并非必须要使用原生的WebSocket连接。 就像HTTP在TCP套接字之上添加了请求-响应模型层一样, STOMP在WebSocket之上提供了一个基于帧的线路格式(frame-based wire format) 层, 用来定义消息的语义。
就想HTTP在TCP上层一样,而且STOMP的消息格式非常类似于HTTP请求的结构。 与HTTP请求和响应类似, STOMP帧由命令、 一个或多个头信息以及负载所组成。 例如, 如下就是发送数据的一个STOMP帧:
STOMP命令是send, 表明会发送一些内容。 紧接着是两个头信息: 一个用来表示消息要发送到哪里的目的地, 另外一个则包含了负载的大小。 然后, 紧接着是一个空行, STOMP帧的最后是负载内容, 在本例中, 是一个JSON消息。

在WebSocket通信中, 基于浏览器的JavaScript应用可能会发送消息到一个目的地, 这个目的地由服务器端的组件来进行处理。 其实, 反过来是一样的, 服务器端的组件也可以发布消息, 由JavaScript客户端的目的地来接收。
Spring为STOMP消息提供了基于Spring MVC的编程模型。 在Spring MVC控制器中处理STOMP消息与处理HTTP请求并没有太大的差别。 但首先, 我们需要配置Spring启用基于STOMP的消息。

启用STOMP消息功能
在Spring MVC中为控制器方法添加@MessageMapping注解, 使其处理STOMP消息, 它与带有@RequestMapping注解的方法处理HTTP请求的方式非常类似。 但是与@RequestMapping不同的是, @MessageMapping的功能无法通过@EnableWebMvc启用。 Spring的Web消息功能基于消息代理(message broker) 构建, 因此除了告诉Spring我们想要处理消息以外, 还有其他的内容需要配置。 我们必须要配置一个消息代理和其他的一些消息目的地。
如下的程序展现了如何通过Java配置启用基于代理的Web消息功能:@EnableWebSocketMessageBroker注解能够在WebSocket之上启用STOMP
WebSocketStompConfig使用了@EnableWebSocketMessageBroker注解。 这表明这个配置类不仅配置了WebSocket, 还配置了基于代理的STOMP消息。 它重载了registerStompEndpoints()方法, 将“/marcopolo”注册为STOMP端点。 这个路径与之前发送和接收消息的目的地路径有所不同。 这是一个端点, 客户端在订阅或发布消息到目的地路径前, 要连接该端点。
WebSocketStompConfig还通过重载configureMessageBroker()方法配置了一个简单的消息代理。 这个方法是可选的, 如果不重载它的话, 将会自动配置一个简单的内存消息代理, 用它来处理以“/topic”为前缀的消息。 但是在本例中, 我们重载了这个方法, 所以消息代理将会处理前缀为“/topic”和“/queue”的消息。 除此之外, 发往应用程序的消息将会带有“/app”前缀。 图18.2展现了这个配置中的消息流。  
Spring简单的STOMP代理是基于内存的, 它模拟了STOMP代理的多项功能  

当消息到达时, 目的地的前缀将会决定消息该如何处理。 应用程序的目的地以“/app”作为前缀, 而代理的目的地以“/topic”和“/queue”作为前缀。 以应用程序为目的地的消息将会直接路由到带有@MessageMapping注解的控制器方法中。 而发送到代理上的消息, 其中也包括@MessageMapping注解方法的返回值所形成的消息, 将会路由到代理上, 并最终发送到订阅这些目的地的客户端。

启用
STOMP代理中继
对于初学来讲, 简单的代理是很不错的, 但是它也有一些限制。 尽管它模拟了
STOMP消息代理, 但是它只支持STOMP命令的子集。 因为它是基于内存的, 所以它并不适合集群, 因为如果集群的话, 每个节点也只能管理自己的代理和自己的那部分消息。
对于生产环境下的应用来说, 你可能会希望使用真正支持
STOMP的代理来支撑WebSocket消息, 如RabbitMQ或ActiveMQ。 这样的代理提供了可扩展性和健壮性更好的消息功能, 当然它们也会完整支持STOMP命令。 我们需要根据相关的文档来为STOMP搭建代理。 搭建就绪之后, 就可以使用STOMP代理来替换内存代理了, 只需按照如下方式重载configureMessageBroker()方法即可  
上述configureMessageBroker()方法的第一行代码启用了STOMP代理中继(broker relay) 功能, 并将其目的地前缀设置为“/topic”和“/queue”。 这样的话, Spring就能知道所有目的地前缀为“/topic”或“/queue”的消息都会发送到STOMP代理中。 根据你所选择的STOMP代理不同, 目的地的可选前缀也会有所限制。 例如, RabbitMQ只允许目的地的类型为“/temp-queue”、 “/exchange”、 “/topic”、“/queue”、 “/amq/queue”和“/reply-queue”。

除了目的地前缀, 在第二行的
configureMessageBroker()方法中将应用的前缀设置为“/app”。 所有目的地以“/app”打头的消息都将会路由到带有@MessageMapping注解的方法中, 而不会发布到代理队列或主题中。
如下图阐述了代理中继如何应用于Spring的STOMP消息处理之中。 我们可以看到, 关键的区别在于这里不再模拟STOMP代理的功能, 而是由代理中继将消息传送到一个真正的消息代理中来进行处理。  
注意, enableStompBrokerRelay()和setApplicationDestinationPrefixes()方法都接收可变长度的String参数, 所以我们可以配置多个目的地和应用前缀。 

默认情况下, STOMP代理中继会假设代理监听localhost的61613端口, 并且客户端的username和password均为“guest”。 如果你的STOMP代理位于其他的服务器上, 或者配置成了不同的客户端凭证, 那么我们可以在启用STOMP代理中继的时候, 需要配置这些细节信息:  

处理来自客户端的STOMP消息  
STOMP和WebSocket更多的是关于异步消息, 与HTTP的请求-响应方式有所不同。 但是, Spring提供了非常类似于Spring MVC的编程模型来处理STOMP消息。 它非常地相似, 以至于对STOMP消息的处理器方法也会包含在带有@Controller注解的类中。
Spring 4.0引入了@MessageMapping注解, 它用于STOMP消息的处理, 类似于Spring MVC的@RequestMapping注解。 当消息抵达某个特定的目的地时, 带有@MessageMapping注解的方法能够处理这些消息。 例如:
handleShout()方法接收一个Shout参数, 所以Spring的某一个消息转换器会将STOMP消息的负载转换为Shout对象。 
消息转换器 描 述
ByteArrayMessageConverter 实现MIME类型为“application/octet-stream”的消息与byte[]之间的相互转换
MappingJackson2MessageConverter 实现MIME类型为“application/json”的消息与Java对象之间的相互转换
StringMessageConverter 实现MIME类型为“text/plain”的消息与String之间的相互转换

假设handleShout()方法所处理消息的内容类型为“application/json”(这应该是一个安全的假设, 因为Shout不是byte[]和String) , MappingJackson2MessageConverter会负责将JSON消息转换为Shout对象。 就像在HTTP中对应的MappingJackson2HttpMessageConverter一样, MappingJackson2MessageConverter会将其任务委托给底层的Jackson 2 JSON处理器。 默认情况下, Jackson会使用反射将JSON属性映射为Java对象的属性。我们可以通过在Java类型上使用Jackson注解, 影响具体的转换行为。

处理订阅
除了@MessagingMapping注解以外, Spring还提供了@SubscribeMapping注解。 与@MessagingMapping注解方法类似, 当收到STOMP订阅消息的时候, 带有@SubscribeMapping注解的方法将会触发。
与@MessagingMapping方法类似, @SubscribeMapping方法也是通过AnnotationMethodMessageHandler接收消息的,@SubscribeMapping方法只能处理目的地以“/app”为前缀的消息。
因为应用发出的消息都会经过代理, 目的地要以“/topic”或“/queue”打头。 客户端会订阅这些目的地, 而不会订阅前缀为“/app”的目的地。 如果客户端订阅“/topic”和“/queue”这样的目的地, 那么@SubscribeMapping方法也就无法处理这样的订阅了。

@SubscribeMapping的主要应用场景是实现请求-回应模式。 在请求-回应模式中, 客户端订阅某一个目的地, 然后预期在这个目的地上获得一个一次性的响应。
例如, @SubscribeMapping注解标注的方法:
 handleSubscription()方法使用了@SubscribeMapping注解, 用这个方法来处理对“/app/marco”目的地的订阅(与@MessageMapping类似, “/app”是隐含的) 。 
如果你觉得这种请求
-回应模式与HTTP GET的请求-响应模式关键区别在于HTTPGET请求是同步的, 而订阅的请求-回应模式则是异步的, 这样客户端能够在回应可用时再去处理, 而不必等待。
编写
JavaScript客户端
不再直接使用SockJS, 而是通过调用Stomp.over(sock)创建了一个STOMP客户端实例。 这实际上封装了SockJS, 这样就能在WebSocket连接上发送STOMP消息。
接下来, 我们使用STOMP进行连接, 假设连接成功, 然后发送带有JSON负载的消息到名为“/marco”的目的地。 往send()方法传递的第二个参数是一个头信息的Map, 它会包含在STOMP的帧中, 不过在这个例子中, 我们没有提供任何参数, Map是空的。

发送消息到客户端  
Spring提供了两种发送数据给客户端的方法:
  • 作为处理消息或处理订阅的附带结果;
  • 使用消息模板。
我们已经了解了一些处理消息和处理订阅的方法, 所以首先看一下如何通过这些方法发送消息给客户端。 然后, 再看一下
Spring的SimpMessagingTemplate, 它能够在应用的任何地方发送消息。
在处理消息之后, 发送消息
handleShout()只是简单地返回void。 它的任务就是处理消息, 并不需要给客户端回应。
如果你想要在接收消息的时候, 同时在响应中发送一条消息, 那么需要做的仅仅是将内容返回就可以了, 方法签名不再是使用
void。 例如,如果你想发送“Polo!”消息作为“Marco!”消息的回应, 那么只需将handleShout()修改为如下所示:  
默认情况下, 帧所发往的目的地会与触发处理器方法的目的地相同, 只不过会添加上“/topic”前缀。 就本例而言, 这意味着handleShout()方法所返回的Shout对象会写入到STOMP帧的负载中, 并发布到“/topic/marco”目的地。 我们可以通过为方法添加@SendTo注解, 重载目的地:  
按照这个@SendTo注解, 消息将会发布到“/topic/shout”。 所有订阅这个主题的应用(如客户端) 都会收到这条消息。
这样的话,
handleShout()在收到一条消息的时候, 作为响应也会发送一条消息。 按照类似的方式, @SubscribeMapping注解标注的方式也能发送一条消息, 作为订阅的回应。 例如, 通过为控制器添加如下的方法, 当客户端订阅的时候, 将会发送一条Shout信息:  

在应用的任意地方发送消息    
@MessageMapping和@SubscribeMapping提供了一种很简单的方式来发送消息, 这是接收消息或处理订阅的附带结果。Spring的SimpMessagingTemplate能够在应用的任何地方发送消息, 甚至不必以首先接收一条消息作为前提。使用SimpMessagingTemplate的最简单方式是将它(或者其接口SimpMessage-SendingOperations) 自动装配到所需的对象中。  
不必要求用户刷新页面, 而是让首页订阅一个STOMP主题, 在Spittle创建的时候, 该主题能够收到Spittle更新的实时feed。 
在服务器端, 我们可以使用SimpMessagingTemplate将所有新创建的Spittle以消息的形式发布到“/topic/spittlefeed”主题上。 

4、为目标用户发送消息  
在使用Spring和STOMP消息功能的时候, 我们有三种方式利用认证用户:
  • @MessageMapping和@SubscribeMapping标注的方法能够使用Principal来获取认证用户;
  • @MessageMapping、 @SubscribeMapping和@MessageException方法返回的值能够以消息的形式发送给认证用户;
  • SimpMessagingTemplate能够发送消息给特定用户。  
在控制器中处理用户的消息  
在处理器方法中, 通过简单地添加一个Principal参数, 这个方法就能知道用户是谁并利用该信息关注此用户相关的数据。 除此之外, 处理器方法还可以使用@SendToUser注解, 表明它的返回值要以消息的形式发送给某个认证用户的客户端(只发送给该客户端) 。  
handleSpittle()方法接受Principal对象和SpittleForm对象作为参数。 它使用这两个对象创建一个Spittle实例并借助SpittleRepository将实例保存起来。  
当有发往“/app/spittle”目的地的消息到达时, 该方法就会触发, 并且会根据消息创建SpittleForm对象, 如果用户已经认证过的话, 将会根据STOMP帧上的头信息得到Principal对象。  
需要特别关注的是, 返回的Notification到哪里去了。 @SendToUser注解指定返回的Notification要以消息的形式发送到“/queue/notifications”目的地上。 在表面上, “/queue/notifications”并没有与特定用户关联。 但因为这里使用的是@SendToUser注解而不是@SendTo, 所以内部会有更多的操作。
让我们先退后一步, 看一下针对控制器方法发布Notification对象的目的地, 客户端该如何进行订阅。 考虑如下的这行JavaScript代码, 它订阅了一个用户特定的目的地:
这个目的地使用了“/user”作为前缀, 在内部, 以“/user”作为前缀的目的地将会以特殊的方式进行处理。 这种消息不会通过AnnotationMethodMessageHandler(像应用消息那样) 来处理, 也不会通过SimpleBrokerMessageHandler或StompBrokerRelayMessageHandler(像代理消息那样) 来处理, 以“/user”为前缀的消息将会通过UserDestinationMessageHandler进行处理
UserDestinationMessageHandler的主要任务是将用户消息重新路由到某个用户独有的目的地上。 在处理订阅的时候, 它会将目标地址中的“/user”前缀去掉, 并基于用户的会话添加一个后缀。 例如, 对“/user/queue/notifications”的订阅最后可能路由到名为“/queue/notificationsuser6hr83v6t”的目的地上。
代码中,
handleSpittle()方法使用了@SendToUser("/queue/notifications")注解。 这个新的目的地以“/queue”作为前缀, 根据配置, 这是StompBrokerRelayMessageHandler(或SimpleBrokerMessageHandler) 要处理的前缀, 所以消息接下来会到达这里。 最终, 客户端会订阅这个目的地, 因此客户端会收到Notification消息。
在控制器方法中,
@SendToUser注解和Principal参数是很有用的。 但是在程序清单18.8中, 我们看到借助消息模板, 可以在应用的任何位置发送消息。 接下来看一下如何使用SimpMessagingTemplate将消息发送给特定用户。 

为指定用户发送消息  
除了convertAndSend()以外, SimpMessagingTemplate还提供了convertAndSendToUser()方法。 按照名字就可以判断出来, convertAndSendToUser()方法能够让我们给特定用户发送消息。  
在broadcastSpittle()中, 如果给定Spittle对象的消息中包含了类似于用户名的内容(也就是以“@”开头的文本) , 那么一个新的Notification将会发送到名为“/queue/notifications”的目的地上。 因此, 如果Spittle中包含“@jbauer”的话, Notification将会发送到“/user/jbauer/queue/notifications”目的地上。  

5、处理消息异常  
在Spring MVC中, 如果在请求处理中, 出现异常的话, @ExceptionHandler方法将有机会处理异常。 与之类似, 我们也可以在某个控制器方法上添加@MessageException-Handler注解, 让它来处理@MessageMapping方法所抛出的异常。  
如下:它会处理消息方法所抛出的异常:  
以参数的形式声明它所能处理的异常:  
只是以日志的方式记录了所发生的错误, 但是这个方法可以做更多的事情。  
在这里, 如果抛出SpittleException的话, 将会记录这个异常, 然后将其返回。 

  

四、Spring集成之2-使用Spring MVC创建REST API

发表于 2017-09-28 | 分类于 Spring实战4版
  • 编写处理REST资源的控制器
  • 以XML、 JSON及其他格式来表述资源
  • 使用REST资源
数据为王
近几年来, 以信息为中心的表述性状态转移(Representational State Transfer, REST) 已成为替换传统SOAP Web服务的流行方案。 SOAP一般会关注行为和处理, 而REST关注的是要处理的数据。
从Spring 3.0版本开始, Spring为创建REST API提供了良好的支持。Spring对REST的支持是构建在Spring MVC之上的, 使用Spring MVC知识来开发处理RESTful资源的控制器。

1、了解REST
对于许多应用程序而言, 使用SOAP可能会有些大材小用了, 而REST提供了一个更简单的可选方案。 另外, 很多的现代化应用都会有移动或富JavaScript客户端, 它们都会使用运行在服务器上REST API。
REST的基础知识
当谈论REST时, 有一种常见的错误就是将其视为“基于URL的Web服务”——将REST作为另一种类型的远程过程调用(remote procedurecall, RPC) 机制, 就像SOAP一样, 只不过是通过简单的HTTP URL来触发, 而不是使用SOAP大量的XML命名空间。
其实,REST与RPC没有任何关系。 RPC是面向服务的, 并关注于行为和动作; 而REST是面向资源的, 强调描述应用程序的事物和名词。

为了理解REST是什么, 我们将它的首字母缩写拆分为不同的构成部分:
表述性(Representational) : REST资源实际上可以用各种形式来进行表述, 包括XML、 JSON(JavaScript Object Notation) 甚至HTML——最适合资源使用者的任意形式;
状态(State) : 当使用REST的时候, 我们更关注资源的状态而不是对资源采取的行为;
转移(Transfer) : REST涉及到转移资源数据, 它以某种表述性形式从一个应用转移到另一个应用。

简单来讲,REST就是将资源的状态以最适合客户端或服务端的形式从服务器端转移到客户端(或者反过来) 。
在REST中, 资源通过URL进行识别和定位。 至于RESTful URL的结构并没有严格的规则, 但是URL应该能够识别资源, 而不是简单的发一条命令到服务器上。关注的核心是事物, 而不是行为。

REST中会有行为, 它们是通过HTTP方法来定义的。 也就是GET、 POST、 PUT、 DELETE、 PATCH以及其他的HTTP方法构成了REST中的动作。 这些HTTP方法通常会匹配为如下的CRUD动作:
Create: POST
Read: GET
Update: PUT或PATCH
Delete: DELETE

HTTP方法会映射为CRUD动作, 也并不是严格的限制,PUT可以用来创建新资源, POST可以用来更新资源。 实际上, POST请求非幂等性(non-idempotent) 的特点使其成为一个非常灵活的方法, 对于无法适应其他HTTP方法语义的操作, 它都能够胜任。
基于对REST的这种观点, 尽量避免使用诸如REST服务、 REST Web服务或类似的术语, 这些术语会不恰当地强调行为。 而更好的描述是强调REST面向资源的本质, 并讨论RESTful资源。

Spring是如何支持REST的
从3.0版本开始, Spring针对Spring MVC的一些增强功能对REST提供了良好的支持。 4.0版本中, Spring支持以下方式来创建REST资源:注意:控制器可以处理所有的HTTP方法, 包含四个主要的REST方法: GET、 PUT、 DELETE以及POST。 Spring 3.2及以上版本还支持PATCH方法;

借助@PathVariable注解, 控制器能够处理参数化的URL(将变量输入作为URL的一部分) ;
借助Spring的视图和视图解析器, 资源能够以多种方式进行表述, 包括将模型数据渲染为XML、 JSON、 Atom以及RSS的View实现;
可以使用ContentNegotiatingViewResolver来选择最适合客户端的表述;
借助@ResponseBody注解和各种HttpMethodConverter实现, 能够替换基于视图的渲染方式;
类似地, @RequestBody注解以及HttpMethodConverter实现可以将传入的HTTP数据转化为传入控制器处理方法的Java对象;
借助RestTemplate, Spring应用能够方便地使用REST资源。

2、创建第一个REST端点
实现RESTful功能的Spring MVC控制器
客户端和服务器端针对某一资源是如何通信的,任何给定的资源都几乎可以用任意的形式来进行表述。 如果资源的使用者愿意使用JSON, 那么资源就可以用JSON格式来表述。 如果使用者喜欢尖括号, 那相同的资源可以用XML来进行表述。 同时, 如果用户在浏览器中查看资源的话, 可能更愿意以HTML的方式来展现(或者PDF、 Excel及其他便于人类阅读的格式) 。
资源没有变化——只是它的表述方式变化了。

尽管Spring支持多种资源表述形式, 但是在定义REST API的时候, 不一定要全部使用它们。 对于大多数客户端来说, 用JSON和XML来进行表述就足够了。
如果内容要由人类用户来使用的话, 那么我们可能需要支持HTML格式的资源。 根据资源的特点和应用的需求, 我们还可能选择使用PDF文档或Excel表格来展现资源。
对于非人类用户的使用者, 比如其他的应用或调用REST端点的代码, 资源表述的首选应该是XML和JSON。
而JSON更是会成为优胜者, 因为在JavaScript中使用JSON数据根本就不需要编排和解排(marshaling/demarshaling) 。

控制器本身通常并不关心资源如何表述。 控制器以Java对象的方式来处理资源。 控制器完成了它的工作之后, 资源才会被转化成最适合客户端的形式。Spring提供了两种方法将资源的Java表述形式转换为发送给客户端的表述形式:
内容协商(Content negotiation) : 选择一个视图, 它能够将模型渲染为呈现给客户端的表述形式;
消息转换器(Message conversion) : 通过一个消息转换器将控制器所返回的对象转换为呈现给客户端的表述形式。
协商资源表述  
当控制器的处理方法完成时, 通常会返回一个逻辑视图名。 如果方法不直接返回逻辑视图名(例如方法返回void) , 那么逻辑视图名会根据请求的URL判断得出。 DispatcherServlet接下来会将视图的名字传递给一个视图解析器, 要求它来帮助确定应该用哪个视图来渲染请求结果。
在面向人类访问的
Web应用程序中, 选择的视图通常来讲都会渲染为HTML。 视图解析方案是个简单的一维活动。 如果根据视图名匹配上了视图, 那这就是我们要用的视图了。
当要将视图名解析为能够产生资源表述的视图时, 我们就有另外一个维度需要考虑了。 视图不仅要匹配视图名, 而且所选择的视图要适合客户端。 如果客户端想要
JSON, 那么渲染HTML的视图就不行。
Spring的ContentNegotiatingViewResolver是一个特殊的视图解析器, 它考虑到了客户端所需要的内容类型。 按照其最简单的形式, ContentNegotiatingViewResolver可以按照下述形式进行配置:  
要理解ContentNegotiating-ViewResolver是如何工作的, 这涉及内容协商的两个步骤:
1. 确定请求的媒体类型;
2. 找到适合请求媒体类型的最佳视图。
让我们深入了解每个步骤来了解ContentNegotiatingViewResolver是如何完成其任务的, 首先从弄明白客户端需要什么类型的内容开始。

确定请求的资源类型
首先查看URL的文件扩展名。如果URL在结尾处有文件扩展名的话, ContentNegotiatingViewResolver将会基于该扩展名确定所需的类型。 如果扩展名是“.json”的话, 那么所需的内容类型必须是“application/json”。 如果扩展名是“.xml”, 那么客户端请求的就是“application/xml”。 当然, “.html”扩展名表明客户端所需的资源表述为HTML(text/html) 。
其次,如果根据文件扩展名不能得到任何媒体类型的话, 那就会考虑请求中的Accept头部信息。 在这种情况下, Accept头部信息中的值就表明了客户端想要的MIME类型。
最后, 如果没有Accept头部信息, 并且扩展名也无法提供帮助的话, ContentNegotiatingViewResolver将会使用“/”作为默认的内容类型,这样任何形式的类型都可以。

一旦内容类型确定之后, ContentNegotiatingViewResolver就该将逻辑视图名解析为渲染模型的View。 与Spring的其他视图解析器不同, ContentNegotiatingViewResolver本身不会解析视图。 而是委托给其他的视图解析器, 让它们来解析视图。
ContentNegotiatingViewResolver要求其他的视图解析器将逻辑视图名解析为视图。 解析得到的每个视图都会放到一个列表中。 这个列表装配完成后, ContentNegotiatingViewResolver会循环客户端请求的所有媒体类型, 在候选的视图中查找能够产生对应内容类型的视图。 第一个匹配的视图会用来渲染模型。

影响媒体类型的选择
还可以主动修改选择的类型。 借助ContentNegotiationManager我们所能做到的事情如下所示:
指定默认的内容类型, 如果根据请求无法得到内容类型的话, 将会使用默认值;
通过请求参数指定内容类型;
忽视请求的Accept头部信息;
将请求的扩展名映射为特定的媒体类型;
将JAF(Java Activation Framework) 作为根据扩展名查找媒体类型的备用方案。

有三种配置ContentNegotiationManager的方法:
直接声明一个ContentNegotiationManager类型的bean;
通过ContentNegotiationManagerFactoryBean间接创建bean;
重载WebMvcConfigurerAdapter的configureContentNegotiation()方法。

直接创建ContentNegotiationManager有一些复杂, 后两种方案能够让创建ContentNegotiationManager更加简单。
ContentNegotiationManager是Spring中相对比较新的功能, 是在Spring 3.2中引入的。 在Spring 3.2之前, ContentNegotiatingViewResolver的很多行为都是通过直接设置ContentNegotiatingViewResolver的属性进行配置的。 从Spring 3.2开始, Content-NegotiatingViewResolver的大多数Setter方法都废弃了, 鼓励通过ContentNegotiationManager来进行配置。
尽管我不会在本章中介绍配置ContentNegotiatingViewResolver的旧方法, 但是我们在创建ContentNegotiationManager所设置的很多属性, 在ContentNegotiatingViewResolver中都有对应的属性。 如果你使用较早版本的Spring的话, 应该能够很容易地将新的配置方式对应到旧配置方式中。

一般而言, 如果我们使用XML配置ContentNegotiationManager的话, 那最有用的将会是ContentNegotiationManagerFactoryBean。 例如, 我们可能希望在XML中配置ContentNegotiationManager使用“application/json”作为默认的内容类型:
因为ContentNegotiationManagerFactoryBean是FactoryBean的实现, 所以它会创建一个ContentNegotiationManagerbean。 这个ContentNegotiationManager能够注入到ContentNegotiatingViewResolver的contentNegotiationManager属性中。
如果使用
Java配置的话, 获得ContentNegotiationManager的最简便方法就是扩展WebMvcConfigurerAdapter并重载configureContentNegotiation()方法。 在创建Spring MVC应用的时候, 我们很可能已经扩展了WebMvcConfigurerAdapter。例如, 在Spittr应用中, 我们已经有了WebMvcConfigurerAdapter的扩展类, 名为WebConfig, 所以需要做的就是重载configureContentNegotiation()方法。 如下就是configureContentNegotiation()的一个实现, 它设置了默认的内容类型:  
现在, 我们已经有了ContentNegotiationManagerbean, 接下来就需要将它注入到ContentNegotiatingViewResolver的contentNegotiationManager属性中。 这需要我们稍微修改一下之前声
明
ContentNegotiatingViewResolver的@Bean方法:

这个
@Bean方法注入了ContentNegotiationManager, 并使用它调用了setContentNegotiationManager()。 这样的结果就是ContentNegotiatingView、 Resolver将会使用ContentNegotiationManager所定义的行为。
配置
ContentNegotiationManager有很多的细节,如下的程序清单是一个非常简单的配置样例, 当我使用ContentNegotiating-ViewResolver的时候, 通常会采用这种用法: 它默认会使用HTML视图, 但是对特定的视图名称将会渲染为JSON输出。  
还应该有一个能够处理HTML的视图解析器(如InternalResourceViewResolver或TilesViewResolver) 。 在大多数场景下, ContentNegotiatingViewResolver会假设客户端需要HTML, 如ContentNegotiationManager配置所示。 但是, 如果客户端指定了它想要JSON(通过在请求路径上使用“.json”扩展名或Accept头部信息) 的话, 那么ContentNegotiatingViewResolver将会查找能够处理JSON视图的视图解析器。
如果逻辑视图的名称为“spittles”, 那么我们所配置的BeanNameViewResolver将会解析spittles()方法中所声明的View。 这是因为bean名称匹配逻辑视图的名称。 如果没有匹配的View的话, ContentNegotiatingViewResolver将会采用默认的行为, 将其输出为HTML。
ContentNegotiatingViewResolver一旦能够确定客户端想要什么样的媒体类型, 接下来就是查找渲染这种内容的视图。ContentNegotiatingViewResolver的优势与限制
ContentNegotiatingViewResolver最大的优势在于, 它在Spring MVC之上构建了REST资源表述层, 控制器代码无需修改。 相同的一套控制器方法能够为面向人类的用户产生HTML内容, 也能针对不是人类的客户端产生JSON或XML。
如果面向人类用户的接口与面向非人类客户端的接口之间有很多重叠的话, 那么内容协商是一种很便利的方案。 在实践中, 面向人类用户的视图与REST API在细节上很少能够处于相同的级别。 如果面向人类用户的接口与面向非人类客户端的接口之间没有太多重叠的话, 那么ContentNegotiatingViewResolver的优势就体现不出来了。
ContentNegotiatingViewResolver还有一个严重的限制。 作为ViewResolver的实现, 它只能决定资源该如何渲染到客户端, 并没有涉及到客户端要发送什么样的表述给控制器使用。 如果客户端发送JSON或XML的话, 那么ContentNegotiatingViewResolver就无法提供帮助了。
ContentNegotiatingViewResolver还有一个相关的小问题, 所选中的View会渲染模型给客户端, 而不是资源。 这里有个细微但很重要的区别。 当客户端请求JSON格式的Spittle对象列表时, 客户端希望得到的响应可能如下所示:
尽管这不是很严重的问题, 但确实可能不是客户端所预期的结果。通常建议不要使用ContentNegotiatingViewResolver。 推荐使用Spring的消息转换功能来生成资源表述。  

使用HTTP信息转换器  
消息转换(message conversion) 提供了一种更为直接的方式, 它能够将控制器产生的数据转换为服务于客户端的表述形式。 当使用消息转换功能时, DispatcherServlet不再需要那么麻烦地将模型数据传送到视图中。 实际上, 这里根本就没有模型, 也没有视图, 只有控制器产生的数据, 以及消息转换器(message converter) 转换数据之后所产生的资源表述。

Spring自带了各种各样的转换器, 假设客户端通过请求的Accept头信息表明它能接受“application/json”, 并且Jackson JSON在类路径下, 那么处理方法返回的对象将交给MappingJacksonHttp-MessageConverter, 并由它转换为返回客户端的JSON表述形式。 
HTTP信息转换器除了其中的五个以外都是自动注册的, 使用它们的话, 不需要Spring配置。 但为了支持它们, 你需要添加一些库到应用程序的类路径下。 例如, 如果你想使用MappingJacksonHttpMessageConverter来实现JSON消息和Java对象的互相转换, 那么需要将Jackson JSON Processor库添加到类路径中。 类似地, 如果你想使用Jaxb2RootElementHttpMessageConverter来实现XML消息和Java对象的互相转换, 那么需要JAXB库。 如果信息是Atom或RSS格式的话, 那么Atom-FeedHttpMessageConverter和RssChannelHttpMessageConverter会需要Rome库。  
信息转换器 描 述
AtomFeedHttpMessageConverter Rome Feed对象和Atom feed(媒体类型application/atom+xml) 之间的互相转换。
如果
Rome 包在类路径下将会进行注册
BufferedImageHttpMessageConverter BufferedImages与图片二进制数据之间互相转换
ByteArrayHttpMessageConverter 读取/写入字节数组。 从所有媒体类型(*/*) 中读取, 并以application/octet-stream格式写入
FormHttpMessageConverter 将application/x-www-form-urlencoded内容读入到MultiValueMap<String,String>中, 也会
将
MultiValueMap<String,String>写入到application/x-www-form- urlencoded中或将MultiValueMap<String, Object>写入
到
multipart/form-data中
Jaxb2RootElementHttpMessageConverter 在XML(text/xml或application/xml) 和使用JAXB2注解的对象间互相读取和写入。
如果
JAXB v2 库在类路径下, 将进行注册
MappingJacksonHttpMessageConverter 在JSON和类型化的对象或非类型化的HashMap间互相读取和写入。
如果
Jackson JSON 库在类路径下, 将进行注册
MappingJackson2HttpMessageConverter 在JSON和类型化的对象或非类型化的HashMap间互相读取和写入。
如果
Jackson 2 JSON 库在类路径下, 将进行注册
MarshallingHttpMessageConverter 使用注入的编排器和解排器(marshaller和unmarshaller) 来读入和写入XML。 支持的编排器和解排器包括Castor、 JAXB2、
JIBX、 XMLBeans以及Xstream
ResourceHttpMessageConverter 读取或写入Resource
RssChannelHttpMessageConverter 在RSS feed和Rome Channel对象间互相读取或写入。
如果
Rome 库在类路径下, 将进行注册
SourceHttpMessageConverter 在XML和javax.xml.transform.Source对象间互相读取和写入。
默认注册
StringHttpMessageConverter 将所有媒体类型(*/*) 读取为String。 将String写入为text/plain
XmlAwareFormHttpMessageConverter FormHttpMessageConverter的扩展, 使用SourceHttp MessageConverter来支持基于XML的部分

在响应体中返回资源状态  
正常情况下, 当处理方法返回Java对象(除String外或View的实现以外) 时, 这个对象会放在模型中并在视图中渲染使用。 但是, 如果使用了消息转换功能的话, 我们需要告诉Spring跳过正常的模型/视图流程, 并使用消息转换器。 有不少方式都能做到这一点, 但是最简单的方法是为控制器方法添加@ResponseBody注解。
添加@ResponseBody注解, 这样就能让Spring将方法返回的List<Spittle>转换为响应体:  
@ResponseBody注解会告知Spring, 我们要将返回的对象作为资源发送给客户端, 并将其转换为客户端可接受的表述形式。 更具体地讲, DispatcherServlet将会考虑到请求中Accept头部信息, 并查找能够为客户端提供所需表述形式的消息转换器。
举例来讲, 假设客户端的Accept头部信息表明它接受“application/json”, 并且Jackson JSON库位于应用的类路径下, 那么将会选择MappingJacksonHttpMessage-Converter或MappingJackson2HttpMessageConverter(这取决于类路径下是哪个版本的Jackson) 。 消息转换器会将控制器返回的Spittle列表转换为JSON文档信息的数据格式。

Jackson默认会使用反射  
注意在默认情况下, Jackson JSON库在将返回的对象转换为JSON资源表述时, 会使用反射。 对于简单的表述内容来讲, 这没有什么问题。 但是如果你重构了Java类型, 比如添加、 移除或重命名属性, 那么所产生的JSON也将会发生变化(如果客户端依赖这些属性的话, 那客户端有可能会出错) 。
但是, 我们可以在
Java类型上使用Jackson的映射注解, 从而改变产生JSON的行为。 这样我们就能更多地控制所产生的JSON, 从而防止它影响到API或客户端。

Jackson映射注解在http://wiki.fasterxml.com/Jackson-Annotations上查看使用。
谈及
Accept头部信息, getSpitter()的@RequestMapping注解。 在这里, 我使用了produces属性表明这个方法只处理预期输
出为
JSON的请求。 也就是说, 这个方法只会处理Accept头部信息包含“application/json”的请求。 其他任何类型的请求, 即使它的URL
匹配指定的路径并且是GET请求也不会被这个方法处理。 这样的请求会被其他的方法来进行处理(如果存在适当方法的话) , 或者返回客户端HTTP 406(Not Acceptable) 响应。

@ResponseBody能够告诉Spring在把数据发送给客户端的时候, 要使用某一个消息器, 与之类似, @RequestBody也能告诉Spring查找一个消息转换器, 将来自客户端的资源表述转换为对象。 例如, 假设我们需要一种方式将客户端提交的新Spittle保存起来。 我们可以按照如下的方式编写控制器方法来处理这种请求:  
Spittle参数上使用了@RequestBody, 所以Spring将会查看请求中的Content-Type头部信息, 并查找能够将请求体转换为Spittle的消息转换器。
例如, 如果客户端发送的
Spittle数据是JSON表述形式, 那么Content-Type头部信息可能就会是“application/json”。 在这种情况下, DispatcherServlet会查找能够将JSON转换为Java对象的消息转换器。 如果Jackson 2库在类路径中, 那么MappingJackson2HttpMessageConverter将会担此重任, 将JSON表述转换为Spittle, 然后传递到saveSpittle()方法中。 这个方法还使用了@ResponseBody注解, 因此方法返回的Spittle对象将会转换为某种资源表述, 发送给客户端。

注意,
@RequestMapping有一个consumes属性, 我们将其设置为“application/ json”。 consumes属性的工作方式类似于produces, 不过它会关注请求的Content-Type头部信息。 它会告诉Spring这个方法只会处理对“/spittles”的POST请求, 并且要求请求的Content-Type头部信息为“application/json”。

为控制器默认设置消息转换
当处理请求时,
@ResponseBody和@RequestBody是启用消息转换的一种简洁和强大方式。 但是, 如果你所编写的控制器有多个方法, 并且每个方法都需要信息转换功能的话, 那么这些注解就会带来一定程度的重复性。
Spring 4.0引入了@RestController注解, 能够在这个方面给我们提供帮助。 如果在控制器类上使用@RestController来代替@Controller的话, Spring将会为该控制器的所有处理方法应用消息转换功能。 我们不必为每个方法都添加@ResponseBody了。 我们所定义的SpittleController可能就会如下所示:
这两个处理器方法都没有使用@ResponseBody注解, 因为控制器使用了@RestController, 所以它的方法所返回的对象将会通过消息转换机制, 产生客户端所需的资源表述。
到目前为止, 我们看到了如何使用
Spring MVC编程模型将RESTful资源发布到响应体之中。 但是响应除了负载以外还会有其他的内容。 头部信息和状态码也能够为客户端提供响应的有用信息。 接下来, 我们看一下在提供资源的时候, 如何填充头部信息和设置状态码。  

3、提供资源之外的其他内容  
发送错误信息到客户端  
例如, 我们为SpittleController添加一个新的处理器方法, 它会提供单个Spittle对象:  
通过id参数传入了一个ID, 然后根据它调用Repository的findOne()方法, 查找Spittle对象。 处理器方法会返回findOne()方法得到的Spittle对象, 消息转换器会负责产生客户端所需的资源表述。

如果根据给定的ID, 无法找到某个Spittle对象的ID属性能够与之匹配, findOne()方法返回null的时候, 你觉得会发生什么呢?
结果就是spittleById()方法会返回null, 响应体为空, 不会返回任何有用的数据给客户端。 同时, 响应中默认的HTTP状态码是200(OK) , 表示所有的事情运行正常。

其实不然,状态码不应该是200, 而应该是404(Not Found) , 告诉客户端它们所要求的内容没有找到。 如果响应体中能够包含错误信息而不是空的话就更好了。
Spring提供了多种方式来处理这样的场景:
使用@ResponseStatus注解可以指定状态码;
控制器方法可以返回ResponseEntity对象, 该对象能够包含更多响应相关的元数据;
异常处理器能够应对错误场景, 这样处理器方法就能关注于正常的状况。

使用ResponseEntity

作为@ResponseBody的替代方案, 控制器方法可以返回一个ResponseEntity对象。 ResponseEntity中可以包含响应相关的元数据(如头部信息和状态码) 以及要转换成资源表述的对象。
因为ResponseEntity允许我们指定响应的状态码, 所以当无法找到Spittle的时候, 我们可以返回HTTP 404错误。 如下是新版本的spittleById(), 它会返回ResponseEntity:
或者:
如果找到Spittle的话, 就会把返回的对象以及200(OK) 的状态码封装到ResponseEntity中。 另一方面, 如果findOne()返回null的话, 将会创建一个Error对象, 并将其与404(Not Found) 状态码一起封
装到
ResponseEntity中, 然后返回。
但是这种方式,更为复杂,涉及到了更多的逻辑, 包括条件语句。
ResponseEntity所使用的泛型为它的解析或出现错误。

不过, 可以借助错误处理器来修正这些问题。

处理错误
spittleById()方法中的if代码块是处理错误的, 这里使用错误处理器(error handler) 更合适。 错误处理器能够处理导致问题的场景, 这样常规的处理器方法就能只关心正常的逻辑处理路径了。
我们重构一下上面代码,首先, 定义能够对应SpittleNotFound-Exception的错误处理器:
@ExceptionHandler注解能够用到控制器方法中, 用来处理特定的异常。 这里, 如果在控制器的任意处理方法中抛出SpittleNotFoundException异常, 就会调用spittleNotFound()方法来处理异常。至于SpittleNotFoundException, 它是一个很简单异常类:  
现在,方法重构为这样:
不过, 我们能够让代码更加干净一些。 现在我们已经知道spittleById()将会返回Spittle并且HTTP状态码始终会是200(OK) , 那么就可以不再使用ResponseEntity, 而是将其替换为@ResponseBody 。
如果控制器类上使用了@RestController, 我们甚至不再需要@ResponseBody:  
鉴于错误处理器的方法会始终返回Error, 并且HTTP状态码为404(Not Found) , 那么现在我们可以对spittleNotFound()方法进行类似的清理:

因为
spittleNotFound()方法始终会返回Error, 所以使用ResponseEntity的唯一原因就是能够设置状态码。 但是通过为spittleNotFound()方法添加@ResponseStatus(HttpStatus.NOT_FOUND)注解, 我们可以达到相同的效果, 而且可以不再使用ResponseEntity了。
同样, 如果控制器类上使用了
@RestController, 那么就可以移除掉@ResponseBody。

在一定程度上, 我们已经圆满达到了想要的效果。 为了设置响应状态码, 我们首先使用ResponseEntity, 但是稍后我们借助异常处理器以及@ResponseStatus, 避免使ResponseEntity, 从而让代码更加整洁。
似乎, 我们不再需要使用
ResponseEntity了。 但是, 有一种场景ResponseEntity能够很好地完成, 但是其他的注解或异常处理器却做不到。 现在, 我们看一下如何在响应中设置头部信息。  

在响应中设置头部信息  
  
在saveSpittle()方法中, 我们在处理POST请求的过程中创建了一个新的Spittle资源。 但是, 按照目前的写法我们无法准确地与客户端交流。在saveSpittle()处理完请求之后, 服务器在响应体中包含了Spittle的表述以及HTTP状态码200(OK) , 但是,假设处理请求的过程中成功创建了资源, 状态可以视为OK。HTTP状态码也将这种情况告诉给了客户端。 HTTP 201不仅能够表明请求成功完成, 而且还能描述创建了新资源。
假如还需要知道创建的资源位置信息,这就不行了。就需要借助ResponseEntity。
HttpHeaders实例, 用来存放希望在响应中包含的头部信息值。 HttpHeaders是MultiValueMap<String, String>的特殊实现, 它有一些便利的Setter方法(如setLocation()) , 用来设置
常见的HTTP头部信息。 使用这个头部信息来创建ResponseEntity。
硬编码值的方式来构建Location头部信息不好,Spring提供了UriComponentsBuilder, 指定URL中的各种组成部分(如host、 端口、 路径以及查询)构建出一个url。
在路径设置完成之后, 调用build()方法来构建UriComponents对象, 根据这个对象调用toUri()就能得到新创建Spittle的URI。

在
REST API中暴露资源只代表了会话的一端。 通常来讲, 移动或JavaScript应用会是REST API的客户端, 但是Spring应用也完全可以使用这些资源。 

4、编写REST客户端  
作为客户端, 编写与REST资源交互的代码可能会比较乏味, 并且所编写的代码都是样板式的。 例如, 假设我们需要借助Facebook的GraphAPI, 编写方法来获取某人的Facebook基本信息。 不过, 获取基本信息的代码会有点复杂。这里介绍RestTemplate。
RestTemplate是Spring提供的用于访问Rest服务的客户端,
RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。
调用RestTemplate的默认构造函数,RestTemplate对象在底层通过使用java.net包下的实现创建HTTP 请求,
可以通过使用ClientHttpRequestFactory指定不同的HTTP请求方式。
ClientHttpRequestFactory接口主要提供了两种实现方式:
        一种是SimpleClientHttpRequestFactory,使用J2SE提供的方式(既java.net包提供的方式)创建底层的Http请求连接。
        一种方式是使用HttpComponentsClientHttpRequestFactory方式,底层使用HttpClient访问远程的Http服务,使用HttpClient可以配置连接池和证书等信息。
RestTemplate的操作
RestTemplate定义了36个与REST资源交互的方法, 其中的大多数都对应于HTTP的方法。  这里面只有11个独立的方法, 其中有十个有三种重载形式, 而第十一个则重载了六次, 这样一共形成了36个方法。
方 法 描 述
delete() 在特定的URL上对资源执行HTTP DELETE操作
exchange() 在URL上执行特定的HTTP方法, 返回包含对象的ResponseEntity, 这个对象是从响应体中映射得到的
execute() 在URL上执行特定的HTTP方法, 返回一个从响应体映射得到的对象
getForEntity() 发送一个HTTP GET请求, 返回的ResponseEntity包含了响应体所映射成的对象
getForObject() 发送一个HTTP GET请求, 返回的请求体将映射为一个对象
headForHeaders() 发送HTTP HEAD请求, 返回包含特定资源URL的HTTP头
optionsForAllow() 发送HTTP OPTIONS请求, 返回对特定URL的Allow头信息
postForEntity() POST数据到一个URL, 返回包含一个对象的ResponseEntity, 这个对象是从响应体中映射得到的
postForLocation() POST数据到一个URL, 返回新创建资源的URL
postForObject() POST数据到一个URL, 返回根据响应体匹配形成的对象
put() PUT资源到特定的URL
除了TRACE以外, RestTemplate涵盖了所有的HTTP动作。 除此之外, execute()和exchange()提供了较低层次的通用方法来使用任意的HTTP方法。
大多数操作都以三种方法的形式进行了重载:
一个使用java.net.URI作为URL格式, 不支持参数化URL;
一个使用String作为URL格式, 并使用Map指明URL参数;
一个使用String作为URL格式, 并使用可变参数列表指明URL参数。

通过对四个主要HTTP方法的支持(也就是GET、 PUT、 DELETE和POST) 来研究RestTemplate的操作。 
GET资源  
除了返回类型, getForEntity()方法就是getForObject()方法的镜像。 实际上, 它们的工作方式大同小异。 它们都执行根据URL检索资源的GET请求。 它们都将资源根据responseType参数匹配为一定的类型。 唯一的区别在于getForObject()只返回所请求类型的对象,而getForEntity()方法会返回请求的对象以及响应相关的额外信息。  

检索资源  
getForObject()方法是检索资源的合适选择。 我们请求一个资源并按照所选择的Java类型接收该资源。 
 
抽取响应的元数据  
作为getForObject()的一个替代方案, RestTemplate还提供了getForEntity()。 getForEntity()方法与getForObject()方法的工作很相似。 getForObject()只返回资源(通过HTTP信息转换器将其转换为Java对象) , getForEntity()会在ResponseEntity中返回相同的对象, 而且ResponseEntity还带有关于响应的额外信息, 如HTTP状态码和响应头。  

PUT资源  
数据进行PUT操作, RestTemplate提供了三个简单的put()方法。 就像其他的RestTemplate方法一样, put()方法有三种形式:  
DELETE资源  
POST资源数据  
postForObject()和postForEntity()对POST请求的处理方式与发送GET请求的getForObject()和getForEntity()方法是类似的。 另一个方法是getForLocation(), 它是POST请求所特有的。  

在POST请求中获取响应对象  
与getForEntity()方法一样, postForEntity()返回一个ResponseEntity<T>对象。 你可以调用这个对象的getBody()方法以获取资源对象(在本示例中是Spitter) 。 getHeaders()会给你一个HttpHeaders, 通过它可以访问响应中返回的各种HTTP头信息。 这里, 我们调用getLocation()来得到java.net.URI形式的Location头信息。  

在POST请求后获取资源位置  
如果要同时接收所发送的资源和响应头, postForEntity()方法是很便利的。 但通常你并不需要将资源发送回来(毕竟, 将其发送到服务器端是第一位的) 。 如果你真正需要的是Location头信息的值, 那么使用RestTemplate的postForLocation()方法会更简单。类似于其他的POST方法, postForLocation()会在POST请求的请求体中发送一个资源到服务器端。 但是, 响应不再是相同的资源对象, postForLocation()的响应是新创建资源的位置。 

交换资源  
到目前为止, 我们已经看到RestTemplate的各种方法来GRT、 PUT、 DELETE以及POST资源。 在它们之中, 我们看到两个特殊的方法: getForEntity()和postForEntity(), 这两个方法将结果资源包含在一个ResponseEntity对象中, 通过这个对象我们可以得到响应头和状态码。
能够从响应中读取头信息是很有用的。 但是如果你想在发送给服务端的请求中设置头信息的话, 使用
RestTemplate的exchange(),像RestTemplate的其他方法一样, exchange()也重载为三个签名格式。 一个使用java.net.URI来标识目标URL, 而其他两个以String的形式传入URL并带有URL变量。 
exchange()方法使用HttpMethod参数来表明要使用的HTTP动作。 根据这个参数的值, exchange()能够执行与其他RestTemplate方法一样的工作。  


四、Spring集成之3-Spring消息

发表于 2017-09-28 | 分类于 Spring实战4版
  • 异步消息简介
  • 基于JMS的消息功能
  • 使用Spring和AMQP发送消息
  • 消息驱动的POJO
1、异步消息简介  
异步消息也是用于应用程序之间通信的。 但是, 在系统之间传递信息的方式上, 它与其他机制有所不同。
像
RMI和Hessian/Burlap这样的远程调用机制是同步的。当客户端调用远程方法时, 客户端必须等到远程方法完成后, 才能继续执行。 即使远程方法不向客户端返回任何信息, 客户端也要被阻塞直到服务完成。  
消息则是异步发送的,客户端不需要等待服务处理消息, 甚至不需要等待消息投递完成。 客户端发送消息, 然后继续执行, 这是因为客户端假定服务最终可以收到并处理这条消息。  
异步消息的优点
虽然同步通信比较容易理解, 建立起来也很简单, 但是采用同步通信机制访问远程服务的客户端存在几个限制, 最主要的是:
同步通信意味着等待。 当客户端调用远程服务的方法时, 它必须等待远程方法结束后才能继续执行。 如果客户端与远程服务频繁通信,或者远程服务响应很慢, 就会对客户端应用的性能带来负面影响。
客户端通过服务接口与远程服务相耦合。 如果服务的接口发生变化, 此服务的所有客户端都需要做相应的改变。
客户端与远程服务的位置耦合。 客户端必须配置服务的网络位置, 这样它才知道如何与远程服务进行交互。 如果网络拓扑进行调整, 客户端也需要重新配置新的网络位置。
客户端与服务的可用性相耦合。 如果远程服务不可用, 客户端实际上也无法正常运行。
虽然同步通信仍然有它的适用场景, 但是在决定应用程序更适合哪种通信机制时, 我们必须考量以上的这些缺点。 如果这些限制正是你所担心的, 那你可能很想知道异步通信是如何解决这些问题的。

无需等待
当使用
JMS发送消息时, 客户端不必等待消息被处理, 甚至是被投递。 客户端只需要将消息发送给消息代理, 就可以确信消息会被投递给相应的目的地。
因为不需要等待, 所以客户端可以继续执行其他任务。 这种方式可以有效地节省时间, 所以客户端的性能能够极大的提高。面向消息和解耦与面向方法调用的
RPC通信不同, 发送异步消息是以数据为中心的。 这意味着客户端并没有与特定的方法签名绑定。 任何可以处理数据的队列或主题订阅者都可以处理由客户端发送的消息, 而客户端不必了解远程服务的任何规范。
位置独立
同步
RPC服务通常需要网络地址来定位。 这意味着客户端无法灵活地适应网络拓扑的改变。 如果服务的IP地址改变了, 或者服务被配置为监听其他端口, 客户端必须进行相应的调整, 否则无法访问服务。
与之相反, 消息客户端不必知道谁会处理它们的消息, 或者服务的位置在哪里。 客户端只需要了解需要通过哪个队列或主题来发送消息。 因此, 只要服务能够从队列或主题中获取消息即可, 消息客户端根本不需要关注服务来自哪里。
在点对点模型中, 可以利用这种位置的独立性来创建服务的集群。 如果客户端不知道服务的位置, 并且服务的唯一要求就是可以访问消息代理, 那么我们就可以配置多个服务从同一个队列中接收消息。 如果服务过载, 处理能力不足, 我们只需要添加一些新的服务实例来监听相同的队列就可以了。
在发布
-订阅模型中, 位置独立性会产生另一种有趣的效应。 多个服务可以订阅同一个主题, 接收相同消息的副本。 但是每一个服务对消息的处理逻辑却可能有所不同。 例如, 假设我们有一组服务可以共同处理描述新员工信息的消息。 一个服务可能会在工资系统中增加该员工, 另一个服务则会将新员工增加到HR门户中, 同时还有一个服务为新员工分配可访问系统的权限。 每一个服务都基于相同的数据(都是从同一个主题接收的) , 但各自进行独立的处理。
确保投递
为了使客户端可以与同步服务通信, 服务必须监听指定的
IP地址和端口。 如果服务崩溃了, 或者由于某种原因无法使用了, 客户端将不能继续处理。
但是, 当发送异步消息时, 客户端完全可以相信消息会被投递。 即使在消息发送时, 服务无法使用, 消息也会被存储起来, 直到服务重新可以使用为止。

2、使用JMS发送消息  
Java消息服务(Java Message Service , JMS) 是一个Java标准, 定义了使用消息代理的通用API。 在JMS出现之前, 每个消息代理都有私有的API, 这就使得不同代理之间的消息代码很难通用。 但是借助JMS, 所有遵从规范的实现都使用通用的接口, 这就类似于JDBC为数据库操作提供了通用的接口一样。
Spring通过基于模板的抽象为JMS功能提供了支持, 也就是JmsTemplate。 使用JmsTemplate, 能够非常容易地在消息生产方发送队列和主题消息, 在消费消息的那一方, 也能够非常容易地接收这些消息。 
Spring还提供了消息驱动POJO的理念: 这是一个简单的Java对象, 它能够以异步的方式响应队列或主题上到达的消息。
在Spring中搭建消息代理  
下载安装ActiveMQ
创建连接工厂
选择了作为我们的消息代理, 所以我们必须配置JMS连接工厂,如何连接到ActiveMQ。 ActiveMQConnectionFactory是ActiveMQ自带的连接工厂, 在Spring中可以使用如下方式进行配置:  
ActiveMQConnectionFactory默认ActiveMQ代理监听localhost的61616端口。如果指定ip和端口:
配置连接工厂还有另外一种方式,使用ActiveMQ自己的Spring配置命名空间来声明连接工厂(适用于ActiveMQ 4.1之后的所有版本) 。 首先, 我们必须确保在Spring的配置文件中声明了amq命名空间:  

使用<amq:connectionFactory>元素声明连接工厂:  
声明ActiveMQ消息目的地  
还需要消息传递的目的地。 目的地可以是一个队列, 也可以是一个主题, 这取决于应用的需求。  
不论使用的是队列还是主题, 我们都必须使用特定的消息代理实现类在Spring中配置目的地bean。 例如, 下面的<bean>声明定义了一个ActiveMQ队列 。
同样, 下面的<bean>声明定义了一个ActiveMQ主题:  
与连接工厂相似的是, ActiveMQ命名空间提供了另一种方式来声明队列和主题。 对于队列, 我们可以使用<amq:quence>元素来声明:  
如果是JMS主题, 我们可以使用<amq:topic>元素来声明:  

到此,所有得组件声明好了,就可以操作发送接收消息了。使用Spring的JmsTemplate——Spring 对JMS支持的核心部分。
首先了解下没有JmsTemplate, JMS是怎样使用操作消息队列, 以此了解JmsTemplate到底提供了些什么。  

Spring的JMS模板  
虽然JMS为Java开发者提供了与消息代理进行交互来发送和接收消息的标准API, 不必因为使用不同的消息代理而学习私有的消息API。但是这种统一接口用起来并不是很方便。 
使用传统的JMS(不使用Spring) 接收消息  
如同JDBC一样,每次使用JMS时都要不断地做很多重复工作。  消除冗长和重复的JMS代码, Spring给出的解决方案是JmsTemplate。 JmsTemplate可以创建连接、 获得会话以及发送和接收消息。 这使得我们可以专注于构建要发送的消息或者处理接收到的消息。  
另外, JmsTemplate可以处理所有抛出JMSException异常。 如果在使用JmsTemplate时抛出JMSException异常, JmsTemplate将捕获该异常, 然后抛出一个非检查型异常, 该异常是Spring自带的JmsException异常的子类。 如下表,标准的JMSException异常与Spring的非检查型异常之间的映射关系。  
Spring(org.springframework.jms.*) 标准的JMS(javax.jms.*)
DestinationResolutionException Spring特有的——当Spring无法解析目的地名称时抛出
IllegalStateException IllegalStateException
InvalidClientIDException InvalidClientIDException
InvalidDestinationException InvalidSelectorException
InvalidSelectorException InvalidSelectorException
JmsSecurityException JmsSecurityException
ListenerExecutionFailedException Spring特有的——当监听器方法执行失败时抛出
MessageConversionException Spring特有的——当消息转换失败时抛出
MessageEOFException MessageEOFException
MessageFormatException MessageFormatException
MessageNotReadableException MessageNotReadableException
MessageNotWriteableException MessageNotWriteableException
ResourceAllocationException ResourceAllocationException
SynchedLocalTransactionFailedException Spring特有的——当同步的本地事务不能完成时抛出
TransactionInprogressException TransactionInprogressException
TransactionRolledBackException TransactionRolledBackException
UncategorizedJmsException Spring特有的——当没有其他异常适用时抛出

使用JmsTemplate,要注入到spring上下文中使用。
发送消息 接口 
AlertServiceImpl实现了AlertService接口, 它使用JmsOperation(JmsTemplate所实现的接口)将Spittle对象发送给消息队列 。
JmsOperations的send()方法的第一个参数是JMS目的地名称, 标识消息将发送给谁。 当调用send()方法时, JmsTemplate将负责获得JMS连接、 会话并代表发送者发送消息  。
sendSpittleAlert()方法专注于组装和发送消息。 在这里没有连接或会话管理的代码, JmsTemplate帮我们处理了所有的相关事项, 而且我们也不需要捕获JMSException异常。 JmsTemplate将捕获抛出的所有JMSException异常, 然后重新抛出表所列的某一种非检查型异常。  

设置默认目的地  
与其每次发送消息时都指定一个目的地, 不如我们为JmsTemplate装配一个默认的目的地:  

在发送时, 对消息进行转换  
除了send()方法, JmsTemplate还提供了convertAndSend()方法。 与send()方法不同, convertAndSend()方法并不需要MessageCreator作为参数。 这是因为convertAndSend()会使用内置的消息转换器(message converter) 为我们创建消息。当我们使用convertAndSend()时, sendSpittleAlert()可以减少到方法体中只包含一行代码:  
JmsTemplate内部会进行一些处理。 它使用一个MessageConverter的实现类将对象转换为Message。MessageConverter是Spring定义的接口, 只有两个需要实现的方法:
通常需自定义实现,spring已经提供多个实现供我们使用:、
消息转换器 功 能
MappingJacksonMessageConverter 使用Jackson JSON库实现消息与JSON格式之间的相互转换
MappingJackson2MessageConverter 使用Jackson 2 JSON库实现消息与JSON格式之间的相互转换
MarshallingMessageConverter 使用JAXB库实现消息与XML格式之间的相互转换
SimpleMessageConverter 实现String与TextMessage之间的相互转换, 字节数组与BytesMessage之间的相互转换, Map与MapMessage之间的相互转换以
及
Serializable对象与ObjectMessage之间的相互转换

默认情况下, JmsTemplate在convertAndSend()方法中会使用SimpleMessage Converter。 但是通过将消息转换器声明为bean并将其注入到JmsTemplate的messageConverter属性中, 我们可以重写这种行为。

接收消息  
当调用JmsTemplate的receive()方法时, JmsTemplate会尝试从消息代理中获取一个消息。 如果没有可用的消息, receive()方法会一直等待, 直到获得消息为止。 

这里抛出的JMSException进行处理。 JmsTemplate可以很好地处理抛出的JmsException检查型异常, 然后把异常转换为Spring非检查型异常JmsException并重新抛出。 但是它只对调用JmsTemplate的方法时才适用。 JmsTemplate无法处理调用ObjectMessage的getObject()方法时所抛出的JMSException异常。
因此, 我们要么捕获
JMSException异常, 要么声明本方法抛出JMSException异常。 为了遵循Spring规避检查型异常的设计理念, 不建议本方法抛出JMSException异常, 所以我们选择捕获该异常。 在catch代码块中, 我们使用Spring中JmsUtils的convertJmsAccessException()方法把检查型异常JMSException转换为非检查型异常JmsException。 这其实是在其他场景中
由
JmsTemplate为我们做的事情。  

在convertAndSend()中, 可以将对象转换为Message。 还可以用在接收端, 也就是使用JmsTemplate的receiveAndConvert():


使用JmsTemplate接收消息的最大缺点在于receive()和receiveAndConvert()方法都是同步的。 这意味着接收者必须耐心等待消息的到来, 因此这些方法会一直被阻塞, 直到有可用消息(或者直到超时) 。 同步接收异步发送的消息, 是不是感觉很怪异?
这就是使用消息驱动
POJO的地方。 让我们看看如何使用能够响应消息的组件异步接收消息, 而不是一直等待消息的到来。  

创建消息驱动的POJO  
EJB2规范的一个重要内容是引入了消息驱动bean(message-driven bean, MDB) 。 MDB是可以异步处理消息的EJB。MDB将JMS目的地中的消息作为事件, 并对这些事件进行响应。 而与之相反的是, 同步消息接收者在消息可用前会一直处于阻塞状态。
MDB是EJB中的一个亮点。 即使那些狂热的EJB反对者也认为MDB可以优雅地处理消息。 EJB 2 MDB的唯一缺点是它们必须要实现java.ejb.MessageDriven-Bean。 此外, 它们还必须实现一些EJB生命周期的回调方法。 简而言之, EJB 2 MDB 不是纯的POJO。
在
EJB 3规范中, MDB进一步简化了, 使其更像POJO。 我们不再需要实现MessageDrivenBean接口, 而是实现更通用的javax.jms.MessageListener接口, 并使用@MessageDriven注解标注MDB。
Spring 2.0提供了它自己的消息驱动bean来满足异步接收消息的需求, 这种形式与EJB 3的MDB很相似。 
  
创建消息监听器  
如果使用EJB的消息驱动模型来创建Spittle的提醒处理器, 我们需要使用@MessageDriven注解进行标注。 即使它不是严格要求的, 但EJB规范还是建议MDB实现MessageListener接口。 Spittle的提醒处理器最终可能是这样的:  
如果消息驱动组件不需要实现MessageListener接口, 世界将是多么的简单。 
Spring提供了以POJO的方式处理消息的能力, 这些消息来自于JMS的队列或主题中。 例如, 基于POJO实现SpittleAlertHandler 。 Spring MDP异步接收和处理消息。
配置消息监听器  
为POJO赋予消息接收能力的诀窍是在Spring中把它配置为消息监听器。 Spring的jms命名空间为我们提供了所需要的一切。 
在这里, 我们在消息监听器容器中包含了一个消息监听器。 消息监听器容器(message listener container) 是一个特殊的bean, 它可以监控JMS目的地并等待消息到达。 一旦有消息到达, 它取出消息, 然后把消息传给任意一个对此消息感兴趣的消息监听器。 
使用了Spring jms命名空间中的两个元素。 <jms:listener-container>中包含了<jms:listener>元素。 这里的connection-factory属性配置了对connectionFactory的引用, 容器中的每
个
<jms:listener>都使用这个连接工厂进行消息监听。 在本示例中, connection-factory属性可以移除, 因为该属性的默认值就是connectionFactory。对于<jms:listener>元素, 它用于标识一个bean和一个可以处理消息的方法。 为了处理Spittle提醒消息, ref元素引用了spittleHandler bean。 当消息到达spitter.alert.queue队列(通过destination属性配置) 时, spittleHandlerbean的handleSpittleAlert()方法(通过method属性指定的) 会被触发。

如果ref属性所标示的bean实现了MessageListener, 那就没有必要再指定method属性了, 默认就会调用onMessage()方法。  

使用基于消息的RPC  
为了支持基于消息的RPC, Spring提供了JmsInvokerServiceExporter, 它可以把bean导出为基于消息的服务; 为客户端提供了JmsInvokerProxyFactoryBean来使用这些服务。  
Spring提供了多种方式把bean导出为远程服务。 我们使用RmiServiceExporter把bean导出为RMI服务, 使用HessianExporter和BurlapExporter导出为基于HTTP的Hessian和Burlap服务, 还使用HttpInvoker Service Exporter创建基于HTTP的HTTP invoker服务。
导出基于JMS的服务  
JmsInvokerServiceExporter很类似于其他的服务导出器。 事实上, JmsInvokerServiceExporter与HttpInvokerServiceExporter在名称上有某种对称型。 如HttpInvokerServiceExporter可以导出基于HTTP通信的服务, 那么JmsInvoker-ServiceExporter就应该可以导出基于JMS的服务。  
AlertServiceImpl是一个处理JMS消息的POJO, 但是不依赖于JMS  
在配置JmsInvokerServiceExporter时, 我们将引用这个bean:  
导出器的属性并没有描述服务如何基于JMS通信的细节。 但好消息是JmsInvokerServiceExporter可以充当JMS监听器。 因此, 我们使用<jms:listenercontainer>元素配置它:  
为JMS监听器容器指定了连接工厂, 所以它能够知道如何连接消息代理, 而<jms:listener>声明指定了远程消息的目的地。  

使用基于JMS的服务  
基于JMS的提醒服务已经准备好了, 等待队列中名字为spitter.alert.queue的RPC消息到达。 在客户端, JmsInvokerProxyFactoryBean用来访问服务。  
connectionFactory和queryName属性指定了RPC消息如何被投递——在这里, 也就是在给定的连接工厂中, 我们所配置的消息代理里面名为spitter.alert.queue的队列。 对于serviceInterface, 指定了代理应该通过AlertService接口暴露功能。

3、
使用AMQP实现消息功能 
高级消息队列协议(Advanced Message Queuing Protocol , AMQP)  
AMQP具有多项JMS所不具备的优势。 首先, AMQP为消息定义了线路层(wire-level protocol) 的协议, 而JMS所定义的是API规范。AMQP能跨不同的AMQP实现, 还能跨语言和平台。AMQP另外一个明显的优势在于它具有更加灵活和透明的消息模型。 使用JMS的话, 只有两种消息模型可供选择: 点对点和发布-订阅。 这两种模型在AMQP当然都是可以实现的, 但AMQP还能够让我们以其他的多种方式来发送消息, 这是通过将消息的生产者与存放消息的队列解耦实现的。

Spring AMQP是Spring框架对AMQP的扩展, 它能够让我们在Spring应用中使用AMQP风格的消息。 Spring AMQP提供了一个API, 借助这个API, 我们能够以非常类似于Spring JMS抽象的形式来使用AMQP。
AMQP的生产者并不会直接将消息发布到队列中。 AMQP在消息的生产者以及传递信息的队列之间引入了一种间接的机制:Exchange。 
消息的生产者将信息发布到一个Exchange。 Exchange会绑定到一个或多个队列上, 它负责将信息路由到队列上。 信息的消费者会从队列中提取数据并进行处理。  

Exchange不是简单地将消息传递到队列中, 不仅仅是一种穿透(pass-through) 机制。
AMQP定义了四种不同类型的Exchange, 每一种都有不同的路由算法, 这些算法决定了是否要将信息放到队列中。 根据Exchange的算法不同, 它可能会使用消息的routing key和/或参数, 并将其与Exchange和队列之间binding的routing key和参数进行对比。 如果对比结果满足相应的算法, 那么消息将会路由到队列上。 否则的话, 将不会路由到队列上。
四种标准的AMQP Exchange如下所示:
  • Direct: 如果消息的routing key与binding的routing key直接匹配的话, 消息将会路由到该队列上;
  • Topic: 如果消息的routing key与binding的routing key符合通配符匹配的话, 消息将会路由到该队列上;
  • Headers: 如果消息参数表中的头信息和值都与bingding参数表中相匹配, 消息将会路由到该队列上;
  • Fanout: 不管消息的routing key和参数表的头信息/值是什么, 消息将会路由到所有队列上。
简单来讲, 生产者将信息发送给Exchange并带有一个routing key, 消费者从队列中获取消息。
我们已经快速了解了AMQP消息的基本知识——此时应该已经能够理解我们接下来所要介绍的如何使用Spring发送和接收消息。 

RabbitMQ
RabbitMQ是一个流行的开源消息代理, 它实现了AMQP。 Spring AMQP为RabbitMQ提供了支持, 包括RabbitMQ连接工厂、 模板以及Spring配置命名空间。
在使用它发送和接收消息之前, 你需要预先安装
RabbitMQ。
配置RabbitMQ连接工厂最简单的方式就是使用Spring AMQP所提供的rabbit配置命名空间。 
默认配置如下:
声明队列、 Exchange以及binding  
在JMS中, 队列和主题的路由行为都是通过规范建立的, AMQP的路由更加丰富和灵活, 依赖于如何定义队列和Exchange以及如何将它们绑定在一起。 声明队列、 Exchange和binding的一种方式是使用RabbitMQ Channel接口的各种方法。 但是直接使用RabbitMQ的Channel接口非常麻烦。 Spring AMQP提供了声明消息路由组件。
Spring AMQP的rabbit命名空间包含了多个元素, 用来创建队列、 Exchange以及将它们结合在一起的binding  
元 素 作 用
<queue> 创建一个队列
<fanout-exchange> 创建一个fanout类型的Exchange
<header-exchange> 创建一个header类型的Exchange
<topic-exchange> 创建一个topic类型的Exchange
<direct-exchange> 创建一个direct类型的Exchange
<bindings><binding/></bindings> 元素定义一个或多个元素的集合。 元素创建Exchange和队列之间的binding

这些配置元素要与<admin>元素一起使用。 <admin>元素会创建一个RabbitMQ管理组件(administrative component) , 它会自动创建(如果它们在RabbitMQ代理中尚未存在的话) 上述这些元素所声明的队列、 Exchange以及binding。
例如, 如果你希望声明名为
spittle.alert.queue的队列, 只需要在Spring配置中添加如下的两个元素即可:  
对于简单的消息来说, 我们只需做这些就足够了。 这是因为默认会有一个没有名称的direct Exchange, 所有的队列都会绑定到这个Exchange上, 并且routing key与队列的名称相同。 在这个简单的配置中, 我们可以将消息发送到这个没有名称的Exchange上, 并将routing key设定为spittle.alert.queue, 这样消息就会路由到这个队列中。 和JMS的点对点模型类似。

例如, 如果要将消息路由到多个队列中, 而不管routingkey是什么, 我们可以按照如下的方式配置一个fanout以及多个队列:  
使用RabbitTemplate发送消息
RabbitMQ连接工厂的作用是创建到RabbitMQ的连接。 如果你希望通过RabbitMQ发送消息, 那么你可以将connectionFactorybean注入到AlertServiceImpl类中, 并使用它来创建Connection, 使用这个Connection来创建Channel, 然后使用这个Channel发布消息到Exchange上。
这种方式需要涉及很多模板化代码,所以通常使用spring提供的RabbitTemplate。

配置RabbitTemplate的最简单方式是使用rabbit命名空间的<template>元素, 如下所示:
现在, 要发送消息的话, 我们只需要将模板bean注入到AlertServiceImpl中, 并使用它来发送Spittle。 
RabbitTemplate有多个重载版本的convertAndSend()方法, 这些方法可以简化它的使用。 例如, 使用某个重载版本的convertAndSend()方法, 我们可以在调用convertAndSend()的时候, 不设置Exchange的名称:还可以同时省略Exchange名称和routing key。
如果在参数列表中省略Exchange名称, 或者同时省略Exchange名称和routing key的话, RabbitTemplate将会使用默认的Exchange名称和routing key。 按照我们之前的配置, 默认的Exchange名称为空(或者说是默认没有名称的那一个Exchange) , 默认的routing key也为空。 但是, 我们可以在<template>元素上借助exchange和routing-key属性配置不同的默认值:
不管设置的默认值是什么, 我们都可以在调用convertAndSend()方法的时候, 以参数的形式显式指定它们, 从而覆盖掉默认值。  

还可以使用较低等级的send()方法来发送org.springframework.amqp.core.Message对象  
与convertAndSend()方法类似, send()方法也有重载形式, 它们不需要提供Exchange名称和/或routing key。
使用
send()方法的技巧在于构造要发送的Message对象。但是有了convertAndSend()方法, 它会自动将对象转换为Message。 它需要一个消息转换器的帮助来完成该任务, 默认的消息转换器是SimpleMessageConverter, 它适用于String、 Serializable实例以及字节数组。 Spring AMQP还提供了其他几个有用的消息转换器, 其中包括使用JSON和XML数据的消息转换器。

接收AMQP消息  
RabbitTemplate提供了多个接收信息的方法。 最简单就是receive()方法, 它位于消息的消费者端, 对应于RabbitTemplate的send()方法。 借助receive()方法, 我们可以从队列中获取一个Message对象:  

或者配置获取消息的默认队列, 这是通过在配置模板的时候, 设置queue属性实现的:  

通常情况,使用receive方法比较繁琐,receiveAndConvert()方法会使用与sendAndConvert()方法相同的消息转换器, 将Message对象转换为原始的类型。  
调用receive()和receiveAndConvert()方法都会立即返回, 如果队列中没有等待的消息时, 将会得到null。 这需要管理轮询(polling) 以及必要的线程, 实现队列的监控。
并非必须同步轮询并等待消息到达,
Spring AMQP还提供了消息驱动POJO的支持。

定义消息驱动的AMQP POJO  
首先定义一个POJO,并且定义为一个bean,注入到spring容器中。
声明一个监听器容器和监听器, 当消息到达的时候, 能够调用SpittleAlertHandler。 

queue-names属性的名称使用了复数形式,允许设置多个队列的名称, 用逗号分割即可 。


四、Spring集成之1-远程服务

发表于 2017-09-27 | 分类于 Spring实战4版
  • 访问和发布RMI服务
  • 使用Hessian和Burlap服务
  • 使用Spring的HTTP invoker
  • 使用Spring开发Web服务
Spring远程调用  
远程调用是客户端应用和服务端之间的会话。  Spring通过多种远程调用技术支持RPC  
RPC模型 适用场景
远程方法调用(RMI) 不考虑网络限制时(例如防火墙) , 访问/发布基于Java的服务
Hessian或Burlap 考虑网络限制时, 通过HTTP访问/发布基于Java的服务。 Hessian是二进制协议, 而Burlap是基于XML的
HTTP invoker 考虑网络限制, 并希望使用基于XML或专有的序列化机制实现Java序列化时, 访问/发布基于Spring的服务
JAX-RPC和JAX-WS 访问/发布平台独立的、 基于SOAP的Web服务
不管你选择哪种远程调用模型, 我们会发现Spring都提供了风格一致的支持。 
客户端向代理发起调用, 就像代理提供了这些服务一样。 代理代表客户端与远程服务进行通信, 由它负责处理连接的细节并向远程服务发起调用。如果调用远程服务时发生java.rmi.RemoteException异常, 代理会处理此异常并重新抛出非检查型异
常
RemoteAccessException。 远程异常通常预示着系统发生了无法优雅恢复的问题, 如网络或配置问题。 既然客户端通常无法从远程异常中恢复, 那么重新抛出RemoteAccessException异常就能让客户端来决定是否处理此异常。
在服务器端, 可以使用表中
任意一种模型将Spring管理的bean发布为远程服务。下面展示了远程导出器(remote exporter)如何将bean方法发布为远程服务。  

在Spring中, 使用远程服务纯粹是一个配置问题。 不需要编写任何Java代码就可以支持远程调用。 服务bean也不需要关心它们是否参与了一个RPC(当然, 任何传递给远程调用的bean或从远程调用返回的bean可能需要实现java.io.Serializable接口) 。  

1、使用RMI  
如果你曾经创建过RMI服务, 应该会知道这会涉及如下几个步骤:
1. 编写一个服务实现类, 类中的方法必须抛出java.rmi.RemoteException异常;
2. 创建一个继承于java.rmi.Remote的服务接口;
3. 运行RMI编译器(rmic) , 创建客户端stub类和服务端skeleton类;
4. 启动一个RMI注册表, 以便持有这些服务;
5. 在RMI注册表中注册服务。  
Spring提供了更简单的方式来发布RMI服务, 不用再编写那些需要抛出RemoteException异常的特定RMI类, 只需简单地编写实现服务功能的POJO就可以了, Spring会处理剩余的其他事项。  
将要创建的RMI服务需要发布SpitterService接口中的方法:
如果用传统的RMI来发布此服务, SpitterService和SpitterServiceImpl中的所有方法都需要抛出java.rmi.RemoteException。 但是如果我们使用Spring的RmiServiceExporter把该类转变为RMI服务,则现有实现不需要做任何改变。
RmiServiceExporter可以把任意Spring管理的bean发布为RMI服务。 RmiServiceExporter把bean包装在一个适配器类中, 然后适配器类被绑定到RMI注册表中, 并且代理到服务实现类的请求——在本例中服务类也就是SpitterServiceImpl。  

使用RmiServiceExporter把服务发布为RMI配置:
这里会把spitterService bean设置到service属性中, 表明RmiServiceExporter要把该bean发布为一个RMI服务。 
serviceName属性命名了RMI服务, serviceInterface属性指定了此服务所实现的接口。
默认情况下,
RmiServiceExporter会尝试绑定到本地机器1099端口上的RMI注册表。 如果在这个端口没有发现RMI注册表, RmiServiceExporter将会启动一个注册表。 如果希望绑定到不同端口或主机上的RMI注册表, 那么我们可以通过registryPort和registryHost属性来指定。 例如, 下面的RmiServiceExporter会尝试绑定rmi.spitter.com主机1199端口上的RMI注册表:  
这就是Spring把某个bean转变为RMI服务的过程,接下来看一下客户端使用发布的rmi服务。

传统方式,RMI客户端必须使用RMI API的Naming类从RMI注册表中查找服务。
这种存在两个问题:
可能会导致3种检查型异常的任意一种(RemoteException、 NotBoundException和MalformedURLException) , 这些异常必须被捕获或重新抛出;
调用服务的任何代码都必须写获取服务的代码。 
这段代码直接违反了依赖注入(DI) 原则。 
因为客户端代码需要负责查找Spitter服务, 并且这个服务是RMI服务, 我们还不可以提供SpitterService对象的不同实现。  应该可以为任意一个bean注入SpitterService对象, 而不是
让
bean自己去查找服务。 利用DI, SpitterService的任何客户端都不需要关心此服务来源于何处。
Spring的RmiProxyFactoryBean是一个工厂bean, 该bean可以为RMI服务创建代理。 使用RmiProxyFactoryBean引用SpitterService的RMI服务是非常简单的, 只需要在客户端的Spring配置中增加如下的@Bean方法:  
服务的URL是通过RmiProxyFactoryBean的serviceUrl属性来设置的, 在这里, 服务名被设置为SpitterService, 并且声明服务是在本地机器上的; 同时, 服务提供的接口由serviceInterface属性来指定。 
RmiProxyFactoryBean生成一个代理对象, 该对象代表客户端来负责与远程的RMI服务进行通信。 客户端通过服务的接口与代理进行交互, 就如同远程服务就是一个本地的POJO。
把RMI服务声明为Spring管理的bean, 就可以把它作为依赖装配进另一个bean中, 就像任意非远程的bean那样。 使用@Autowired注解把服务代理装配进客户端中:  
通过spring方式,客户端代码不需要知道所处理的是一个RMI服务。 它只是通过注入机制接受了一个SpitterService对象, 不要关心他的实现。 代理捕获了这个服务所有可能抛出的RemoteException异常, 并把它包装为运行异常重新抛出, 这样就可以地忽略这些异常。 同样在服务端可以把这个bean替换为其他的实现。

RMI是一种实现远程服务交互的好办法, 但是它存在某些限制。
一是RMI很难穿越防火墙, RMI使用任意端口来交互——这是防火墙通常所不允许的。 在企业内部网络环境中, 我们通常不需要担心这个问题。 但是如果在互联网上运行, 我们用RMI可能会遇到麻烦。 即使RMI提供了对HTTP的通道的支持(通常防火墙都允许) , 但是建立这个通道也不是件容易的事。
另外RMI是基于Java的,所以客户端服务端都必须使用java开发。 RMI使用了Java的序列化机制, 所以通过网络传输的对象类型必须要保证在调用两端的Java运行时中是完全相同的版本。
2、使用Hessian和Burlap发布远程服务  
Hessian和Burlap是Caucho Technology提供的两种基于HTTP的轻量级远程服务解决方案。
为什么Caucho对同一个问题会有两种解决方案。 Hessian和Burlap就如同一个事物的两面, 但是每一个解决方案都服务于略微不同的目的。
Hessian 像RMI一样, 使用二进制消息进行客户端和服务端的交互。 但与其他二进制远程调用技术(例如RMI) 不同的是, 它的二进制消息可以移植到其他开发语言环境。
Burlap 是一种基于XML的远程调用技术, 可以移植到任何能够解析XML的语言上。 基于XML相比二进制格式,Burlap可读性更强。 但是和其他基于XML的远程技术(例如SOAP或XML-RPC) 不同, Burlap的消息结构简单, 不需要额外的外部定义语言(例如WSDL或IDL) 。

如何在Hessian和Burlap之间选择。 其实他们唯一的区别在于Hessian的消息是二进制的, 而Burlap的消息是XML。 由于Hessian的消息是二进制的, 所以它在带宽上更具优势。 但是如果我们更注重可读性或者我们的应用需要与没有Hessian实现的语言交互, 那么Burlap的XML消息会是更好的选择。

使用Hessian和Burlap导出bean的功能
即使没有Spring, 写Hessian服务也简单,只需要编写一个继承com.caucho.hessian.server.HessianServlet的类, 并确保所有的服务方法是public的(在Hessian里, 所有public方法被视为服务方法) 。
因为Hessian服务很容易实现, Spring并没有做更多简化Hessian模型的工作。 但是和Spring一起使用时, Hessian服务可以在各方面利用Spring框架的优势, 这是纯Hessian服务所不具备的。 包括利用Spring的AOP来为Hessian服务提供系统级服务, 例如声明式事务。

导出Hessian服务
在Spring中导出一个Hessian服务和在Spring中实现一个RMI服务相似。 同样的方式, 为了把Spitter服务发布为Hessian服务, 我们需要配置另一个导出bean, 只不过这次是HessianServiceExporter。
HessianServiceExporter对Hessian服务所执行的功能与RmiServiceExporter对RMI服务所执行的功能是相同的: 它把POJO的public方法发布成Hessian服务的方法。 不过, 实现过程与RmiServiceExporter将POJO发布为RMI服务是不同的。
HessianServiceExporter是一个Spring MVC控制器, 它可以接收Hessian请求, 并把这些请求转换成对POJO的调用从而将POJO导出为一个Hessian服务,HessianServiceExporter 是一个Spring MVC控制器, 它接收Hessian请求, 并将这些请求转换成对被导出POJO的方法调用。 在如下Spring的声明中, HessianServiceExporter会把spitterService bean导出为Hessian服务:  
正如RmiServiceExporter一样, service属性的值被设置为实现了这个服务的bean引用。 serviceInterface属性用来标识这个服务实现了SpitterService接口。
与
RmiServiceExporter不同的是, 不需要设置serviceName属性。 在RMI中, serviceName属性用来在RMI注册表中注册一个服务。 Hessian没有注册表, 因此也就没必要为Hessian服务进行命名。
配置
Hessian控制器
RmiServiceExporter和HessianServiceExporter另外一个主要区别就是, 由于Hessian是基于HTTP的, 所以HessianSeriviceExporter实现为一个Spring MVC控制器。 这意味着为了使用导出的Hessian服务, 我们需要执行两个额外的配置步骤:
在
web.xml中配置Spring的DispatcherServlet, 并把我们的应用部署为 Web应用;
在
Spring的配置文件中配置一个URL处理器, 把Hessian服务的URL分发给对应的Hessian服务bean。

首先, 我们需要一个DispatcherServlet。这个我们已经在Spittr应用的web.xml文件中配置了。 但是为了处理Hessian服务, DispatcherServlet还需要配置一个Servlet映射来拦截后缀为“*.service”的URL:  
如果是通过java配置,通过实现WebApplicationInitializer来配置DispatcherServlet的话  方式,那么需要将URL模式作为映射添加到ServletRegistration.Dynamic中:  
如果通过扩展AbstractDispatcherServletInitializer或AbstractAnnotationConfigDispatcherServletInitializer的方式来配置DispatcherServlet, 那么在重载getServletMappings()的时候, 需要包含该映射:  
这样配置后, 任何以“.service”结束的URL请求都将由DispatcherServlet处理, 它会把请求传递给匹配这个URL的控制器。 因此“/spitter.service”的请求最终将被hessianSpitterServicebean所处理(它实际上仅仅是一个SpitterServiceImpl的代理) 。
我们还需要配置一个URL映射来确保DispatcherServlet把请求转给hessianSpitterService。 如下的SimpleUrlHandlerMappingbean可以做到这一点:  

Burlap服务和Hessian服务一样,只不过使用不一样的类,BurlapServiceExporter。
 访问Hessian/Burlap服务  
在客户端代码中, 基于RMI的服务与基于Hessian的服务之间唯一的差别在于要使用Spring的HessianProxyFactoryBean来代替RmiProxyFactoryBean。 客户端调用基于Hessian的Spitter服务可以用如下的配置声明  
就像RMI服务那样, serviceInterface属性指定了这个服务实现的接口。 并 像RmiProxyFactoryBean一样, serviceUrl标识了这个服务的URL。 既然Hessian是基于HTTP的, 当然我们在这里要设置一个HTTP URL(URL是由我们先前定义的URL映射所决定的) 。  
因为Hessian和Burlap都是基于HTTP的, 它们都解决了RMI所头疼的防火墙渗透问题。 但是当传递过来的RPC消息中包含序列化对象时, RMI就完胜Hessian和Burlap了。 因为Hessian和Burlap都采用了私有的序列化机制, 而RMI使用的是Java本身的序列化机制。 如果我们的数据模型非常复杂, Hessian/Burlap的序列化模型就可能无法胜任了。
还有一个两全其美的解决方案。 就是Spring的HTTP invoker, 它基于HTTP提供了RPC(像Hessian/Burlap一样) , 同时又使用了Java的对象序列化机制。
3、Spring的HttpInvoker  
HTTP invoker是一个新的远程调用模型, 作为Spring框架的一部分, 能够执行基于HTTP的远程调用(让防火墙不为难) , 并使用Java的序列化机制(让开发者也乐观其变) 。 使用基于HTTP invoker的服务和使用基于Hessian/Burlap的服务非常相似。

要将bean导出HTTP invoker服务, 我们需要使用HttpInvokerServiceExporter。
和Hessian和Burlap导出器一样,都是使用HTTP方式。只不过内部序列化方式不一样而已。使用服务都差不多。

使用HTTP invoker有一个限制: 它只是一个Spring框架所提供的远程调用解决方案。 这意味着客户端和服务端必须都是Spring应用。
RMI、 Hessian、 Burlap和HTTP invoker都是远程调用的可选解决方案。 但是当面临无所不在的远程调用时, Web服务是势不可挡的。 
4、发布和使用Web服务  
近几年, 最流行SOA(面向服务的架构) 。  SOA的核心理念是,应用程序可以并且应该被设计成依赖于一组公共的核心服务, 而不是为每个应用都重新实现相同的功能。
例如, 一个金融机构可能有若干个应用, 其中很多都需要访问借款者的账户信息。 在这种情况下, 应用应该都依赖于一个公共的获取账户信息的服务, 而不应该在每一个应用中都建立账户访问逻辑(其中大部分逻辑都是重复的) 。
Spring为使用Java API for XML Web Service(JAX-WS) 来发布和使用SOAP Web服务提供了大力支持,这里使用Spring对JAX-WS的支持来把Spitter服务发布为Web服务并使用此Web服务。
首先, 我们来看一下如何在Spring中创建JAX-WS Web服务。

创建基于Spring的JAX-WS端点

Spring提供了一个JAX-WS服务导出器SimpleJaxWsServiceExporter,要求JAX-WS运行时支持将端点发布到指定地址上。 Sun JDK1.6自带的JAX-WS可以符合要求, 但是其他的JAX-WS实现可能不符合需求。
如果将要部署的JAX-WS不支持将其发布到指定地址上, 那要使用传统的方式来编写JAX-WS端点。 这样生命周期由JAX-WS运行时来进行管理, 而不是Spring,同时也可以使用spring来装配。

在Spring中自动装配JAX-WS端点
JAX-WS编程模型使用@WebService注解所标注的类被认为Web服务的端点, 使用@WebMethod注解所标注的方法被认为是操作。如果端点的生命周期由JAX-WS运行时来管理, 而不是由Spring来管理的话, 就可能把Spring管理的bean装配进JAX-WS管理的实例中。

Spring装配JAX-WS端点的细节在于继承SpringBeanAutowiringSupport。 通过继承SpringBeanAutowiringSupport, 我们可以使用@Autowired注解标注端点的属性, 依赖就会自动注入了。


导出独立的JAX-WS端点
当对象的生命周期不是由Spring管理的, 而对象的属性又需要注入Spring所管理的bean时, SpringBeanAutowiringSupport很有用。 在合适场景下, 还是可以把Spring管理的bean导出为JAX-WS端点。

SpringSimpleJaxWsServiceExporter的工作方式类似于前边其他服务导出器。 它把Spring管理的bean发布为JAX-WS运行时中的服务端点。 与其他服务导出器不同, SimpleJaxWsServiceExporter不需要为它指定一个被导出bean的引用, 它会将使用JAX-WS注解所标注的所有bean发布为JAX-WS服务。
SimpleJaxWsServiceExporter可以使用如下的@Bean方法来配置
SimpleJaxWsServiceExporter不需要依赖其他,当启动的时候, 它会搜索Spring应用上下中@WebService注解的bean。 当找到符合的bean时, SimpleJaxWsServiceExporter使用http://localhost:8080/ 地址将bean发布为JAX-WS端点。 
SpitterServiceEndpoint的新实现不再继承SpringBeanAutowiring-Support了。 它完全就是一个Spring bean, 因此SpitterServiceEndpoint不需要继承任何特殊的支持类就可以实现自动装配。
因为SimpleJaxWsServiceEndpoint的默认基本地址为http://localhost:8080/, 而SpitterServiceEndpoint使用了@Webservice(servicename="SpitterService")注解, 所以这两个bean所形成的Web服务地址均为http://localhost:8080/SpitterService。
如果希望调整服务URL的话, 如下SimpleJaxWsServiceEndpoint的配置把相同的服务端点发布到http://localhost:8888 /srvices/SpitterService。
SimpleJaxWsServiceEndpoint 它只能用在支持将端点发布到指定地址的JAX-WS运行时中。 这包含了Sun 1.6 JDK自带的JAX-WS运行时。 其他的JAX-WS运行时, 例如JAX-WS 2.1的参考实现, 不支持这种类型的端点发布, 因此也就不能使用SimpleJaxWsServiceEndpoint。  

在客户端代理JAX-WS服务  
使用Spring发布Web服务与我们使用RMI、 Hessian、 Burlap和HTTP invoker发布服务是有所不同的。 但是,借助Spring使用Web服务的客户端代理的工作方式与基于Spring的客户端使用其他远程调用技术是相同的。
使用JaxWsProxyFactoryBean, 我们可以在Spring中装配Spitter Web服务, JaxWsProxyFactoryBean是Spring工厂bean, 它能生成一个与SOAP Web服务交互的代理。 
wsdlDocumentUrl属性标识了远程Web服务定义文件的位置。 JaxWsPortProxyFactory bean将使用这个位置上可用的WSDL来为服务创建代理。 由JaxWsPortProxyFactoryBean所生成的代理实现了serviceInterface属性所指定的SpitterService接口。
剩下的三个属性的值通常可以通过查看服务的
WSDL来确定。 假设为Spitter服务的WSDL如下所示:
  
JaxWsPortProxyFactoryBean需要我们使用portName和serviceName属性指定端口和服务名称。 WSDL中<wsdl:port>和<wsdl:service>元素的name属性可以帮助我们识别
出这些属性该设置成什么。
最后,
namespaceUri属性指定了服务的命名空间。 命名空间将有助于JaxWsPortProxyFactoryBean去定位WSDL中的服务定义。 正如端口和服务名一样, 我们可以在WSDL中找到该属性的正确值。 它通常会在<wsdl:definitions>的targetNamespace属性中。  

总之,在客户端, Spring提供了代理工厂bean, 能让我们在Spring应用中配置远程服务。 不管是使用RMI、 Hessian、 Burlap、 Spring的HTTPinvoker, 还是Web服务, 都可以把远程服务装配进我们的应用中, 好像它们就是POJO一样。 Spring甚至捕获了所有的RemoteExecption异常, 并在发生异常的地方重新抛出运行期异常RemoteAccessException。

本质上来讲, 比本地服务更低效。 当编写访问远程服务的代码时, 必须考虑到这一点, 限制远程调用, 以规避性能瓶颈。尽管这些远程调用方案在分布式应用中很有价值, 但也仅涉及面向服务架构(SOA) 的一小部分。
基于SOAP的Web服务是开发Web服务的一种简单方式, 但从架构角度来看, 也许不是最好的选择,在后面将学习构建分布式应用的另一种选择, 把应用暴露为RESTful资源。


三、Spring后端之5-保护方法应用

发表于 2017-09-26 | 分类于 Spring实战4版
  • 保护方法调用
  • 使用表达式定义安全规则
  • 创建安全表达式计算器
1、使用注解保护方法  
Spring Security提供了三种不同的安全注解:
  • Spring Security自带的@Secured注解;
  • JSR-250的@RolesAllowed注解;
  • 表达式驱动的注解, 包括@PreAuthorize、 @PostAuthorize、 @PreFilter和@PostFilter。  
@Secured和@RolesAllowed类似, 能够基于用户所授予的权限限制对方法的访问。 当我们需要在方法上定义更灵活的安全规则时, Spring Security提供了@PreAuthorize和@PostAuthorize, 而@PreFilter/@PostFilter能够过滤方法返回的以及传入方法的集合。  
使用@Secured注解限制方法调用  
在Spring中, 如果要启用基于注解的方法安全性, 关键之处在于要在配置类上使用@EnableGlobalMethodSecurity, 如下所示:  
除了使用@EnableGlobalMethodSecurity注解,还要扩展GlobalMethodSecurityConfiguration。 在Web安全中,配置类扩展了WebSecurityConfigurerAdapter, 与之类似, 这个类能够为方法级别的安全性提供更精细的配置。
例如, 如果我们在
Web层的安全配置中设置认证, 那么可以通过重载GlobalMethodSecurityConfiguration的configure()方法实现该功能  
@EnableGlobalMethodSecurity注解securedEnabled属性设置成了true将会创建一个切点, 这样的话Spring Security切面就会包装带有@Secured注解的方法。 
@Secured注解会使用一个String数组作为参数。 每个String值是一个权限, 调用这个方法至少需要具备其中的一个权限。 通过传递进来ROLE_SPITTER, 我们告诉Spring Security只允许具有ROLE_SPITTER权限的认证用户才能调用addSpittle ()方法。  
如果方法被没有认证的用户或没有所需权限的用户调用, 保护这个方法的切面将抛出一个Spring Security异常(可能是AuthenticationException或AccessDeniedException的子类) 。 是非检查型异常,异常最终必须要被捕获和处理。
如果被保护的方法是在
Web请求中调用的, 这个异常会被Spring Security的过滤器自动处理。 否则的话, 你需要编写代码来处理这个异常。  

在Spring Security中使用JSR-250的@RolesAllowed注解  
@RolesAllowed注解和@Secured注解在各个方面基本上都是一致的。 唯一显著的区别在于@RolesAllowed是JSR-250定义的Java标准注解。  
如果选择使用@RolesAllowed的话, 需要将@EnableGlobalMethodSecurity的jsr250Enabled属性设置为true。 
这与securedEnabled并不冲突。 这两种注解风格可以同时启用。  
在将jsr250Enabled设置为true之后, 将会启用一个切点, 这样带有@RolesAllowed注解的方法都会被Spring Security的切面包装起来。 因此, 在方法上使用@RolesAllowed的方式与使用@Secured类似。 

这两个注解有一个共同的不足。 它们只能根据用户有没有授予特定的权限来限制方法的调用。 在判断方式是否执行方面, 无法使用其他的因素。 

2、使用表达式实现方法级别的安全性  
有时候, 安全性约束不仅仅涉及用户是否有权限。  
Spring Security 3.0引入了几个新注解, 它们使用SpEL能够在方法调用上实现更有意思的安全性约束。 
注 解 描 述
@PreAuthorize 在方法调用之前, 基于表达式的计算结果来限制对方法的访问
@PostAuthorize 允许方法调用, 但是如果表达式计算结果为false, 将抛出一个安全性异常
@PostFilter 允许方法调用, 但必须按照表达式来过滤方法的结果
@PreFilter 允许方法调用, 但必须在进入方法之前过滤输入值
这些注解的值参数中都可以接受一个SpEL表达式。 如果表达式的计算结果为true, 那么安全规则通过, 否则就会失败。 

需要将@EnableGlobalMethod-Security注解的prePostEnabled属性设置为true, 从而启用它们。
表述方法访问规则  
在方法调用前验证权限  
例如:@PreAuthorize乍看起来可能只是添加了SpEL支持的@Secured和@RolesAllowed。 也可以基于用户所授予的角色, 使用@PreAuthorize来限制访问。
例如: Spittr应用程序的一般用户只能写140个字以内的Spittle, 而付费用户不限制字数。  

在方法调用之后验证权限  
例如, 假设我们想对getSpittleById()方法进行保护, 确保返回的Spittle对象属于当前的认证用户。 我们只有得到Spittle对象之后, 才能判断它是否属于当前用户。 因此, getSpittleById()方法必须要先执行。 在得到Spittle之后, 如果它不属于当前用户的话,将会抛出安全性异常。  
returnObject 返回对象
在Spittle对象所包含Spitter中, 如果username属性与principal的username属性相同, 这个Spittle将返回给调用者。 否则, 会抛出一个AccessDeniedException异常, 而调用者也不会得到Spittle对象。  
过滤方法的输入和输出  
事后对方法的返回值进行过滤  
例如, 我们有一个名为getOffensiveSpittles()的方法, 这个方法会返回标记为具有攻击性的Spittle列表。 这个方法主要会给管理员使用, 以保证Spittr应用中内容的和谐。 但是, 普通用户也可以使用这个方法, 用来查看他们所发布的Spittle有没有被标记为具有攻击性。 
@PreAuthorize限制只有具备ROLE_SPITTER或ROLE_ADMIN权限的用户才能访问该方法。 如果用户能够通过这个检查点, 那么方法将会执行, 并且会返回Spittle所组成的一个List。 但是, @PostFilter注解将会过滤这个列表, 确保用户只能看到允许的Spittle。 具体来讲, 管理员能够看到所有攻击性的Spittle, 非管理员只能看到属于自己的Spittle。
表达式中的
filterObject对象引用的是这个方法所返回List中的某一个元素(我们知道它是一个Spittle) 。 在这个Spittle对象中,如果Spitter的用户名与认证用户(表达式中的principal.name) 相同或者用户具有ROLE_ADMIN角色, 那这个元素将会最终包含在过滤后的列表中。 否则, 它将被过滤掉。  

事先对方法的参数进行过滤  
对于没有ROLE_SPITTER或ROLE_ADMIN权限的用户, @PreAuthorize注解会阻止对这个方法的调用。 但同时, @PreFilter注解能够保证传递给deleteSpittles()方法的列表中, 只包含当前用户有权限删除的Spittle。 这个表达式会针对集合中的每个元素进行计算, 只有表达式计算结果为true的元素才会保留在列表中。 targetObject是Spring Security提供的另外一个值, 它代表了要进行计算的当前列表元素。  
  
上一页1…181920…25下一页
初晨

初晨

永远不要说你知道本质,更别说真相了。

249 日志
46 分类
109 标签
近期文章
  • WebSocket、Socket、TCP、HTTP区别
  • Springboot项目的接口防刷
  • 深入理解Volatile关键字及其实现原理
  • 使用vscode搭建个人笔记环境
  • HBase介绍安装与操作
© 2018 — 2020 Copyright
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4