
JSSE(Java Security Socket Extension)是Sun公司为了解决互联网信息安全传输提出的一个解决方案,它实现了SSL和TSL协议,包含了数据加密、服务器验证、消息完整性和客户端验证等技术。通过使用JSSE简洁的API,可以在客户端和服务器端之间通过SSL/TSL协议安全地传输数据。 首先,需要将OpenSSL生成根证书CA及签发子证书一文中生成的客户端及服务端私钥和数字证书进行导出,生成Java环境可用的keystore文件。 客户端私钥与证书的导出: ? 1 2 openssl pkcs12 -export -clcerts -name www.mydomain.com \ -inkey private/client-key.pem -in certs/client.cer -out certs/client.keystore 服务器端私钥与证书的导出: ? 1 2 openssl pkcs12 -export -clcerts -name www.mydomain.com \ -inkey private/server-key.pem -in certs/server.cer -out certs/server.keystore 受信任的CA证书的导出: ? 1 2 keytool -importcert -trustcacerts -alias www.mydomain.com -file certs/ca.cer \ -keystore certs/ca-trust.keystore 之后,便会在certs文件夹下生成ca-trust.keystore文件。加上上面生成的server.keystore和client.keystore,certs下会生成这三个文件: Java实现的SSL通信客户端: ? 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 package com.demo.ssl; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; import java.security.KeyStore; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManagerFactory; public class SSLClient { private SSLSocket sslSocket; public static void main(String[] args) throws Exception { SSLClient client = new SSLClient(); client.init(); System.out.println("SSLClient initialized."); client.process(); } //客户端将要使用到client.keystore和ca-trust.keystore public void init() throws Exception { String host = "127.0.0.1"; int port = 1234; String keystorePath = "/home/user/CA/certs/client.keystore"; String trustKeystorePath = "/home/user/CA/certs/ca-trust.keystore"; String keystorePassword = "abc123_"; SSLContext context = SSLContext.getInstance("SSL"); //客户端证书库 KeyStore clientKeystore = KeyStore.getInstance("pkcs12"); FileInputStream keystoreFis = new FileInputStream(keystorePath); clientKeystore.load(keystoreFis, keystorePassword.toCharArray()); //信任证书库 KeyStore trustKeystore = KeyStore.getInstance("jks"); FileInputStream trustKeystoreFis = new FileInputStream(trustKeystorePath); trustKeystore.load(trustKeystoreFis, keystorePassword.toCharArray()); //密钥库 KeyManagerFactory kmf = KeyManagerFactory.getInstance("sunx509"); kmf.init(clientKeystore, keystorePassword.toCharArray()); //信任库 TrustManagerFactory tmf = TrustManagerFactory.getInstance("sunx509"); tmf.init(trustKeystore); //初始化SSL上下文 context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); sslSocket = (SSLSocket)context.getSocketFactory().createSocket(host, port); } public void process() throws Exception { //往SSLSocket中写入数据 String hello = "hello boy!"; OutputStream out = sslSocket.getOutputStream(); out.write(hello.getBytes(), 0, hello.getBytes().length); out.flush(); //从SSLSocket中读取数据 InputStream in = sslSocket.getInputStream(); byte[] buffer = new byte[50]; in.read(buffer); System.out.println(new String(buffer)); } } 初始化时,首先取得SSLContext、KeyManagerFactory、TrustManagerFactory实例,然后加载客户端的密钥库和信任库到相应的KeyStore,对KeyManagerFactory和TrustManagerFactory进行初始化,最后用KeyManagerFactory和TrustManagerFactory对SSLContext进行初始化,并创建SSLSocket。 Java实现的SSL通信服务器端: ? 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 package com.demo.ssl; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.security.KeyStore; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.TrustManagerFactory; public class SSLServer { private SSLServerSocket sslServerSocket; public static void main(String[] args) throws Exception { SSLServer server = new SSLServer(); server.init(); System.out.println("SSLServer initialized."); server.process(); } //服务器端将要使用到server.keystore和ca-trust.keystore public void init() throws Exception { int port = 1234; String keystorePath = "/home/user/CA/certs/server.keystore"; String trustKeystorePath = "/home/user/CA/certs/ca-trust.keystore"; String keystorePassword = "abc123_"; SSLContext context = SSLContext.getInstance("SSL"); //客户端证书库 KeyStore keystore = KeyStore.getInstance("pkcs12"); FileInputStream keystoreFis = new FileInputStream(keystorePath); keystore.load(keystoreFis, keystorePassword.toCharArray()); //信任证书库 KeyStore trustKeystore = KeyStore.getInstance("jks"); FileInputStream trustKeystoreFis = new FileInputStream(trustKeystorePath); trustKeystore.load(trustKeystoreFis, keystorePassword.toCharArray()); //密钥库 KeyManagerFactory kmf = KeyManagerFactory.getInstance("sunx509"); kmf.init(keystore, keystorePassword.toCharArray()); //信任库 TrustManagerFactory tmf = TrustManagerFactory.getInstance("sunx509"); tmf.init(trustKeystore); //初始化SSL上下文 context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); //初始化SSLSocket sslServerSocket = (SSLServerSocket)context.getServerSocketFactory().createServerSocket(port); //设置这个SSLServerSocket需要授权的客户端访问 sslServerSocket.setNeedClientAuth(true); } public void process() throws Exception { String bye = "Bye!"; byte[] buffer = new byte[50]; while(true) { Socket socket = sslServerSocket.accept(); InputStream in = socket.getInputStream(); in.read(buffer); System.out.println("Received: " + new String(buffer)); OutputStream out = socket.getOutputStream(); out.write(bye.getBytes()); out.flush(); } } } 先运行服务器端,再运行客户端。服务器端执行结果: 客户端执行结果:
ELK平台介绍 在搜索ELK资料的时候,发现这篇文章比较好,于是摘抄一小段: 以下内容来自:http://baidu.blog.51cto.com/71938/1676798 日志主要包括系统日志、应用程序日志和安全日志。系统运维和开发人员可以通过日志了解服务器软硬件信息、检查配置过程中的错误及错误发生的原因。经常分析日志可以了解服务器的负荷,性能安全性,从而及时采取措施纠正错误。 通常,日志被分散的储存不同的设备上。如果你管理数十上百台服务器,你还在使用依次登录每台机器的传统方法查阅日志。这样是不是感觉很繁琐和效率低下。当务之急我们使用集中化的日志管理,例如:开源的syslog,将所有服务器上的日志收集汇总。 集中化管理日志后,日志的统计和检索又成为一件比较麻烦的事情,一般我们使用grep、awk和wc等Linux命令能实现检索和统计,但是对于要求更高的查询、排序和统计等要求和庞大的机器数量依然使用这样的方法难免有点力不从心。 开源实时日志分析ELK平台能够完美的解决我们上述的问题,ELK由ElasticSearch、Logstash和Kiabana三个开源工具组成。官方网站:https://www.elastic.co/products Elasticsearch是个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等。 Logstash是一个完全开源的工具,他可以对你的日志进行收集、过滤,并将其存储供以后使用(如,搜索)。 Kibana 也是一个开源和免费的工具,它Kibana可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助您汇总、分析和搜索重要数据日志。 ----------------------------摘抄内容结束------------------------------- 画了一个ELK工作的原理图: 如图:Logstash收集AppServer产生的Log,并存放到ElasticSearch集群中,而Kibana则从ES集群中查询数据生成图表,再返回给Browser。 ELK平台搭建 系统环境 System: Centos release 6.7 (Final) ElasticSearch: 2.1.0 Logstash: 2.1.1 Kibana: 4.3.0 Java: openjdk version "1.8.0_65" 注:由于Logstash的运行依赖于Java环境, 而Logstash 1.5以上版本不低于java 1.7,因此推荐使用最新版本的Java。因为我们只需要Java的运行环境,所以可以只安装JRE,不过这里我依然使用JDK,请自行搜索安装。 ELK下载:https://www.elastic.co/downloads/ ElasticSearch 配置ElasticSearch: ? 1 2 tar -zxvf elasticsearch-2.1.0.tar.gz cd elasticsearch-2.1.0 安装Head插件(Optional): ? 1 ./bin/plugin install mobz/elasticsearch-head 然后编辑ES的配置文件: ? 1 vi config/elasticsearch.yml 修改以下配置项: ? 1 2 3 4 5 6 7 cluster.name=es_cluster node.name=node0 path.data=/tmp/elasticsearch/data path.logs=/tmp/elasticsearch/logs #当前hostname或IP,我这里是centos2 network.host=centos2 network.port=9200 其他的选项保持默认,然后启动ES: ? 1 ./bin/elasticsearch 可以看到,它跟其他的节点的传输端口为9300,接受HTTP请求的端口为9200。 使用ctrl+C停止。当然,也可以使用后台进程的方式启动ES: ? 1 ./bin/elasticsearch & 然后可以打开页面localhost:9200,将会看到以下内容: 返回展示了配置的cluster_name和name,以及安装的ES的版本等信息。 刚刚安装的head插件,它是一个用浏览器跟ES集群交互的插件,可以查看集群状态、集群的doc内容、执行搜索和普通的Rest请求等。现在也可以使用它打开localhost:9200/_plugin/head页面来查看ES集群状态: 可以看到,现在,ES集群中没有index,也没有type,因此这两条是空的。 Logstash Logstash的功能如下: 其实它就是一个收集器而已,我们需要为它指定Input和Output(当然Input和Output可以为多个)。由于我们需要把Java代码中Log4j的日志输出到ElasticSearch中,因此这里的Input就是Log4j,而Output就是ElasticSearch。 配置Logstash: ? 1 2 tar -zxvf logstash-2.1.1.tar.gz cd logstash-2.1.1 编写配置文件(名字和位置可以随意,这里我放在config目录下,取名为log4j_to_es.conf): ? 1 2 mkdir config vi config/log4j_to_es.conf 输入以下内容: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # For detail structure of this file # Set: https://www.elastic.co/guide/en/logstash/current/configuration-file-structure.html input { # For detail config for log4j as input, # See: https://www.elastic.co/guide/en/logstash/current/plugins-inputs-log4j.html log4j { mode => "server" host => "centos2" port => 4567 } } filter { #Only matched data are send to output. } output { # For detail config for elasticsearch as output, # See: https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html elasticsearch { action => "index" #The operation on ES hosts => "centos2:9200" #ElasticSearch host, can be array. index => "ec" #The index to write data to, can be any string. } } logstash命令只有2个参数: 因此使用agent来启动它(使用-f指定配置文件): ? 1 ./bin/logstash agent -f config/log4j_to_es.conf 到这里,我们已经可以使用Logstash来收集日志并保存到ES中了,下面来看看项目代码。 Java项目 照例先看项目结构图: pom.xml,很简单,只用到了Log4j库: ? 1 2 3 4 5 <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> log4j.properties,将Log4j的日志输出到SocketAppender,因为官网是这么说的: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 log4j.rootLogger=INFO,console # for package com.demo.elk, log would be sent to socket appender. log4j.logger.com.demo.elk=DEBUG, socket # appender socket log4j.appender.socket=org.apache.log4j.net.SocketAppender log4j.appender.socket.Port=4567 log4j.appender.socket.RemoteHost=centos2 log4j.appender.socket.layout=org.apache.log4j.PatternLayout log4j.appender.socket.layout.ConversionPattern=%d [%-5p] [%l] %m%n log4j.appender.socket.ReconnectionDelay=10000 # appender console log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.target=System.out log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%d [%-5p] [%l] %m%n 注意:这里的端口号需要跟Logstash监听的端口号一致,这里是4567。 Application.java,使用Log4j的LOGGER打印日志即可: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 package com.demo.elk; import org.apache.log4j.Logger; public class Application { private static final Logger LOGGER = Logger.getLogger(Application.class); public static void main(String[] args) throws Exception { for (int i = 0; i < 10; i++) { LOGGER.error("Info log [" + i + "]."); Thread.sleep(500); } } } 用Head插件查看ES状态和内容 运行Application.java,先看看console的输出(当然,这个输出只是为了做验证,不输出到console也可以的): 再来看看ES的head页面: 切换到Browser标签: 单击某一个文档(doc),则会展示该文档的所有信息: 可以看到,除了基础的message字段是我们的日志内容,Logstash还为我们增加了许多字段。而在https://www.elastic.co/guide/en/logstash/current/plugins-inputs-log4j.html中也明确说明了这一点: 上面使用了ES的Head插件观察了ES集群的状态和数据,但这只是个简单的用于跟ES交互的页面而已,并不能生成报表或者图表什么的,接下来使用Kibana来执行搜索并生成图表。 Kibana 配置Kibana: ? 1 2 3 tar -zxvf kibana-4.3.0-linux-x86.tar.gz cd kibana-4.3.0-linux-x86 vi config/kibana.yml 修改以下几项(由于是单机版的,因此host的值也可以使用localhost来代替,这里仅仅作为演示): ? 1 2 3 4 server.port: 5601 server.host: “centos2” elasticsearch.url: http://centos2:9200 kibana.index: “.kibana” 启动kibana: ? 1 ./bin/kibana 用浏览器打开该地址: 为了后续使用Kibana,需要配置至少一个Index名字或者Pattern,它用于在分析时确定ES中的Index。这里我输入之前配置的Index名字applog,Kibana会自动加载该Index下doc的field,并自动选择合适的field用于图标中的时间字段: 点击Create后,可以看到左侧增加了配置的Index名字: 接下来切换到Discover标签上,注意右上角是查询的时间范围,如果没有查找到数据,那么你就可能需要调整这个时间范围了,这里我选择Today: 接下来就能看到ES中的数据了: 执行搜索看看呢: 点击右边的保存按钮,保存该查询为search_all_logs。接下来去Visualize页面,点击新建一个柱状图(Vertical Bar Chart),然后选择刚刚保存的查询search_all_logs,之后,Kibana将生成类似于下图的柱状图(只有10条日志,而且是在同一时间段的,比较丑,但足可以说明问题了:) ): 你可以在左边设置图形的各项参数,点击Apply Changes按钮,右边的图形将被更新。同理,其他类型的图形都可以实时更新。 点击右边的保存,保存此图,命名为search_all_logs_visual。接下来切换到Dashboard页面: 单击新建按钮,选择刚刚保存的search_all_logs_visual图形,面板上将展示该图: 如果有较多数据,我们可以根据业务需求和关注点在Dashboard页面添加多个图表:柱形图,折线图,地图,饼图等等。当然,我们可以设置更新频率,让图表自动更新: 如果设置的时间间隔够短,就很趋近于实时分析了。 到这里,ELK平台部署和基本的测试已完成。
引言 前段时间写的《Spring+Log4j+ActiveMQ实现远程记录日志——实战+分析》得到了许多同学的认可,在认可的同时,也有同学提出可以使用Kafka来集中管理日志,于是今天就来学习一下。 特别说明,由于网络上关于Kafka+Log4j的完整例子并不多,我也是一边学习一边使用,因此如果有解释得不好或者错误的地方,欢迎批评指正,如果你有好的想法,也欢迎留言探讨。 第一部分 搭建Kafka环境 安装Kafka 下载:http://kafka.apache.org/downloads.html ? 1 2 tar zxf kafka-<VERSION>.tgz cd kafka-<VERSION> 启动Zookeeper 启动Zookeeper前需要配置一下config/zookeeper.properties: 接下来启动Zookeeper ? 1 bin/zookeeper-server-start.sh config/zookeeper.properties 启动Kafka Server 启动Kafka Server前需要配置一下config/server.properties。主要配置以下几项,内容就不说了,注释里都很详细: 然后启动Kafka Server: ? 1 bin/kafka-server-start.sh config/server.properties 创建Topic ? 1 bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test 查看创建的Topic ? 1 >bin/kafka-topics.sh --list --zookeeper localhost:2181 启动控制台Producer,向Kafka发送消息 ? 1 2 3 4 bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test This is a message This is another message ^C 启动控制台Consumer,消费刚刚发送的消息 ? 1 2 3 bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic test --from-beginning This is a message This is another message 删除Topic ? 1 bin/kafka-topics.sh --delete --zookeeper localhost:2181 --topic test 注:只有当delete.topic.enable=true时,该操作才有效 配置Kafka集群(单台机器上) 首先拷贝server.properties文件为多份(这里演示4个节点的Kafka集群,因此还需要拷贝3份配置文件): ? 1 2 3 cp config/server.properties config/server1.properties cp config/server.properties config/server2.properties cp config/server.properties config/server3.properties 修改server1.properties的以下内容: ? 1 2 3 broker.id=1 port=9093 log.dir=/tmp/kafka-logs-1 同理修改server2.properties和server3.properties的这些内容,并保持所有配置文件的zookeeper.connect属性都指向运行在本机的zookeeper地址localhost:2181。注意,由于这几个Kafka节点都将运行在同一台机器上,因此需要保证这几个值不同,这里以累加的方式处理。例如在server2.properties上: ? 1 2 3 broker.id=2 port=9094 log.dir=/tmp/kafka-logs-2 把server3.properties也配置好以后,依次启动这些节点: ? 1 2 3 bin/kafka-server-start.sh config/server1.properties & bin/kafka-server-start.sh config/server2.properties & bin/kafka-server-start.sh config/server3.properties & Topic & Partition Topic在逻辑上可以被认为是一个queue,每条消费都必须指定它的Topic,可以简单理解为必须指明把这条消息放进哪个queue里。为了使得Kafka的吞吐率可以线性提高,物理上把Topic分成一个或多个Partition,每个Partition在物理上对应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。 现在在Kafka集群上创建备份因子为3,分区数为4的Topic: ? 1 bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 4 --topic kafka 说明:备份因子replication-factor越大,则说明集群容错性越强,就是当集群down掉后,数据恢复的可能性越大。所有的分区数里的内容共同组成了一份数据,分区数partions越大,则该topic的消息就越分散,集群中的消息分布就越均匀。 然后使用kafka-topics.sh的--describe参数查看一下Topic为kafka的详情: 输出的第一行是所有分区的概要,接下来的每一行是一个分区的描述。可以看到Topic为kafka的消息,PartionCount=4,ReplicationFactor=3正是我们创建时指定的分区数和备份因子。 另外:Leader是指负责这个分区所有读写的节点;Replicas是指这个分区所在的所有节点(不论它是否活着);ISR是Replicas的子集,代表存有这个分区信息而且当前活着的节点。 拿partition:0这个分区来说,该分区的Leader是server0,分布在id为0,1,2这三个节点上,而且这三个节点都活着。 再来看下Kafka集群的日志: 其中kafka-logs-0代表server0的日志,kafka-logs-1代表server1的日志,以此类推。 从上面的配置可知,id为0,1,2,3的节点分别对应server0, server1, server2, server3。而上例中的partition:0分布在id为0, 1, 2这三个节点上,因此可以在server0, server1, server2这三个节点上看到有kafka-0这个文件夹。这个kafka-0就代表Topic为kafka的partion0。 第二部分 Kafka+Log4j项目整合 先来看下Maven项目结构图: 作为Demo,文件不多。先看看pom.xml引入了哪些jar包: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka_2.9.2</artifactId> <version>0.8.2.1</version> </dependency> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>0.8.2.1</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> 重要的内容是log4j.properties: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 log4j.rootLogger=INFO,console # for package com.demo.kafka, log would be sent to kafka appender. log4j.logger.com.demo.kafka=DEBUG,kafka # appender kafka log4j.appender.kafka=kafka.producer.KafkaLog4jAppender log4j.appender.kafka.topic=kafka # multiple brokers are separated by comma ",". log4j.appender.kafka.brokerList=localhost:9092, localhost:9093, localhost:9094, localhost:9095 log4j.appender.kafka.compressionType=none log4j.appender.kafka.syncSend=true log4j.appender.kafka.layout=org.apache.log4j.PatternLayout log4j.appender.kafka.layout.ConversionPattern=%d [%-5p] [%t] - [%l] %m%n # appender console log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.target=System.out log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%d [%-5p] [%t] - [%l] %m%n App.java里面就很简单啦,主要是通过log4j输出日志: ? 1 2 3 4 5 6 7 8 9 10 11 package com.demo.kafka; import org.apache.log4j.Logger; public class App { private static final Logger LOGGER = Logger.getLogger(App.class); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 20; i++) { LOGGER.info("Info [" + i + "]"); Thread.sleep(1000); } } } MyConsumer.java用于消费kafka中的信息: ? 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 package com.demo.kafka; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import com.google.common.collect.ImmutableMap; import kafka.consumer.Consumer; import kafka.consumer.ConsumerConfig; import kafka.consumer.KafkaStream; import kafka.javaapi.consumer.ConsumerConnector; import kafka.message.MessageAndMetadata; public class MyConsumer { private static final String ZOOKEEPER = "localhost:2181"; //groupName可以随意给,因为对于kafka里的每条消息,每个group都会完整的处理一遍 private static final String GROUP_NAME = "test_group"; private static final String TOPIC_NAME = "kafka"; private static final int CONSUMER_NUM = 4; private static final int PARTITION_NUM = 4; public static void main(String[] args) { // specify some consumer properties Properties props = new Properties(); props.put("zookeeper.connect", ZOOKEEPER); props.put("zookeeper.connectiontimeout.ms", "1000000"); props.put("group.id", GROUP_NAME); // Create the connection to the cluster ConsumerConfig consumerConfig = new ConsumerConfig(props); ConsumerConnector consumerConnector = Consumer.createJavaConsumerConnector(consumerConfig); // create 4 partitions of the stream for topic “test”, to allow 4 // threads to consume Map<String, List<KafkaStream<byte[], byte[]>>> topicMessageStreams = consumerConnector.createMessageStreams( ImmutableMap.of(TOPIC_NAME, PARTITION_NUM)); List<KafkaStream<byte[], byte[]>> streams = topicMessageStreams.get(TOPIC_NAME); // create list of 4 threads to consume from each of the partitions ExecutorService executor = Executors.newFixedThreadPool(CONSUMER_NUM); // consume the messages in the threads for (final KafkaStream<byte[], byte[]> stream : streams) { executor.submit(new Runnable() { public void run() { for (MessageAndMetadata<byte[], byte[]> msgAndMetadata : stream) { // process message (msgAndMetadata.message()) System.out.println(new String(msgAndMetadata.message())); } } }); } } } MyProducer.java用于向Kafka发送消息,但不通过log4j的appender发送。此案例中可以不要。但是我还是放在这里: ? 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 package com.demo.kafka; import java.util.ArrayList; import java.util.List; import java.util.Properties; import kafka.javaapi.producer.Producer; import kafka.producer.KeyedMessage; import kafka.producer.ProducerConfig; public class MyProducer { private static final String TOPIC = "kafka"; private static final String CONTENT = "This is a single message"; private static final String BROKER_LIST = "localhost:9092"; private static final String SERIALIZER_CLASS = "kafka.serializer.StringEncoder"; public static void main(String[] args) { Properties props = new Properties(); props.put("serializer.class", SERIALIZER_CLASS); props.put("metadata.broker.list", BROKER_LIST); ProducerConfig config = new ProducerConfig(props); Producer<String, String> producer = new Producer<String, String>(config); //Send one message. KeyedMessage<String, String> message = new KeyedMessage<String, String>(TOPIC, CONTENT); producer.send(message); //Send multiple messages. List<KeyedMessage<String,String>> messages = new ArrayList<KeyedMessage<String, String>>(); for (int i = 0; i < 5; i++) { messages.add(new KeyedMessage<String, String> (TOPIC, "Multiple message at a time. " + i)); } producer.send(messages); } } 到这里,代码就结束了。 第三部分 运行与验证 先运行MyConsumer,使其处于监听状态。同时,还可以启动Kafka自带的ConsoleConsumer来验证是否跟MyConsumer的结果一致。最后运行App.java。 先来看看MyConsumer的输出: 再来看看ConsoleConsumer的输出: 可以看到,尽管发往Kafka的消息去往了不同的地方,但是内容是一样的,而且一条也不少。最后再来看看Kafka的日志。 我们知道,Topic为kafka的消息有4个partion,从之前的截图可知这4个partion均匀分布在4个kafka节点上,于是我对每一个partion随机选取一个节点查看了日志内容。 上图中黄色选中部分依次代表在server0上查看partion0,在server1上查看partion1,以此类推。 而红色部分是日志内容,由于在创建Topic时准备将20条日志分成4个区存储,可以很清楚的看到,这20条日志确实是很均匀的存储在了几个partion上。 摘一点Infoq上的话:每个日志文件都是一个log entrie序列,每个log entrie包含一个4字节整型数值(值为N+5),1个字节的"magic value",4个字节的CRC校验码,其后跟N个字节的消息体。每条消息都有一个当前Partition下唯一的64字节的offset,它指明了这条消息的起始位置。磁盘上存储的消息格式如下: ? 1 2 3 4 message length : 4 bytes (value: 1+4+n) "magic" value : 1 byte crc : 4 bytes payload : n bytes 这里我们看到的日志文件的每一行,就是一个log entrie,每一行前面无法显示的字符(蓝色选中部分),就是(message length + magic value + crc)了。而log entrie的后部分,则是消息体的内容了。 问题: 1. 如果要使用此种方式,有一种场景是提取某天或者某小时的日志,那么如何设计Topic呢?是不是要在Topic上带入日期或者小时数?还有更好的设计方案吗? 2. 假设按每小时设计Topic,那么如何在使用诸如logger.info()这样的方法时,自动根据时间去改变Topic呢?有类似的例子吗? ----欢迎交流,共同进步。
应用场景 随着项目的逐渐扩大,日志的增加也变得更快。Log4j是常用的日志记录工具,在有些时候,我们可能需要将Log4j的日志发送到专门用于记录日志的远程服务器,特别是对于稍微大一点的应用。这么做的优点有: 可以集中管理日志:可以把多台服务器上的日志都发送到一台日志服务器上,方便管理、查看和分析 可以减轻服务器的开销:日志不在服务器上了,因此服务器有更多可用的磁盘空间 可以提高服务器的性能:通过异步方式,记录日志时服务器只负责发送消息,不关心日志记录的时间和位置,服务器甚至不关心日志到底有没有记录成功 远程打印日志的原理:项目A需要打印日志,而A调用Log4j来打印日志,Log4j的JMSAppender又给配置的地址(ActiveMQ地址)发送一条JMS消息,此时绑定在Queue上的项目B的监听器发现有消息到来,于是立即唤醒监听器的方法开始输出日志。 本文将使用两个Java项目Product和Logging,其中Product项目就是模拟线上的项目,而Logging项目模拟运行在专用的日志服务器上的项目。说明:本文的例子是在Windows平台下。 安装ActiveMQ 1. 下载:http://activemq.apache.org/download.html 2. 解压后不需要任何配置,进入到bin下对应的系统架构文件夹 3. 双击activemq.bat启动,如果看到类似下面的页面,就代表activemq启动好了: 然后打开浏览器,输入地址:http://localhost:8161进入管理页面,用户名admin,密码admin: 可以点击Manage ActiveMQ broker进入Queue的查看界面。 实战 我用Maven来管理项目,方便维护各种依赖的jar包。先看下项目结构: 项目不复杂,主要是4个文件:pom.xml,Main.java,log4j.properties和jndi.properties pom.xml中主要是声明项目的依赖包,其余没有什么东西了: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <!-- Use to call write log methods --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- Log4j uses this lib --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.13</version> </dependency> <!-- Spring jms lib --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jms</artifactId> <version>4.0.0.RELEASE</version> </dependency> <!-- ActiveMQ lib --> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-core</artifactId> <version>5.7.0</version> </dependency> Main.java: ? 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 package com.demo.product; import javax.jms.Connection; import javax.jms.Destination; import javax.jms.Message; import javax.jms.MessageConsumer; import javax.jms.MessageListener; import javax.jms.Session; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.command.ActiveMQObjectMessage; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggingEvent; public class Main implements MessageListener { public Main() throws Exception { // create consumer and listen queue ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616"); Connection connection = factory.createConnection(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); connection.start(); //////////////注意这里JMSAppender只支持TopicDestination,下面会说到//////////////// Destination topicDestination = session.createTopic("logTopic"); MessageConsumer consumer = session.createConsumer(topicDestination); consumer.setMessageListener(this); // log a message Logger logger = Logger.getLogger(Main.class); logger.info("Info Log."); logger.warn("Warn Log"); logger.error("Error Log."); // clean up Thread.sleep(1000); consumer.close(); session.close(); connection.close(); System.exit(1); } public static void main(String[] args) throws Exception { new Main(); } public void onMessage(Message message) { try { // receive log event in your consumer LoggingEvent event = (LoggingEvent)((ActiveMQObjectMessage)message).getObject(); System.out.println("Received log [" + event.getLevel() + "]: "+ event.getMessage()); } catch (Exception e) { e.printStackTrace(); } } } 说明:然后是log4j.properties: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 log4j.rootLogger=INFO, stdout, jms ## Be sure that ActiveMQ messages are not logged to 'jms' appender log4j.logger.org.apache.activemq=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %-5p %c - %m%n ## Configure 'jms' appender. You'll also need jndi.properties file in order to make it work log4j.appender.jms=org.apache.log4j.net.JMSAppender log4j.appender.jms.InitialContextFactoryName=org.apache.activemq.jndi.ActiveMQInitialContextFactory log4j.appender.jms.ProviderURL=tcp://localhost:61616 log4j.appender.jms.TopicBindingName=logTopic log4j.appender.jms.TopicConnectionFactoryBindingName=ConnectionFactory 其实按理说只需要这么三个文件就可以了,但是这时候执行会报错: ? 1 2 3 4 5 6 7 8 9 10 11 12 javax.naming.NameNotFoundException: logTopic at org.apache.activemq.jndi.ReadOnlyContext.lookup(ReadOnlyContext.java:235) at javax.naming.InitialContext.lookup(Unknown Source) at org.apache.log4j.net.JMSAppender.lookup(JMSAppender.java:245) at org.apache.log4j.net.JMSAppender.activateOptions(JMSAppender.java:222) at org.apache.log4j.config.PropertySetter.activate(PropertySetter.java:307) ... at org.apache.activemq.ActiveMQPrefetchPolicy.<clinit>(ActiveMQPrefetchPolicy.java:39) at org.apache.activemq.ActiveMQConnectionFactory.<init>(ActiveMQConnectionFactory.java:84) at org.apache.activemq.ActiveMQConnectionFactory.<init>(ActiveMQConnectionFactory.java:137) at com.demo.product.Main.<init>(Main.java:20) at com.demo.product.Main.main(Main.java:43) 为什么会报错呢?来看看JMSAppender的javadoc文档,它是这么描述的: 大意是说,JMSAppender需要一个jndi配置来初始化一个JNDI上下文(Context)。因为有了这个上下文才能管理JMS Topic和topic的连接。于是为项目配置一个叫jndi.properties的文件,其内容为: ? 1 topic.logTopic=logTopic 然后再运行就不会报错了。我们先来看看ActiveMQ(注意切换到Topic标签页下): 可以看到,主题为logTopic的消息,有3条进Queue,这3条也出Queue了。而出Queue的消息,已经被我们的监听器收到并打印出来了: Spring整合 需要注意的是,本例只是一个很简单的例子,目的是阐明远程打印日志的原理。实际项目中,一般日志服务器上运行着的,不是项目,而是专用的日志记录器。下面,我们就把这个项目拆分成两个项目,并用Spring来管理这些用到的Bean 修改Product项目 修改后的Product的项目结构并没有改变,改变的只是Main类: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.demo.product; import org.apache.log4j.Logger; public class Main{ private static final Logger logger = Logger.getLogger(Main.class); public static void main(String[] args) throws Exception { // just log a message logger.info("Info Log."); logger.warn("Warn Log"); logger.error("Error Log."); System.exit(0); } } 这个Main类和普通的logger调用一样,仅仅负责打印日志。有没有觉得太简单了呢? Logging项目 来看看项目结构图: 为了让监听器一直活着,我把Logging写成了一个Web项目,跑在Tomcat上。index.jsp就是个Hello World字符串而已,用来验证Logging活着。注意,在Logging项目中,已没有Product项目中的log4j.properties和jndi.properties两个文件。 来看看另外几个文件: pom.xml(每个包的目的都写在注释里了): ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <!-- Use to cast object to LogEvent when received a log --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- Use to receive jms message --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jms</artifactId> <version>4.0.0.RELEASE</version> </dependency> <!-- Use to load spring.xml --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.0.0.RELEASE</version> </dependency> <!-- ActiveMQ lib --> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-core</artifactId> <version>5.7.0</version> </dependency> web.xml ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring.xml</param-value> </context-param> <!-- Use to load spring.xml --> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app> spring.xml ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <property name="connectionFactory" ref="connectionFactory"/> </bean> <bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory"> <property name="targetConnectionFactory" ref="targetConnectionFactory"/> </bean> <bean id="targetConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://localhost:61616"/> </bean> <!-- As JMSAppender only support the topic way to send messages, thus queueDestination here is useless. <bean id="queueDestination" class="org.apache.activemq.command.ActiveMQQueue"> <constructor-arg name="name" value="queue" /> </bean> --> <bean id="topicDestination" class="org.apache.activemq.command.ActiveMQTopic"> <constructor-arg name="name" value="logTopic" /> </bean> <bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <property name="connectionFactory" ref="connectionFactory" /> <!-- <property name="destination" ref="queueDestination" /> --> <property name="destination" ref="topicDestination" /> <property name="messageListener" ref="logMessageListener" /> </bean> <bean id="logMessageListener" class="com.demo.logging.LogMessageListener"/> </beans> logMessageListener指向我们自己实现的日志消息处理逻辑类,topicDestination则关注topic为“logTopic”的消息,而jmsContainer把这两个对象绑在一起,这样就能接收并处理消息了。 最后就是伟大的监听器了LogMessageListener了: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.demo.logging; import javax.jms.Message; import javax.jms.MessageListener; import org.apache.activemq.command.ActiveMQObjectMessage; import org.apache.log4j.spi.LoggingEvent; public class LogMessageListener implements MessageListener { public void onMessage(Message message) { try { // receive log event in your consumer LoggingEvent event = (LoggingEvent)((ActiveMQObjectMessage)message).getObject(); System.out.println("Logging project: [" + event.getLevel() + "]: "+ event.getMessage()); } catch (Exception e) { e.printStackTrace(); } } } 哈哈,说伟大,其实太简单了。但是可以看到,监听器里面就是之前Product项目中Main类里面移除的实现了MessageListener接口中的代码。 测试 在执行测试前,删掉ActiveMQ中所有的Queue,确保测试效果。 先运行Logging项目,开始Queue的监听。再运行Product的Main类的main函数,可以先看到Main类打印到控制台的日志: 接下来去看看Queue中的情况: 可以看到有个叫logTopic的主题的消息,进了3条,出了3条。不用想,出Queue的3条日志已经被Logging项目的Listener接收并打印出来了,现在去看看Tomcat的控制台: 还要注意Queue中的logTopic的Consumer数量为1而不是0,这与开始的截图不同。我们都知道这个Consumer是Logging项目中的LogMessageListener对象,它一直活着,是因为Tomcat一直活着;之前的Consumer数量为0,是因为在main函数执行完后,Queue的监听器(也是写日志的对象)就退出了。 通过把Product和Logging项目分别放在不同的机器上执行,在第三台机器上部署ActiveMQ(当然你可以把ActiveMQ搭建在任意可以访问的地方),再配置一下Product项目的log4j.properties文件和Logging项目的spring.xml文件就能用于生产环境啦。 JMSAppender类的分析 JMSAppender类将LoggingEvent实例序列化成ObjectMessage,并将其发送到JMS Server的一个指定Topic中,因此,使用此种将日志发送到远程的方式只支持Topic方式发送,不支持Queue方式发送。我们再log4j.properties中配置了这一句: ? 1 log4j.appender.jms=org.apache.log4j.net.JMSAppender 这一句指定了使用的Appender,打开这个Appender,在里面可以看到很多setter,比如: 这些setter不是巧合,而正是对应了我们在log4j.properties中设置的其他几个选项: ? 1 2 3 4 log4j.appender.jms.InitialContextFactoryName=org.apache.activemq.jndi.ActiveMQInitialContextFactory log4j.appender.jms.ProviderURL=tcp://localhost:61616 log4j.appender.jms.TopicBindingName=logTopic log4j.appender.jms.TopicConnectionFactoryBindingName=ConnectionFactory 来看看JMSAppender的activeOptions方法,这个方法是用于使我们在log4j.properties中的配置生效的: ? 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 /** * Options are activated and become effective only after calling this method. */ public void activateOptions() { TopicConnectionFactory topicConnectionFactory; try { Context jndi; LogLog.debug("Getting initial context."); if (initialContextFactoryName != null) { Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryName); if (providerURL != null) { env.put(Context.PROVIDER_URL, providerURL); } else { LogLog.warn("You have set InitialContextFactoryName option but not the " + "ProviderURL. This is likely to cause problems."); } if (urlPkgPrefixes != null) { env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes); } if (securityPrincipalName != null) { env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName); if (securityCredentials != null) { env.put(Context.SECURITY_CREDENTIALS, securityCredentials); } else { LogLog.warn("You have set SecurityPrincipalName option but not the " + "SecurityCredentials. This is likely to cause problems."); } } jndi = new InitialContext(env); } else { jndi = new InitialContext(); } LogLog.debug("Looking up [" + tcfBindingName + "]"); topicConnectionFactory = (TopicConnectionFactory) lookup(jndi, tcfBindingName); LogLog.debug("About to create TopicConnection."); ///////////////////////////////注意这里只会创建TopicConnection//////////////////////////// if (userName != null) { topicConnection = topicConnectionFactory.createTopicConnection(userName, password); } else { topicConnection = topicConnectionFactory.createTopicConnection(); } LogLog.debug("Creating TopicSession, non-transactional, " + "in AUTO_ACKNOWLEDGE mode."); topicSession = topicConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE); LogLog.debug("Looking up topic name [" + topicBindingName + "]."); Topic topic = (Topic) lookup(jndi, topicBindingName); LogLog.debug("Creating TopicPublisher."); topicPublisher = topicSession.createPublisher(topic); LogLog.debug("Starting TopicConnection."); topicConnection.start(); jndi.close(); } catch (JMSException e) { errorHandler.error("Error while activating options for appender named [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } catch (NamingException e) { errorHandler.error("Error while activating options for appender named [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } catch (RuntimeException e) { errorHandler.error("Error while activating options for appender named [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } } 上面初始化了一个TopicConnection,一个TopicSession,一个TopicPublisher。咱们再来看看这个Appender的append方法: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /** * This method called by {@link AppenderSkeleton#doAppend} method to do most * of the real appending work. */ public void append(LoggingEvent event) { if (!checkEntryConditions()) { return; } try { ObjectMessage msg = topicSession.createObjectMessage(); if (locationInfo) { event.getLocationInformation(); } msg.setObject(event); topicPublisher.publish(msg);///////////////注意这一句////////////// } catch (JMSException e) { errorHandler.error("Could not publish message in JMSAppender [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } catch (RuntimeException e) { errorHandler.error("Could not publish message in JMSAppender [" + name + "].", e, ErrorCode.GENERIC_FAILURE); } } 这里使用TopicPublisher.publish()方法,把序列化的消息发布出去。可见这也证明了JMSAppender只支持以Topic方式发送消息。
本文介绍在同一个tomcat下运行mydomain.com和mysite.com这两个实例的步骤。 有时候,我们希望周期性的更新Tomcat;有时候,我们又想统一管理安装在一台机器上的tomcat(比如让tomcat版本统一,让多个实例的tomcat的依赖统一、配置统一等)。在这些场景下,我们都不希望把Web应用程序的文件放入Tomcat发行版的目录结构中,而是让一个tomcat运行多个实例,并把Web应用放在tomcat的安装目录之外。 一般在使用Tomcat时,服务器会从conf及webapps目录中读取配置文件,并将文件写入logs、temp和work目录,当然一些jar文件和class文件需要从服务器的公共目录树中予以加载。因此,为了让多个实例能同时运行,每一个Tomcat实例都必须有自己的目录集。 首先,下载tomcat安装包,并解压,这里我使用的tomcat版本是tomcat-8.0.33: 然后,创建一个文件夹tomcat-instance(该文件夹用于存放tomcat所有实例),并在这个文件夹下分别创建mydomain.com和mysite.com两个实例文件夹: ? 1 2 3 4 mkdir tomcat-instance cd tomcat-instance mkdir mydomain.com mkdir mysite.com 对于mydomain.com,依次做以下步骤: 1. 拷贝Tomcat安装目录的conf文件夹下的所有内容,到mydomain.com文件夹下: ? 1 2 3 cd mydomain.com cp -a /home/user/Software/apache-tomcat-8.0.33/conf . mkdir common logs temp server shared webapps work 2.修改mydomain.com/conf/server.xml,将停止端口号修改为不同的端口号: 3.修改Connector的端口号: 4.删除server.xml中所有的Context元素(因为这份server.xml来自于tomcat的安装目录,如果曾经用该tomcat部署过项目,server.xml中就会有Context元素,由于现在没有将这些项目复制到mydomain.com实例的webapps文件夹下)及嵌套的所有元素,并加入与自己的webapps相关的内容。 5.为了简化变量设置步骤,创建tomcat启动脚本start-mydomain.sh,并将该文件放在tomcat-instance目录下,该文件的内容如下: 6.修改脚本的权限,使其可执行: 7.用脚本启动tomcat实例: 可以看到,这个实例使用的CATALINA_BASE是instance/mydomain.com,这里的CATALINA_HOME是安装tomcat的目录。 8.拷贝示例文件到mydomain.com/webapps目录,从浏览器验证启动的tomcat实例: 到这里,tomcat实例mydomain.com已经正常运行了。 另一个实例mysite.com也按照1~8的步骤依次进行,但是注意以下步骤的不同配置: 2.Server端口号修改为8013。 3.Connector端口号修改为8082。 5.脚本中的CATALINA_BASE修改为/home/user/Software/tomcat-instance/mysite.com。 7.用脚本启动mysite.com实例: 可以看到,这个实例使用的CATALINA_BASE是instance/mysite.com。而这里的CATALINA_HOME依然是安装tomcat的目录,这和mydomain.com实例的配置是一样的,说明二者确实共用了一个安装目录。 8.拷贝示例文件到mysite.com/webapps目录,从浏览器验证启动的tomcat实例: 至此,tomcat的多实例已能正常运行,当然,也可以为这些实例创建停止tomcat的脚本。 当把Web应用的文件和Tomcat发行版的文件分开管理后,升级Tomcat将会变得十分容易,因为我们可以用新目录直接替换整个Tomcat发行版的目录。
在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能,本节将通过若干实例来验证异常发生的场景。并且会初步介绍几个与内存相关的最基本的虚拟机参数。 本节内容的目的有两个:第一,通过代码验证Java虚拟机规范中描述的各个运行时区域存储的内容;第二,希望读者在工作中遇到实际的内存溢出异常时,能根据异常的信息快速判断是哪个区的内存溢出,知道什么样的代码可能导致这些区域内存溢出,以及出现这些异常后该如何处理。 下文代码的开头都注释了执行时所需要设置的虚拟机启动参数(注释中“VM args”后面跟着的参数),这些参数对实验的结果有直接影响,在调试代码的时候千万不要忽略。如果使用控制台命令来执行程序,那直接跟在Java命令后书写就可以。如果使用的是Eclipse IDE,则可以参考下图在Debug/Run页签中的设置: 下文的代码都是基于Sun公司的HotSpot虚拟机运行的,对于不同公司的不同版本的虚拟机,参数和程序运行的结果可能会有所差别。 Java堆溢出 Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制来清除这些对象,那么对象数量到达最大堆容量限制后就会产生内存溢出异常。 下面代码中,Java堆的大小限制为20M,不可扩展(将堆的最小值-Xms参数与最大值-Xmx最大值参数设置为一样,避免自动扩展)通过参数-XX:+HeapDumpOnOutOfMemoryError,可以让虚拟机在出现内存溢出时Dump出当前的内存转储快照以便事后进行分析。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 /** * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOM { static class OOMObject {} public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } } } 运行结果: Java堆内存的OOM异常时实际应用中常见的内存溢出异常情况。当出现Java对内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heap space”。 要解决这个区域的异常,一般的手段是先通过内存映像工具如(Eclipse MemoryAnalyzer)对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分析到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow)。 如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾回收器无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较容易确定发生泄露的代码位置。 如果不存在内存泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。 虚拟机栈和本地方法栈溢出 由于在Hotspot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于Hotspot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定,关于虚拟机栈和本地方法栈可以出现以下两周异常: 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常 如果虚拟机在扩展时无法申请到足够的内存空间,则抛出OutOfMemoryError异常 下面这个例子,将实验范围限制于单线程中的操作,尝试了下面两种方法均无法让需积极产生OutOfMemoryError异常,尝试的结果都是获得StackOverflowError异常: 使用-Xss 参数减少栈内存容量,结果:抛出StackOverflowError异常,异常出现时输出的栈的深度相应缩小 定义了大量的本地变量,增大此方法帧中本地变量表的长度。结果:抛出StackOverflowError异常时输出的堆栈深度相应减小。 测试代码如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /** * VM Args: -Xss128K */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } } 运行结果: 实验结果表明:在单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配的时候虚拟机都抛出的是StackOverflowError。 如果测试不限于单线程,通过不断的建立线程的方式倒是可以产生内除溢出异常,但是这样产生的内存溢出与栈空间是否够大不存在任何联系,或者说,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。 原因是,操作系统分配给每个线程的内存是有限的,譬如32位Windows限制为2GB。虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2G(操作系统内存)减去Xmx(堆最大容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗的内存很小,可以忽略。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到栈容量越大,可以建立的线程数自然越少,建立线程时越容易把剩余的内存耗尽。 这一点需要在开发多线程的应用时特别注意,出现StackOverflowError异常时有错误堆栈可以阅读,相对来说,比较容易找到问题的所在。而且,如果使用虚拟机默认参数,栈深度在大多数情况下(因为每个方法压入栈的帧大小并不是一样的,所以只能说在大多数情况下)达到1000~2000完全没有问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程了。如果没有这方面的处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。 以下代码通过创建多线程导致内存溢出异常: ? 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 /** * VM Args: -Xss2M(这时候不妨设置大些) */ public class JavaVMStackOOM { private void dontStop() { while(true) {} } public void stackLeakByThread() { while(true) { Thread thread = new Thread(new Runnable() { public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } } 注意:特别提示一下,如果要尝试运行上面这段代码,记得要先保存当前的工作。由于在Windows平台的虚拟机中,Java的线程时映射到操作系统的内核线程上的,因此上述代码执行时有较大风险,可能会导致操作系统假死。 运行结果: 方法区和运行时常量池溢出 由于运行时常量池是方法去的一部分,因此这两个区域的溢出测试可以放在一起进行。 String.intern()方法是一个native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的string对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。 在JDK1.6及之前的版本中,由于常量池分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量,代码如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.ArrayList; import java.util.List; /** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */ public class RuntimeConstantPoolOOM{ public static void main(String[] args){ // 使用List保持着常量池引用,避免Full GC 回收常量池行为 List<String> list = new ArrayList<String>(); //10MB的PermSize在int范围内足够产生OOM了 int i = 0; while(true) { //调用intern方法,将字符串全部放在常量池中 list.add(String.valueOf(i++).intern()); } } } 运行结果(JDK1.6 HotSpot JVM): 从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。 方法区用于存放Class相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。这里借助CGLib直接操作字节码运行时生成了大量的动态类。 值得注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应用经常会出现在实际应用中:当前很多主流框架如Spring、Hibernate,在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以载入内存。另外,JVM上的动态语言(例如Groovy等)通常都会持续创建类来实现语言的动态性,随着这类语言越来越流行,也越来越容易遇到以下代码类似的溢出场景: ? 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 import java.lang.reflect.Method; import com.jvm.oom.HeapOOM.OOMObject; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; /** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M */ public class JavaMethodAreaOOM{ public static void main(String[] args) { while(true){ Enhancer e = new Enhancer(); e.setSuperclass(OOMObject.class); e.setUseCache(false); e.setCallback(new MethodInterceptor(){ public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable{ return proxy.invokeSuper(obj,args); } }); e.create(); } } } 运行结果: ? 1 Caused by: java.lang.OutOfMemoryError: PermGen space 方法区的溢出是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判断条件是比较苛刻的。在经常动态生成大量Class应用中,需要特别注意类的回收情况。这类除了上面提到的程序使用了CGLib字节码增强和动态语言之外,常见的还有:还有大量jsp或动态产生jsp文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。 本机直接内存溢出 DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样,下面的代码越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回示例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.lang.reflect.Field; import sun.misc.Unsafe; /** * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M */ public class DirectMemoryOOM{ private static final int _1MB = 1024*1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe)unsafeField.get(null); while(true){ unsafe.allocateMemory(_1MB); } } } 运行结果: 由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后文件很小,而程序中有直接或简介使用了NIO,那就可以考虑一下是不是这方面的原因。
keytool是Java的数字证书管理工具,用于数字证书的生成,导入,导出与撤销等操作。它与本地密钥库关联,并可以对本地密钥库进行管理,可以将私钥存放于密钥库中,而公钥使用数字证书进行输出。keytool在jdk安装目录的bin文件夹下: 构建自签名证书 在构建CSR之前,需要先在密钥库中生成本地数字证书,此时需要提供用户的身份、加密算法、有效期等一些数字证书的基本信息: ? 1 2 keytool -genkeypair -keyalg RSA -keysize 1024 -sigalg MD5withRSA \ -validity 365 -alias www.mydomain.com -keystore ~/my.keystore 参数介绍: -genkeypair——产生密钥对 -keyalg——指定加密算法 -keysize——指定密钥长度 -sigalg——指定签名算法 -validity——证书有效期 -alias——证书别名 -keystore——指定密钥库的位置 执行结果: 证书导出 执行了上面的命令后,我们已经生成了一个本地数字证书,虽然还没有经过证书认证机构进行认证,但并不影响使用,我们可以使用相应的命令对证书进行导出。 ? 1 2 keytool -exportcert -alias \ -keystore ~/my.keystore -file /tmp/my.cer -rfc 参数介绍: -exportcert——执行证书导出 -alias——密钥库中的证书别名 -keystore——指定密钥库文件 -file——导出的文件输出路径 -rfc——使用Base64格式输出 执行结果: 证书导出后,可以使用打印证书命令查看证书内容: ? 1 keytool -printcert -file /tmp/my.cer 参数介绍: -printcert——执行证书打印命令 -file——指定证书文件路径 执行结果: 导出CSR文件 如果想得到证书认证机构的认证,需要导出数字证书并签发申请(Cerificate Signing Request),经证书认证机构认证并颁发后,再将认证后的证书导入本地密钥库与信任库。 导出CSR文件命令: ? 1 2 keytool -certreq -alias \ -keystore ~/my.keystore -file /tmp/my.csr -v 参数介绍: -certreq——执行证书签发申请导出操作 -alias——证书的别名 -keystore——使用的密钥库文件 -file——输出的csr文件路径 -v——显示详细情况 执行结果: 导出CSR文件后,便可以到VeriSign、GeoTrust等权威机构进行证书认证了。 证书导入 获得证书认证机构办法的数字证书后,需要将其导入信任库。 导入数字证书的命令: ? 1 2 keytool -importcert -trustcacerts -alias www.mydomain.com \ -file /tmp/my.cer -keystore ~/my.keystore 参数介绍: -importcert——执行导入证书操作 -trustcacerts——将证书导入信任库 -alias——证书别名 -file——要导入的证书文件的路径 -keystore——指定密钥库文件 导入证书成功后,可以通过list命令来查看密钥库中的证书: ? 1 keytool -list -alias www.mydomain.com -keystore ~/my.keystore 执行结果如下:
系统:CentOS7 32位 目标:使用OpenSSL生成一个CA根证书,并用这个根证书颁发两个子证书server和client。 先确保系统中安装了OpenSSL,若没安装,可以通过以下命令安装: ? 1 sudo yum install openssl 修改OpenSSL的配置 安装好之后,定位一下OpenSSL的配置文件openssl.cnf: ? 1 locate openssl.cnf 如图,我这里的目录是/etc/pki/tls/openssl.cnf。 修改配置文件,修改其中的dir变量,重新设置SSL的工作目录: 由于配置文件中,dir变量下还有几个子文件夹需要用到,因此在自定义的文件夹下面也创建这几个文件夹或文件,它们是: certs——存放已颁发的证书 newcerts——存放CA指令生成的新证书 private——存放私钥 crl——存放已吊销的整数 index.txt——OpenSSL定义的已签发证书的文本数据库文件,这个文件通常在初始化的时候是空的 serial——证书签发时使用的序列号参考文件,该文件的序列号是以16进制格式进行存放的,该文件必须提供并且包含一个有效的序列号 生成证书之前,需要先生成一个随机数: ? 1 openssl rand -out private/.rand 1000 该命令含义如下: rand——生成随机数 -out——指定输出文件 1000——指定随机数长度 生成根证书 a).生成根证书私钥(pem文件) OpenSSL通常使用PEM(Privacy Enbanced Mail)格式来保存私钥,构建私钥的命令如下: ? 1 openssl genrsa -aes256 -out private/cakey.pem 1024 该命含义如下: genrsa——使用RSA算法产生私钥 -aes256——使用256位密钥的AES算法对私钥进行加密 -out——输出文件的路径 1024——指定私钥长度 b).生成根证书签发申请文件(csr文件) 使用上一步生成的私钥(pem文件),生成证书请求文件(csr文件): ? 1 2 openssl req -new -key private/cakey.pem -out private/ca.csr -subj \ "/C=CN/ST=myprovince/L=mycity/O=myorganization/OU=mygroup/CN=myname" 该命令含义如下: req——执行证书签发命令 -new——新证书签发请求 -key——指定私钥路径 -out——输出的csr文件的路径 -subj——证书相关的用户信息(subject的缩写) c).自签发根证书(cer文件) csr文件生成以后,可以将其发送给CA认证机构进行签发,当然,这里我们使用OpenSSL对该证书进行自签发: ? 1 2 openssl x509 -req -days 365 -sha1 -extensions v3_ca -signkey \ private/cakey.pem -in private/ca.csr -out certs/ca.cer 该命令的含义如下: x509——生成x509格式证书 -req——输入csr文件 -days——证书的有效期(天) -sha1——证书摘要采用sha1算法 -extensions——按照openssl.cnf文件中配置的v3_ca项添加扩展 -signkey——签发证书的私钥 -in——要输入的csr文件 -out——输出的cer证书文件 之后看一下certs文件夹里生成的ca.cer证书文件: 用根证书签发server端证书 和生成根证书的步骤类似,这里就不再介绍相同的参数了。 a).生成服务端私钥 ? 1 openssl genrsa -aes256 -out private/server-key.pem 1024 b).生成证书请求文件 ? 1 2 openssl req -new -key private/server-key.pem -out private/server.csr -subj \ "/C=CN/ST=myprovince/L=mycity/O=myorganization/OU=mygroup/CN=myname" c).使用根证书签发服务端证书 ? 1 2 openssl x509 -req -days 365 -sha1 -extensions v3_req -CA certs/ca.cer -CAkey private/cakey.pem \ -CAserial ca.srl -CAcreateserial -in private/server.csr -out certs/server.cer 这里有必要解释一下这几个参数: -CA——指定CA证书的路径 -CAkey——指定CA证书的私钥路径 -CAserial——指定证书序列号文件的路径 -CAcreateserial——表示创建证书序列号文件(即上方提到的serial文件),创建的序列号文件默认名称为-CA,指定的证书名称后加上.srl后缀 注意:这里指定的-extensions的值为v3_req,在OpenSSL的配置中,v3_req配置的basicConstraints的值为CA:FALSE,如图: 而前面生成根证书时,使用的-extensions值为v3_ca,v3_ca中指定的basicConstraints的值为CA:TRUE,表示该证书是颁发给CA机构的证书,如图: 在x509指令中,有多重方式可以指定一个将要生成证书的序列号,可以使用set_serial选项来直接指定证书的序列号,也可以使用-CAserial选项来指定一个包含序列号的文件。所谓的序列号是一个包含一个十六进制正整数的文件,在默认情况下,该文件的名称为输入的证书名称加上.srl后缀,比如输入的证书文件为ca.cer,那么指令会试图从ca.srl文件中获取序列号,可以自己创建一个ca.srl文件,也可以通过-CAcreateserial选项来生成一个序列号文件。 用根证书签发client端证书 和签发server端的证书的过程类似,只是稍微改下参数而已。 a).生成客户端私钥 ? 1 openssl genrsa -aes256 -out private/client-key.pem 1024 b).生成证书请求文件 ? 1 2 openssl req -new -key private/client-key.pem -out private/client.csr -subj \ "/C=CN/ST=myprovince/L=mycity/O=myorganization/OU=mygroup/CN=myname" c).使用根证书签发客户端证书 ? 1 2 openssl x509 -req -days 365 -sha1 -extensions v3_req -CA certs/ca.cer -CAkey private/cakey.pem \ -CAserial ca.srl -in private/client.csr -out certs/client.cer 需要注意的是,上方签发服务端证书时已经使用-CAcreateserial生成过ca.srl文件,因此这里不需要带上这个参数了。 至此,我们已经使用OpenSSL自签发了一个CA证书ca.cer,并用这个CA证书签发了server.cer和client.cer两个子证书了: 导出证书 a).导出客户端证书 ? 1 2 openssl pkcs12 -export -clcerts -name myclient -inkey \ private/client-key.pem -in certs/client.cer -out certs/client.keystore 参数含义如下: pkcs12——用来处理pkcs#12格式的证书 -export——执行的是导出操作 -clcerts——导出的是客户端证书,-cacerts则表示导出的是ca证书 -name——导出的证书别名 -inkey——证书的私钥路径 -in——要导出的证书的路径 -out——输出的密钥库文件的路径 b).导出服务端证书 ? 1 2 openssl pkcs12 -export -clcerts -name myserver -inkey \ private/server-key.pem -in certs/server.cer -out certs/server.keystore c).信任证书的导出 ? 1 2 keytool -importcert -trustcacerts -alias www.mydomain.com \ -file certs/ca.cer -keystore certs/ca-trust.keystore
启动命令 ? 1 2 /usr/local/bin/memcached -d -m 10 -u root -l 192.168.56.101 \ -p 11211 -c 32 -P /tmp/memcached.pid 基本选项 -p 端口 监听tcp端口 -d 以守护进程方式运行memcached -u username 以username运行 -m <num> 最大的内存使用,单位是MB ,缺省是64MB -c <num> 软连接数量,缺省是1024 -v 输出警告和错误信息 -vv 打印客户端的请求和返回信息 检查memcached是否正常运行 ? 1 >ps aux | grep memcached #telnet localhost 11211 .... stats ... 会显示memcached的基本信息 启动报错 如果启动时出现“memcached: error while loading shared libraries:libevent-2.0.so.5: cannot open shared object file: No such file or directory”之类的信息,表示memcached 找不到libevent 的位置。所以,请先使用whereis libevent 得到位置,然后连接到memcached 所寻找的路径。 首先查看libevent 在哪里 ? 1 2 >whereis libevent libevent: /usr/local/lib/libevent.la /usr/local/lib/libevent.so /usr/local/lib/libevent.a 然后,再看memcached 从哪里找它 ? 1 >LD_DEBUG=libs memcached -v 2>&1 > /dev/null | less 可以看到:是/usr/lib/libevent-2.0.so.5,所以,创建软链: ? 1 >sudo ln -s /usr/local/lib/libevent-2.0.so.5 /usr/lib/libevent-2.0.so.5 再次启动,问题解决。 原文地址:http://blog.csdn.net/keda8997110/article/details/8767606
使用javamail发送邮件时,老是提示Network is Network: ? 1 2 3 4 com.sun.mail.util.MailConnectException: Couldn't connect to host, port: smtp.163.com, 25; timeout -1; nested exception is: java.net.SocketException: Network is unreachable: connect at com.sun.mail.smtp.SMTPTransport.openServer(SMTPTransport.java:2053) 于是ping了下,也telnet连了下,都没有问题,使用Outlook客户端配置该smtp地址也没有问题。由于之前使用过同样的代码和同样的配置发送成功过,所以代码应该没有问题的。找了好久,终于在Stackoverflow上找到了办法:为系统设置以下变量: ? 1 java.net.preferIPv4Stack=true 又顺便去官网上找了找这个变量的作用: java.net.preferIPv4Stack (default: false)If IPv6 is available on the operating system the underlying native socket will be, by default, an IPv6 socket which lets applications connect to, and accept connections from, both IPv4 and IPv6 hosts. However, in the case an application would rather use IPv4 only sockets, then this property can be set to true. The implication is that it will not be possible for the application to communicate with IPv6 only hosts. 大意是指:如果系统的IPv6可用的话,底层的Socket连接默认会使用IPv6的,因为它可以同时支持IPv4和IPv6的连接和被连接。如果应用只需要使用IPv4的socket连接,就把这个选项设置为true,这意味着该应用将不能与仅支持IPv6的机器通讯。 再看了看自己的机器上的IP: 果然是IPv6在作祟。 该选项可以通过以下命令在启动java时设置: ? 1 java -Djava.net.preferIPv4Stack=true 也可以通过setProperty API来设置: ? 1 System.setProperty("java.net.preferIPv4Stack", "true"); 如果使用tomcat服务器,则可以给tomcat加上启动参数: ? 1 -Djava.net.preferIPv4Stack=true 如果在eclipse中使用tomcat,可以通过下图的方式配置: 之后的弹出框中切换到Argument标签,然后配置该变量: 之后问题解决。
如果你的Linux服务器突然负载暴增,告警短信快发爆你的手机,如何在最短时间内找出Linux性能问题所在?来看Netflix性能工程团队的这篇博文,看它们通过十条命令在一分钟内对机器性能问题进行诊断。 概述 通过执行以下命令,可以在1分钟内对系统资源使用情况有个大致的了解。 uptime dmesg | tail vmstat 1 mpstat -P ALL 1 pidstat 1 iostat -xz 1 free -m sar -n DEV 1 sar -n TCP,ETCP 1 top 其中一些命令需要安装sysstat包,有一些由procps包提供。这些命令的输出,有助于快速定位性能瓶颈,检查出所有资源(CPU、内存、磁盘IO等)的利用率(utilization)、饱和度(saturation)和错误(error)度量,也就是所谓的USE方法。 下面我们来逐一介绍下这些命令,有关这些命令更多的参数和说明,请参照命令的手册。 uptime ? 1 2 $ uptime 23:51:26 up 21:31, 1 user, load average: 30.02, 26.43, 19.02 这个命令可以快速查看机器的负载情况。在Linux系统中,这些数据表示等待CPU资源的进程和阻塞在不可中断IO进程(进程状态为D)的数量。这些数据可以让我们对系统资源使用有一个宏观的了解。 命令的输出分别表示1分钟、5分钟、15分钟的平均负载情况。通过这三个数据,可以了解服务器负载是在趋于紧张还是区域缓解。如果1分钟平均负载很高,而15分钟平均负载很低,说明服务器正在命令高负载情况,需要进一步排查CPU资源都消耗在了哪里。反之,如果15分钟平均负载很高,1分钟平均负载较低,则有可能是CPU资源紧张时刻已经过去。 上面例子中的输出,可以看见最近1分钟的平均负载非常高,且远高于最近15分钟负载,因此我们需要继续排查当前系统中有什么进程消耗了大量的资源。可以通过下文将会介绍的vmstat、mpstat等命令进一步排查。 dmesg | tail ? 1 2 3 4 5 6 $ dmesg | tail [1880957.563150] perl invoked oom-killer: gfp_mask=0x280da, order=0, oom_score_adj=0 [...] [1880957.563400] Out of memory: Kill process 18694 (perl) score 246 or sacrifice child [1880957.563408] Killed process 18694 (perl) total-vm:1972392kB, anon-rss:1953348kB, file-rss:0kB [2320864.954447] TCP: Possible SYN flooding on port 7001. Dropping request. Check SNMP counters. 该命令会输出系统日志的最后10行。示例中的输出,可以看见一次内核的oom kill和一次TCP丢包。这些日志可以帮助排查性能问题。千万不要忘了这一步。 vmstat 1 ? 1 2 3 4 5 6 7 8 9 $ vmstat 1 procs ---------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 34 0 0 200889792 73708 591828 0 0 0 5 6 10 96 1 3 0 0 32 0 0 200889920 73708 591860 0 0 0 592 13284 4282 98 1 1 0 0 32 0 0 200890112 73708 591860 0 0 0 0 9501 2154 99 1 0 0 0 32 0 0 200889568 73712 591856 0 0 0 48 11900 2459 99 0 0 0 0 32 0 0 200890208 73712 591860 0 0 0 0 15898 4840 98 1 1 0 0 ^C vmstat(8) 命令,每行会输出一些系统核心指标,这些指标可以让我们更详细的了解系统状态。后面跟的参数1,表示每秒输出一次统计信息,表头提示了每一列的含义,这几介绍一些和性能调优相关的列: r:等待在CPU资源的进程数。这个数据比平均负载更加能够体现CPU负载情况,数据中不包含等待IO的进程。如果这个数值大于机器CPU核数,那么机器的CPU资源已经饱和。 free:系统可用内存数(以千字节为单位),如果剩余内存不足,也会导致系统性能问题。下文介绍到的free命令,可以更详细的了解系统内存的使用情况。 si, so:交换区写入和读取的数量。如果这个数据不为0,说明系统已经在使用交换区(swap),机器物理内存已经不足。 us, sy, id, wa, st:这些都代表了CPU时间的消耗,它们分别表示用户时间(user)、系统(内核)时间(sys)、空闲时间(idle)、IO等待时间(wait)和被偷走的时间(stolen,一般被其他虚拟机消耗)。 上述这些CPU时间,可以让我们很快了解CPU是否出于繁忙状态。一般情况下,如果用户时间和系统时间相加非常大,CPU出于忙于执行指令。如果IO等待时间很长,那么系统的瓶颈可能在磁盘IO。 示例命令的输出可以看见,大量CPU时间消耗在用户态,也就是用户应用程序消耗了CPU时间。这不一定是性能问题,需要结合r队列,一起分析。 mpstat -P ALL 1 ? 1 2 3 4 5 6 7 8 9 $ mpstat -P ALL 1 Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) 07:38:49 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle 07:38:50 PM all 98.47 0.00 0.75 0.00 0.00 0.00 0.00 0.00 0.00 0.78 07:38:50 PM 0 96.04 0.00 2.97 0.00 0.00 0.00 0.00 0.00 0.00 0.99 07:38:50 PM 1 97.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 2.00 07:38:50 PM 2 98.00 0.00 1.00 0.00 0.00 0.00 0.00 0.00 0.00 1.00 07:38:50 PM 3 96.97 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 3.03 [...] 该命令可以显示每个CPU的占用情况,如果有一个CPU占用率特别高,那么有可能是一个单线程应用程序引起的。 pidstat 1 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ pidstat 1 Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) 07:41:02 PM UID PID %usr %system %guest %CPU CPU Command 07:41:03 PM 0 9 0.00 0.94 0.00 0.94 1 rcuos/0 07:41:03 PM 0 4214 5.66 5.66 0.00 11.32 15 mesos-slave 07:41:03 PM 0 4354 0.94 0.94 0.00 1.89 8 java 07:41:03 PM 0 6521 1596.23 1.89 0.00 1598.11 27 java 07:41:03 PM 0 6564 1571.70 7.55 0.00 1579.25 28 java 07:41:03 PM 60004 60154 0.94 4.72 0.00 5.66 9 pidstat 07:41:03 PM UID PID %usr %system %guest %CPU CPU Command 07:41:04 PM 0 4214 6.00 2.00 0.00 8.00 15 mesos-slave 07:41:04 PM 0 6521 1590.00 1.00 0.00 1591.00 27 java 07:41:04 PM 0 6564 1573.00 10.00 0.00 1583.00 28 java 07:41:04 PM 108 6718 1.00 0.00 0.00 1.00 0 snmp-pass 07:41:04 PM 60004 60154 1.00 4.00 0.00 5.00 9 pidstat ^C pidstat命令输出进程的CPU占用率,该命令会持续输出,并且不会覆盖之前的数据,可以方便观察系统动态。如上的输出,可以看见两个JAVA进程占用了将近1600%的CPU时间,既消耗了大约16个CPU核心的运算资源。 iostat -xz 1 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 $ iostat -xz 1 Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 73.96 0.00 3.73 0.03 0.06 22.21 Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util xvda 0.00 0.23 0.21 0.18 4.52 2.08 34.37 0.00 9.98 13.80 5.42 2.44 0.09 xvdb 0.01 0.00 1.02 8.94 127.97 598.53 145.79 0.00 0.43 1.78 0.28 0.25 0.25 xvdc 0.01 0.00 1.02 8.86 127.79 595.94 146.50 0.00 0.45 1.82 0.30 0.27 0.26 dm-0 0.00 0.00 0.69 2.32 10.47 31.69 28.01 0.01 3.23 0.71 3.98 0.13 0.04 dm-1 0.00 0.00 0.00 0.94 0.01 3.78 8.00 0.33 345.84 0.04 346.81 0.01 0.00 dm-2 0.00 0.00 0.09 0.07 1.35 0.36 22.50 0.00 2.55 0.23 5.62 1.78 0.03 [...] ^C iostat命令主要用于查看机器磁盘IO情况。该命令输出的列,主要含义是: r/s, w/s, rkB/s, wkB/s:分别表示每秒读写次数和每秒读写数据量(千字节)。读写量过大,可能会引起性能问题。 await:IO操作的平均等待时间,单位是毫秒。这是应用程序在和磁盘交互时,需要消耗的时间,包括IO等待和实际操作的耗时。如果这个数值过大,可能是硬件设备遇到了瓶颈或者出现故障。 avgqu-sz:向设备发出的请求平均数量。如果这个数值大于1,可能是硬件设备已经饱和(部分前端硬件设备支持并行写入)。 %util:设备利用率。这个数值表示设备的繁忙程度,经验值是如果超过60,可能会影响IO性能(可以参照IO操作平均等待时间)。如果到达100%,说明硬件设备已经饱和。 如果显示的是逻辑设备的数据,那么设备利用率不代表后端实际的硬件设备已经饱和。值得注意的是,即使IO性能不理想,也不一定意味这应用程序性能会不好,可以利用诸如预读取、写缓存等策略提升应用性能。 free –m ? 1 2 3 4 5 $ free -m total used free shared buffers cached Mem: 245998 24545 221453 83 59 541 -/+ buffers/cache: 23944 222053 Swap: 0 0 0 free命令可以查看系统内存的使用情况,-m参数表示按照兆字节展示。最后两列分别表示用于IO缓存的内存数,和用于文件系统页缓存的内存数。需要注意的是,第二行-/+ buffers/cache,看上去缓存占用了大量内存空间。这是Linux系统的内存使用策略,尽可能的利用内存,如果应用程序需要内存,这部分内存会立即被回收并分配给应用程序。因此,这部分内存一般也被当成是可用内存。 如果可用内存非常少,系统可能会动用交换区(如果配置了的话),这样会增加IO开销(可以在iostat命令中提现),降低系统性能。 sar -n DEV 1 ? 1 2 3 4 5 6 7 8 9 10 11 $ sar -n DEV 1 Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) 12:16:48 AM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil 12:16:49 AM eth0 18763.00 5032.00 20686.42 478.30 0.00 0.00 0.00 0.00 12:16:49 AM lo 14.00 14.00 1.36 1.36 0.00 0.00 0.00 0.00 12:16:49 AM docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 12:16:49 AM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil 12:16:50 AM eth0 19763.00 5101.00 21999.10 482.56 0.00 0.00 0.00 0.00 12:16:50 AM lo 20.00 20.00 3.25 3.25 0.00 0.00 0.00 0.00 12:16:50 AM docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 ^C sar命令在这里可以查看网络设备的吞吐率。在排查性能问题时,可以通过网络设备的吞吐量,判断网络设备是否已经饱和。如示例输出中,eth0网卡设备,吞吐率大概在22 Mbytes/s,既176 Mbits/sec,没有达到1Gbit/sec的硬件上限。 sar -n TCP,ETCP 1 ? 1 2 3 4 5 6 7 8 9 10 11 $ sar -n TCP,ETCP 1 Linux 3.13.0-49-generic (titanclusters-xxxxx) 07/14/2015 _x86_64_ (32 CPU) 12:17:19 AM active/s passive/s iseg/s oseg/s 12:17:20 AM 1.00 0.00 10233.00 18846.00 12:17:19 AM atmptf/s estres/s retrans/s isegerr/s orsts/s 12:17:20 AM 0.00 0.00 0.00 0.00 0.00 12:17:20 AM active/s passive/s iseg/s oseg/s 12:17:21 AM 1.00 0.00 8359.00 6039.00 12:17:20 AM atmptf/s estres/s retrans/s isegerr/s orsts/s 12:17:21 AM 0.00 0.00 0.00 0.00 0.00 ^C sar命令在这里用于查看TCP连接状态,其中包括: active/s:每秒本地发起的TCP连接数,既通过connect调用创建的TCP连接; passive/s:每秒远程发起的TCP连接数,即通过accept调用创建的TCP连接; retrans/s:每秒TCP重传数量; TCP连接数可以用来判断性能问题是否由于建立了过多的连接,进一步可以判断是主动发起的连接,还是被动接受的连接。TCP重传可能是因为网络环境恶劣,或者服务器压力过大导致丢包。 top ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ top top - 00:15:40 up 21:56, 1 user, load average: 31.09, 29.87, 29.92 Tasks: 871 total, 1 running, 868 sleeping, 0 stopped, 2 zombie %Cpu(s): 96.8 us, 0.4 sy, 0.0 ni, 2.7 id, 0.1 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem: 25190241+total, 24921688 used, 22698073+free, 60448 buffers KiB Swap: 0 total, 0 used, 0 free. 554208 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 20248 root 20 0 0.227t 0.012t 18748 S 3090 5.2 29812:58 java 4213 root 20 0 2722544 64640 44232 S 23.5 0.0 233:35.37 mesos-slave 66128 titancl+ 20 0 24344 2332 1172 R 1.0 0.0 0:00.07 top 5235 root 20 0 38.227g 547004 49996 S 0.7 0.2 2:02.74 java 4299 root 20 0 20.015g 2.682g 16836 S 0.3 1.1 33:14.42 java 1 root 20 0 33620 2920 1496 S 0.0 0.0 0:03.82 init 2 root 20 0 0 0 0 S 0.0 0.0 0:00.02 kthreadd 3 root 20 0 0 0 0 S 0.0 0.0 0:05.35 ksoftirqd/0 5 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H 6 root 20 0 0 0 0 S 0.0 0.0 0:06.94 kworker/u256:0 8 root 20 0 0 0 0 S 0.0 0.0 2:38.05 rcu_sched t top命令包含了前面好几个命令的检查的内容。比如系统负载情况(uptime)、系统内存使用情况(free)、系统CPU使用情况(vmstat)等。因此通过这个命令,可以相对全面的查看系统负载的来源。同时,top命令支持排序,可以按照不同的列排序,方便查找出诸如内存占用最多的进程、CPU占用率最高的进程等。 但是,top命令相对于前面一些命令,输出是一个瞬间值,如果不持续盯着,可能会错过一些线索。这时可能需要暂停top命令刷新,来记录和比对数据。 总结 排查Linux服务器性能问题还有很多工具,上面介绍的一些命令,可以帮助我们快速的定位问题。例如前面的示例输出,多个证据证明有JAVA进程占用了大量CPU资源,之后的性能调优就可以针对应用程序进行。 原文地址:http://www.infoq.com/cn/news/2015/12/linux-performance
You work with sensitive data in Elasticsearch indices that you do not want everyone to see in their Kibana dashboards. Like a hospital with patient names. You could give each department their own Elasticsearch cluster in order to prevent all departments to see the patient's names, for example. But wouldn't it be great if there was only one Elasticsearch cluster and every departments could manage their own Kibana dashboards? And still have the security in place to prevent leaking of private data? With Elasticsearch Shield, you can create a configurable layer of security on top of your Elasticsearch cluster.In this article, we will explore a small example setup with Shield and Kibana. In this article, we'll use Elasticsearch 1.4.4, Shield 1.0.1 and Kibana 4.0.0. These are at the time of writing the most-recent versions. Beginner knowledge of Elasticsearch is expected. Suppose we want to keep our patient’s names private. An index of patients will be available only for one department. An index of cases will be available to all departments. First, we'll add some test data to the cluster. Create a file hospital.json with the following content: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "index" : { "_index" : "cases", "_type" : "case", "_id" : "101" } } { "admission" : "2015-01-03", "discharge" : "2015-01-04", "injury" : "broken arm" } { "index" : { "_index" : "cases", "_type" : "case", "_id" : "102" } } { "admission" : "2015-01-03", "discharge" : "2015-01-06", "injury" : "broken leg" } { "index" : { "_index" : "cases", "_type" : "case", "_id" : "103" } } { "admission" : "2015-01-06", "discharge" : "2015-01-07", "injury" : "broken nose" } { "index" : { "_index" : "cases", "_type" : "case", "_id" : "104" } } { "admission" : "2015-01-07", "discharge" : "2015-01-07", "injury" : "bruised arm" } { "index" : { "_index" : "cases", "_type" : "case", "_id" : "105" } } { "admission" : "2015-01-08", "discharge" : "2015-01-10", "injury" : "broken arm" } { "index" : { "_index" : "patients", "_type" : "patient", "_id" : "101" } } { "name" : "Adam", "age" : 28 } { "index" : { "_index" : "patients", "_type" : "patient", "_id" : "102" } } { "name" : "Bob", "age" : 45 } { "index" : { "_index" : "patients", "_type" : "patient", "_id" : "103" } } { "name" : "Carol", "age" : 34 } { "index" : { "_index" : "patients", "_type" : "patient", "_id" : "104" } } { "name" : "David", "age" : 14 } { "index" : { "_index" : "patients", "_type" : "patient", "_id" : "105" } } { "name" : "Eddie", "age" : 72 } Then bulk index these documents in the cluster: ? 1 $ curl -X POST 'http://localhost:9200/_bulk' --data-binary @./hospital.json Without security, every user can access all documents. Let's install Shield to add security. Directions on how to install Shield can be found at the Elasticsearch website . We will do this step by step. Shield is a commercial product, so first we need to install the license manager: ? 1 2 3 4 5 $ elasticsearch/bin/plugin -i elasticsearch/license/latest -> Installing elasticsearch/license/latest... Trying http://download.elasticsearch.org/elasticsearch/license/license-latest.zip... Downloading .....................................DONE Installed elasticsearch/license/latest into /home/patrick/blog/elasticsearch/plugins/license Now install Shield itself in the same manner: ? 1 2 3 4 5 $ elasticsearch/bin/plugin -i elasticsearch/shield/latest -> Installing elasticsearch/shield/latest... Trying http://download.elasticsearch.org/elasticsearch/shield/shield-latest.zip... Downloading .....................................DONE Installed elasticsearch/shield/latest into /home/patrick/blog/elasticsearch/plugins/shield You will need to restart the nodes of your cluster to activate the plugins. In the logs of Elasticsearch you'll see some messages from the new plugins: ? 1 2 3 4 5 6 7 8 9 [2015-02-12 08:18:01,347][INFO ][shield.license ] [node0] enabling license for [shield] [2015-02-12 08:18:01,347][INFO ][license.plugin.core ] [node0] license for [shield] - valid [2015-02-12 08:18:01,355][ERROR][shield.license ] [node0] # # Shield license will expire on [Saturday, March 14, 2015]. Cluster health, # cluster stats and indices stats operations are blocked on Shield license expiration. # All data operations (read and write) continue to work. If you have a new license, # please update it. Otherwise, please reach out to your support contact. # Notice that you will get a 30-day trial period to experiment with Shield. Now all our data is protected. See what happens when we try to get a document: ? 1 2 3 4 5 6 $ curl localhost:9200/cases/case/101?pretty=true { "error" : "AuthenticationException[missing authentication token for REST request [/cases/case/1]]", "status" : 401 } We need to add some roles and users to configure the role-based access control of Shield. First we define the roles and their privileges. The definition of these are found in the elasticsearch/config/shield/roles.yml file. Some roles, like admin and user are predefined: ? 1 2 3 4 5 6 7 8 9 10 # All cluster rights # All operations on all indices admin: cluster: all indices: '*': all # Read-only operations on indices user: indices: '*': read Let's edit this roles.yml file to describe our needs. We do not want for every user to access all indices, so we'll change user.indices . We'll add the two roles needed for our organization: doctor and nurse. A doctor has more privileges than a nurse. Doctors can access all indices. Nurses can only access the cases index: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Read-only operations on indices #user: # indices: # '*': read # Doctors can access all indices doctor: indices: '*': read # Nurses can only access the cases index nurse: indices: 'cases': read Now that the roles are defined, we can create users that have these roles. Shield provides three realms to store the users: an internal realm, LDAP or Active Directory. For now, we use the internal realm. The realm is configured in elasticsearch/config/elasticsearch.yml . If nothing is explicitly configured, the internal realm is used. To add users the esusers command line tool can be used. Let's create two users (both with abc123 as password), one for each role: ? 1 2 $ elasticsearch/bin/shield/esusers useradd alice -r nurse $ elasticsearch/bin/shield/esusers useradd bob -r doctor Just to check if the security works: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ curl --user alice:abc123 localhost:9200/_count?pretty=true { "count" : 5, "_shards" : { "total" : 1, "successful" : 1, "failed" : 0 } } $ curl --user bob:abc123 localhost:9200/_count?pretty=true { "count" : 10, "_shards" : { "total" : 2, "successful" : 2, "failed" : 0 } } Alice can see the 5 cases in our cases index. Bob can see those 5 cases plus the 5 patients in the patients index. Now it's time to add Kibana in the mix. Instructions to download and install Kibana 4 can be found on the Elasticsearch website. When we start the Kibana server we notice that Kibana is not allowed access to the Elasticsearch cluster: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ kibana/bin/kibana { "@timestamp": "2015-02-26T08:24:48.958Z", "level": "fatal", "message": "AuthenticationException[missing authentication token for REST request [/.kibana/config/_search]]", "node_env": "production", "error": { "message": "AuthenticationException[missing authentication token for REST request [/.kibana/config/_search]]", "name": "Error", "stack": "Error: AuthenticationException[missing authentication token for REST request [/.kibana/config/_search]] at respond (/home/patrick/kibana/src/node_modules/elasticsearch/src/lib/transport.js:235:15) at checkRespForFailure (/home/patrick/kibana/src/node_modules/elasticsearch/src/lib/transport.js:203:7) at HttpConnector. (/home/patrick/kibana/src/node_modules/elasticsearch/src/lib/connectors/http.js:156:7) at IncomingMessage.bound (/home/patrick/kibana/src/node_modules/elasticsearch/node_modules/lodash-node/modern/internals/baseBind.js:56:17) at IncomingMessage.emit (events.js:117:20) at _stream_readable.js:944:16 at process._tickCallback (node.js:442:13)" } } We need to tell Shield that Kibana is allowed to access our cluster. Extra information of how to let Kibana work with Shield can be found in the Elasticsearch guide . Shield is shipped with a default configuration for Kibana 4. We find the following role definition in elasticsearch/config/shield/roles.yml. ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # The required role for kibana 4 users kibana4: cluster: cluster:monitor/nodes/info indices: '*': - indices:admin/mappings/fields/get - indices:admin/validate/query - indices:data/read/search - indices:data/read/msearch - indices:admin/get '.kibana': - indices:admin/exists - indices:admin/mapping/put - indices:admin/mappings/fields/get - indices:admin/refresh - indices:admin/validate/query - indices:data/read/get - indices:data/read/mget - indices:data/read/search - indices:data/write/delete - indices:data/write/index - indices:data/write/update When the Kibana Server starts it needs to access the .kibana index. So we need to create a user in Shield for Kibana to connect with: ? 1 $ elasticsearch/bin/shield/esusers useradd kibana -r kibana4 This account must be configured in Kibana. Modify kibana/conf/kibana.yml : ? 1 2 3 4 5 6 7 8 9 10 11 12 # If your Elasticsearch is protected with basic auth, this is the user credentials # used by the Kibana server to perform maintence on the kibana_index at statup. Your Kibana # users will still need to authenticate with Elasticsearch (which is proxied thorugh # the Kibana server) kibana_elasticsearch_username: kibana kibana_elasticsearch_password: abc123 #---------------------------------------------------------------------------- # In newly version of ElasticSearch 2.1.0, you have to use the following way: elasticsearch.username: kibana elasticsearch.password: abc123 #---------------------------------------------------------------------------- The Kibana users must have the kibana4 role to be able to work with Kibana. They must be able to store their visualizations and dashboards in the .kibana index: ? 1 2 $ elasticsearch/bin/shield/esusers roles alice -a kibana4 $ elasticsearch/bin/shield/esusers roles bob -a kibana4 Since the default kibana4 role has read access on all indices, alice and bob will be granted all access on all indices. Therefore the role permissions must be modified: ? 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 # Doctors can access all indices doctor: indices: 'cases,patients': - indices:admin/mappings/fields/get - indices:admin/validate/query - indices:data/read/search - indices:data/read/msearch - indices:admin/get # Nurses can only access the cases index nurse: indices: 'cases': - indices:admin/mappings/fields/get - indices:admin/validate/query - indices:data/read/search - indices:data/read/msearch - indices:admin/get # The required role for kibana 4 users kibana4: cluster: - cluster:monitor/nodes/info - cluster:monitor/health indices: '.kibana': - indices:admin/exists - indices:admin/mapping/put - indices:admin/mappings/fields/get - indices:admin/refresh - indices:admin/validate/query - indices:data/read/get - indices:data/read/mget - indices:data/read/search - indices:data/write/delete - indices:data/write/index - indices:data/write/update - indices:admin/create With this configuration any user with the kibana4 role is able to use Kibana but only sees data that he or she has the proper clearance for. We can now start the Kibana Server and see that it runs as it should: ? 1 2 3 4 5 6 7 $ kibana/bin/kibana { "@timestamp": "2015-02-26T08:53:18.961Z", "level": "info", "message": "Listening on 0.0.0.0:5601", "node_env": "production" } We can open a browser and head to localhost:5601 to open the Kibana web interface. Log in as Alice: After logging in, Kibana will ask for the index pattern. We'll keep it simple: Then in the discover tab you can add fields to your view. Notice that Alice only sees cases: When we log in as Bob our discover tab shows both cases and patients: To summarize: we added security to Elasticsearch with Shield and configured some users and roles. There's nothing more to it! 原文地址:http://blog.trifork.com/2015/03/05/shield-your-kibana-dashboards/
如图,安装完Weblogic 12C R2(12.2.1)并启动后,Eclipse中控制台乱码: 解决办法: 在Weblogic安装目录下打开: [C:\Oracle\Middleware\Oracle_Home]\user_projects\domains\[base_domain]\bin\setDomainEnv.cmd: 找到脚本中的最后一个set JAVA_OPTIONS=%JAVA_OPTIONS%(在倒数20~30行左右): 将其修改为(在这一行后面增加-Dfile.encoding=utf-8): 保存后,重启Weblogic:
Redis同样支持消息的发布/订阅(Pub/Sub)模式,这和中间件activemq有些类似。订阅者(Subscriber)可以订阅自己感兴趣的频道(Channel),发布者(Publisher)可以将消息发往指定的频道(Channel),正式通过这种方式,可以将消息的发送者和接收者解耦。另外,由于可以动态的Subscribe和Unsubscribe,也可以提高系统的灵活性和可扩展性。 关于如何搭建Redis环境,请参考其他文章。这里假设有一个可用的Redis环境(单节点和集群均可)。 在redis-cli中使用Pub/Sub 普通channel的Pub/Sub 先用一个客户端来订阅频道: 上图中先使用redis-cli作为客户端连接了Redis,之后使用了SUBSCRIBE命令,后面的参数表示订阅了china和hongkong两个channel。可以看到"SUBSCRIBE china hongkong"这条命令的输出是6行(可以分为2组,每一组是一个Message)。因为订阅、取消订阅的操作跟发布的消息都是通过消息(Message)的方式发送的,消息的第一个元素就是消息类型,它可以是以下几种类型: subscribe: means that we successfully subscribed to the channel given as the second element in the reply. The third argument represents the number of channels we are currently subscribed to. unsubscribe: means that we successfully unsubscribed from the channel given as second element in the reply. The third argument represents the number of channels we are currently subscribed to. When the last argument is zero, we are no longer subscribed to any channel, and the client can issue any kind of Redis command as we are outside the Pub/Sub state. message: it is a message received as result of a PUBLISH command issued by another client. The second element is the name of the originating channel, and the third argument is the actual message payload. --from http://redis.io/topics/pubsub 上图的订阅命令将使得发往这两个channel的消息会被这个客户端接收到。需要注意的是,redis-cli客户端在进入subscribe模式以后,将不能再响应其他的任何命令: A client subscribed to one or more channels should not issue commands, although it can subscribe and unsubscribe to and from other channels. The commands that are allowed in the context of a subscribed client are SUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PING and QUIT --from http://redis.io/topics/pubsub 官网说客户端在subscribe下除了可以使用以上命令外,不能使用其他命令了。但是本人在Subscribe状态下使用上述几个命令,根本没反应。也就是说,使用redis-cli订阅channel后,该客户端将不能响应任何命令。除非按下(ctrl+c),但该操作不是取消订阅,而是退出redis-cli,此时将回到shell命令行下。 关于这个情况,我在官网上没有找到对这种情况的解释,也有不少的人在网上问,找来找去,本人觉得还算合理的解释是: On this page: http://redis.io/commands/subscribe applies only to those clients. The redis-cli is among those clients. So, the comment is not an instruction for users of redis-cli. Instead, redis-cli blocks waiting for messages on the bus (only to be unsubcribed via a ctrl+c). --from http://stackoverflow.com/questions/17621371/redis-unsubscribe 就是说,官网中说明的client,并不包含这里使用的redis-cli,于是它可以和其他的client有不同表现。(先不纠结这个问题,稍后再用jedis来测试一下。) 接下来再用一个客户端来发布消息: 可以看到,新的一个客户端使用PUBLISH命令往china频道发布了一条叫"China News"的消息,接下来再看看订阅端: 可以看见,这条消息已经被接收到了。可以看到,收到的消息中第一个参数是类型"message",第二个参数是channel名字"china",第三个参数是消息内容"China News",这和开始说的message类型的结构一致。 通配符的Pub/Sub Redis还支持通配符的订阅和发布。客户端可以订阅满足一个或多个规则的channel消息,相应的命令是PSUBSCRIBE和PUNSUBSCRIBE。接下来我们再用另一个redis-cli客户端来订阅"chi*"的channel,如图: 和subscribe/unsubscribe的输出类似,可以看到第一部分是消息类型“psubscribe”,第二部分是订阅的规则“chi*”,第三部分则是该客户端目前订阅的所有规则个数。 接下来再发布一条消息到china这个channel中,此时,两个订阅者应该都能收到该消息: 实际测试结果跟预期相同。需要注意的是,订阅者2通过通配符订阅的,收到的消息类型是“pmessage”: pmessage: it is a message received as result of a PUBLISH command issued by another client, matching a pattern-matching subscription. The second element is the original pattern matched, the third element is the name of the originating channel, and the last element the actual message payload. --from http://redis.io/topics/pubsub 第二部分是匹配的模式“chi*”,第三部分是实际的channel名字“china”,第四部分是消息内容“China Daily”。 我们再发布一条消息到chinnna中,此时只有订阅者2能接收到消息了: 同样,在使用PSUBSCRIBE进入订阅模式以后,该redis-cli也不能再监听其他任何的命令,要退出该模式,只能使用ctrl+c。 使用Jedis实现Pub/Sub Jedis是Redis客户端的一种Java实现,在http://redis.io/clients#java中也能找到。 这里使用maven来管理包的依赖,由于使用了Log4j来输出日志,因此会用到log4j的jar包: ? 1 2 3 4 5 6 7 8 9 10 <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.8.0</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> Jedis中的JedisPubSub抽象类提供了订阅和取消的功能。想处理订阅和取消订阅某些channel的相关事件,我们得扩展JedisPubSub类并实现相关的方法: ? 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 package com.demo.redis; import org.apache.log4j.Logger; import redis.clients.jedis.JedisPubSub; public class Subscriber extends JedisPubSub {//注意这里继承了抽象类JedisPubSub private static final Logger LOGGER = Logger.getLogger(Subscriber.class); @Override public void onMessage(String channel, String message) { LOGGER.info(String.format("Message. Channel: %s, Msg: %s", channel, message)); } @Override public void onPMessage(String pattern, String channel, String message) { LOGGER.info(String.format("PMessage. Pattern: %s, Channel: %s, Msg: %s", pattern, channel, message)); } @Override public void onSubscribe(String channel, int subscribedChannels) { LOGGER.info("onSubscribe"); } @Override public void onUnsubscribe(String channel, int subscribedChannels) { LOGGER.info("onUnsubscribe"); } @Override public void onPUnsubscribe(String pattern, int subscribedChannels) { LOGGER.info("onPUnsubscribe"); } @Override public void onPSubscribe(String pattern, int subscribedChannels) { LOGGER.info("onPSubscribe"); } } 有了订阅者,我们还需要一个发布者: ? 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 package com.demo.redis; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import org.apache.log4j.Logger; import redis.clients.jedis.Jedis; public class Publisher { private static final Logger LOGGER = Logger.getLogger(Publisher.class); private final Jedis publisherJedis; private final String channel; public Publisher(Jedis publisherJedis, String channel) { this.publisherJedis = publisherJedis; this.channel = channel; } /** * 不停的读取输入,然后发布到channel上面,遇到quit则停止发布。 */ public void startPublish() { LOGGER.info("Type your message (quit for terminate)"); try { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); while (true) { String line = reader.readLine(); if (!"quit".equals(line)) { publisherJedis.publish(channel, line); } else { break; } } } catch (IOException e) { LOGGER.error("IO failure while reading input", e); } } } 为简单起见,这个发布者接收控制台的输入,然后将输入的消息发布到指定的channel上面,如果输入quit,则停止发布消息。 接下来是主函数: ? 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 package com.demo.redis; import org.apache.log4j.Logger; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class Program { public static final String CHANNEL_NAME = "MyChannel"; //我这里的Redis是一个集群,192.168.56.101和192.168.56.102都可以使用 public static final String REDIS_HOST = "192.168.56.101"; public static final int REDIS_PORT = 7000; private final static Logger LOGGER = Logger.getLogger(Program.class); private final static JedisPoolConfig POOL_CONFIG = new JedisPoolConfig(); private final static JedisPool JEDIS_POOL = new JedisPool(POOL_CONFIG, REDIS_HOST, REDIS_PORT, 0); public static void main(String[] args) throws Exception { final Jedis subscriberJedis = JEDIS_POOL.getResource(); final Jedis publisherJedis = JEDIS_POOL.getResource(); final Subscriber subscriber = new Subscriber(); //订阅线程:接收消息 new Thread(new Runnable() { public void run() { try { LOGGER.info("Subscribing to \"MyChannel\". This thread will be blocked."); //使用subscriber订阅CHANNEL_NAME上的消息,这一句之后,线程进入订阅模式,阻塞。 subscriberJedis.subscribe(subscriber, CHANNEL_NAME); //当unsubscribe()方法被调用时,才执行以下代码 LOGGER.info("Subscription ended."); } catch (Exception e) { LOGGER.error("Subscribing failed.", e); } } }).start(); //主线程:发布消息到CHANNEL_NAME频道上 new Publisher(publisherJedis, CHANNEL_NAME).startPublish(); publisherJedis.close(); //Unsubscribe subscriber.unsubscribe(); subscriberJedis.close(); } } 主类Program中定义了channel名字、连接redis的地址和端口,并使用JedisPool来获取Jedis实例。由于订阅者(subscriber)在进入订阅状态后会阻塞线程,因此新起一个线程(new Thread())作为订阅线程,并是用主线程来发布消息。待发布者(类中的new Publisher)停止发布消息(控制台中输入quit即可)时,解除订阅者的订阅(subscriber.unsubscribe()方法)。此时订阅线程解除阻塞,打印结束的日志并退出。 运行程序之前,还需要一个简单的log4j配置以观察输出: ? 1 2 3 4 5 log4j.rootLogger=INFO,stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} %m%n 运行Program,以下是执行结果: 从结果看,当订阅者订阅后,订阅线程阻塞,主线程中的Publisher接收输入后,发布消息到MyChannel中,此时订阅该channel的订阅者收到消息并打印。 Jedis源码简要分析 关于使用UNSUBSCRIBE 开始使用redis-cli时,在subscriber进入监听状态后,并不能使用UNSUBSCRIBE和PUNSUBSCRIBE命令,现在在Jedis中,在订阅线程阻塞时,通过在main线程中调用改subscriber的unsubscribe()方法来解除阻塞。查看Jedis源码,其实该方法也就是给redis发送了一个UNSUBSCRIBE命令而已: 因此这里是支持在“客户端”使用UNSUBSCRIBE命令的。 关于订阅者接收消息 在接收消息前,需要订阅channel,订阅完成之后,会执行一个循环,这个循环会一直阻塞,直到该Client没有订阅数为止,如下图: 中间省略的其他行,主要是用于解析收到的Redis响应,这段代码也是根据响应的第一部分确定响应的消息类型,然后挨个解析响应的后续内容,最后根据解析到消息类型,并使用后续解析到的内容作为参数来回调相应的方法,省略的内容如下: ? 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 final byte[] resp = (byte[]) firstObj; if (Arrays.equals(SUBSCRIBE.raw, resp)) { subscribedChannels = ((Long) reply.get(2)).intValue(); final byte[] bchannel = (byte[]) reply.get(1); final String strchannel = (bchannel == null) ? null : SafeEncoder.encode(bchannel); //调用onSubscribe方法,该方法在我们的Subscriber类中实现 onSubscribe(strchannel, subscribedChannels); } else if (Arrays.equals(UNSUBSCRIBE.raw, resp)) { subscribedChannels = ((Long) reply.get(2)).intValue(); final byte[] bchannel = (byte[]) reply.get(1); final String strchannel = (bchannel == null) ? null : SafeEncoder.encode(bchannel); //调用onUnsubscribe方法,该方法在我们的Subscriber类中实现 onUnsubscribe(strchannel, subscribedChannels); } else if (Arrays.equals(MESSAGE.raw, resp)) { final byte[] bchannel = (byte[]) reply.get(1); final byte[] bmesg = (byte[]) reply.get(2); final String strchannel = (bchannel == null) ? null : SafeEncoder.encode(bchannel); final String strmesg = (bmesg == null) ? null : SafeEncoder.encode(bmesg); //调用onMessage方法,该方法在我们的Subscriber类中实现 onMessage(strchannel, strmesg); } else if (Arrays.equals(PMESSAGE.raw, resp)) { final byte[] bpattern = (byte[]) reply.get(1); final byte[] bchannel = (byte[]) reply.get(2); final byte[] bmesg = (byte[]) reply.get(3); final String strpattern = (bpattern == null) ? null : SafeEncoder.encode(bpattern); final String strchannel = (bchannel == null) ? null : SafeEncoder.encode(bchannel); final String strmesg = (bmesg == null) ? null : SafeEncoder.encode(bmesg); //调用onPMessage方法,该方法在我们的Subscriber类中实现 onPMessage(strpattern, strchannel, strmesg); } else if (Arrays.equals(PSUBSCRIBE.raw, resp)) { subscribedChannels = ((Long) reply.get(2)).intValue(); final byte[] bpattern = (byte[]) reply.get(1); final String strpattern = (bpattern == null) ? null : SafeEncoder.encode(bpattern); onPSubscribe(strpattern, subscribedChannels); } else if (Arrays.equals(PUNSUBSCRIBE.raw, resp)) { subscribedChannels = ((Long) reply.get(2)).intValue(); final byte[] bpattern = (byte[]) reply.get(1); final String strpattern = (bpattern == null) ? null : SafeEncoder.encode(bpattern); //调用onPUnsubscribe方法,该方法在我们的Subscriber类中实现 onPUnsubscribe(strpattern, subscribedChannels); } else { //对于其他Redis没有定义的返回消息类型,则直接报错 throw new JedisException("Unknown message type: " + firstObj); } 以上就是为什么我们需要在Subscriber中实现这几个方法的原因了(这些方法并不是抽象的,可以选择实现使用到的方法)。
创建一个RedisCluster之前,我们需要有一些以cluster模式运行的Redis实例,这是因为cluster模式下Redis实例将会开启cluster的特征和命令。 现在我有2台Vbox搭建的CentOS6虚拟机【CentOS1(192.168.56.101)和CentOS2(192.168.56.102)】,准备在此上搭建Redis集群。 由于最小的Redis集群需要3个Master节点,本次测试使用另外3个节点作为备份的节点(Replicas),于是此次搭建需要6个Redis实例。由于可在同一台机器上运行多个Redis实例,因此我将在CentOS1上运行以下实例: ? 1 2 3 192.168.56.101:7000 192.168.56.101:7001 192.168.56.101:7002 并在CentOS2上运行以下实例: ? 1 2 3 192.168.56.102:7003 192.168.56.102:7004 192.168.56.102:7005 1. 下载Redis,目前的stable版本为3.0.6: http://redis.io/download 2. 安装Redis ? 1 2 3 tar zxvf redis-3.0.6.tar.gz cd redis-3.0.6 make 安装完成后,redis-3.0.6/src文件夹下会出现redis-server、redis-cli等可执行文件,稍后将使用。 3. 修改配置文件 由于需要在CentOS1上运行多个实例,为了便于管理,在CentOS1上建立/home/user/Software/redis-cluster文件夹,并分别创建7000、7001和7002这三个子文件夹: 然后分别拷贝redis-3.0.6/redis.conf到这三个子文件夹中。redis.conf是redis服务器启动的必要配置文件,分别在这几个文件夹中打开该文件,修改以下选项(这几个选项是搭建Redis集群的必须选项),其它的保持默认即可: ? 1 2 3 4 5 6 #注意每个子文件夹下的配置中,端口号不同 port 7000 cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes 在redis.conf中,有对这些选项的详细说明,这里不赘述。 最后拷贝redis-3.0.6/src/redis-server文件到这几个子文件夹中。 4. 启动Redis 在这3个文件夹中分别启动Redis: ? 1 ./redis-server redis.conf 可以通过进程命令来查看启动结果 从每个实例的启动日志(redis.conf文件中的logfile选项可以配置日志文件)中,可以看到每一个节点都给自己分配了一个新的ID: 这个ID将被该Redis实例作为集群中的唯一名字永久使用,节点之间会互相记住这个名字。 5. 创建集群 在创建Redis集群前,在CentOS2机器中也需要按照步骤1~4完成相应的配置并运行Redis实例。 现在CentOS1上的3个Redis实例和CentOS2上的3个Redis实例都已经启动。目前这些实例虽然都开启了cluster模式,但是彼此还不认识对方,接下来可以通过Redis集群的命令行工具redis-trib.rb来完成集群创建。redis-trib.rb是一个Ruby写的可执行程序,它可以完成创建集群、为已存在的集群重新分片等功能。 而要想运行ruby程序,则需要系统先安装ruby运行环境: ? 1 sudo yum install ruby 接下来运行以下命令(在CentOS1和CentOS2上运行均可): ? 1 2 3 ./redis-trib.rb create --replicas 1 \ 192.168.56.101:7000 192.168.56.101:7001 192.168.56.101:7002 \ 192.168.56.102:7003 192.168.56.102:7004 192.168.56.102:7005 此时会遇到以下问题: 提示缺少rubygems组件,可以使用yum来安装该组件: ? 1 sudo yum -y install rubygems 再次运行创建Redis集群的命令,还会报以下错误: ? 1 2 3 /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require': no such file to load -- redis (LoadError) from /usr/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `require' from ./redis-trib.rb:25 搜索了一下,是因为缺少redis的ruby接口,可以通过以下命令安装: ? 1 sudo gem install redis 如果安装时出现错误,也可以下载文件后进行离线安装,下载地址:http://rubygems.org/gems/redis/versions,选择合适的版本,然后安装: ? 1 sudo gem install -l /mnt/Share/redis-3.0.6.gem 接下来再次运行创建Redis集群的命令: 上图中: create为redis-trib.rb脚本的子命令,表示创建redis集群 --replicas 1表示创建的集群有1套数据备份 后面的参数为多个redis实例地址,表示使用那些实例来创建节点 左边的“M:”表示该节点是master节点,相应的“S:”表示该节点为slave节点 在本次测试中,有7000~7005共6个节点,有一组节点是备份,那么很显然会有3个master和3个replica,这正好符合一开始的要求。从上图中可以看到,端口号为7000、7003和7004的三个节点被选为master,另外三个节点则成为replica。而下图则展示了redis的16384个数据槽(data slot)在三个master节点上的分布情况: 6. 测试集群 redis目前的客户端实现并不多,接下来我们用自带的redis-cli工具来测试搭建好的集群。 最简单的测试集群是使用redis-cli连接上redis后使用cluster命令(更多命令请参见:http://redis.io/commands): cluster info,查看集群信息: cluster nodes:查看集群中的节点信息: 下面使用redis-cli工具进行数据的读写操作: 开始连接redis的时候没有加入-c选项,则只能在该节点上存取,举个例子:上图中一开始我登录的是7002端口的redis实例,然后 ? 1 set i 0 这一句给i设置值为0,经集群计算后,这个i应该落在15759这个slot中,这个slot又在7004这个redis实例上,于是准备跳转到7004的redis实例上,由于没有开启集群模式,这次跳转失败了,值也就没有写进redis。 第二次连接的还是7002,但这一次使用-c选项打开了集群模式,后续的存取不管落在哪一个节点上,都能跳转过去并正确的读写。蓝色方框的变化展示了连接跳转的过程,redis集群中的读写都会发生在指定的slot上,因此都会发生相应的跳转。 另外,由于每个节点都会记住集群中其他节点的名字以及数据槽的分布情况,我们可以打开每个redis实例的文件夹查看其nodes.conf(redis.conf文件中的cluster-config-file选项来配置)文件,文件内容大致相同,仅仅是节点列表的顺序不同而已。
安装MySQL Yum安装: 官方安装步骤:http://dev.mysql.com/doc/refman/5.7/en/linux-installation-yum-repo.html ? 1 >sudo yum install mysql-community-server 源码安装: 官方安装步骤:https://dev.mysql.com/doc/refman/5.7/en/installing-source-distribution.html 注意,不同的mysql版本,有不同的安装步骤,请从官网查阅资料。 1.下载:http://dev.mysql.com/downloads/mysql,选择最新版的Source Code,我这里是5.7.9 2.安装必要的软件包,如果有些包已经安装,可以根据情况从后面的列表中去除这些包: ? 1 >yum -y install gcc gcc-c++ autoconf automake zlib* libxml ncurses-devel libmcrypt* libtool-ltdl-devel* make cmake 3.解压 ? 1 2 >tar -zxvf mysql-5.7.9.tar.gz >cd mysql-5.7.9 --默认情况下是安装在/usr/local/mysql 4.编译 ? 1 >cmake . --注意后面有个点‘.’,代表当前目录 注意,以上的cmake . 命令是使用所有默认的配置进行编译,如果希望调整部分设置,请参考官网描述: https://dev.mysql.com/doc/refman/5.7/en/source-configuration-options.html 但是我们用源码安装的目的就是希望自定义有些属性,因此一般会修改某些配置,比如: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 >cmake . \ -DCMAKE_INSTALL_PREFIX=/usr/local/mysql \ -DMYSQL_DATADIR=/usr/local/mysql/data \ -DSYSCONFDIR=/etc/mysql/ \ -DWITH_MYISAM_STORAGE_ENGINE=1 \ -DWITH_INNOBASE_STORAGE_ENGINE=1 \ -DWITH_MEMORY_STORAGE_ENGINE=1 \ -DMYSQL_UNIX_ADDR=/var/lib/mysql/mysql.sock \ -DMYSQL_TCP_PORT=3306 \ -DENABLED_LOCAL_INFILE=1 \ -DWITH_PARTITION_STORAGE_ENGINE=1 \ -DWITH_EXTRA_CHARSETS=all \ -DDEFAULT_CHARSET=utf8 \ -DDEFAULT_COLLATION=utf8_general_ci 5.问题解决 在cmake这一步可能遇到以下错误: 说不能找到boost库,但可以通过加上-DDOWNLOAD_BOOST=1 –DWITH_BOOST=<directory>来解决。从上图描述来看,如果boost不在<directory>下,则该选项可以在自动下载boost并且自动解压缩。于是增加了这两个参数: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 >cmake . \ -DCMAKE_INSTALL_PREFIX=/usr/local/mysql \ -DMYSQL_DATADIR=/usr/local/mysql/data \ -DSYSCONFDIR=/etc/mysql/ \ -DWITH_MYISAM_STORAGE_ENGINE=1 \ -DWITH_INNOBASE_STORAGE_ENGINE=1 \ -DWITH_MEMORY_STORAGE_ENGINE=1 \ -DMYSQL_UNIX_ADDR=/var/lib/mysql/mysql.sock \ -DMYSQL_TCP_PORT=3306 \ -DENABLED_LOCAL_INFILE=1 \ -DWITH_PARTITION_STORAGE_ENGINE=1 \ -DWITH_EXTRA_CHARSETS=all \ -DDEFAULT_CHARSET=utf8 \ -DDEFAULT_COLLATION=utf8_general_ci \ -DDOWNLOAD_BOOST=1 \ -DWITH_BOOST=/home/user/Software/boost_1_59_0 然后被告知解压的时候失败了,查看了自动下载的boost库: 发现这个文件存在,文件大小为173Byte,于是尝试手动解压,结果解压失败: 于是怀疑这个文件有问题,因此去这个地址手动下载一个: ? 1 >wget http://sourceforge.net/projects/boost/files/boost/1.59.0/boost_1_59_0.tar.gz 结果新下载的文件长度还是173Byte,解压还是失败。 接下来去物理机上下载这个文件,在浏览器上打开这个链接,发现文件有80多MB: 下载后,拷贝到虚拟机,再解压,再使用cmake命令: 这就是为什么之前的包里面包含ncurses-devel了。 ? 1 >yum install ncurses-devel 之后再使用cmake,不过,这次的结果跟上图一样。原因是: 上图来自:http://dev.mysql.com/doc/refman/5.7/en/installing-source-distribution.html 删除CMakeCache.txt后再重新编译,这次终于成功了: 6.然后就是make了: 接下来,make会利用源代码生成可执行的库文件,我们会看到构建的进度,这一步要很久,请耐心等待,直到到达100%: 7.最后,make install,直到安装完成。 总结:尽管在官方安装步骤中有说明系统需求,但没有提前说明安装需要依赖哪些库,导致在安装过程中出现了不必要的重复。因此,在参照官方安装步骤的同时建议多搜集资料。 8.初始化: ? 1 2 3 4 5 6 7 8 >cd /usr/local/mysql >chown -R mysql . >chgrp -R mysql . >bin/mysql_install_db --user=mysql # Before MySQL 5.7.6 >bin/mysqld --initialize --user=mysql # MySQL 5.7.6 and up >bin/mysql_ssl_rsa_setup # MySQL 5.7.6 and up >chown -R root . >chown -R mysql data 注意,在执行bin/mysqld --initialize --user=mysql这一句的时候,会生成一个root账户的临时密码(随机的),密码直接写在 log-error 日志文件中(在5.6版本中是放在 ~/.mysql_secret 文件里,更加隐蔽,不熟悉的话可能会无所适从)。而且官方说In this case, the password is marked as expired and you will need to choose a new one. 当然,也可以在执行mysqld使用--initialize-insecure参数,这将不会为root账户设置密码。 官方说明: To initialize the data directory, invoke mysqld with the --initialize or --initialize-insecure option, depending on whether you want the server to generate a random initial password for the 'root'@'localhost' account. 下图是使用--initialize-insecure参数的初始化过程: 在初始化过程中,mysql会检查data目录,如果该目录不存在,则mysql会创建;如果存在且不为空,则会报错。因此,如果遇到这样的错误: [ERROR] --initialize specified but the data directory exists. Aborting. 或 [ERROR] --initialize specified but the data directory has files in it. Aborting. 请在执行mysqld前保证datadir 目标目录下是空的,避免误操作破坏已有数据。同时指定 ? 1 2 --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data 这两个参数,像上图一样。 当然,这两个参数也可以写在mysql的配置文件my.cnf中: ? 1 2 3 [mysqld] basedir=/opt/mysql/mysql datadir=/opt/mysql/mysql/data 然后把这个文件传给mysqld: ? 1 2 >bin/mysqld --defaults-file=/opt/mysql/mysql/etc/my.cnf \ --initialize --user=mysql 9.启动: 9.1.使用mysqld_safe启动 ? 1 >bin/mysqld_safe --user=mysql & 如果该命令执行失败,并且打印了mysqld ended字样,则说明启动失败,此时需要查看日志文件,默认的位置是 data目录下的host_name.err。 顺便补充一下mysqld_safe这个命令。 参考:https://dev.mysql.com/doc/refman/5.7/en/mysqld-safe.html mysqld_safe是在Unix平台启动mysqld的推荐方式,它是一个执行脚本,会去调用mysqld这个脚本。 许多mysqld_safe的选项与mysqld相同。对于mysqld_safe不能识别的参数,将传给mysqld,但是在配置文件(15步有说明)中的选项将被忽略掉。 mysqld_safe会读取配置文件中的[mysqld]、[server]和[mysqld_safe]部分的所有配置。 mysqld_safe会把它的错误或提示消息跟mysqld的放在同一个地方 关于mysqld_safe能支持的所有选项,请参照上方链接。 9.2.使用mysql.server启动 参考:https://dev.mysql.com/doc/refman/5.7/en/mysql-server.html ? 1 >support_files/mysql.server start MySQL的Linux发行版中还包含了一个叫mysql.server的脚本,它通过调用mysqld_safe脚本来启动mysql。这个脚本在安装完MySQL后被放在support-files文件夹中: mysql.server读取[mysqld]、[mysql.server]部分的选项。我们可以在/etc/mysql/my.cnf文件中配置相关选项。关于其支持的所有选项,请参照上方链接。 如果初始化的时候使用的是--initialize选项,登录的时候,粘贴刚刚生成的密码: 如果使用的是--initialize-insecure选项,登录的时候,直接按回车键即可: 10.修改临时密码: 参考:http://dev.mysql.com/doc/refman/5.7/en/resetting-permissions.html 11.重设root与其他用户的密码: 重设root密码,在设置设置密码之后重新登录时,是需要输入密码的: 创建其他用户,设置密码,并授权: 12.将mysql的bin加入到path中 我把path添加到当前用户目录的bashrc中,如果需要全局设定,请修改`/etc/profile` ? 1 2 >cd ~ >vi .bashrc 加入以下内容: PATH=/usr/local/mysql/bin:$PATH ? 1 2 >source .bashrc >echo $PATH 这样以后就可以直接使用mysql而不用./mysql这样执行了。 13.将mysql加入到系统服务: ? 1 2 3 4 5 #Next command is optional >cd /usr/local/mysql >cp support-files/mysql.server /etc/init.d/mysql --拷贝脚本到init.d目录下 >chmod +x /etc/init.d/mysql --增加可执行权限 >chkconfig --add mysql --将mysql增加为系统运行级别的服务 之后,就可以使用service来启动mysql了: ? 1 >service mysql start 注意上面的命令要用sudo或者用root用户执行。 14.停止mysql 使用mysql.server来停止mysql ? 1 2 >cd /usr/local/mysql/support-files >./mysql.server stop 当拷贝这个文件到/etc/init.d文件夹后,可以这样停止: ? 1 >/etc/init.d/mysql stop 当加入到系统服务后,可以这样停止: ? 1 >service mysql stop 还可以使用mysqladmin命令来停止: ? 1 >mysqladmin shutdown 15.在MySQL中使用配置文件 参考: https://dev.mysql.com/doc/refman/5.7/en/option-files.html https://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html 在MySQL5.7.5之前,在Unix平台上, mysql_install_db 会在安装目录下创建一个默认的叫my.cnf的配置文件. 该文件是从发行包中一个叫my-default.cnf的模板文件复制而来。当通过使用mysqld_safe来启动MySQL时,服务器默认使用my.cnf 文件做配置。 从第8步可以看到,由于当前安装的版本为5.7.9,因此使用的是mysqld来初始化mysql的,因此这个my.cnf文件需要自己拷贝。 可以通过以下命令查看MySQL完整配置: ? 1 >mysqld --verbose --help 在Unix, Linux和OS X系统上,MySQL程序会按表格中的先后顺序从以下文件中读取启动配置: File Name Purpose /etc/my.cnf Global options /etc/mysql/my.cnf Global options SYSCONFDIR/my.cnf Global options $MYSQL_HOME/my.cnf Server-specific options defaults-extra-file The file specified with --defaults-extra-file=file_name, if any ~/.my.cnf User-specific options ~/.mylogin.cnf Login path options 其中: ~代表当前用户的主目录( 即$HOME) SYSCONFDIR 代表在安装MySQL时在CMake参数上指定的SYSCONFIGDIR选项(从我上面的参数来看,此变量的值为/etc/mysql/)。 既然my.cnf是由my-default.cnf复制而来,那我们就去找找这个my-default.cnf文件: 其中,/home/user/Soft ware/mysql-5.7.9是我用源码解压的位置,这里面的my-default.cnf就是最初的配置模板了。而/usr/local/mysql/是我安装mysql的目录,这下面的my-default.cnf文件是在安装时从解压目录拷贝而来的。把这个文件拷贝到上述表格中的某个文件夹下: ? 1 >cp /usr/local/mysql/support-files/my-default.cnf /etc/mysql/my.cnf 并打开看看: 其实里面只有一个配置,其余的都被注释掉了。模板文件中也提到了,需要什么配置,以及怎样配置,请参考: https://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html 修改配置后,重启一下MySQL就能应用了。 安装MySQL Workbench MySQL-Workbench是一款很好用的MySQL图形化工具。无论是写SQL,分析数据,数据库建模都比mysql方便的多。 1.Yum安装 先去这里下载mysql的yum repository: http://dev.mysql.com/downloads/repo/yum/ 之后安装: ? 1 >sudo rpm -Uvh mysql57-community-release-el6-n.noarch.rpm 然后就能找到mysql-workbench-community了: 安装: ? 1 >sudo yum install mysql-workbench-community 之后会提醒这一大堆库需要先安装: 有个tinyxml库实在不好找,但我在这里下载到了: ftp://ftp.pbone.net/mirror/download.fedora.redhat.com/pub/fedora/epel/6/x86_64/tinyxml-2.6.1-1.el6.x86_64.rpm (64bit) http://dl.fedoraproject.org/pub/epel/6/i386/tinyxml-2.6.1-1.el6.i686.rpm(32bit) 之后安装这个库: ? 1 >sudo rpm -i tinyxml-2.6.1-1.el6.i686.rpm 并等待安装完成: 2.RPM安装 先去这里下载对应的rmp包:http://dev.mysql.com/downloads/workbench/ ? 1 >sudo rpm -i mysql-workbench-community-6.3.5-1.el6.x86_64.rpm 3.源码安装 下载:http://dev.mysql.com/downloads/workbench/ 选择Source Code 由于自行编译依赖的包实在太多,而且限于网速,还有就是没有多少资料可供参考,因此不建议使用此方式安装。 接下来就享受愉快的MySQL Workbench之旅吧!
安装VirtualBox 在Windows系统中安装VitualBox,很简单 下载地址: https://www.virtualbox.org/wiki/Downloads 在VirtualBox中安装CentOS 首先下载CentOS:https://wiki.centos.org/Download 打开安装好的VirtualBox,点击新建: 然后一路默认吧,这个不难,就不截图了。 创建好虚拟机后,启动它,启动的时候会弹出框,让我们选择系统镜像: 选择之后就可以引导了,选择安装操作系统即可。 将用户添加到超级用户组 该操作需要root用户来执行,因此需要先切换用户: ? 1 2 3 >su >chmod +w /etc/sudoers >vi /etc/sudoers 查找 "## Allows people in group wheel to run all commands" 并添加下面这一行并保存: ? 1 user ALL=(ALL) ALL 修改完成后,记得删除修改的权限: ? 1 >chmod –w /etc/sudoers 更新yum源 ? 1 2 3 >cd /etc/yum.repos.d/ --进入yum配置文件目录 >sudo mv CentOS-Base.repo CentOS-Base.repo.bak --备份配置文件 >sudo wget http://mirrors.163.com/.help/CentOS6-Base-163.repo --下载163的配置 下载下来的文件名为 CentOS6-Base-163.repo ? 1 2 >sudo mv CentOS6-Base-163.repo CentOS-Base.repo --重命名 >sudo yum update --更新 修改Hostname 修改/etc/sysconfig/network下的HOSTNAME变量 ----需要重启生效,永久性修改。 ? 1 >sudo vi /etc/sysconfig/network ? 1 >sudo sysctl kernel.hostname=centos ----使其立即生效 安装中文支持(将安装ibus输入法) ? 1 >sudo yum install "@Chinese Support" 如果遇到以下错误: Invalid GPG Key from http://mirror.centos.org/centos/RPM-GPG-KEY-CentOS-6: No key found in given key data ? 1 >sudo vi /etc/yum.repos.d/CentOS-Base.repo 查找并将 gpgcheck=1 替换为 gpgcheck=0 然后尝试重新安装 之后进入System>Preferences>InputMethod,之后勾选Enable input method feature,并按下“Input Method Preferences”按钮: 切换到Input Method选项卡(没安装中文支持之前,是没有InputMethod这一项的),在Select an input method下拉列表中选择Chinese>PinYin,并单击右边的Add。 重新登录之后就可以通过Ctrl+Space切换输入法了: 切换系统中英文 切换前: 如果在安装系统的时候设置的英文或中文,现在需要切换为另一种语言,可以使用以下方式: ? 1 >sudo vi /etc/sysconfig/i18n 然后注释(或删除)掉中文,增加英文: 最后重启即可: ? 1 >sudo shutdown -r now 当然,以上是对于所有用户的修改,如果只需要修改当前用户的语言: ? 1 >vi ~/.bashrc 在最后增加: ? 1 export LANG=”zh_CN.UTF-8” 保存并重启系统即可。 切换后: 安装共享文件夹工具 先给虚拟机挂载Vbox工具包(该工具还可以使光标在物理机和虚拟机自由移动和切换): 选择Vbox安装目录下的VBoxGuestAddtions.iso: 重启系统,之后: ? 1 2 3 4 >sudo yum install gcc.i686 >sudo yum install kernel-devel-2.6.32-573.el6.i686 >cd /media/VBOXADDITIONS_4.3.24_98716 >sudo ./VBoxLinuxAdditions.run 等待安装完成: 安装完成之后,鼠标就可以任意在虚拟系统和物理系统切换和移动了(无需在切换时按Alt键了)。接下来可以挂载共享文件夹了: ? 1 2 >sudo mkdir /mnt/Share --这是在Linux下的文件夹名称 >sudo mount -t vboxsf Share /mnt/Share 将Vbox提供的共享文件夹Share挂载到/mnt/Share中。注意第一个Share是从VirturlBox中设置到共享文件夹名称。 之后可以尝试从Windows系统中拷贝文件到共享文件夹,并在Linux系统中去查看。 如果将虚拟的centos重启,则重启后需要重新执行 ? 1 >sudo mount -t vboxsf Share /mnt/Share 才能继续使用共享文件夹。 安装右键打开终端快捷方式 ? 1 2 >sudo yum -y install nautilus-open-terminal --安装nautilus-open-terminal >sudo shutdown –r now --重启后就可以使用了 安装SSH服务 ? 1 2 >sudo yum install openssh.i686 >sudo yum install openssh-server.i686 使用putty访问Vbox中的虚拟CentOS 首先,在Vbox中为该虚拟机设置网络: 选择连接方式为:仅主机(Host-only)适配器,这种方式可以让主机与虚拟机相连,但是虚拟机不能连接外网。 界面名称为:VirtualBox Host-Only Ethernet Adapter 然后重启虚拟系统。 之后CentOS上检设置: 1. 关闭防火墙(可选) ? 1 2 >sudo service iptables stop >sudo chkconfig iptables off 2. 启动ssh服务 ? 1 >sudo service sshd start 3. 查看CentOS 的IP地址: ? 1 >ifconfig -a 看到以下内容:inet addr:192.168.56.101 Bcast:192.168.56.255 Mask:255.255.255.0 然后,回到WIN7下PING 192.168.56.101 ,确保虚拟机有回应。 最后,PuTTY上场,会话方式选择SSH,填入虚拟主机IP地址192.168.56.101. 连接上后: 安装JDK ? 1 2 >sudo yum search jdk –-可选操作 >sudo yum install java-1.8.0-openjdk-devel.i686 安装Redis ? 1 2 3 4 >wget http://download.redis.io/releases/redis-3.0.5.tar.gz >tar xzf redis-3.0.5.tar.gz >cd redis-3.0.5 >make make完后 redis-3.0.5/src目录下会出现编译后的redis服务程序redis-server,还有用于测试的客户端程序redis-cli,现在可以运行redis服务端了: ? 1 >sudo src/redis-server redis.conf 后面的redis.conf参数是redis的配置文件,可以省略,若省略,则使用默认的redis配置 开启服务后,这个窗口是不能运行命令,这个窗口可以关闭,服务不会关闭。运行redis-server后,就可以运行redis客户端redis-cli了,但此操作是可选的: 安装nginx 为了有足够的权限去创建文件,切换到root用户下: ? 1 >su 1.nginx的rewrite模块需要 pcre 库,因此需要先安装pcre 获取pcre编译安装包,在http://www.pcre.org/上可以获取当前最新的版本 解压缩pcre-xx.tar.gz包 进入解压缩目录,执行 ? 1 2 >./configure >make & make install 在安装pcre过程中可能会遇到下面的错误: configure: error: You need a C++ compiler for C++ support 此时需要先安装gcc和gcc-c++: ? 1 >yum install -y gcc gcc-c++ 2.在./configure配置nginx的时候,可能会遇到下面的错误: ./configure: error: the HTTP gzip module requires the zlib library. 此时,应先安装zlib: 获取zlib编译安装包,在http://www.zlib.net/上可以获取当前最新的版本 解压缩zlib-xx.tar.gz包 进入解压缩目录,执行 ? 1 2 >./configure >make & make install 3.安装nginx 和前面的步骤类似,先去http://nginx.org/en/download.html上获取nginx 解压缩nginx-xx.tar.gz ? 1 2 >./configure >make & make install 若安装时找不到上述依赖模块,使用 ? 1 --with-openssl=<openssl_dir> --with-pcre=<pcre_dir> --with-zlib=<zlib_dir> 指定依赖的模块目录。如已安装过,此处的路径为安装目录;若未安装,则此路径为编译安装包路径,nginx将执行模块的默认编译安装。 4. 启动nginx ? 1 2 >cd /usr/local/nginx/sbin >./nginx 启动的时候可能会遇到以下错误: 解决办法: 确认已经安装PCRE: ? 1 2 >cd /lib >ls *pcre* 如果列表中有libpcre.so.0.0.1,则为该文件创建软链: ? 1 >ln –s /lib/libpcre.so.0.0.1 /lib/libpcre.so.1 5. 重新启动nginx ? 1 >./nginx 之后打开浏览器看看: 6. 停止nginx ? 1 >./nginx –s stop 安装Tomcat 下载 http://tomcat.apache.org/download-80.cgi 解压 ? 1 2 >tar zxvf apache-tomcat-xx.tar.gz >cd apache-tomcat-xx.tar.gz 配置环境变量 ? 1 2 >export CATALINA_HOME=/home/user/Software/apache-tomcat-8.0.28 >echo $CATALINA_HOME 启动tomcat ? 1 2 >cd $CATALINA_HOME/bin >./startup.sh 查看日志 ? 1 2 >cd $CATALINA_HOME/logs >cat catalina.out 打开浏览器验证 停止tomcat ? 1 2 >cd $CATALINA_HOME/bin >./shutdown.sh 安装Maven 下载:http://maven.apache.org/download.cgi 解压: ? 1 >tar zxvf apache-maven-3.3.3.tar.gz 配置环境变量: ? 1 >sudo vi /etc/profile 添加下面的内容: ? 1 2 MAVEN_HOME=/home/user/Software/apache-maven-3.3.3 PATH=$MAVEN_HOME/bin:$PATH 保存退出,然后使配置立即生效: ? 1 >source /etc/profile 现在看看吧: 安装SVN 参考:http://subversion.apache.org/packages.html#centos ? 1 >sudo yum –y install subversion 安装Eclipse 下载:http://www.eclipse.org/downloads/?osType=linux ? 1 2 3 >tar zxvf eclipse-jee-mars-1-linux-gtk.tar.gz >cd eclipse >./eclipse --也可以双击打开这个可执行文件 来看看吧: 由于图片太多,篇幅较长,后续部分将写在下一篇博客中。
Java虚拟机的字节码指令集的数量自从Sun公司的第一款Java虚拟机问世至JDK 7来临之前的十余年时间里,一直没有发生任何变化[1]。随着JDK 7的发布,字节码指令集终于迎来了第一位新成员——invokedynamic指令。这条新增加的指令是JDK 7实现“动态类型语言(Dynamically Typed Language)”支持而进行的改进之一,也是为JDK 8可以顺利实现Lambda表达式做技术准备。在这篇文章中,我们将去了解JDK 7这项新特性的出现前因后果和它的意义。 动态类型语言 在介绍JDK 7提供的动态类型语言支持之前,我们要先弄明白动态类型语言是什么?它与Java语言、Java虚拟机有什么关系?了解JDK 7提供动态类型语言支持的技术背景,对理解这个语言特性是很必要的。 什么是动态类型语言[2]?动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,满足这个特征的语言有很多,常用的包括:APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tcl等等。那相对地,在编译期就进行类型检查过程的语言,如C++和Java等就是最常用的静态类型语言。 觉得上面定义过于概念化?那我们不妨通过两个例子以最浅显的方式来说明什么是“在编译期/运行期进行”和什么是“类型检查”。首先看这段简单的Java代码,它是否能正常编译和运行? ? 1 2 3 public static void main(String[] args) { int[][][] array = new int[1][0][-1]; } 这段代码能够正常编译,但运行的时候会报NegativeArraySizeException异常。在《Java虚拟机规范》中明确规定了NegativeArraySizeException是一个运行时异常,通俗一点说,运行时异常就是只要代码不运行到这一行就不会有问题。与运行时异常相对应的是连接时异常,例如很常见的NoClassDefFoundError便属于连接时异常,即使会导致连接时异常的代码放在一条无法执行到的分支路径上,类加载时(Java的连接过程不在编译阶段,而在类加载阶段)也照样会抛出异常。 不过,在C语言里,含义相同的代码的代码就会在编译期报错: ? 1 2 3 4 int main(void) { int i[1][0][-1]; // GCC拒绝编译,报“size of array is negative” return 0; } 由此看来,一门语言的哪一种检查行为要在运行期进行,哪一种检查要在编译期进行并没有必然的因果逻辑关系,关键是在语言规范中人为规定的,再举一个例子来解释“类型检查”,例如下面这一句再普通不过的代码: ? 1 obj.println(“hello world”); 显然,这行代码需要一个具体的上下文才有讨论的意义,假设它在Java语言中,并且变量obj的类型为java.io.PrintStream,那obj的值就必须是PrintStream的子类(实现了PrintStream接口的类)才是合法的。否则,哪怕obj属于一个确实有用println(String)方法,但与PrintStream接口没有继承关系,代码依然不能运行——因为类型检查不合法。 但是相同的代码在ECMAScript(JavaScript)中情况则不一样,无论obj具体是何种类型,只要这种类型的定义中确实包含有println(String)方法,那方法调用便可成功。 这种差别产生的原因是Java语言在编译期间却已将println(String)方法完整的符号引用(本例中为一项CONSTANT_InterfaceMethodref_info常量)生成出来,作为方法调用指令的参数存储到Class文件中,例如下面这个样子: ? 1 invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 这个符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,虚拟机就可以翻译出这个方法的直接引用(譬如方法内存地址或者其他实现形式)。而在ECMAScript等动态类型语言中,变量obj本身是没有类型的,变量obj的值才具有的类型,编译时候最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型(方法接收者不固定)。“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征。 了解了动态和静态类型语言的区别后,也许读者的下一个问题就是动态、静态类型语言两者谁更好,或者谁更加先进?这种比较不会有确切答案,它们都有自己的优点,选择哪种语言是需要经过权衡的。静态类型语言在编译期确定类型,最显著的好处是编译器可以提供严谨的类型检查,这样与类型相关的问题能在编码的时候就及时发现,利于稳定性及代码达到更大的规模。而动态类型语言在运行期确定类型,这可以为开发人员提供更大的灵活性,某些在静态类型语言中要花大量臃肿代码来实现的功能,由动态类型语言来实现可能会很清晰简洁,清晰简洁通常也就意味着开发效率的提升。 JDK 7与动态类型 现在,我们回到本节的主题,来看看Java语言、虚拟机与动态类型语言之间有什么关系。Java虚拟机毫无疑问是Java语言的运行平台,但它的使命并不仅限于此,早在1997年出版的《Java虚拟机规范》第一版中就规划了这样一个愿景:“在未来,我们会对Java虚拟机进行适当的扩展,以便更好的支持其他语言运行于Java虚拟机之上”。而目前确实已经有许多动态类型语言运行于Java虚拟机之上了,如Clojure、Groovy、Jython和JRuby等等,能够在同一个虚拟机之上可以实现静态类型语言的严谨与动态类型语言的灵活,这是一件很美妙的事情。 但遗憾的是Java虚拟机层面对动态类型语言的支持一直都有所欠缺,主要表现在方法调用方面:JDK 7以前字节码指令集中,四条方法调用指令(invokevirtual、invokespecial、invokestatic、invokeinterface)的第一个参数都是被调用的方法的符号引用(CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量),前面已经提到过,方法的符号引用在编译时产生,而动态类型语言只有在运行期才能确定接收者类型。这样,在Java虚拟机上实现的动态类型语言就不得不使用“曲线救国”的方式(如编译时留个占位符类型,运行时动态生成字节码实现具体类型到占位符类型的适配)来实现,这样势必让动态类型语言实现的复杂度增加,也可能带来额外的性能或者内存开销。尽管可以想一些办法(如Call Site Caching)让这些开销尽量变小,但这种底层问题终归是应当在虚拟机层次上去解决才最合适,因此在Java虚拟机层面上提供动态类型的直接支持就成为了Java平台的发展趋势之一,这就是JDK 7(JSR-292)中invokedynamic指令以及java.lang.invoke包出现的技术背景。 java.lang.invoke包 JDK 7实现了JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》,新加入的java.lang.invoke包[3]是就是JSR 292的一个重要组成部分,这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制,称为Method Handle。这个表达也不好懂?那不妨把Method Handle与C/C++中的Function Pointer,或者C#里面的Delegate类比一下。举个例子,如果我们要实现一个带谓词的排序函数,在C/C++中常用做法是把谓词定义为函数,用函数指针来把谓词传递到排序方法,像这样: ? 1 void sort(int list[], const int size, int (*compare)(int, int)) 但Java语言中做不到这一点,没有办法单独把一个函数作为参数进行传递。普遍的做法是设计一个带有compare()方法的Comparator接口,以实现了这个接口的对象作为参数,例如Collections.sort()就是这样定义的: ? 1 void sort(List list, Comparator c) 不过,在拥有Method Handle之后,Java语言也可以拥有类似于函数指针或者委托的方法别名的工具了。下面代码演示了MethodHandle的基本用途,无论obj是何种类型(临时定义的ClassA抑或是实现PrintStream接口的实现类System.out),都可以正确调用到println()方法。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import static java.lang.invoke.MethodHandles.lookup; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; /** * JSR 292 MethodHandle基础用法演示 * @author IcyFenix */ public class MethodHandleTest { static class ClassA { public void println(String s) { System.out.println(s); } } public static void main(String[] args) throws Throwable { Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA(); // 无论obj最终是哪个实现类,下面这句都能正确调用到println方法。 getPrintlnMH(obj).invokeExact("icyfenix"); } private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable { // MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数) // 和具体参数(methodType()第二个及以后的参数)。 MethodType mt = MethodType.methodType(void.class, String.class); // lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定 // 的方法名称、方法类型,并且符合调用权限的方法句柄。 // 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表 // 该方法的接收者,也即是this指向的对象,这个参数以前是放在参数列表中进行传递, // 现在提供了bindTo()方法来完成这件事情。 return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver); } } 方法getPrintlnMH()中实际上是模拟了invokevirtual指令的执行过程,只不过它的分派逻辑并非固化在Class文件的字节码上的,而是通过一个具体方法来实现。而这个方法本身的返回值(MethodHandle对象),可以视为对最终调用方法的一个“引用”。以此为基础,有了MethodHandle就可以写出类似于这样的函数声明了: ? 1 void sort(List list, MethodHandle compare) 从上面的例子看来,使用MethodHandle并没有多少困难,不过看完它的用法之后,读者大概就会疑问到,相同的事情,用反射不是早就可以实现了吗? 确实,仅站在Java语言的角度看,MethodHandle的使用方法和效果上与Reflection都有众多相似之处。不过,它们也有以下这些区别: Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的三个方法findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual & invokeinterface和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。 Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级。 由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。 MethodHandle与Reflection除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后:Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已。 invokedynamic指令 本文一开始就提到了JDK 7为了更好地支持动态类型语言,引入了第五条方法调用的字节码指令invokedynamic,但前面一直没有再提到它,甚至把之前使用MethodHandle的示例代码反编译后也不会看见invokedynamic的身影,它到底有什么应用呢? 某种程度上可以说invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原有四条invoke*指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包含其他语言的设计者)有更高的自由度。而且,它们两者的思路也是可类比的,可以想象作为了达成同一个目的,一个用上层代码和API来实现,另一个是用字节码和Class中其他属性和常量来完成。因此,如果前面MethodHandle的例子看懂了,理解invokedynamic指令并不困难。 每一处含有invokedynamic指令的位置都被称作“动态调用点(Dynamic Call Site)”,这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法上。我们还是照例拿一个实际例子来解释这个过程吧。如下面代码清单所示: ? 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 import static java.lang.invoke.MethodHandles.lookup; import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class InvokeDynamicTest { public static void main(String[] args) throws Throwable { INDY_BootstrapMethod().invokeExact("icyfenix"); } public static void testMethod(String s) { System.out.println("hello String:" + s); } public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable { return new ConstantCallSite( lookup.findStatic(InvokeDynamicTest.class, name, mt)); } private static MethodType MT_BootstrapMethod() { return MethodType.fromMethodDescriptorString( "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;" + "Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null); } private static MethodHandle MH_BootstrapMethod() throws Throwable { return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod()); } private static MethodHandle INDY_BootstrapMethod() throws Throwable { CallSite cs = (CallSite) MH_BootstrapMethod() .invokeWithArguments(lookup(), "testMethod", MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null)); return cs.dynamicInvoker(); } } 这段代码与前面MethodHandleTest的作用基本上是一样的,虽然笔者没有加以注释,但是阅读起来应当不困难。真没读懂也不要紧,我没写注释的原因是这段代码并非写给人看的(@_ @,我不是在骂人)。由于目前光靠Java语言的编译器javac没有办法生成带有invokedynamic 指令的字节码(曾经有一个java.dyn.InvokeDynamic的语法糖可以实现,但后来被取消了),所以只能用一些变通的办法,John Rose(Da Vinci Machine Project的Leader)编写了一个把程序的字节码转换为使用invokedynamic的简单工具INDY[4]来完成这件事情,我们要使用这个工具来产生最终要的字节码,因此这个示例代码中的方法名称不能乱改,更不能把几个方法合并到一起写。 把上面代码编译、转换后重新生成的字节码如下(结果使用javap输出,因版面原因,精简了许多无关的内容): ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Constant pool: #121 = NameAndType #33:#30 // testMethod:(Ljava/lang/String;)V #123 = InvokeDynamic #0:#121 // #0:testMethod:(Ljava/lang/String;)V public static void main(java.lang.String[]) throws java.lang.Throwable; Code: stack=2, locals=1, args_size=1 0: ldc #23 // String abc 2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V 7: nop 8: return public static java.lang.invoke.CallSite BootstrapMethod(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.invoke.MethodType) throws java.lang.Throwable; Code: stack=6, locals=3, args_size=3 0: new #63 // class java/lang/invoke/ConstantCallSite 3: dup 4: aload_0 5: ldc #1 // class org/fenixsoft/InvokeDynamicTest 7: aload_1 8: aload_2 9: invokevirtual #65 // Method java/lang/invoke/MethodHandles$Lookup.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle; 12: invokespecial #71 // Method java/lang/invoke/ConstantCallSite."<init>":(Ljava/lang/invoke/MethodHandle;)V 15: areturn 从main()方法的字节码中可见,原本的方法调用指令已经被替换为invokedynamic了,它的参数为第123项常量(第二个值为0的参数在HotSpot中用不到,与invokeinterface那个的值为0的参数一样是占位的): ? 1 2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V 从常量池中可见,第123项常量显示“#123 = InvokeDynamic #0:#121”说明它是一项CONSTANT_InvokeDynamic_info类型常量,常量值中前面“#0”代表引导方法取BootstrapMethods属性表的第0项(javap没有列出属性表的具体内容,不过示例中仅有一个引导方法,即BootstrapMethod()),而后面的“#121”代表引用第121项类型为CONSTANT_NameAndType_info的常量,从个常量中可以获取方法名称和描述符,既后面输出的“testMethod:(Ljava/lang/String;)V”。 再看BootstrapMethod(),这个方法Java源码中没有,是INDY产生的,但是它的字节码很容易读懂,所有逻辑就是调用MethodHandles$Lookup的findStatic()方法,产生testMethod()方法的MethodHandle,然后用它创建一个ConstantCallSite对象。最后,这个对象返回给invokedynamic指令实现对testMethod()方法的调用,invokedynamic指令的调用过程到此就宣告完成了。 参考资料 http://java.sun.com/developer/technicalArticles/DynTypeLang/ http://www.iteye.com/topic/477934 https://wikis.oracle.com/display/mlvm/InterfaceDynamic#InterfaceDynamic-2.dynamicinvocation [1] 这里特指数量上没有变化。前面有少量指令的语义、格式发生过变化,例如invokespecial指令,也有少量指令在JDK 7中被禁用掉,例如jsr、jsr_w指令。 [2] 动态类型语言与动态语言、弱类型语言并不是一个概念,需要区别对待。 [3] 这个包在不算短的时间里的名称是java.dyn,也曾经短暂更名为java.lang.mh,如果读者在其他资料上看到这两个包名可以把它们与java.lang.invoke理解为同一样东西。 [4] INDY:http://blogs.oracle.com/jrose/entry/a_modest_tool_for_writing。 原文地址:http://www.infoq.com/cn/articles/jdk-dynamically-typed-language/
据日志级别设置来决定是否发送到mq,不然会大量占用网络资源。于是经过了一番搜索后,实现了这个功能。现在记录在这里。 目标:将debug,info级别的日志输出到本地文件,将warn,error级别的日志输出到ActiveMQ。 说明:本文还是使用之前的两个项目:Product和Logging。 经过一番搜索后,发现log4j还可以按照级别过滤日志,但过滤只能使用log4j.xml配置: Filters can be defined at appender level. For example, to filter only certain levels, the LevelRangeFilter can be used like this: ? 1 2 3 4 5 6 7 8 9 10 <appender name="TRACE" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="[%t] %-5p %c - %m%n" /> </layout> <filter class="org.apache.log4j.varia.LevelRangeFilter"> <!-- 注意这个min和max是相等的 --> <param name="levelMin" value="DEBUG" /> <param name="levelMax" value="DEBUG" /> </filter> </appender> 在搜索资料的过程中,也看到了有网友说log4j.properties方式也可以实现按级别过滤日志,具体步骤请参看《Log4j按级别输出日志到不同文件配置分析》。此种方式的缺点是,如果有多个Appender,则需要多个继承的类(每个Appender需要重新定义一个),因此感觉不如log4j.xml方式通过为appender配置filter来的直接。 然后我在这里找到了一份log4j.xml样本:https://code.google.com/p/log4j-jms-sample/source/browse/trunk/src/main/resources/log4j.xml,拿过来后,我只是在jms appender里面增加了一个filter而已: ? 1 2 3 4 5 6 7 8 9 10 11 12 <appender name="jms" class="org.apache.log4j.net.JMSAppender"> <param name="InitialContextFactoryName" value="org.apache.activemq.jndi.ActiveMQInitialContextFactory" /> <param name="ProviderURL" value="tcp://localhost:61616" /> <param name="TopicBindingName" value="logTopic" /> <param name="TopicConnectionFactoryBindingName" value="ConnectionFactory" /> <!-- Only log WARN & ERROR msg --> <filter class="org.apache.log4j.varia.LevelRangeFilter"> <param name="LevelMin" value="WARN" /> <param name="LevelMax" value="ERROR" /> </filter> </appender> 而Product项目的测试代码相当简单: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 package com.demo.product; import org.apache.log4j.Logger; public class Main{ public static void main(String[] args) throws Exception { Logger logger = Logger.getLogger(Main.class); logger.debug("Debug"); logger.info("Info"); logger.warn("Warn"); logger.error("Error"); logger.fatal("Fatal"); System.exit(0); } } 我以为就这样配置就能让WARN和ERROR级别的日志输出到jms了,但是我运行的时候却报错了: ? 1 2 3 4 5 6 javax.jms.JMSException: Wire format negotiation timeout: peer did not send his wire format. at org.apache.activemq.util.JMSExceptionSupport.create(JMSExceptionSupport.java:62) at org.apache.activemq.ActiveMQConnection.syncSendPacket(ActiveMQConnection.java:1395) at org.apache.activemq.ActiveMQConnection.ensureConnectionInfoSent(ActiveMQConnection.java:1481) at org.apache.activemq.ActiveMQConnection.createSession(ActiveMQConnection.java:323) at org.apache.activemq.ActiveMQConnection.createTopicSession(ActiveMQConnection.java:1112) 搜索这个问题,有很多人遇到了,这里列出了三种可能的原因: 1. You're connecting to the port not used by ActiveMQ TCP transport Make sure to check that you're connecting to the appropriate host:port 2. You're using log4j JMS appender and doesn't filter out ActiveMQ log messages Be sure to read How do I use log4j JMS appender with ActiveMQ and more importantly to never send ActiveMQ log messages to JMS appender 3. Your broker is probably under heavy load (or network connection is unreliable), so connection setup cannot be completed in a reasonable time If you experience sporadic exceptions like this, the best solution is to use failover transport, so that your clients can try connecting again if the first attempt fails. If you're getting these kind of exceptions more frequently you can also try extending wire format negotiation period (default 10 sec). You can do that by using wireFormat.maxInactivityDurationInitalDelay property on the connection URL in your client. For example: tcp://localhost:61616?wireFormat.maxInactivityDurationInitalDelay=30000 第一种情况显然不是。 第三种情况,由于我就一个jms connection,也没有往这个连接发送jms消息,所以不可能负载过重。 第二种情况是不要把activemq的日志发送到JMSAppender了,How do I use log4j JMS appender with ActiveMQ 一文中有以下配置: ? 1 2 ## Be sure that ActiveMQ messages are not logged to 'jms' appender log4j.logger.org.apache.activemq=INFO, stdout 上面的意思是,对于org.apache.activemq包下的INFO级别以上的日志,都输出到stdout appender中。我对比了一下,从拷贝而来的log4j.xml中也包含了类似的配置: ? 1 2 3 4 <!-- Be sure that ActiveMQ messages are not logged to 'jms' appender --> <logger name="org.apache.activemq"> <appender-ref ref="console" /> </logger> 但是为何结果还是这样?几经思考,我重新查看了一下报错的日志: 后面的内容是这样的: ? 1 org.apache.activemq.transport.WireFormatNegotiator.negociate-118 | Received WireFormat ... 于是我去找到这个类,在这个negociate方法上打上断点(Maven项目的好处还包括可以自动下载jar包对应版本的源代码),开始调试,然后发现是这一句报错: 然后我想了想能不能不打印这个debug消息呢,于是我在开始的org.apache.activemq包中加上了level限制: ? 1 2 3 4 <logger name="org.apache.activemq"> <level value="INFO" /> <appender-ref ref="console" /> </logger> 这样以后,问题解决。其实,只要我稍微细心一点,可以发现 ? 1 log4j.logger.org.apache.activemq=INFO, stdout 这个配置不仅指明了org.apache.activemq包下的日志信息输出到stdout这个appender中,而且还指明了只有INFO以上的级别才能输出。二者同时指定才能达到目的,这在刚刚的xml文件中也得到体现。 现在,WARN和ERROR级别的日志就可以输出到ActiveMQ了: 在Logging项目中,和之前一样,LogMessageListener也只是简单打印了级别和内容: ? 1 2 3 4 5 6 7 8 9 public void onMessage(Message message) { LoggingEvent event; try { event = (LoggingEvent)((ActiveMQObjectMessage)message).getObject(); System.out.println("[" + event.getLevel() + "] | " + event.getMessage()); } catch (JMSException e) { e.printStackTrace(); } } 从结果中能看到输出的日志级别仅仅包括了WARN和ERROR: 至于剩下的DEBUG和INFO级别的日志,则直接配置输出到RollingFileAppender即可。日志文件的内容也当然和预期一样了: 最后贴出完整的log4j.xml配置内容: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <!-- Console Appender, used to record activemq log. --> <appender name="console" class="org.apache.log4j.ConsoleAppender"> <param name="Target" value="System.out" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="[Log4j-JMS-Sample] %d{yyyy-MM-dd HH:mm:ss,SSS} %p [%t] %c.%M-%L | %m%n" /> </layout> </appender> <!-- File Appender, used to record debug & info log. --> <appender name="file" class="org.apache.log4j.RollingFileAppender"> <param name="File" value="C:\\logs\\log_debug_info.log" /> <param name="Append" value="true" /> <param name="MaxFileSize" value="500KB" /> <param name="MaxBackupIndex" value="2" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%c %d{ISO8601} [%p] -- %m%n" /> </layout> <!-- Only log DEBUG & INFO msg --> <filter class="org.apache.log4j.varia.LevelRangeFilter"> <param name="LevelMin" value="DEBUG" /> <param name="LevelMax" value="INFO" /> </filter> </appender> <!-- JMS Appender, used to record warn & info log. --> <appender name="jms" class="org.apache.log4j.net.JMSAppender"> <param name="InitialContextFactoryName" value="org.apache.activemq.jndi.ActiveMQInitialContextFactory" /> <param name="ProviderURL" value="tcp://localhost:61616" /> <param name="TopicBindingName" value="logTopic" /> <param name="TopicConnectionFactoryBindingName" value="ConnectionFactory" /> <!-- Only log WARN & ERROR msg --> <filter class="org.apache.log4j.varia.LevelRangeFilter"> <param name="LevelMin" value="WARN" /> <param name="LevelMax" value="ERROR" /> </filter> </appender> <!-- Log in org.apache.activemq are logged to console. --> <logger name="org.apache.activemq"> <level value="INFO" /> <appender-ref ref="console" /> </logger> <root> <priority value="DEBUG" /> <appender-ref ref="console" /> <appender-ref ref="jms" /> <appender-ref ref="file" /> </root> </log4j:configuration> 当然,如果希望再把info和debug分开,可以多配置一个fileappender,让每个过滤器的LevelMax和LevelMin的值相等并为它们配置不同的文件即可。
今天在Linux下源码安装好MySQL后,将mysql添加到系统的服务的过程引起了我的兴趣,我能不能自己写一个简单的脚本,也添加为系统的服务呢? 于是开干: ? 1 2 su vi myservice 然后模仿着mysql在里面开写: ? 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 #!/bin/bash start() { echo 'This is my service, start command' echo '' } stop() { echo 'This is my service, stop command' echo '' } restart() { echo 'This is my service, restart command' echo '' } status() { echo 'This is my service, status command' echo '' } case "$1" in start) start ;; stop) stop ;; restart) restart ;; status) status ;; *) echo 'Usage: service myservice {start|status|stop|restart}' echo '' exit 1 esac exit 0 很简单,myservice脚本执行时需要接受一个参数,这个参数可以是start, status, stop, restart中间的一个,收到参数后,仅仅做回显使用。写好之后,在拷贝到/etc/init.d/下面去,增加可执行权限,并添加到系统服务: ? 1 2 3 cp myservice /etc/init.d/myservice chmod +x /etc/init.d/myservice chkconfig --add myservice 然后就报错了: google之,发现是chkconfig的注释不能少: The script must have 2 lines: # chkconfig: <levels> <start> <stop> # description: <some description> 之后再打开/etc/init.d/mysql,看看哪里不对,结果发现里面真有这个注释: 然后自己也跟着写了个这样的注释,于是脚本就变成了: ? 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 #!/bin/bash # For example: following config would generate link # S51myservice in rc2.d, rc3.d, rc4.d, rc5.d and # K49myservice in rc0.d, rc1.d, rc6.d # Comments to support chkconfig on RedHat Linux # run level, start order, stop order # chkconfig: 2345 51 49 # description: Customized service written by Alvis. start() { echo 'This is my service, start command' echo '' } stop() { echo 'This is my service, stop command' echo '' } restart() { echo 'This is my service, restart command' echo '' } status() { echo 'This is my service, status command' echo '' } case "$1" in start) start ;; stop) stop ;; restart) restart ;; status) status ;; *) echo 'Usage: service myservice {start|status|stop|restart}' echo '' exit 1 esac exit 0 这下好了,再次执行chkconfig: 去看看/etc/rc.d/rc2.d,/etc/rc.d/rc3.d,/etc/rc.d/rc4.d,/etc/rc.d/rc5.d下面都生成了什么: 可以看到,在rc2.d和rc3.d目录下都生成了一个链接S51myservice,S代表在该运行级别启动,51代表该服务的启动优先级。同理可推,在rc0.d、rc1.d和rc6.d这三个文件夹下会生成K49myservice链接: 接下来,可以测试刚刚编写的service 了: 到这里,简单的service就算是编写完成了。真正的service只是包含了更多的逻辑而已,本质上和这个例子没什么区别。
准备步骤 1. 安装Maven,下载解压即可。官网下载 2. 修改maven_home/conf/settings.xml中的<localRepository>D:/MavenRepo</localRepository>指定本地仓库位置,这个位置是本地计算机上用来存放所有jar包的地方。 3. 修改settings.xml中的<mirrors></mirrors>标签,添加常用的maven远程仓库地址。这些仓库地址就是用来下载jar包的时候用的。由于中央仓库的访问速度较慢(或者因为某些原因导致你根本不能访问),因此一般需要设置其他的仓库地址以提高访问速度。比如: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <mirror> <id>oschina</id> <mirrorOf>central</mirrorOf> <url>http://maven.oschina.net/content/groups/public/</url> </mirror> <mirror> <id>repo2</id> <mirrorOf>central</mirrorOf> <url>http://repo2.maven.org/maven2/</url> </mirror> <mirror> <id>net-cn</id> <mirrorOf>central</mirrorOf> <url>http://maven.net.cn/content/groups/public/</url> </mirror> 如果使用mvn命令行来创建、构建和运行maven项目,则需要配置环境变量,路径指向maven_home/bin即可。配置好后,可以查看mvn命令: 由于使用命令太麻烦而且难记,我直接使用Eclipse的maven插件来创建和运行maven项目。 4. 在Eclipse中集成Maven。 先安装Eclipse的maven插件(具体过程网上一大堆,比如:安装Eclipse Maven插件的几种方法) 在Eclipse中通过Windows->Preferences->Maven菜单下指定安装的maven: 并指定自己的配置文件settings.xml: 创建Maven项目 5. New->Maven Project->Next,选择webapp类型的项目结构。由于不同类型的项目有不同的项目结构,因此Maven自带了很多套项目骨架(archetype),这里我们选择webapp类型的骨架即可: 6. 输入Group ID, Artifact ID, Version和Package, Finish. 7. 创建好后如图,默认情况下已经将junit3.8导入到项目中: 8. 先把默认使用的JRE环境替换成当前Eclipse中使用的JRE环境。 9. 每个Maven项目都有一个pom.xml文件,这个文件描述了这个项目的依赖关系,以及自身的一些属性,包括properties的定义,以及Maven Modules的版本声明,父模块以及子模块的名字等。同时这个文件还包括了该项目在构建过程中做的事情的定义。现在打开这个pom.xml文件,先在<dependencies>标签上方添加该项目用到的属性定义(为了集中管理spring的版本,因此将其定义为属性,在依赖spring的jar包时直接使用这个属性即可): ? 1 2 3 4 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>4.0.0.RELEASE</spring.version> </properties> 并在<dependencies></dependencies>标签中添加如下依赖关系,其他的内容无需修改: ? 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 <dependencies> <!-- MyBatis相关 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.0</version> </dependency> <!-- MySQL相关 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.36</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <!-- Spring相关,这里的spring.version就是上方声明的版本号,这样引用更方便修改和维护 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-ibatis</artifactId> <version>2.0.8</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- 测试相关 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <!-- Servlet相关 --> <dependency> <groupId>tomcat</groupId> <artifactId>servlet-api</artifactId> <version>5.5.23</version> </dependency> <!-- Log相关 --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> </dependencies> 10. 在Maven的世界中,每一个jar包都可以通过Group ID, Artifact ID, Version这三个字段(一般简写为GAV)来唯一定位,因此如果需要使用哪个jar包,只需要提供这三个字段即可。 如果不知道版本号或者GroupID,可以去公共的Maven仓库搜索关键字。比如搜索:log4j,即可出现仓库中已经收录的关于log4j的jar包: 如图,我在oschina提供的maven库中搜索log4j,出现了一些可用的jar包列表(这里需要注意:有些jar包名称看上去很相近,因此需要注意区别,选择正确的jar包)。选择某一个,右下方会有该包的GAV属性。直接将这一段拷贝到maven项目pom.xml文件中即可。 还有一个很好用的maven仓库地址,推荐给大家:http://mvnrepository.com/ 11. Jar包准备完毕后,开始项目接口的定义了。修改后的结构如图: 12. web.xml仅仅定义了基本的DispatchServlet,用于转发请求: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> 13. spring.xml(xml头有点冗余,如果觉得用不到,可以删除相应的xmlns和schemaLocation声明) ? 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 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <context:component-scan base-package="com.abc" /> <!-- 属性注入器,用于读取项目配置文件中的属性 --> <bean id="PropertiesConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:log4j.properties</value> <value>classpath:jdbc.properties</value> </list> </property> </bean> <!-- 数据源,不需要解释 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="${jdbc.driverClassName}" /> <property name="jdbcUrl" value="${jdbc.url}" /> <property name="user" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </bean> <!-- SqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <!-- <property name="mapperLocations" value="classpath*:com/abc/dao/*.xml" /> --> <property name="configLocation" value="classpath:mybatis-config.xml" /> </bean> <!-- Mybatis sql session --> <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> </bean> <!-- Mybatis mapper scanner, scans for java mapper --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.abc.dao" /> <property name="sqlSessionTemplateBeanName" value="sqlSession" /> </bean> </beans> 14. log4j.properties,用于定义Log4j的日志输出内容及格式,我这里就不凑字数了。 jdbc.properties,上方的配置中引用到的关于数据库的配置,请在这个文件中配置。 ? 1 2 3 4 jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc\:mysql\://192.168.12.1\:3306/abc?useUnicode\=true&amp;characterEncoding\=UTF-8 jdbc.username=abc jdbc.password=abc123_ 15. mybatis-config.xml文件,这里面指定了哪些xml文件可以作为DAO接口的映射文件: ? 1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <mappers> <mapper resource="com/abc/entity/UserMap.xml"/> </mappers> </configuration> 16. UserMap.xml文件定义了对于User对象的操作的sql语句: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" " <mapper namespace="com.abc.dao.TestDao"> <resultMap id="UserResultMap" type="com.abc.entity.User"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="userName" jdbcType="VARCHAR" property="name" /> <result column="userAge" jdbcType="INTEGER" property="age" /> <result column="userAddress" jdbcType="VARCHAR" property="address" /> </resultMap> <select id="testQuery" resultMap="UserResultMap"> SELECT * FROM user </select> </mapper> 17. Controller, Service和DAO的声明,都是很标准很简单的Controller调用Service,Service再调用DAO接口的过程。 TestDao(完成数据读写): ? 1 2 3 4 5 6 7 8 package com.abc.dao; import java.util.List; import com.abc.entity.User; public interface TestDao { public List<User> testQuery() throws Exception; } TestService(接口编程,在面向多实现的时候非常有用): ? 1 2 3 4 5 package com.abc.service; public interface TestService { public String testQuery() throws Exception; } TestServiceImpl(完成主要的业务逻辑): ? 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 package com.abc.service.impl; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.abc.dao.TestDao; import com.abc.entity.User; import com.abc.service.TestService; @Service public class TestServiceImpl implements TestService { @Autowired private TestDao dao; public String testQuery() throws Exception { List<User> users = dao.testQuery(); String res = ""; if (users != null && users.size() > 0) { for (User user : users) { res += user.toString() + "|"; } } else { res = "Not found."; } return res; } } TestController(完成请求转发,响应封装): ? 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 package com.abc.controller; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.abc.service.TestService; @Controller @RequestMapping("/testController") public class TestController { public static final Logger LOGGER = Logger.getLogger(TestController.class); @Autowired private TestService testService; @RequestMapping("/test") public void test(HttpServletRequest request, HttpServletResponse response) { try { String result = testService.testQuery(); response.getWriter().print(result); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } } 代码部分到此就结束了。 构建和运行 18. 编写好以后,在项目上右键->Run As->Maven Build…准备构建这个maven项目。 19. 在BaseDirectory中指定需要构建的项目(点击图中的Brose Workspace或Browse File System按钮可以选择),并在Goals框中指定构建的目标(Maven有自己的构建的阶段,有的地方又叫生命周期,如果不清楚的同学,可以参看Maven生命周期详解)。并可以选择一些附加的属性(绿色框中),如图: 20. 如果构建成功,则会出现类似于下面的输出: 21. 当构建成功后,可以像普通的Web Project一样来运行这个项目。这里将其添加到Tomcat中,并启动之。 22. 先看看数据库的内容: 23. 在浏览器中访问指定的接口,查看结果(在我的实现类TestServiceImpl中,仅仅是打印了查询到的结果): 附:例子下载:AbcDemo.zip 链接: http://pan.baidu.com/s/1pJ3pSBT 密码: 3gpt
有这么一个文件,它在Eclipse属性中看到是UTF8编码的,里面包含了中文: 但是当在Windows控制台中查看这个文件(为了方便,我将文件拷贝到桌面了)的时候,就是乱码了: 那如何让cmd可以显示这些UTF8编码的字符呢?这里需要先了解些相关内容: chcp命令 chcp是MS DOS中的命令,用来显示或设置活动代码页编号的。用法是: ? 1 2 3 4 5 6 7 8 C:\Users\002778\Desktop>chcp /? 显示或设置活动代码页编号。 CHCP [nnn] nnn 指定代码页编号。 不带参数键入 CHCP 以显示活动代码页编号。 比如,在默认的cmd窗口中,我们输入chcp,显示的将类似: ? 1 2 C:\Users\002778\Desktop>chcp 活动代码页: 936 这里的936表示当前使用的是简体中文(GB2312)编码。更多代码页编号请查阅这里。 UTF8编码 你也需要了解编码的一些知识,为了完成支持UTF8的工作,你至少需要知道UTF8代码页的编号:65001。更多关于编码的内容,这里不赘述,请自行查找相关内容。 有这两个知识点,接下来,让cmd支持UTF8就变得容易了。 1. 运行cmd; 2. 输入 chcp,回车查看当前的编码; 3. 输入chcp 65001,将输出: ? 1 2 3 Active code page: 65001 C:\Users\002778\Desktop> 4. 如果仅如此,有可能还是不能支持UTF8的正常显示,你还要在窗体上右键,选择属性,来设置字体。在之前的936当中,是没有Lucida Console这个字体的,但是切换到65001后,就能看到了: 6. 选择Lucida Console并只应用到本窗体,确认,然后再试试: 这样就用cmd成功的显示UTF8的字符了。
花了一下午,解决MySQL在Windows的cmd下中文乱码的问题。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 mysql> use abc; Database changed mysql> select * from school; +----------+--------------------+-------------------------------------------+ | schoolid | name | address | +----------+--------------------+-------------------------------------------+ | 1 | 鍖椾含澶у | 鍖椾含甯備腑鍏虫潙鍖楀ぇ琛?7鍙? | 2 | 娓呭崕澶у | 鍖椾含甯傛捣娣€鍖轰腑鍏虫潙澶ц | 3 | 鍗椾含澶у | 姹熻嫃鐪佸崡浜競榧撴ゼ鍖烘眽鍙h矾22鍙? | 4 | 涓浗浜烘皯澶у | 鍖椾含甯傛捣娣€鍖轰腑鍏虫潙澶ц59鍙? | 5 | 鍘﹂棬澶у | 绂忓缓鐪佸帵闂ㄥ競鎬濇槑鍗楄矾422鍙? +----------+--------------------+-------------------------------------------+ 5 rows in set (0.00 sec) 数据是通过SQL文件导入的,这个SQL文件也是UTF8编码的: 数据库、表都重建了,数据文件也保证是UTF8了,但cmd窗口中还是乱码。。。伤心。。。 首先,安装MySQL的时候,我很清楚的记得我设置的编码为UTF8,所以在my.ini文件中: ? 1 2 3 4 5 6 7 # ... [mysql] default-character-set=utf8 # ... [mysqld] # ... character-set-server=utf8 然后再mysql中验证: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 mysql> show variables like '%character%'; +--------------------------+------------------------------------ | Variable_name | Value +--------------------------+------------------------------------ | character_set_client | utf8 | character_set_connection | utf8 | character_set_database | utf8 | character_set_filesystem | binary | character_set_results | utf8 | character_set_server | utf8 | character_set_system | utf8 | character_sets_dir | C:\Program Files (x86)\MySQL\MySQL +--------------------------+------------------------------------ 8 rows in set (0.00 sec) 全部都是utf8编码。接下来验证数据库的默认编码: ? 1 2 3 4 5 6 7 mysql> show create database abc; +----------+--------------------------------------------------------------+ | Database | Create Database | +----------+--------------------------------------------------------------+ | abc | CREATE DATABASE `abc` /*!40100 DEFAULT CHARACTER SET utf8 */ | +----------+--------------------------------------------------------------+ 1 row in set (0.00 sec) 可以看到,数据库的默认编码是utf8。再来验证数据表的默认编码: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 mysql> show create table school; +--------+--------------------------------------------+ | Table | Create Table | +--------+--------------------------------------------+ | school | CREATE TABLE `school` ( `schoolid` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(45) NOT NULL, `address` varchar(200) NOT NULL, `phone` varchar(11) NOT NULL, `master` varchar(10) NOT NULL, PRIMARY KEY (`schoolid`), UNIQUE KEY `master_UNIQUE` (`phone`), UNIQUE KEY `address_UNIQUE` (`address`), UNIQUE KEY `name_UNIQUE` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 | +--------+--------------------------------------------+ 可以看到,表的编码依然为utf8,那么为什么还是会乱码呢??百度谷歌都找遍了,还是乱码,然并卵。郁闷半天,无意中在一个网页上看到有人说“这是因为Windows的cmd默认编码是GBK,MySQL里面为UTF8,自然就乱码了,改用工具试试”,这才一语惊醒梦中人,于是使用工具查询数据库,结果是这样滴: 这才发现其实MySQL早就按照我设置的UTF编码存储数据了。我晕,竟然被cmd的外表蒙骗了。。。。。。那么,有没有办法设置让cmd也正确显示UTF8的编码呢??又去百度了。。。结果是:还真有! 在cmd中登录mysql后,在输入sql语句前,先设置编码:set names gbk; ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 mysql> set names gbk; Query OK, 0 rows affected (0.00 sec) mysql> select * from school; +----------+--------------+------------------------------+- | schoolid | name | address | +----------+--------------+------------------------------+- | 1 | 北京大学 | 北京市中关村北大街47号 | 2 | 清华大学 | 北京市海淀区中关村大街 | 3 | 南京大学 | 江苏省南京市鼓楼区汉口路2 | 4 | 中国人民大学 | 北京市海淀区中关村大街 | 5 | 厦门大学 | 福建省厦门市思明南路422号 +----------+--------------+------------------------------+- 5 rows in set (0.00 sec) 那么,为什么这样设置后,就能正确显示以UTF8存储的数据了呢? ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 mysql> show variables like '%character%'; +--------------------------+----------------------------------- | Variable_name | Value +--------------------------+----------------------------------- | character_set_client | gbk | character_set_connection | gbk | character_set_database | utf8 | character_set_filesystem | binary | character_set_results | gbk | character_set_server | utf8 | character_set_system | utf8 | character_sets_dir | C:\Program Files (x86)\MySQL\MySQL +--------------------------+----------------------------------- 8 rows in set (0.00 sec) 可以看到,client,connection和result的编码已经设置为gbk了,但server,database,filesystem的编码还是utf8!这就是说,虽然数据依然是使用utf8编码存储的,但是客户端以及返回的结果集是gbk的,而此时cmd窗口的编码正好是gbk,因此该结果集能正确显示了。 搞了半天,结果是这样。。。但这样也好,毕竟让我印象深刻了,应该不会有下次了。 完。
今天整理了Maven的pom.xml文件后,把多个项目用maven集成在了一起,结果在启动Tomcat的时候,遇到一个奇葩的错误: ? 1 2 3 4 5 6 7 8 9 10 严重: Servlet [spring] in web application [/AbcWeb] threw load() exception java.lang.IncompatibleClassChangeError: class org.springframework.core.type.classreading.ClassMetadataReadingVisitor has interface org.springframework.asm.ClassVisitor as super class at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:800) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at org.apache.catalina.loader.WebappClassLoaderBase.findClassInternal(WebappClassLoaderBase.java:2495) at org.apache.catalina.loader.WebappClassLoaderBase.findClass(WebappClassLoaderBase.java:859) at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1301) at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1166) at java.lang.ClassLoader.defineClass1(Native Method) 重启Tomcat,Project>Clean都玩遍了,也没找到问题在哪里。。。于是Google之,结果从这个页面里找到了这样一段话: Your newly packaged library is not backward binary compatible (BC) with old version. For this reason some of the library clients that are not recompiled may throw the exception. This is a complete list of changes in Java library API that may cause clients built with an old version of the library to throw java.lang.IncompatibleClassChangeError if they run on a new one (i.e. breaking BC): Non-final field become static, Non-constant field become non-static, Class become interface, Interface become class, if you add a new field to class/interface (or add new super-class/super-interface) then a static field from a super-interface of a client class C may hide an added field (with the same name) inherited from the super-class of C (very rare case). 其实,细心一点可以发现,错误描述其实很清晰了: ? 1 java.lang.IncompatibleClassChangeError: class org.springframework.core.type.classreading.ClassMetadataReadingVisitor has interface org.springframework.asm.ClassVisitor as super class 意思是说,有个叫ClassMetadataReadingVisitor的类,以一个叫ClassVisitor的接口作为父类了。但是大家都知道,Java中类和接口的关系只能是实现,而不是继承。那么为什么会出现这个类呢?我尝试着在Eclipse中打开这个报错的类: 可以看到,我的Workspace中可以发现两个版本(3.2.6和4.0.0)的ClassMetadataReadingVisitor类,分别打开这两个类(Maven会自动下载源代码),可以看到类的声明均为: ? 1 class ClassMetadataReadingVisitor extends ClassVisitor implements ClassMetadata 于是我又打开错误描述中提到的ClassVisitor这个类,结果却是这样的: 可以看到,Eclipse在Workspace中发现了3个这样的类,而且包名类名都完全一样。可以看到,在spring的3.1.4版本中,这个叫ClassVisitor的类其实是一个接口,这个接口被放在spring-asm模块中。而在3.2.6和4.0.0版本中,这个ClassVisitor就变成了一个抽象类。 发现这个差别后,我检查了我的spring-core模块的版本: ? 1 2 3 4 5 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>3.2.6</version> </dependency> 但同时我的pom.xml中还有这样一个包的依赖: ? 1 2 3 4 5 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-asm</artifactId> <version>3.1.4.RELEASE</version> </dependency> 正好这个3.1.4的接口就在Workspace中,于是这两个包当中都有这个ClassVisitor,删掉spring-asm模块后,错误消失,问题解决。 这个spring-asm包也不知道什么时候导入的,这就告诉我们: 在使用Maven处理依赖包的时候,一定不要随便乱添加依赖,用到的包才导入,没用到包的声明要及时删掉。 在整合多个maven项目的时候,要注意依赖包的版本,因为有些类(可能包含有的字段)在不同的版本中声明可能会不同,错误的类或字段声明将导致IncompatibleClassChangeError。
提到Java序列化,相信大家都不陌生。我们在序列化的时候,需要将被序列化的类实现Serializable接口,这样的类在序列化时,会默认将所有的字段都序列化。那么当我们在序列化Java对象时,如果不希望对象中某些字段被序列化(如密码字段),怎么实现呢?看一个例子: ? 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 import java.io.Serializable; import java.util.Date; public class LoginInfo implements Serializable { private static final long serialVersionUID = 8364988832581114038L; private String userName; private transient String password;//Note this key word "transient" private Date loginDate; //Default Public Constructor public LoginInfo() { System.out.println("LoginInfo Constructor"); } //Non-Default constructor public LoginInfo(String username, String password) { this.userName = username; this.password = password; loginDate = new Date(); } public String toString() { return "UserName=" + userName + ", Password=" + password + ", LoginDate=" + loginDate; } } 测试类: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Test { static String fileName = "C:/x.file"; public static void main(String[] args) throws Exception { LoginInfo info = new LoginInfo("name", "123"); System.out.println(info); //Write System.out.println("Serialize object"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName)); oos.writeObject(info); oos.close(); //Read System.out.println("Deserialize object."); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName)); LoginInfo info2 = (LoginInfo)ois.readObject(); ois.close(); System.out.println(info2); } } 执行结果: ? 1 2 3 4 UserName=name, Password=123, LoginDate=Wed Nov 04 16:41:49 CST 2015 Serialize object Deserialize object. UserName=name, Password=null, LoginDate=Wed Nov 04 16:41:49 CST 2015 另一种可以达到此目的的方法可能就比较少用了,那就是——不实现Serializable而实现Externalizable接口。这个Externalizable接口有两个方法,分别表示在序列化的时候需要序列化哪些字段和反序列化的时候能够反序列化哪些字段: ? 1 2 void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; 于是就有了下面的代码: ? 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 import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.Date; public class LoginInfo2 implements Externalizable { private static final long serialVersionUID = 8364988832581114038L; private String userName; private String password; private Date loginDate; //Default Public Constructor public LoginInfo2() { System.out.println("LoginInfo Constructor"); } //Non-Default constructor public LoginInfo2(String username, String password) { this.userName = username; this.password = password; loginDate = new Date(); } public String toString() { return "UserName=" + userName + ", Password=" + password + ", LoginDate=" + loginDate; } @Override public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Externalizable.writeExternal(ObjectOutput out) is called"); out.writeObject(loginDate); out.writeUTF(userName); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Externalizable.readExternal(ObjectInput in) is called"); loginDate = (Date)in.readObject(); userName = (String)in.readUTF(); } } 测试类除了类名使用LoginInfo2以外,其他保持不变。下面是执行结果: ? 1 2 3 4 5 6 7 UserName=name, Password=123, LoginDate=Wed Nov 04 16:36:39 CST 2015 Serialize object Externalizable.writeExternal(ObjectOutput out) is called Deserialize object. LoginInfo Constructor //-------------------------Note this line Externalizable.readExternal(ObjectInput in) is called UserName=name, Password=null, LoginDate=Wed Nov 04 16:36:39 CST 2015 可以看到,反序列化后的Password一项依然为null。 需要注意的是:对于恢复Serializable对象,对象完全以它存储的二进制为基础来构造,而不调用构造器。而对于一个Externalizable对象,public的无参构造器将会被调用(因此你可以看到上面的测试结果中有LoginInfoConstructor这一行),之后再调用readExternal()方法。在Externalizable接口文档中,也给出了相关描述: When an Externalizable object is reconstructed, an instance is created using the public no-arg constructor, then the readExternal method called. Serializable objects are restored by reading them from an ObjectInputStream. 如果没有发现public的无参构造器,那么将会报错。(把LoginInfo2类的无参构造器注释掉,就会产生错误了): ? 1 2 3 4 5 6 7 8 9 10 UserName=name, Password=123, LoginDate=Wed Nov 04 17:03:24 CST 2015 Serialize object Deserialize object. Exception in thread "main" java.io.InvalidClassException: LoginInfo2; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1772) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370) at Test2.main(Test2.java:19) 那么,如果把Externalizable接口和transient关键字一起用,会是什么效果呢?我们在LoginInfo2中的password加上关键字transient,再修改writeExternal()和readExternal()方法: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(loginDate); out.writeUTF(userName); out.writeUTF(password);//强行将transient修饰的password属性也序列化进去 } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { loginDate = (Date)in.readObject(); userName = (String)in.readUTF(); password = (String)in.readUTF();//反序列化password字段 } 执行结果: ? 1 2 3 4 5 UserName=name, Password=123, LoginDate=Wed Nov 04 16:58:27 CST 2015 Serialize object Deserialize object. LoginInfo Constructor UserName=name, Password=123, LoginDate=Wed Nov 04 16:58:27 CST 2015 从结果中可以看到,尽管在password字段上使用了transient关键字,但是这还是没能阻止被序列化。因为不是以Serializable方式去序列化和反序列化的。也就是说:transient关键字只能与Serializable接口搭配使用。
当Set使用自己创建的类型时,存储的顺序如何维护,在不同的Set实现中会有不同,而且它们对于在特定的Set中放置的元素类型也有不同的要求: Set(interface) 存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set和Collection具有完全一样的接口,但Set不保证元素的顺序。 HashSet* 为快速查找而设计的Set。存入HashSet的元素必须定义hashCode()方法。 TreeSet 一种可维护元素顺序的Set,底层为树结构。使用它可以从Set中提取有序的序列。元素必须实现Comparable接口。 LinkedHashSet 具有HashSet的查询速度,而且内部使用链表维护元素的顺序(插入的顺序),于是在使用迭代器遍历Set时,结果会按元素插入的顺序显示。元素也必须定义hashCode()方法。 在HashSet打*号,表示如果没有其他的限制,这就应该是默认的选择,因为它的速度很快。 你必须为散列存储和树形存储都定义一个equals()方法,但是hashCode()只有在这个类将会被放入HashSet或者LinkedHashSet中才是必须的。但是对于良好的变成风格而言,你应该在覆盖equals()方法的同时覆盖hashCode()方法。下面的示例演示了为了成功的使用特定的Set实现类而必须定义的方法: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import java.util.TreeSet; class SetType { int i; public SetType (int n) { i = n; } public boolean equals(Object obj) { return obj instanceof SetType && (i == ((SetType)obj).i); } public String toString() { return Integer.toString(i); } } //能正常被HashSet,LinkedHashSet使用的类型 class HashSetType extends SetType { public HashSetType(int n) { super(n); } public int hashCode() { return i; } } //能正常被TreeSet使用的类型 class TreeSetType extends SetType implements Comparable<TreeSetType>{ public TreeSetType(int n) { super(n); } public int compareTo(TreeSetType o) { return (o.i < i ? -1 : (o.i > i ? 1 : 0));//降序排列 } } public class TypesForSets { static <T> Set<T> fill(Set<T> set, Class<T> clazz) { try { for (int i = 0; i < 10; i++) { set.add(clazz.getConstructor(int.class).newInstance(i)); } } catch (Exception e) { throw new RuntimeException(e); } return set; } static <T> void test(Set<T> set, Class<T> clazz) { fill(set, clazz); fill(set, clazz);//尝试重复向Set中添加 fill(set, clazz); System.out.println(set); } public static void main(String[] args) { //正确的装法 System.out.println("---------Correct----------"); test(new HashSet<HashSetType>(), HashSetType.class); test(new LinkedHashSet<HashSetType>(), HashSetType.class); test(new TreeSet<TreeSetType>(), TreeSetType.class); //错误的装法 System.out.println("---------Wrong----------"); test(new HashSet<SetType>(), SetType.class); test(new HashSet<TreeSetType>(), TreeSetType.class); test(new LinkedHashSet<SetType>(), SetType.class); test(new LinkedHashSet<TreeSetType>(), TreeSetType.class); try { test(new TreeSet<SetType>(), SetType.class); } catch (Exception e) { System.out.println(e.getMessage()); } try { test(new TreeSet<SetType>(), SetType.class); } catch (Exception e) { System.out.println(e.getMessage()); } } } 执行结果(样例): ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ---------Correct---------- [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] ---------Wrong---------- [1, 1, 5, 0, 7, 3, 4, 6, 5, 9, 8, 0, 7, 5, 9, 6, 8, 2, 4, 1, 7, 4, 3, 6, 8, 2, 2, 0, 9, 3] [3, 5, 1, 8, 5, 4, 1, 0, 9, 3, 0, 8, 5, 7, 6, 9, 7, 3, 4, 0, 7, 6, 2, 1, 2, 8, 6, 9, 4, 2] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] java.lang.ClassCastException: SetType cannot be cast to java.lang.Comparable java.lang.ClassCastException: SetType cannot be cast to java.lang.Comparable 为了证明哪些方法是对于某种特殊的Set是必须的,同时也为了避免代码重复,我们创建了三个类型。基类SetType只存储了一个int,并且通过toString()方法产生它的值。因为所有在Set中存储的类型都必须具有equals()方法,因此在基类中也有该方法。 HashSetType继承自SetType,并添加了hashCode()方法,该方法对于放入Set的散列实现中的对象来说是必需的。 TreeType实现了Comparable接口,如果一个对象被用于任何种类的排序容器中,例如TreeSet,那么它必须实现这个接口。注意:在compareTo()方法中,我没有使用简洁明了的形式return o.i-i,因为这是一个常见的编程错误,它只有在i和i2都是无符号的int(如果Java确实有unsigned关键字的话)才能正常工作。对于有符号的int,它就会出错,因为int不够大,不足以表现两个有符号的int的差。例如o.i是很大的正数而且i是很小的负数时,i-j就会溢出并返回负值,这就不对了。 你通常希望compareTo()产生与equals()一致的自然顺序。如果equals()对于某个特定的比较返回true,那么compareTo()对于该比较就应该返回0,反之亦然。 在TypesForSets中,fill()和test()方法都是用泛型定义的,这是为了避免代码重复。为了验证某个Set的行为,test()会在被测Set上调用三次,尝试着在其中添加重复对象。fill()方法接受任意类型的Set,以及对应类型的Class对象,它使用Class对象来发现构造器并构造对象后添加到Set中。 从输出可以看到,HashSet以某种顺序保存所有的元素(这结果是我用 jdk1.7.0_79 跑出来的,而书中描述是用的jdk1.5,因此不知道是不是这里存在的差异。我这里使用HashSet的元素的结果是有序的,但书中顺序是乱的),LinkedHashSet按照元素插入的顺序保存元素,而TreeSet则按照排序(按照compareTo()定义的顺序,这里是降序)保存元素。 如果我们尝试着将没有恰当地支持必须操作的类型用于这些方法的Set,那么就会有麻烦了。对于没有重新定义hashCode()方法的SetType或TreeType,如果将它们放置到任何散列表中都会产生重复值,这样就违反了Set的基本约定。这相当烦人,因为这种情况甚至不会有运行时错误。
先看一个例子: ? 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 import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; public class UnsupportedOperation { static void test(String msg, List<String> list) { System.out.println("----------" + msg + "----------"); Collection<String> c = list; Collection<String> subList = list.subList(1, 4); Collection<String> subList2 = new ArrayList<>(subList); try { c.retainAll(subList2); } catch (Exception e) { System.out.println("retainAll: " + e); } try { c.removeAll(subList2); } catch (Exception e) { System.out.println("removeAll: " + e); } try { c.clear(); } catch (Exception e) { System.out.println("clear: " + e); } try { c.add("X"); } catch (Exception e) { System.out.println("add: " + e); } try { c.addAll(subList2); } catch (Exception e) { System.out.println("addAll: " + e); } try { c.remove("C"); } catch (Exception e) { System.out.println("remove: " + e); } try { list.set(0, "W"); } catch (Exception e) { System.err.println("List.set: " + e); } System.out.println("List.className=" + list.getClass().getSimpleName()); } public static void main(String[] args) { List<String> list = Arrays.asList("A B C D E F G".split(" ")); test("Modifiable Copy", new ArrayList<String>(list)); test("Arrays.asList", list); test("Unmodifiable List", Collections.unmodifiableList(list)); } } 执行结果: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ----------Modifiable Copy---------- List.className=ArrayList ----------Arrays.asList---------- retainAll: java.lang.UnsupportedOperationException removeAll: java.lang.UnsupportedOperationException clear: java.lang.UnsupportedOperationException add: java.lang.UnsupportedOperationException addAll: java.lang.UnsupportedOperationException remove: java.lang.UnsupportedOperationException List.className=ArrayList ----------Unmodifiable List---------- retainAll: java.lang.UnsupportedOperationException removeAll: java.lang.UnsupportedOperationException clear: java.lang.UnsupportedOperationException add: java.lang.UnsupportedOperationException addAll: java.lang.UnsupportedOperationException remove: java.lang.UnsupportedOperationException List.set: java.lang.UnsupportedOperationException List.className=UnmodifiableRandomAccessList 那么为什么会出现这样的结果呢?对于Unmodifiable的List不能做修改很好理解,但从结果中可以看到,同样是ArrayList,为什么Array.asList()返回的List就不能做修改呢? Arrays.asList()会生成一个List,它基于一个固定大小的数组,仅支持那些不会改变数组大小的操作,任何对引起底层数据结构的尺寸进行修改的方法都会产生一个UnsupportedOperationException异常,以表示对未获支持操作的调用。 其实上面的代码中,我故意使用的是getSimpleName(),因为这个方法不会显示包名。如果我们将 ? 1 System.out.println("List.className=" + list.getClass().getSimpleName()); 替换为: ? 1 System.out.println("List.className=" + list.getClass().getName()); 那么结果就是这样的了: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ----------Modifiable Copy---------- List.className=java.util.ArrayList ----------Arrays.asList---------- retainAll: java.lang.UnsupportedOperationException removeAll: java.lang.UnsupportedOperationException clear: java.lang.UnsupportedOperationException add: java.lang.UnsupportedOperationException addAll: java.lang.UnsupportedOperationException remove: java.lang.UnsupportedOperationException List.className=java.util.Arrays$ArrayList ----------Unmodifiable List---------- retainAll: java.lang.UnsupportedOperationException removeAll: java.lang.UnsupportedOperationException clear: java.lang.UnsupportedOperationException add: java.lang.UnsupportedOperationException addAll: java.lang.UnsupportedOperationException remove: java.lang.UnsupportedOperationException List.className=java.util.Collections$UnmodifiableRandomAccessList List.set: java.lang.UnsupportedOperationException 可以看到,Arrays.asList返回的名字虽然叫ArrayList,但此ArrayList是Arrays中的一个内部类,而非java.util.ArrayList。 查看其源码,可以发现此Arrays.ArrayList内部类的声明如下: ? 1 2 private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable 而该类的实现方法中,并没有实现修改E[]数组(增、删、清除等)的方法 因此对这种对象调用这些方法时会调用AbstractList类中的实现,于是就直接抛出了UnsupportedOperationException: 因此,通常的做法是,把Arrays.asList()的结果作为构造方法的参数传递给任何Collection(或者使用addAll()方法或Collections.addAll()静态方法),这样就可以生成允许使用所有的方法的普通容器——这在main()中的第一个对test()的调用中得到了展示,这样的调用会产生新的尺寸可调的底层数据结构。Collections类的“不可修改”方法将容器包装到了一个代理中,只要你执行任何试图修改容器的操作,这个代理就会产生UnsupportedOperationException异常。使用这些方法的目标就是产生“常量”容器对象。 test()中的最后一个try语句块将检查作为List的一部分set()方法。Arrays.asList()返回固定尺寸的List,而Collections.unmodifiableList()产生不可修改的列表。正如输出中看到的,修改Arrays.asList()返回的List中的元素是可以的,因为这没有违反“尺寸固定”这一特性。但很明显,unmodifiableList()的结果在任何情况下都是不可修改的。 -------------------------------------------------------------- 像上述抛出异常的操作,在Java容器中被称为“未获支持的操作”。那么为什么会将方法定义为可选的呢?那是因为这样做可以防止在设计中出现接口爆炸的情况。为了让这种方式能工作: 1. UnsupportedOperationException必须是一种罕见的事件。即,对于大多数类来说,所有操作都应该可以工作,只有在特例中才会有这类操作。在Java容器中也确实如此,因为你在99%的时间里使用的容器类,如ArrayList、LinkedList、HashSet和HashMap,以及其他的具体实现,都支持所有的操作。 2. 如果一个操作是未获支持的,那么在实现接口的时候可能就会导致UnsupportedOperationException异常,而不是将产品交付给客户以后才出现此异常,这种情况是有道理的,毕竟,它表示编程上有错误:使用了不正确的接口实现。 值得注意的是,这类操作只有在运行时才能探测到。
既然Java包括老式的synchronized关键字和Java SE5中心的Lock和Atomic类,那么比较这些不同的方式,更多的理解他们各自的价值和适用范围,就会显得很有意义。 比较天真的方式是在针对每种方式都执行一个简单的测试,就像下面这样: ? 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 import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; abstract class Incrementable { protected long counter = 0; public abstract void increment(); } class SynchronizingTest extends Incrementable { public synchronized void increment() { ++counter; } } class LockingTest extends Incrementable { private Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { ++counter; } finally { lock.unlock(); } } } public class SimpleMicroBenchmark { static long test(Incrementable inc) { long start = System.nanoTime(); for (long i = 0; i < 10000000; i++) { inc.increment(); } return System.nanoTime() - start; } public static void main(String[] args) { long syncTime = test(new SynchronizingTest()); long lockTime = test(new LockingTest()); System.out.println(String.format("Synchronized: %1$10d", syncTime)); System.out.println(String.format("Lock: %1$10d", lockTime)); System.out.println(String.format( "Lock/Synchronized: %1$.3f", lockTime/(double)syncTime)); } } 执行结果(样例): ? 1 2 3 Synchronized: 209403651 Lock: 257711686 Lock/Synchronized: 1.231 从输出中可以看到,对synchronized方法的调用看起来要比使用ReentrantLock快,这是为什么呢? 本例演示了所谓的“微基准测试”危险,这个属于通常指在隔离的、脱离上下文环境的情况下对某个个性进行性能测试。当然,你仍旧必须编写测试来验证诸如“Lock比synchronized更快”这样的断言,但是你需要在编写这些测试的时候意识到,在编译过程中和在运行时实际会发生什么。 上面的示例存在着大量的问题。首先也是最重要的是,我们只有在这些互斥存在竞争的情况下,才能看到真正的性能差异,因此必须有多个任务尝试访问互斥代码区。而在上面的示例中,每个互斥都由单个的main()线程在隔离的情况下测试的。 其次,当编译器看到synchronized关键字时,有可能会执行特殊的优化,甚至有可能会注意到这个程序时单线程的。编译器甚至可能会识别出counter被递增的次数是固定数量的,因此会预先计算出其结果。不同的编译器和运行时系统在这方面存在着差异,因此很难确切了解将会发生什么,但是我们需要防止编译器去预测结果的可能性。 为了创建有效的测试,我们必须是程序更加复杂。首先,我们需要多个任务,但并不只是会修改内部值的任务,还包括读取这些值的任务(否则优化器可以识别出这些值从来不会被使用)。另外,计算必须足够复杂和不可预测,以使得编译器没有机会执行积极优化。这可以通过预加载一个大型的随机int数组(预加载可以减小在主循环上调用Random.nextInt()所造成的影响),并在计算总和时使用它们来实现: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 import java.util.Random; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; abstract class Accumulator { public static long cycles = 50000L; // Number of modifiers and readers during each test private static final int N = 4; public static ExecutorService exec = Executors.newFixedThreadPool(2 * N); private static CyclicBarrier barrier = new CyclicBarrier(2 * N + 1); protected volatile int index = 0; protected volatile long value = 0; protected long duration = 0; protected String id = ""; // A big int array protected static final int SIZE = 100000; protected static int[] preLoad = new int[SIZE]; static { // Load the array of random numbers: Random random = new Random(47); for (int i = 0; i < SIZE; i++) { preLoad[i] = random.nextInt(); } } public abstract void accumulate(); public abstract long read(); private class Modifier implements Runnable { public void run() { for (int i = 0; i < cycles; i++) { accumulate(); } try { barrier.await(); } catch (Exception e) { throw new RuntimeException(e); } } } private class Reader implements Runnable { private volatile long value; public void run() { for (int i = 0; i < cycles; i++) { value = read(); } try { barrier.await(); } catch (Exception e) { throw new RuntimeException(e); } } } public void timedTest() { long start = System.nanoTime(); for (int i = 0; i < N; i++) { exec.execute(new Modifier());//4 Modifiers exec.execute(new Reader());//4 Readers } try { barrier.await(); } catch (Exception e) { throw new RuntimeException(e); } duration = System.nanoTime() - start; System.out.println(String.format("%-13s: %13d", id, duration)); } public static void report(Accumulator a1, Accumulator a2) { System.out.println(String.format("%-22s: %.2f", a1.id + "/" + a2.id, a1.duration / (double)a2.duration)); } } class BaseLine extends Accumulator { {id = "BaseLine";} public void accumulate() { value += preLoad[index++]; if (index >= SIZE - 5) index = 0; } public long read() { return value; } } class SynchronizedTest extends Accumulator { {id = "Synchronized";} public synchronized void accumulate() { value += preLoad[index++]; if (index >= SIZE - 5) index = 0; } public synchronized long read() { return value; } } class LockTest extends Accumulator { {id = "Lock";} private Lock lock = new ReentrantLock(); public void accumulate() { lock.lock(); try { value += preLoad[index++]; if (index >= SIZE - 5) index = 0; } finally { lock.unlock(); } } public long read() { lock.lock(); try { return value; } finally { lock.unlock(); } } } class AtomicTest extends Accumulator { {id = "Atomic"; } private AtomicInteger index = new AtomicInteger(0); private AtomicLong value = new AtomicLong(0); public void accumulate() { //Get value before increment. int i = index.getAndIncrement(); //Get value before add. value.getAndAdd(preLoad[i]); if (++i >= SIZE - 5) index.set(0); } public long read() {return value.get(); } } public class SynchronizationComparisons { static BaseLine baseLine = new BaseLine(); static SynchronizedTest synchronizedTest = new SynchronizedTest(); static LockTest lockTest = new LockTest(); static AtomicTest atomicTest = new AtomicTest(); static void test() { System.out.println("============================"); System.out.println(String.format( "%-13s:%14d", "Cycles", Accumulator.cycles)); baseLine.timedTest(); synchronizedTest.timedTest(); lockTest.timedTest(); atomicTest.timedTest(); Accumulator.report(synchronizedTest, baseLine); Accumulator.report(lockTest, baseLine); Accumulator.report(atomicTest, baseLine); Accumulator.report(synchronizedTest, lockTest); Accumulator.report(synchronizedTest, atomicTest); Accumulator.report(lockTest, atomicTest); } public static void main(String[] args) { int iterations = 5;//Default execute time if (args.length > 0) {//Optionally change iterations iterations = Integer.parseInt(args[0]); } //The first time fills the thread pool System.out.println("Warmup"); baseLine.timedTest(); //Now the initial test does not include the cost //of starting the threads for the first time. for (int i = 0; i < iterations; i++) { test(); //Double cycle times. Accumulator.cycles *= 2; } Accumulator.exec.shutdown(); } } 执行结果(样例): ? 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 Warmup BaseLine : 12138900 ============================ Cycles : 50000 BaseLine : 12864498 Synchronized : 87454199 Lock : 27814348 Atomic : 14859345 Synchronized/BaseLine : 6.80 Lock/BaseLine : 2.16 Atomic/BaseLine : 1.16 Synchronized/Lock : 3.14 Synchronized/Atomic : 5.89 Lock/Atomic : 1.87 ============================ Cycles : 100000 BaseLine : 25348624 Synchronized : 173022095 Lock : 51439951 Atomic : 32804577 Synchronized/BaseLine : 6.83 Lock/BaseLine : 2.03 Atomic/BaseLine : 1.29 Synchronized/Lock : 3.36 Synchronized/Atomic : 5.27 Lock/Atomic : 1.57 ============================ Cycles : 200000 BaseLine : 47772466 Synchronized : 348437447 Lock : 104095347 Atomic : 59283429 Synchronized/BaseLine : 7.29 Lock/BaseLine : 2.18 Atomic/BaseLine : 1.24 Synchronized/Lock : 3.35 Synchronized/Atomic : 5.88 Lock/Atomic : 1.76 ============================ Cycles : 400000 BaseLine : 98804055 Synchronized : 667298338 Lock : 212294221 Atomic : 137635474 Synchronized/BaseLine : 6.75 Lock/BaseLine : 2.15 Atomic/BaseLine : 1.39 Synchronized/Lock : 3.14 Synchronized/Atomic : 4.85 Lock/Atomic : 1.54 ============================ Cycles : 800000 BaseLine : 178514302 Synchronized : 1381579165 Lock : 444506440 Atomic : 300079340 Synchronized/BaseLine : 7.74 Lock/BaseLine : 2.49 Atomic/BaseLine : 1.68 Synchronized/Lock : 3.11 Synchronized/Atomic : 4.60 Lock/Atomic : 1.48 这个程序使用了模板方法设计模式,将所有的共用代码都放置到基类中,并将所有不同的代码隔离在子类的accumulate()和read()的实现中。在每个子类SynchronizedTest、LockTest和AtomicTest中,你可以看到accumulate()和read()如何表达了实现互斥现象的不同方式。 在这个程序中,各个任务都是经由FixedThreadPool执行的,在执行过程中尝试着在开始时跟踪所有线程的创建,并且在测试过程中方式产生任何额外的开销。为了保险起见,初始测试执行了两次,而第一次的结果被丢弃,因为它包含了初试线程的创建。 程序中有一个CyclicBarrier,因为我们希望确保所有的任务在声明每个测试完成之前都已经完成。 每次调用accumulate()时,它都会移动到preLoad数组的下一个位置(到达数组尾部时在回到开始位置),并将这个位置的随机生成的数字加到value上。多个Modifier和Reader任务提供了在Accumulator对象上的竞争。 注意,在AtomicTest中,我发现情况过于复杂,使用Atomic对象已经不适合了——基本上,如果涉及多个Atomic对象,你就有可能会被强制要求放弃这种用法,转而使用更加常规的互斥(JDK文档特别声明:当一个对象的临界更新被限制为只涉及单个变量时,只有使用Atomic对象这种方式才能工作)。但是,这个测试人就保留了下来,使你能够感受到Atomic对象的性能优势。 在main()中,测试时重复运行的,并且你可以要求其重复的次数超过5次,对于每次重复,测试循环的数量都会加倍,因此你可以看到当运行次数越来越多时,这些不同的互斥在行为方面存在着怎样的差异。正如你从输出中看到的那样,测试结果相当惊人。抛开预加载数组、初始化线程池和线程的影响,synchronized关键字的效率明显比Lock和Atomic的低。 记住,这个程序只是给出了各种互斥方式之间的差异的趋势,而上面的输出也仅仅表示这些差异在我的特定环境下的特定机器上的表现。如你所见,如果自己动手实验,当所有的线程数量不同,或者程序运行的时间更长时,在行为方面肯定会存在着明显的变化。例如,某些hotspot运行时优化会在程序运行后的数分钟之后被调用,但是对于服务器端程序,这段时间可能长达数小时。 也就是说,很明显,使用Lock通常会比使用synchronized高效许多,而且synchronized的开销看起来变化范围太大,而Lock则相对一致。 这是否意味着你永远不应该选择synchronized关键字呢?这里有两个因素需要考虑:首先,在上面的程序中,互斥方法体是非常小的。通常,这是一个好的习惯——只互斥那些你绝对必须互斥的部分。但是,在实际中,被互斥部分可能会比上面示例中的那些大许多,因此在这些方法体中花费的时间的百分比可能会明显大于进入和退出互斥的开销,这样也就湮没了提高互斥速度带来的所有好处。当然,唯一了解这一点的方式是——当你在对性能调优时,应该立即——尝试各种不同的方法并观察它们造成的影响。 其次,在阅读本文的代码你就会发现,很明显,synchronized关键字所产生的代码,与Lock所需要的“加锁-try/finally-解锁”惯用法所产生的代码量相比,可读性提高了很多。在编程时,与其他人交流对于与计算机交流而言要重要得多,因此代码的可读性至关重要。因此,在编程时,以synchronized关键字入手,只有在性能调优时才替换为Lock对象这种做法,是具有实际意义的。 最后,当你在自己的并发程序中可以使用Atomic类时,这肯定非常好,但是要意识到,正如我们在上例中看到的,Atomic对象只有在非常简单的情况下才有用,这些情况通常包括你只有一个要被修改的Atomic对象,并且这个对象独立于其他所有的对象。更安全的做法是:以更加传统的方式入手,只有在性能方面的需求能够明确指示时,才替换为Atomic。
Exchanger是在两个任务之间交换对象的栅栏。当两个任务进入栅栏时,它们各自拥有一个对象,当它们离开时,它们都拥有对方的对象。Exchanger的典型应用场景是:一个任务在创建对象,而这些对象的生产代价很高,另一个任务在消费这些对象。通过这种方式,可以有更多的对象在被创建的同时被消费。 为了演示Exchanger类,我们将创建生产者和消费者任务。ExchangerProducer和ExchangerConsumer使用一个List<Fat>作为要求交换的对象,它们都包含一个用于这个List<Fat>的Exchanger。当你调用Exchanger.exchange()方法时,它将阻塞直至对方任务调用它自己的exchange()方法,那时,这两个exchange()方法将同时完成,而List<Fat>被交换: ? 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 import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Exchanger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; class ExchangerProducer implements Runnable { private List<Fat> holder; private Exchanger<List<Fat>> exchanger; public ExchangerProducer(Exchanger<List<Fat>> exchanger, List<Fat> holder) { this.exchanger = exchanger; this.holder = holder; } @Override public void run() { try { while(!Thread.interrupted()) { //填充列表 for (int i = 0;i < ExchangerDemo.size; i++) { holder.add(new Fat()); } //等待交换 holder = exchanger.exchange(holder); } } catch (InterruptedException e) { } System.out.println("Producer stopped."); } } class ExchangerConsumer implements Runnable { private List<Fat> holder; private Exchanger<List<Fat>> exchanger; private volatile Fat value; private static int num = 0; public ExchangerConsumer(Exchanger<List<Fat>> exchanger, List<Fat> holder) { this.exchanger = exchanger; this.holder = holder; } @Override public void run() { try { while(!Thread.interrupted()) { //等待交换 holder = exchanger.exchange(holder); //读取列表并移除元素 for (Fat x : holder) { num++; value = x; //在循环内删除元素,这对于CopyOnWriteArrayList是没有问题的 holder.remove(x); } if (num % 10000 == 0) { System.out.println("Exchanged count=" + num); } } } catch (InterruptedException e) { } System.out.println("Consumer stopped. Final value: " + value); } } public class ExchangerDemo { static int size = 10; static int delay = 5; //秒 public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); List<Fat> producerList = new CopyOnWriteArrayList<>(); List<Fat> consumerList = new CopyOnWriteArrayList<>(); Exchanger<List<Fat>> exchanger = new Exchanger<>(); exec.execute(new ExchangerProducer(exchanger, producerList)); exec.execute(new ExchangerConsumer(exchanger, consumerList)); TimeUnit.SECONDS.sleep(delay); exec.shutdownNow(); } } class Fat { private volatile double d; private static int counter = 1; private final int id = counter++; public Fat() { //执行一段耗时的操作 for (int i = 1; i<10000; i++) { d += (Math.PI + Math.E) / (double)i; } } public void print() {System.out.println(this);} public String toString() {return "Fat id=" + id;} } 执行结果(可能的结果): ? 1 2 3 4 5 6 7 8 9 10 Exchanged count=10000 Exchanged count=20000 Exchanged count=30000 Exchanged count=40000 Exchanged count=50000 Exchanged count=60000 Exchanged count=70000 Exchanged count=80000 Consumer stopped. Final value: Fat id=88300 Producer stopped. 在main()中,创建了用于两个任务的单一的Exchanger,以及两个用于互换的CopyOnWriteArrayList。这个特定的List变体允许列表在被遍历的时候调用remove()方法,而不会抛出ConcurrentModifiedException异常。ExchangerProducer将填充这个List,然后将这个满列表跟ExchangerConsumer的空列表交换。交换之后,ExchangerProducer可以继续的生产Fat对象,而ExchangerConsumer则开始使用满列表中的对象。因为有了Exchanger,填充一个列表和消费另一个列表便同时发生了。
An unbounded blocking queue that uses the same ordering rules as class PriorityQueue and supplies blocking retrieval operations. While this queue is logically unbounded, attempted additions may fail due to resource exhaustion (causing OutOfMemoryError). PriorityBlockingQueue是一个很基础的优先级队列,它在PriorityQueue的基础上提供了可阻塞的读取操作。它是无限制的,就是说向Queue里面增加元素可能会失败(导致OurOfMemoryError)。下面是一个示例,其中在优先级队列中的对象是按照优先级顺序依次出队列的: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; class PrioritizedTask implements Runnable, Comparable<PrioritizedTask> { private static int counter = 1; private final int priority; private Random random = new Random(47); private final int id = counter++;//这个id不是static的,因此 protected static List<PrioritizedTask> sequence = new ArrayList<>(); public PrioritizedTask(int priority) { this.priority = priority; sequence.add(this); } @Override public int compareTo(PrioritizedTask o) { int val = this.priority - o.priority; //higher value, higher priority return val < 0 ? 1 : (val > 0 ? -1 : 0); } @Override public void run() { try { TimeUnit.MILLISECONDS.sleep(random.nextInt(250)); } catch (InterruptedException e) { } System.out.println(this); } @Override public String toString() { return String.format("P=[%1$-3d]", priority) + ", ID=" + id; } public static class EndFlagTask extends PrioritizedTask { private ExecutorService exec; public EndFlagTask(ExecutorService executorService) { super(-1);//最低的优先级 exec = executorService; } @Override public void run() { System.out.println(this + " calling shutdownNow()"); exec.shutdownNow(); } } } class PrioritizedTaskProducer implements Runnable { private Queue<Runnable> queue; private ExecutorService exec; public PrioritizedTaskProducer(Queue<Runnable> queue, ExecutorService exec) { this.queue = queue; this.exec = exec; } @Override public void run() { try { //慢慢的添加高优先级的任务 for (int i = 0; i < 6; i++) { TimeUnit.MILLISECONDS.sleep(250); queue.add(new PrioritizedTask(9)); //6个优先级10 } //先创建2个P=0的任务 queue.add(new PrioritizedTask(0)); queue.add(new PrioritizedTask(0)); //添加低优先级的任务 for (int i = 0; i < 6; i++) {// 优先级0-5 queue.add(new PrioritizedTask(i)); } //添加一个结束标志的任务 queue.add(new PrioritizedTask.EndFlagTask(exec)); } catch (InterruptedException e) { // TODO: handle exception } System.out.println("Finished PrioritizedTaskProducer."); } } class PrioritizedTaskConsumer implements Runnable { private PriorityBlockingQueue<Runnable> queue; public PrioritizedTaskConsumer(PriorityBlockingQueue<Runnable> queue) { this.queue = queue; } @Override public void run() { try { //不停的从queue里面取任务,直到exec停止。 while(!Thread.interrupted()) { //使用当前线程来跑这些任务 queue.take().run(); } } catch (InterruptedException e) { } System.out.println("Finished PrioritizedTaskConsumer."); } } public final class PriorityBlockingQueueDemo { public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); PriorityBlockingQueue<Runnable> queue = new PriorityBlockingQueue<>(); exec.execute(new PrioritizedTaskProducer(queue, exec)); exec.execute(new PrioritizedTaskConsumer(queue)); } } 执行结果: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 P=[9 ], ID=1 P=[9 ], ID=2 P=[9 ], ID=3 P=[9 ], ID=4 P=[9 ], ID=5 Finished PrioritizedTaskProducer. P=[9 ], ID=6 P=[5 ], ID=14 P=[4 ], ID=13 P=[3 ], ID=12 P=[2 ], ID=11 P=[1 ], ID=10 P=[0 ], ID=7 P=[0 ], ID=9 P=[0 ], ID=8 P=[-1 ], ID=15 calling shutdownNow() Finished PrioritizedTaskConsumer. PrioritizedTask对象的创建序列被记录在sequenceList中,用于和实际的顺序比较。run()方法将休眠一小段随机的时间,然后打印对象信息,而EndFlagTask提供了停止ExecutorService的功能,要确保它是队列中的最后一个对象,因此给它设置了最低的优先级(-1,优先级值越大,优先级越高)。 PrioritizedTaskProducer和PrioritizedTaskConsumer通过PriorityBlockingQueue彼此链接。因为这种队列的阻塞特性提供了所有必须的同步,所以你应该注意到了,这里不需要任何显式的同步——不必考虑当你从这种队列中读取时,其中是否有元素,因为这个队列在没有元素时,将直接阻塞读取者。 从执行结果中可以看到,最先出队列的是Priority为9的6个Task,因为这几个任务先创建。 ? 1 Finished PrioritizedTaskProducer. 这句话的打印表示生产者已经将所有的任务放到队列中了,由于将任务放到Queue中和从Queue中提取任务并执行时两个不同的任务(即Producer和Consumer),因此Producer先输出“Finished PrioritizedTaskProducer.”。输出这句话的时候,前面只有5个P=9的任务出列了,因此队列中还有1个P=9的任务没出列,同时还有后续放入各种任务。由于Queue中的任务里面,优先级P最高的是P=9的,因此第6个P=9的任务先出队列。剩下的任务按照P的大小依次出列。 任务的ID属性表示了它们的创建顺序,因为ID是自增的,每创建一个任务,ID就增加。因此从 ? 1 P=[5 ], ID=14 可以很明显的看出:P=5的任务,它的ID最大,所以是最后创建的。从我们的代码中也可以看出来,P=5的任务的确是最后创建的。 还有一点可以看出,当P相同的时候,出Queue的顺序是不确定的,例如: ? 1 2 3 P=[0 ], ID=7 P=[0 ], ID=9 P=[0 ], ID=8 另外,在使用此类的时候需要注意: This class does not permit null elements. A priority queue relying on natural ordering also does not permit insertion of non-comparable objects (doing so results in ClassCastException).
DelayQueue主要用于放置实现了Delay接口的对象,其中的对象只能在其时刻到期时才能从队列中取走。这种队列是有序的,即队头的延迟到期时间最短。如果没有任何延迟到期,那么久不会有任何头元素,并且poll()将返回null(正因为这样,你不能将null放置到这种队列中) 下面是一个示例,其中的Delayed对象自身就是任务,而DelayedTaskConsumer将最“紧急”的任务从队列中取出来,然后运行它: ? 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 import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import static java.util.concurrent.TimeUnit.*; class DelayedTask implements Runnable, Delayed { private static int counter = 0; protected static List<DelayedTask> sequence = new ArrayList<>(); private final int id = counter++; private final int delayTime; private final long triggerTime; public DelayedTask(int delayInMillis) { delayTime = delayInMillis; triggerTime = System.nanoTime() + NANOSECONDS.convert(delayTime, MILLISECONDS); sequence.add(this); } @Override public int compareTo(Delayed o) { DelayedTask that = (DelayedTask)o; if (triggerTime < that.triggerTime) return -1; if (triggerTime > that.triggerTime) return 1; return 0; } /** * 剩余的延迟时间 */ @Override public long getDelay(TimeUnit unit) { return unit.convert(triggerTime - System.nanoTime(), NANOSECONDS); } @Override public void run() { System.out.println(this + " "); } @Override public String toString() { return String.format("[%1$-4d]", delayTime) + " Task " + id; } public static class EndSentinel extends DelayedTask { private ExecutorService exec; public EndSentinel(int delay, ExecutorService exec) { super(delay); this.exec = exec; } @Override public void run() { System.out.println(this + " calling shutDownNow()"); exec.shutdownNow(); } } } class DelayedTaskConsumer implements Runnable { private DelayQueue<DelayedTask> tasks; public DelayedTaskConsumer(DelayQueue<DelayedTask> tasks) { this.tasks = tasks; } @Override public void run() { try { while(!Thread.interrupted()) { tasks.take().run();//run tasks with current thread. } } catch (InterruptedException e) { // TODO: handle exception } System.out.println("Finished DelaytedTaskConsumer."); } } public class DelayQueueDemo { public static void main(String[] args) { int maxDelayTime = 5000;//milliseconds Random random = new Random(47); ExecutorService exec = Executors.newCachedThreadPool(); DelayQueue<DelayedTask> queue = new DelayQueue<>(); //填充10个休眠时间随机的任务 for (int i = 0; i < 10; i++) { queue.put(new DelayedTask(random.nextInt(maxDelayTime))); } //设置结束的时候。 queue.add(new DelayedTask.EndSentinel(maxDelayTime, exec)); exec.execute(new DelayedTaskConsumer(queue)); } } 执行结果: ? 1 2 3 4 5 6 7 8 9 10 11 12 [200 ] Task 7 [429 ] Task 5 [555 ] Task 1 [961 ] Task 4 [1207] Task 9 [1693] Task 2 [1861] Task 3 [4258] Task 0 [4522] Task 8 [4868] Task 6 [5000] Task 10 calling shutDownNow() Finished DelaytedTaskConsumer. DelayedTask包含一个称为sequence的List<DelayedTask>,它保存了在任务被创建的顺序,因此我们可以看到排序是按照实际发生的顺序执行的(即到期时间短的先出队列)。 Delayed接口有一个方法名为getDelay(),它可以用来告知延迟到期还有多长时间,或者延迟在多长时间之前已经到期。这个方法将强制我们去使用TimeUnit类,因为这就是参数类型。这会产生一个非常方便的类,因为你可以很容易地转换单位而无需做任何声明。例如,delayTime的值是以毫秒为单位的,但是System.nanoTime()产生的时间则是以纳秒为单位的。你可以转换delayTime的值,方法是声明它的单位以及你希望以什么单位来表示,就像下面这样: ? 1 NANOSECONDS.convert(delayTime, MILLISECONDS); 为了排序,Delayed接口还继承了Comparable接口,因此必须实现compareTo()方法,使其可以产生合理的比较。toString()则提供了输出格式化,而嵌套的EndSentinel类提供了一种关闭所有事物的途径,具体做法是将其放置为队列的最后一个元素。 注意,因为DelayedTaskConsumer自身是一个任务,所以它有自己的Thread,它可以使用这个线程来运行从队列中获取的所有任务。由于任务是按照队列优先级的顺序来执行的,因此在本例中不需要启动任何单独的线程来运行DelayedTask。
CountDownLatch主要用于同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。 你可以向CountDownLatch对象设置一个初始计数值,任何在这个对象上调用wait()的方法都将阻塞,直到这个计数值达到0.其他任务在结束其工作时,可以在该对象上调用countDown()来减小这个计数值,你可以通过调用getCount()方法来获取当前的计数值。CountDownLatch被设计为只触发一次,计数值不能被重置。如果你需要能够重置计数值的版本,则可以使用CyclicBarrier。 调用countDown()的任务在产生这个调用时并没有阻塞,只有对await()的调用会被阻塞,直到计数值到达0。 CountDownLatch的典型用法是将一个程序分为n个互相独立的可解决人物,并创建值为0的CountDownLatch。当每个任务完成是,都会在这个锁存器上调用countDown()。等待问题被解决的任务在这个锁存器上调用await(),将它们自己锁住,直到锁存器计数结束。下面是演示这种技术的一个框架示例: ? 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 import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.DelayQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; class TaskPortion implements Runnable { private static int counter = 0; private final int id = counter++; private static Random random = new Random(); private final CountDownLatch latch; public TaskPortion(CountDownLatch latch) { this.latch = latch; } @Override public void run() { try { doWork(); latch.countDown();//普通任务执行完后,调用countDown()方法,减少count的值 System.out.println(this + " completed. count=" + latch.getCount()); } catch (InterruptedException e) { } } public void doWork() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(random.nextInt(2000)); } @Override public String toString() { return String.format("%1$-2d ", id); } } class WaitingTask implements Runnable { private static int counter = 0; private final int id = counter++; private final CountDownLatch latch; public WaitingTask(CountDownLatch latch) { this.latch = latch; } @Override public void run() { try { //这些后续任务需要等到之前的任务都执行完成后才能执行,即count=0时 latch.await(); System.out.println("Latch barrier passed for " + this); } catch (InterruptedException e) { System.out.println(this + " interrupted."); } } @Override public String toString() { return String.format("WaitingTask %1$-2d ", id); } } public class CountDownLatchDemo { static final int SIZE = 10; public static void main(String[] args) { CountDownLatch latch = new CountDownLatch(SIZE); ExecutorService exec = Executors.newCachedThreadPool(); //10个WaitingTask for (int i = 0; i < 5; i++) { exec.execute(new WaitingTask(latch)); } //100个任务,这100个任务要先执行才会执行WaitingTask for (int i = 0; i < SIZE; i++) { exec.execute(new TaskPortion(latch)); } System.out.println("Launched all tasks."); exec.shutdown();//当所有的任务都结束时,关闭exec } } 执行结果(可能的结果): ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Launched all tasks. 4 completed. count=9 6 completed. count=8 3 completed. count=7 0 completed. count=6 2 completed. count=5 1 completed. count=4 5 completed. count=3 7 completed. count=2 9 completed. count=1 8 completed. count=0 Latch barrier passed for WaitingTask 0 Latch barrier passed for WaitingTask 2 Latch barrier passed for WaitingTask 1 Latch barrier passed for WaitingTask 3 Latch barrier passed for WaitingTask 4 从结果中可以看到,所有的WaitingTask都是在所有的TaskPortion执行完成之后执行的。 TaskPortion将随机的休眠一段时间,以模拟这部分工作的完成。而WaitingTask表示系统中必须等待的部分,它要等到问题的初始部分完成后才能执行。注意:所有任务都使用了在main()中定义的同一个CountDownLatch对象。
通过I/O在线程间进行通信通常很有用。提供线程功能的类库以“管道”的形式对线程间的 I/O 提供了支持。它们在Java I/O 类库中的对应物就是PipedWriter(允许任务向管道写)和PipedReader(允许不同的任务从同一个管道中读取)。这个模型可以看做是“生产者-消费者”问题的变体,这里的管道就是一个封装好的解决方案。管道基本上是一个阻塞队列, 存在于多个引入BlockingQueue之前的Java版本中。 下面是一个简单的例子,两个任务使用一个管道进行通信: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import java.io.IOException; import java.io.PipedReader; import java.io.PipedWriter; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * 发送端 */ class Sender implements Runnable { private Random rand = new Random(47); private PipedWriter writer = new PipedWriter(); public PipedWriter getWriter() { return writer; } @Override public void run() { try { while(true) { for (char c = 'A'; c < 'z'; c++) { writer.write(c); TimeUnit.MILLISECONDS.sleep(rand.nextInt(500)); } } } catch (IOException e) { System.out.println(e + " Sender write Exception"); } catch (InterruptedException e) { System.out.println(e + " Sender sleep Interrupted"); } } } /** * 接收端 */ class Receiver implements Runnable { private PipedReader reader; public Receiver(Sender sender) throws IOException { reader = new PipedReader(sender.getWriter()); } @Override public void run() { int count = 0; try { while(true) { //在读取到内容之前,会一直阻塞 char s = (char)reader.read(); System.out.print("Read: " + s + ", "); if (++count % 5 == 0) { System.out.println(); } } } catch (IOException e) { System.out.println(e + " Receiver read Exception."); } } } public class PipedIO { public static void main(String[] args) throws Exception { Sender sender = new Sender(); Receiver receiver = new Receiver(sender); ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(sender); exec.execute(receiver); TimeUnit.SECONDS.sleep(5); exec.shutdownNow(); } } 执行结果(可能的结果): ? 1 2 3 4 5 6 Read: A, Read: B, Read: C, Read: D, Read: E, Read: F, Read: G, Read: H, Read: I, Read: J, Read: K, Read: L, Read: M, Read: N, Read: O, Read: P, Read: Q, Read: R, Read: S, Read: T, Read: U, java.io.InterruptedIOException Receiver read Exception. java.lang.InterruptedException: sleep interrupted Sender sleep Interrupted Sender和Receiver代表了需要互相通信的两个任务。Sender创建了一个PipedWriter,它是一个单独的对象;但是对于Receiver,PipedReader的建立必须在构造器中与一个PipedWriter相关联。就是说,PipedReader与PipedWriter的构造可以通过如下两种方式: ? 1 2 3 4 5 6 7 //方式一:先构造PipedReader,再通过它构造PipedWriter。 PipedReader reader = new PipedReader(); PipedWriter writer = new PipedWriter(reader); //方式二:先构造PipedWriter,再通过它构造PipedReader。 PipedWriter writer2 = new PipedWriter(); PipedReader reader2 = new PipedReader(writer2); Sender把数据放进Writer,然后休眠一段时间(随机数)。然而,Receiver没有sleep()和wait。但当它调用read()时,如果没有更多的数据,管道将自动阻塞。 注意Sender和Receiver是在main()中启动的,即对象构造彻底完毕之后。如果你启动了一个没有构造完毕的对象,在不同的平台上管道可能会产生不一致的行为(注意,BlockingQueue使用起来更加健壮而容易)。 在shutdownNow()被调用时,可以看到PipedReader与普通I/O之间最重要的差异——PipedReader是可以中断的。如果你将reader.read()替换为System.in.read(),那么interrupt()将不能打断read()调用。
wait()和notifyAll()方法以一种非常低级的方式解决了任务互操作的问题,即每次交互时都需要握手。在许多情况下,你可以瞄准更高的抽象级别,使用同步队列来解决任务协作的问题。同步队列在任何时刻都只允许一个任务插入或移除元素。在java.util.concurrent.BlockingQueue接口中提供了这个队列,这个接口有大量的标准实现。你通常可以使用LinkedBlockingQueue,它是一个无届队列,你还可以使用ArrayBlockingQueue,它具有固定的尺寸,因此你可以在它被阻塞之前,向其中放置有限数量的元素。 如果消费者任务试图从队列中获取对象,而该队列此时为空,那么这些队列还可以挂起消费者任务,并且当有更多的元素可用时回复消费者任务。阻塞队列可以解决非常大的问题,而其方式与wait()和notifyAll()相比,则要简单并可靠许多。 考虑下面这个BlockingQueue的示例,有一台机器具有三个任务:一个制作吐司,一个给吐司抹黄油,还有一个给吐司涂果酱。我们可以通过各个处理过程之间的BlockingQueue来运行这个吐司制作程序: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; class Toast { /** * 吐司的状态: * DRY: 烘干的 * BUTTERED: 涂了黄油的 * JAMMED: 涂了果酱的 * <p>吐司的状态只能由DRY->BUTTERED->JAMMED转变 */ public enum Status {DRY, BUTTERED, JAMMED} private Status status = Status.DRY;//默认状态为DRY private final int id; public Toast(int id) { this.id = id;} public void butter() {status = Status.BUTTERED;} public void jam() {status = Status.JAMMED;} public Status getStatus() {return status;} public int getId() {return id;} public String toString() { return "Toast id: " + id + ", status: " + status; } } @SuppressWarnings("serial") class ToastQueue extends LinkedBlockingQueue<Toast> {} /** * 生产吐司的任务。 */ class Toaster implements Runnable { private ToastQueue toastQueue; private int count = 0; private Random random = new Random(47); public Toaster(ToastQueue queue) { this.toastQueue = queue; } @Override public void run() { try { while(!Thread.interrupted()) { TimeUnit.MILLISECONDS.sleep(300 + random.nextInt(500)); //生产一片吐司,这些吐司是有序的 Toast toast = new Toast(count++); System.out.println(toast); //放到toastQueue中 toastQueue.put(toast); } } catch (InterruptedException e) { System.out.println("Toaster interrupted."); } System.out.println("Toaster off."); } } /** * 涂黄油的任务。 */ class Butterer implements Runnable { private ToastQueue dryQueue; private ToastQueue butteredQueue; public Butterer(ToastQueue dryQueue, ToastQueue butteredQueue) { this.dryQueue = dryQueue; this.butteredQueue = butteredQueue; } @Override public void run() { try { while(!Thread.interrupted()) { //在取得下一个吐司之前会一直阻塞 Toast toast = dryQueue.take(); toast.butter(); System.out.println(toast); butteredQueue.put(toast); } } catch (InterruptedException e) { System.out.println("Butterer interrupted."); } System.out.println("Butterer off."); } } /** * 涂果酱的任务。 */ class Jammer implements Runnable { private ToastQueue butteredQueue; private ToastQueue finishedQueue; public Jammer(ToastQueue butteredQueue, ToastQueue finishedQueue) { this.finishedQueue = finishedQueue; this.butteredQueue = butteredQueue; } @Override public void run() { try { while(!Thread.interrupted()) { //在取得下一个吐司之前会一直阻塞 Toast toast = butteredQueue.take(); toast.jam(); System.out.println(toast); finishedQueue.put(toast); } } catch (InterruptedException e) { System.out.println("Jammer interrupted."); } System.out.println("Jammer off."); } } /** * 吃吐司的人,消费者。 */ class Eater implements Runnable { private ToastQueue finishedQueue; private int count = 0; public Eater (ToastQueue finishedQueue) { this.finishedQueue = finishedQueue; } @Override public void run() { try { while(!Thread.interrupted()) { //在取得下一个吐司之前会一直阻塞 Toast toast = finishedQueue.take(); //验证取得的吐司是有序的,而且状态是JAMMED的 if (toast.getId() != count++ || toast.getStatus() != Toast.Status.JAMMED) { System.out.println("Error -> " + toast); System.exit(-1); } else { //吃掉吐司 System.out.println(toast + "->Eaten"); } } } catch (InterruptedException e) { System.out.println("Eater interrupted."); } System.out.println("Eater off."); } } public class ToastOMatic { public static void main(String[] args) throws Exception { ToastQueue dryQueue = new ToastQueue(); ToastQueue butteredQueue = new ToastQueue(); ToastQueue finishedQueue = new ToastQueue(); ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new Toaster(dryQueue)); exec.execute(new Butterer(dryQueue, butteredQueue)); exec.execute(new Jammer(butteredQueue, finishedQueue)); exec.execute(new Eater(finishedQueue)); TimeUnit.SECONDS.sleep(5); exec.shutdownNow(); } } 执行结果(可能的结果): ? 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 Toast id: 0, status: DRY Toast id: 0, status: BUTTERED Toast id: 0, status: JAMMED Toast id: 0, status: JAMMED->Eaten Toast id: 1, status: DRY Toast id: 1, status: BUTTERED Toast id: 1, status: JAMMED Toast id: 1, status: JAMMED->Eaten Toast id: 2, status: DRY Toast id: 2, status: BUTTERED Toast id: 2, status: JAMMED Toast id: 2, status: JAMMED->Eaten Toast id: 3, status: DRY Toast id: 3, status: BUTTERED Toast id: 3, status: JAMMED Toast id: 3, status: JAMMED->Eaten Toast id: 4, status: DRY Toast id: 4, status: BUTTERED Toast id: 4, status: JAMMED Toast id: 4, status: JAMMED->Eaten Toast id: 5, status: DRY Toast id: 5, status: BUTTERED Toast id: 5, status: JAMMED Toast id: 5, status: JAMMED->Eaten Toast id: 6, status: DRY Toast id: 6, status: BUTTERED Toast id: 6, status: JAMMED Toast id: 6, status: JAMMED->Eaten Toast id: 7, status: DRY Toast id: 7, status: BUTTERED Toast id: 7, status: JAMMED Toast id: 7, status: JAMMED->Eaten Eater interrupted. Eater off. Butterer interrupted. Toaster interrupted. Toaster off. Jammer interrupted. Jammer off. Butterer off. Toast是一个使用enum值的优秀示例。注意,这个示例中没有任何显式的同步(即使用Lock对象或者synchronized关键字的同步),因为同步已经由队列和系统的设计隐式的管理了——每片Toast在任何时刻都只由一个任务在操作。因为队列的阻塞,使得处理过程将被自动的挂起和恢复。你可以看到由BlockingQueue产生的简化十分明显。在使用显式的wait()和notifyAll()时存在的类和类之间的耦合被消除了,因为每个类都只和它的BlockingQueue通信。
在之前的Java并发(一)wait()与notifyAll()一文中的例子中,我们使用了wait()和notifyAll()来模拟了给汽车打蜡和抛光的情景。在JavaSE5中,还提供了java.util.concurrent.locks.Condition对象供我们使用。你可以在Condition上调用await()来挂起一个任务。当外部条件发生变化,意味着某个任务应该继续执行时,你可以通过调用signal()来通知这个任务,或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务(与使用signal()相比,signalAll()是更安全的方式)。 下面是WaxOnMatic.java的重写版本,它包含了一个Condition,用来在waitForWaxing()或waitForBuffing()内部挂起一个任务: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Car { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private boolean waxOn = false;//是否上蜡 //上蜡 public void waxed() { lock.lock(); try { waxOn = true; condition.signalAll(); } finally { lock.unlock(); } } //抛光 public void buffed() { lock.lock(); try { waxOn = false; condition.signalAll(); } finally { lock.unlock(); } } //等待上蜡 public void waitForWaxing() throws InterruptedException { lock.lock(); try { while(waxOn == false) { condition.await(); } } finally { lock.unlock(); } } //等待抛光 public void waitForBuffing() throws InterruptedException { lock.lock(); try { while(waxOn == true) { condition.await(); } } finally { lock.unlock(); } } } class WaxOnTask implements Runnable { private Car car; private String name; public WaxOnTask(String name, Car car) { this.name = name; this.car = car; } @Override public void run() { try { while(!Thread.interrupted()) { System.out.println("[" + name + "] is Wax on!");//正在上蜡 TimeUnit.MILLISECONDS.sleep(300); car.waxed();//上蜡完成 car.waitForBuffing();//等待抛光 } } catch (InterruptedException e) { System.out.println("[" + name + "] Exiting WaxOnTask via interrupt."); } } } class BuffTask implements Runnable { private Car car; private String name; public BuffTask(String name, Car car) { this.name = name; this.car = car; } @Override public void run() { try { while(!Thread.interrupted()) { car.waitForWaxing();//等待上蜡 System.out.println("[" + name + "] Buffing...");//正在抛光 TimeUnit.MILLISECONDS.sleep(300); car.buffed();//抛光完成 } } catch (InterruptedException e) { System.out.println("[" + name + "] Exiting BuffTask via interrupt."); } } } public class WaxOMatic2 { public static void main(String[] args) throws Exception { Car car = new Car(); ExecutorService exec = Executors.newCachedThreadPool(); //上蜡 exec.execute(new WaxOnTask("Waxx", car)); //抛光 exec.execute(new BuffTask("Buff", car)); //运行一段时间,停止ExecutorService TimeUnit.SECONDS.sleep(3); exec.shutdownNow(); } } 执行结果: ? 1 2 3 4 5 6 7 8 9 10 11 12 [Waxx] is Wax on! [Buff] Buffing... [Waxx] is Wax on! [Buff] Buffing... [Waxx] is Wax on! [Buff] Buffing... [Waxx] is Wax on! [Buff] Buffing... [Waxx] is Wax on! [Buff] Buffing... [Buff] Exiting BuffTask via interrupt. [Waxx] Exiting WaxOnTask via interrupt. 从代码中可以看到,Car的构造器中,单个的Lock将产生一个Condition对象,这个对象被用来管理任务之间的通信。但是,这个Condition对象不包含任何有关处理状态的信息,因此你需要管理额外的表示处理状态的信息,即boolean waxOn。 注意:每个lock()的调用都必须紧跟一个try-finally子句,用来保证在所有情况下都可以释放锁。在使用内建版本时,任务在可以调用await(),signal()或signalAll()之前,必须拥有这个锁。 另外还需要注意的是,这个解决方案比之前一个更加复杂,在本例中这种复杂性并未使你收获更多。Lock和Condition对象只有在更加困难的多线程问题中才是必需的。
考虑这样一个饭店,它有一个厨师(Chef)和一个服务员(Waiter)。这个服务员必须等待厨师准备好菜品。当厨师准备好时,他会通知服务员,之后服务员上菜,然后返回继续等待。这是一个任务协作的示例:厨师代表生产者,而服务员代表消费者。两个任务必须在菜品被生产和消费时进行握手,而系统必须以有序的方式关闭。下面是对这个叙述建模的代码: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; class Meal { private final int orderNum; public Meal(int orderNum) { this.orderNum = orderNum; } @Override public String toString() { return "Meal " + orderNum; } } class Waiter implements Runnable { private Restaurant r; public Waiter(Restaurant r) { this.r = r; } @Override public void run() { try { while(!Thread.interrupted()) { synchronized (this) { while(r.meal == null) { wait();//等待厨师做菜 } } System.out.println("Waiter got " + r.meal); synchronized (r.chef) { r.meal = null;//上菜 r.chef.notifyAll();//通知厨师继续做菜 } } } catch (InterruptedException e) { System.out.println("Waiter task is over."); } } } class Chef implements Runnable { private Restaurant r; private int count = 0;//厨师做的菜品数量 public Chef(Restaurant r) { this.r = r; } @Override public void run() { try { while(!Thread.interrupted()) { synchronized (this) { while(r.meal != null) { wait();//等待服务员上菜 } } if (++count > 10) { System.out.println("Meal is enough, stop."); r.exec.shutdownNow(); } System.out.print("Order up! "); synchronized (r.waiter) { r.meal = new Meal(count);//做菜 r.waiter.notifyAll();//通知服务员上菜 } TimeUnit.MILLISECONDS.sleep(100); } } catch (InterruptedException e) { System.out.println("Chef task is over."); } } } public class Restaurant { Meal meal; ExecutorService exec = Executors.newCachedThreadPool(); //厨师和服务员都服务于同一个饭店 Waiter waiter = new Waiter(this); Chef chef = new Chef(this); public Restaurant() { exec.execute(waiter); exec.execute(chef); } public static void main(String[] args) { new Restaurant(); } } 执行结果: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 Order up! Waiter got Meal 1 Order up! Waiter got Meal 2 Order up! Waiter got Meal 3 Order up! Waiter got Meal 4 Order up! Waiter got Meal 5 Order up! Waiter got Meal 6 Order up! Waiter got Meal 7 Order up! Waiter got Meal 8 Order up! Waiter got Meal 9 Order up! Waiter got Meal 10 Meal is enough, stop. Order up! Waiter task is over. Chef task is over. Restaurant是Waiter和Chef的焦点,它们都必须知道在为哪个饭店工作,因为他们必须和这家饭店的窗口打交道,一边放置或拿取菜品r.meal。在run()中,waiter进入wait()模式,停止其任务,直至被Chef的notifyAll()唤醒。由于这是一个非常简单的程序,因此我们知道只有一个任务将在Waiter的锁上等待:即Waiter任务自身。出于这个原因,理论上可以调用notify()而不是notifyAll()。但是,在更复杂的情况下,可能会有多个任务在某个特定对象锁上等待,因此你不知道哪个任务应该被唤醒。因此调用notifyAll()要更安全一些,这样可以唤醒等待这个锁的所有任务,而每个任务都必须决定这个通知是否与自己相关。 一旦Chef送上Meal并通知Waiter,这个Chef就将等待,知道Waiter收集到订单并通知Chef,之后Chef就可以做下一份菜品了。 注意,wait()被包装在一个while()字句中,这个语句在不断的测试正在等待的事物。乍一看有点怪——如果在等待一个订单,一单你被唤醒,这个订单就必定是可获得的,对吗?正如前面注意到的,在更复杂的并发应用中,某个其他的任务可能在Waiter被唤醒时突然插足并拿走订单。因此唯一安全的方式是使用下面这种wait()的惯用法: ? 1 2 3 while(conditionIsNotMet) { wait(); } 这可以保证在你退出等待循环之前,条件将得到满足,并且如果你收到了关于某事物的通知,而它与这个条件并无关系,或者在你完全退出等待循环之前,这个条件发生了变化,都可以确保你重返等待状态。 请注意观察,对notifyAll()的调用必须首先捕获Waiter上的锁,而在Waiter.run()中的对wait()的调用会自动的释放这个所,因此这是由可能实现的。因为调用notifyAll()必然拥有这个锁,所以这可以保证两个试图在同一个对象上调用notifyAll()的任务不会互相冲突。 通过把整个run()方法体放到一个try语句块中,可以使得这两个run()方法都被设计为可以有序的关闭。catch子句将紧挨着run()方法的括号之前结束,因此,如果这个任务收到了InterruptedException,它将在捕获到异常后立即结束。 注意,在Chef中,在调用shutdownNow()之后,你应该直接从run()返回,并且通常这就是你应该做的。但是,以这种方式执行还有一些更有趣的东西。记住,shutdownNow()将向所有由ExecutorService启动的任务发送interrupt(),但是在Chef中,任务并没有在获得该interrupt()立即结束,因为当任务试图进入一个(可中断的)阻塞操作时,这个中断只能抛出InterruptedException。因此你将首先看到“Order up!”,然后Chef试图调用sleep()方法时,抛出了InterruptedException。如果你移除对sleep()的调用,那么这个任务将回到run()循环的顶部,并由于Thread.interrupted()测试而退出,同时并不抛异常。 在这两个示例中,对于一个任务而言,只有一个单一的地方用于存放对象,从而使得另一个任务稍后可以使用这个对象。但是,在典型的生产者-消费者实现中,应使用先进先出队列来存储被生产和消费的对象。
当你使用线程来同时执行多个任务时,可以通过使用锁(互斥)来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。也就是说,如果两个任务在交替着使用某项共享资源(通常是内存),你可以使用互斥来是的任何时刻只有一个任务可以访问这项资源。那么,如果线程之间是协作关系,我们必须保证某些步骤在其他步骤之前先被处理。举个例子:必须先挖房子的地基,接下来才能并行的铺设钢结构和构建水泥部件,而这两项任务又必须在浇注混凝土之前完成,等等。 当任务协作时,关键问题是这些任务之间的握手。为了实现握手,我们使用了相同的基础特性:互斥。在这种情况下,互斥能够确保只有一个任务可以响应某个信号,这样就可以根除任何可能的竞争条件。在互斥之上,我们为任务添加了一种途径,可以将其自身挂起,知道某些外部条件发生变化(例如:地基已经挖好),表示是时候让这个人物向前进行了为止。本文,我们将浏览任务间的握手问题,这种握手可以通过Object的方法wait()和notify()来安全地实现。JavaSE5的并发类库还提供了具有await()和signal()方法的Condition对象。 wait()与notifyAll() wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。通常,这种条件将由另一个任务来改变。你肯定不想在你的任务测试这个条件的同事,不断地进行空循环,这杯称为忙等待,通常是一种不良的CPU周期使用方式。因此wait()会在等待外部世界产生变化的时候将任务挂起,并且只有在notify()或notifyAll()发生时,即表示发生了某些感兴趣的事物,这个任务才被唤醒并去检查所发生的变化。因此wait()提供了一种在任务之间对活动同步的方式。 调用sleep()的时候锁并没有被释放,调用yield()也属于这种情况,理解这一点很重要。另一方面,当一个任务在方法里遇到了对wait()的调用的时候,线程的执行被挂起,对象上的锁被释放。因为wait()将释放锁,这就意味着另一个任务可以获得这个锁,因此在该对象(现在是未锁定的)中的其他synchronized方法可以在wait()期间被调用。因为这些其他的方法通常将会产生改变,而这种改变正是使被挂起的任务重新唤醒所感兴趣的变化。因此,当你调用wait()时,就是在生命:“我已经刚刚做完所有能做的事情,因此我要在这里等待,但是我希望其他的synchronized操作在条件适合的情况下能够执行。” 有两种形式的wait(),分别是sleep(long millis)和sleep(),第一种方式接受好描述作为参数,含义与sleep()方法里参数的意思相同,都是指“在此期间暂停”。但与sleep()不同的是,对于wait()而言: 在wait()期间,对象锁是释放的 可以通过notify()、notifyAll(),或者令时间到期,从wait()中恢复执行 第二种,也是更常用形式,它不接受任何参数,这种wait()将无线等待下去,直到线程接受到notify()或者notifyAll()消息。 wait()、notify()、notifyAll()有一个比较特殊的方面,那就是这些方法的基类是Object的一部分,而不是Thread类的一部分。尽管开始看起来有点奇怪——仅仅针对线程的功能却作为通用基类的一部分而实现,不过这是有道理的,因为这些方法操作的锁也是所有对象的一部分。实际上,只能在同步控制方法或者同步控制块里调用wait()、notify()、和notifyAll()(因为不用操作锁,所以sleep()可以在非同步控制方法里调用)。 如果在非同步控制方法里调用这些方法,程序能通过编译,但在运行的时候,将得到IllegalMonitorStateException异常,并伴随着一些模糊的消息,比如:当前线程不是锁的拥有者。消息的意思是,调用wait()、notify()和notifyAll()的任务在调用这些方法之前必须“拥有”(获取)对象的锁。 可以让另一个对象执行某种操作以维护其自己的锁。要这么做的话,必须首先得到对象的锁。比如,如果要向对象x发送notifyAll(),那么就必须在能够得到x的锁的同步块中这么做: ? 1 2 3 synchronized(x) { x.notifyAll(); } 让我们来看一个简单的示例,WaxOMatic.java有两个过程:一个是将蜡涂到Car上,一个是抛光它。抛光任务在涂蜡任务之后完成,而涂蜡任务在涂另一层蜡之前,必须等待抛光任务完成。WaxOn和WaxOff都使用了Car对象,该对象在这些任务等待条件变化的时候,使用wait()和notifyAll()来挂起和重新启动这些任务: ? 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 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; class Car { private boolean waxOn = false;//是否上蜡 //上蜡 public synchronized void waxed() { waxOn = true; notify(); } //抛光 public synchronized void buffed() { waxOn = false; notify(); } //等待上蜡 public synchronized void waitForWaxing() throws InterruptedException { while(waxOn == false) { this.wait(); } } //等待抛光 public synchronized void waitForBuffing() throws InterruptedException { while(waxOn == true) { this.wait(); } } } class WaxOnTask implements Runnable { private Car car; private String name; public WaxOnTask(String name, Car car) { this.name = name; this.car = car; } @Override public void run() { try { while(!Thread.interrupted()) { System.out.println("[" + name + "] is Wax on!");//正在上蜡 TimeUnit.MILLISECONDS.sleep(500); car.waxed();//上蜡完成 car.waitForBuffing();//等待抛光 } } catch (InterruptedException e) { System.out.println("[" + name + "] Exiting WaxOnTask via interrupt."); } } } class BuffTask implements Runnable { private Car car; private String name; public BuffTask(String name, Car car) { this.name = name; this.car = car; } @Override public void run() { try { while(!Thread.interrupted()) { car.waitForWaxing();//等待上蜡 System.out.println("[" + name + "] Buffing...");//正在抛光 TimeUnit.MILLISECONDS.sleep(500); car.buffed();//抛光完成 } } catch (InterruptedException e) { System.out.println("[" + name + "] Exiting BuffTask via interrupt."); } } } public class WaxOMatic { public static void main(String[] args) throws Exception { Car car = new Car(); ExecutorService exec = Executors.newCachedThreadPool(); //上蜡 exec.execute(new WaxOnTask("Wax", car)); //抛光 exec.execute(new BuffTask("Buff", car)); //运行一段时间,停止ExecutorService TimeUnit.SECONDS.sleep(5); exec.shutdownNow(); } } 执行结果: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 [Wax] is Wax on! [Buff] Buffing... [Wax] is Wax on! [Buff] Buffing... [Wax] is Wax on! [Buff] Buffing... [Wax] is Wax on! [Buff] Buffing... [Wax] is Wax on! [Buff] Buffing... [Wax] is Wax on! [Buff] Exiting BuffTask via interrupt. [Wax] Exiting WaxOnTask via interrupt. 这里,Car有一个单一的boolean属性waxOn,表示涂蜡-抛光的处理状态。 在waitForWaxing()中将检查waxOn标志,如果它为false,那么这个调用任务将通过调用wait()方法而挂起。这个行为发生在synchronized方法中这一点很重要,因为在这样的方法中,任务已经获得了锁。当你调用wait()时,线程被挂起,而锁被释放。所被释放是这一点的本质所在,因为为了安全地改变对象的状态,其他某个任务就必须能够获得这个锁。 WaxOnTask.run()表示给汽车打蜡过程的第一个步骤,因此它将执行它的操作:调用sleep()以模拟需要打蜡的时间,然后告知汽车打蜡结束,并调用waitForBuffing(),这个方法会用一个wait()来挂起这个任务,直至BuffTask任务调用这辆车的buffed(),从而改变状态并调用notifyAll()为止。另一方面,BuffTask.run()立即进入waitForWaxing(),并因此而被挂起,直至WaxOnTask涂完蜡并且waxed()被调用。整个运行过程中,你可以看到当控制权在两个任务之间来回交互传递时,这两个步骤过程在不断的重复。5秒钟之后,shutdownNow()方法发送给每个线程的interrupt信号会终止每个线程。
线程状态 我们知道,一个线程可以处于以下四种状态之一: 1. 新建(New):当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必须的系统资源,并执行了初始化。此刻线程已经有资格获取CPU时间了,之后调度器将把这个线程转变为可运行状态或阻塞状态。 2. 就绪(Runnable):在这种状态下,只要调度器将CPU时间片分给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。 3. 阻塞(Blocked):线程能够运行,但有某个或多个条件阻止它运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间片。直到线程重新进入了就绪状态,它才有可能执行操作。 4. 死亡(Dead):处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已经结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以不被中断。 进入线程状态 而一个任务进入阻塞状态,可能由以下原因造成: 1. 通过调用sleep(milliseconds)方法使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。 2. 通过调用wait()方法使线程挂起。直到线程得到了notify()或notifyAll()消息(或者在JavaSE5的java.util.concurrent类库中等价的signal()活signalAll()消息),线程才会进入就绪状态。 3. 任务在等待某个I/O操作完成。 4. 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。 在较早的代码中,也可能会看到用suspend()和resume()方法来阻塞和唤醒线程,但是在Java新版本中这些方法被废弃了,因为它们可能导致死锁。stop()方法也已经被废弃了,因为它不释放线程获得的锁,并且如果线程处于不一致的状态,其他任务可以在这种状态下浏览并修改它们。 现在我们需要查看的问题是:有事你希望能够终止处于阻塞状态的任务。如果对于阻塞装填的任务,你不能等待其到达代码中可以检查其状态值的某一点,因而决定让它主动终止,那么你就必须强制这个任务跳出阻塞状态。 中断 正如你所想象的,在Runnable.run()方法的中间打断它,与到达程序员准备好离开该方法的其他一些地方相比,要复杂得多。因为当你打断被阻塞的任务时,可能需要清理资源。正因为这一点,在任务的run()方法中间打断,更像是抛出的异常,因此在Java线程中的这种类型的异常中断中用到了异常。为了在以这种方式终止任务时返回良好的状态,你必须仔细考虑代码的执行路径,并仔细编写catch字句以便正确的清楚所有事物。 Thread类包含了interrupt()方法,因此你可以终止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当抛出该异常或者该任务调用Thread.interrupted()时,中断状态将被复位。正如你将看到的,Thread.interrupted()提供了离开run()循环而不抛出异常的第二种方式。 为了调用interrupt(),你必须持有Thread对象。你可能已经注意到了,新的concurrent类库似乎在避免对Thread对象上的直接操作,转而尽量的通过Executor来执行所有操作。如果你在Executor上调用shutdownNow(),那么它将发送一个interrupt()调用给它启动的线程。这么做是有意义的,因为当你完成工程中的某个部分或者整个程序时,通常会希望同时关闭某个特定Executor的所有任务。然而,你有时也会希望只中断某个单一任务。如果使用Executor,那么通过调用submit()方法而不是execute()方法来启动任务,就可以持有该任务的上下文。submit()将返回一个泛型Future<?>,其中有一个未修饰的参数,因为你永远都不会在其上调用get()——持有这种Future的关键在于你可以在其上调用cancel(),并因此可以使用它来中断某个特定任务。如果你将true传递给cancel(),那么它就会拥有在该线程上调用interrupt()以停止这个线程的能力。因此,cancel是一种中断由Executor启动的单个线程的方式。 下面的示例使用Executor展示了基本的interrupt()用法: ? 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 import java.io.IOException; import java.io.InputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; class SleepBlocked implements Runnable { @Override public void run() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { System.out.println("InterruptedException"); } System.out.println("Exiting SleepBlocked.run()"); } } class IOBlocked implements Runnable { private InputStream in; public IOBlocked(InputStream is) { in = is; } @Override public void run() { try { System.out.println("Waiting for read():"); in.read(); } catch (IOException e) { if (Thread.currentThread().isInterrupted()) { System.out.println("Interrupted from Blocked I/O"); } else { throw new RuntimeException(e); } } System.out.println("Exiting IOBlocked.run()"); } } class SynchronizedBlocked implements Runnable { public synchronized void f() { while(true) { //永不释放获得的锁 Thread.yield(); } } public SynchronizedBlocked() { //在构造的时候就获取该对象的锁 new Thread(){ @Override public void run() { f(); } }; } @Override public void run() { System.out.println("Trying to call f()"); f(); System.out.println("Exiting SynchronizedBlocked.run()"); } } public class Interrupting { private static ExecutorService exec = Executors.newCachedThreadPool(); static void test(Runnable r) throws InterruptedException { Future<?> future = exec.submit(r); TimeUnit.SECONDS.sleep(1); System.out.println("Interrupting " + r.getClass().getName()); future.cancel(true);//如果在运行的话,中断该线程。 System.out.println("Interrupting sent to " + r.getClass().getName()); } public static void main(String[] args) throws Exception { test(new SleepBlocked()); test(new IOBlocked(System.in)); test(new SynchronizedBlocked()); TimeUnit.SECONDS.sleep(3); System.out.println("Aborting with System.exit(0);"); //强行停止退出 System.exit(0); } } 执行结果: ? 1 2 3 4 5 6 7 8 9 10 11 Interrupting SleepBlocked Interrupting sent to SleepBlocked InterruptedException Exiting SleepBlocked.run() Waiting for read(): Interrupting IOBlocked Interrupting sent to IOBlocked Trying to call f() Interrupting SynchronizedBlocked Interrupting sent to SynchronizedBlocked Aborting with System.exit(0); 上面的每个任务都表示了一种不同类型的阻塞。SleepBlock是可中断的阻塞示例,而IOBlocked和SynchronizedBlocked是不可中断的阻塞示例。这个程序证明I/O和在synchronized块上的等待是不可中断的,但是通过浏览代码,你也可以预见到这一点——无论是I/O还是尝试调用synchronized方法,都不需要任何InterruptedException处理器。 两个雷很简单直观:在第一个类中run()方法调用了sleep(),在第二个类中调用了read()。但是为了掩饰SynchronizedBlock,我们必须首先获得锁。这是通过在构造器中创建匿名的Thread类的实例来实现的,这个匿名Thread类的对象通过调用f()获得了对象锁(这个线程必须有别于为启动SynchronizedBlock.run()的线程,因为同一个线程可以多次获得某个对象锁,你将在稍后看见)。由于f()永远都不反回,因此这个锁永远不会释放,而SynchronizedBlock.run()在试图调用f(),并阻塞以等待这个锁被释放。 从输出中可以看到,你能够中断对sleep()的调用(或者任何要求抛出InterruptedException的调用)。但是你不能中断正在试图获取synchronized锁或者正在试图执行I/O操作的线程。这有点令人烦恼,特别是在创建执行I/O任务时,因为这意味着I/O具有锁住你的多线程程序的潜在可能。特别是对于急于Web的程序,这更是关乎厉害。 对于这类问题,有一个略显笨拙但是确实行之有效的解决方案,那就是关闭任务在其上发生阻塞的资源: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class CloseResource { public static void main(String[] args) throws Exception { ExecutorService service = Executors.newCachedThreadPool(); ServerSocket server = new ServerSocket(8080); InputStream stream = new Socket("localhost", 8080).getInputStream(); service.execute(new IOBlocked(stream)); TimeUnit.MILLISECONDS.sleep(100); System.out.println("Shutting down all threads"); service.shutdownNow();//尝试停止所有正在执行的任务 TimeUnit.SECONDS.sleep(1); System.out.println("Closing " + stream.getClass().getName()); stream.close();//通过关闭线程操作的资源来释放阻塞的线程 } } 执行结果: ? 1 2 3 4 5 Waiting for read(): Shutting down all threads Closing java.net.SocketInputStream Interrupted from Blocked I/O Exiting IOBlocked.run() 在shutdownNow()被调用之后以及在输入流上调用close()之前的延迟强调的是一旦底层资源被关闭,任务将解除阻塞。 幸运的是,各种NIO类提供了更人性化的I/O中断。被阻塞的nio通道会自动地响应中断: ? 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 import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousCloseException; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.SocketChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; class NIOBlocked implements Runnable { private final SocketChannel channel; public NIOBlocked(SocketChannel channel) { this.channel = channel; } @Override public void run() { try { System.out.println("Waiting for read() in " + this); channel.read(ByteBuffer.allocate(1));//阻塞当前任务 } catch (ClosedByInterruptException e) { System.out.println("ClosedByInterruptException"); } catch (AsynchronousCloseException e) { System.out.println("AsynchronousCloseException"); } catch (IOException e) { throw new RuntimeException(e); } System.out.println("Exiting NIOBlocked.run() " + this); } } public class NIOInterruption { public static void main(String[] args) throws Exception { ExecutorService service = Executors.newCachedThreadPool(); ServerSocket server = new ServerSocket(8080); InetSocketAddress isa = new InetSocketAddress("localhost", 8080); SocketChannel sc1 = SocketChannel.open(isa); SocketChannel sc2 = SocketChannel.open(isa); Future<?> f = service.submit(new NIOBlocked(sc1)); service.execute(new NIOBlocked(sc2)); //尝试关闭任务,但由于任务处于阻塞状态,关闭不了。 service.shutdown(); TimeUnit.SECONDS.sleep(1); // 通过在channel1上调用cancel来产生中断 f.cancel(true); TimeUnit.SECONDS.sleep(1); // 释放channel2 sc2.close(); } } 执行结果: ? 1 2 3 4 5 6 Waiting for read() in NIOBlocked@18b8914 Waiting for read() in NIOBlocked@1d49247 ClosedByInterruptException Exiting NIOBlocked.run() NIOBlocked@18b8914 AsynchronousCloseException Exiting NIOBlocked.run() NIOBlocked@1d49247 如你所见,你还可以关闭底层资源以释放锁,尽管这种做法一般不是必须的。注意,使用execute()来启动两个任务,并调用service.shutdownNow()将可以很容易的终止所有事物,而对于捕获上面示例中的Future,只有在将中断发送给一个线程,同时不发送给另一个线程时才是必须的。
引言 曾几何时,我也敲打过无数次这样的命令: 然而之前的我都只关心过版本号,也就是第一行的内容。今天,我们就来看看第3行输出的内容:JVM的类型和工作模式。 其实说Server和Client是JVM的两种工作模式是不准确的,因为它们就是不同的虚拟机,因此应该说有两种类型的JVM。 第三行的输出中可以看到:JVM的名字(HotSpot)、类型(Client)和build ID(24.79-b02) 。除此之外,我们还知道JVM以混合模式(mixed mode)在运行,这是HotSpot默认的运行模式,意味着JVM在运行时可以动态的把字节码编译为本地代码。我们也可以看到类数据共享(class data sharing)是开启(即第三行最后的sharing)的。类数据共享(class data sharing)是一种在只读缓存(在jsa文件中,”Java Shared Archive”)中存储JRE的系统类,被所有Java进程的类加载器用来当做共享资源,它可能在经常从jar文档中读所有的类数据的情况下显示出性能优势。 JVM的类型 通过百度搜索,只能搜到几篇被重复转载的文章。比如这一篇,这里面基本描述了两种类型的JVM的区别: -Server VM启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。 但我认为仅仅知道这些区别还不够。然而,我在百度的搜索结果中很少看见有描述的比较深入的关于JVM类型和模式区别的文章。不过我倒是找到了这一篇文章。 这篇文章中提到了如下内容: 当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,,服务起来之后,性能更高. 对于这个结果,我觉得还是不够深入。于是FQ通过Google搜索,前几条即为我想要的结果。 两种类型JVM的区别 那么,Client JVM和Server JVM到底在哪些方面不同呢?Oracle官方网站的高频问题上这么解释的: These two systems are different binaries. They are essentially two different compilers (JITs)interfacing to the same runtime system. The client system is optimal for applications which need fast startup times or small footprints, the server system is optimal for applications where the overall performance is most important. In general the client system is better suited for interactive applications such as GUIs. Some of the other differences include the compilation policy,heap defaults, and inlining policy. 大意是说,这两个JVM是使用的不同编译器。Client JVM适合需要快速启动和较小内存空间的应用,它适合交互性的应用,比如GUI;而Server JVM则是看重执行效率的应用的最佳选择。不同之处包括:编译策略、默认堆大小、内嵌策略。 根据《The Java HotSpot Performance Engine Architecture》: The Client VM compiler does not try to execute many of the more complex optimizations performed by the compiler in the Server VM, but in exchange, it requires less time to analyze and compile a piece of code. This means the Client VM can start up faster and requires a smaller memory footprint. Note: It seems that the main cause of the difference in performance is the amount of optimizations. The Server VM contains an advanced adaptive compiler that supports many of the same types of optimizations performed by optimizing C++ compilers, as well as some optimizations that cannot be done by traditional compilers, such as aggressive inlining across virtual method invocations. This is a competitive and performance advantage over static compilers. Adaptive optimization technology is very flexible in its approach, and typically outperforms even advanced static analysis and compilation techniques. Both solutions deliver extremely reliable, secure, and maintainable environments to meet the demands of today’s enterprise customers. 很明显,Client VM的编译器没有像Server VM一样执行许多复杂的优化算法,因此,它在分析和编译代码片段的时候更快。而Server VM则包含了一个高级的编译器,该编译器支持许多和在C++编译器上执行的一样的优化,同时还包括许多传统的编译器无法实现的优化。 官方文档是从编译策略和内嵌策略分析了二者的不同,下面的命令则从实际的情况体现了二者在默认堆大小上的差别: 对于Server JVM: ? 1 2 3 4 5 6 7 8 9 $ java -XX:+PrintFlagsFinal -version 2>&1 | grep -i -E 'heapsize|permsize|version' uintx AdaptivePermSizeWeight = 20 {product} uintx ErgoHeapSizeLimit = 0 {product} uintx InitialHeapSize := 66328448 {product} uintx LargePageHeapSizeThreshold = 134217728 {product} uintx MaxHeapSize := 1063256064 {product} uintx MaxPermSize = 67108864 {pd product} uintx PermSize = 16777216 {pd product} java version "1.6.0_24" 对于Client JVM: ? 1 2 3 4 5 6 7 8 9 $ java -client -XX:+PrintFlagsFinal -version 2>&1 | grep -i -E 'heapsize|permsize|version' uintx AdaptivePermSizeWeight = 20 {product} uintx ErgoHeapSizeLimit = 0 {product} uintx InitialHeapSize := 16777216 {product} uintx LargePageHeapSizeThreshold = 134217728 {product} uintx MaxHeapSize := 268435456 {product} uintx MaxPermSize = 67108864 {pd product} uintx PermSize = 12582912 {pd product} java version "1.6.0_24" 可以很清楚的看到,Server JVM的InitialHeapSize和MaxHeapSize明显比Client JVM大出许多来。 效率对比 下面是一个例子,它展示了二者执行的效率,该例子来自Onkar Joshi’s blog: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 public class LoopTest { public static void main(String[] args) { long start = System.currentTimeMillis(); spendTime(); long end = System.currentTimeMillis(); System.out.println(end-start); } private static void spendTime() { for (int i =500000000;i>0;i--) { } } } 注意:这段代码只编译一次,只是运行这段代码的JVM不同而已。不要使用Eclipse中的Run As,因为它会将代码重新编译。这里,我们使用java命令来执行这段代码: 看到区别了吧? 自动检测Server-Class机器 以下内容引用自:http://docs.oracle.com/javase/6/docs/technotes/guides/vm/server-class.html 从J2SE 5.0开始,当一个应用启动的时候,加载器会尝试去检测应用是否运行在 "server-class" 的机器上,如果是,则使用Java HotSpot Server Virtual Machine (server VM)而不是 Java HotSpot Client Virtual Machine (client VM)。这样做的目的是提高执行效率,即使没有为应用显式配置VM。 注意: 从Java SE 6开始, server-class机器的定义是至少有2个CPU和至少2GB的物理内存 下面这张图展示了各个平台的默认的JVM(注意:—代表不提供该平台的JVM ): -------------------------------引用内容结束-------------------------------------- JVM类型的切换 上面的运行结果中还提到了如何切换JVM的类型,我们就来看看为什么第一个截图里面输出的是: ? 1 Java HotSpot(TM) Client VM 这里需要注意的是,Oracle网站这样说: Client and server systems are both downloaded with the 32-bit Solaris and Linux downloads. For 32-bit Windows, if you download the JRE, you get only the client, you'll need to download the SDK to get both systems. 因此,如果想要在windows平台下从Client JVM切换到Server JVM,需要下载JDK而非JRE。打开JDK安装目录(即%JAVA_HOME%):我们可以看到%JAVA_HOME%\jre\bin下有一个server和client文件夹,这里面各有一个jvm.dll,但是大小不同,这就说明了它们是不同的JVM的文件: 打开 %JAVA_HOME%\jre\lib\i386\jvm.cfg文件(正如第一幅图所见,我这里安装的是32JDK,其他版本的JDK可能不是i386文件夹)(注意是JDK文件夹下的jre,而非和JDK同级的jre6/7/8),会注意到以下内容(灰色选中部分): 再看看下方的配置,第一行就配置了使用client方式,因此首选使用client模式的JVM,这就是为什么一开始的java -version命令会输出Java HotSpot(TM) Client VM的原因了。现在将第33、34行配置交换一下,再在命令行中输入java -version,则会得到以下结果: 这就将JVM的工作模式切换到Server了,这个修改是全局的,以后使用到的这个JVM都是工作在Server模式的。 当然,如果你不想全局改动,也可以按照下面在java命令后加上-server或者-client来明确指定本次java命令需要JVM使用何种模式来工作,例如: 这个就是语句级的修改了。 注意,不管是全局修改还是语句级的修改,实际上会导致下次执行Java程序时会使用对应目录下的jvm.dll。如何证明?这里我将%JAVA_HOME%\jre\bin下面的server文件夹移动到其他位置,再次运行java -server -version命令,则会出现下面的错误: JVM的工作模式 JVM的几种工作模式 在命令行里输入java -X,你会看到以下结果: 其实这两个是JVM工作的模式。JVM有以下几种模式:-Xint, -Xcomp, 和 -Xmixed。从上图的输出结果中也可以看到,mixed是JVM的默认模式,其实在文章一开始的时候就提到了,因为在java -version命令中,输出了以下内容: ? 1 Java HotSpot(TM) Client VM (build 24.79-b02, mixed mode, sharing) 中间的mixed mode就说明当前JVM是工作在mixed模式下的。-Xint和-Xcomp参数和我们的日常工作不是很相关,但是我非常有兴趣通过它来了解下JVM。 -Xint代表解释模式(interpreted mode),-Xint标记会强制JVM以解释方式执行所有的字节码,当然这会降低运行速度,通常低10倍或更多。现在通过刚才的例子(没有重新编译过)来验证一下: 可以看到,在都使用Client JVM的前提下,混合模式下,平均耗时150ms,然而在解释模式下,平均耗时超过1600ms,这基本上是10倍以上的差距。 -Xcomp代表编译模式(compiled mode),与它(-Xint)正好相反,JVM在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化。这听起来不错,因为这完全绕开了缓慢的解释器。然而,很多应用在使用-Xcomp也会有一些性能损失,但是这比使用-Xint损失的少,原因是-Xcomp没有让JVM启用JIT编译器的全部功能。因此在上图中,我们并没有看到-Xcomp比-Xmixed快多少。 -Xmixed代表混合模式(mixed mode),前面也提到了,混合模式是JVM的默认工作模式。它会同时使用编译模式和解释模式。对于字节码中多次被调用的部分,JVM会将其编译成本地代码以提高执行效率;而被调用很少(甚至只有一次)的方法在解释模式下会继续执行,从而减少编译和优化成本。JIT编译器在运行时创建方法使用文件,然后一步一步的优化每一个方法,有时候会主动的优化应用的行为。这些优化技术,比如积极的分支预测(optimistic branch prediction),如果不先分析应用就不能有效的使用。这样将频繁调用的部分提取出来,编译成本地代码,也就是在应用中构建某种热点(即HotSpot,这也是HotSpot JVM名字的由来)。使用混合模式可以获得最好的执行效率。 切换JVM的工作模式 和切换JVM的类型一样,我们可以在命令行里显示指定使用JVM的何种模式,比如: 获取JVM的工作模式 在JVM运行时,我们可以通过下列代码检查JVM的类型和工作模式: ? 1 2 System.out.println(System.getProperty("java.vm.name")); //获取JVM名字和类型 System.out.println(System.getProperty("java.vm.info")); //获取JVM的工作模式 你可能得到以下结果:
由于线程的本质特性,使得你不能捕获从线程中逃逸的异常。一旦异常逃出任务的run()方法它就会向外传播到控制台,除非你采取特殊的步骤捕获这种错误的异常。在Java SE5之前,你可以使用线程组来捕捉这种异常,但是有了Java SE5,就可以用Executor来解决这个问题了。 下面的任务总是会抛出一个异常,该异常会传播到其run()方法的外部,并且main()展示了当你运行它时所发生的事情: ? 1 2 3 4 5 6 7 8 9 10 11 12 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExceptionThread implements Runnable { public void run() { throw new RuntimeException(); } public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); service.execute(new ExceptionThread()); } } 输出如下: ? 1 2 3 4 5 Exception in thread "pool-1-thread-1" java.lang.RuntimeException at com.abc.thread.ExceptionThread.run(ExceptionThread.java:6) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) 将main的主体放在try-catch语句块中也是没有作用的: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExceptionThread implements Runnable { public void run() { throw new RuntimeException(); } public static void main(String[] args) { try { ExecutorService service = Executors.newCachedThreadPool(); service.execute(new ExceptionThread()); } catch (RuntimeException e) { System.out.println("Catched Runtime Exception."); } } } 这将产生于前面示例相同的结果:未捕获的异常。 为了解决这个问题,我们要修改Executor产生线程的方式。Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用。为了使用它,我们创建了一个新类型的ThreadFactory,它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; public class ExceptionThread2 implements Runnable { public void run() { throw new RuntimeException("NullPointer"); } public static void main(String[] args) { ThreadFactory tFactory = new MyThreadFactory(); ExecutorService service = Executors.newCachedThreadPool(tFactory); Runnable task = new ExceptionThread2(); service.execute(task); } } class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { // 处理从线程里抛出来的异常。 public void uncaughtException(Thread t, Throwable e) { System.out.println("Catched Throwable: " + e.getClass().getSimpleName() + ", " + e.getMessage()); } } class MyThreadFactory implements ThreadFactory { // 重新组织创建线程的方式 public Thread newThread(Runnable r) { Thread t = new Thread(r); // 为每一个线程都绑定一个异常处理器。 t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); System.out.println("Thread[" + t.getName() + "] created."); return t; } } 执行的结果如下: 可以看到,线程池中有2个线程,当一个线程发生异常时,该异常被捕捉了。 上面的示例使得你可以按照具体情况(在newThread()方法中使用if, case等语句)为每个线程逐个的设置处理器。如果你知道将要在代码中处处使用相同的异常处理器,那么更简单的方式是在Thread类中设置一个静态域,并将这个处理器设置为默认的处理器即可: ? 1 2 3 4 5 6 7 8 9 10 11 import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SettingDefaultHandler { public static void main(String[] args) { // 为线程设置默认的异常处理器。 Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(new ExceptionThread2()); } } 这个处理器只有在不存在线程专有的未捕获异常处理器的情况下才会被调用。系统会检查线程专有版本,如果没有发现,则检查线程组是否有专有的uncaughtException()方法,如果也没有,才会调用defaultUncaughtExceptionHandler。
关于二维码是什么,以及二维码是如何生成的,我也没有研究得很深入,就不多说了,以免误导大家。请参看: java 二维码原理以及用java实现的二维码的生成、解码 二维码的生成细节和原理 下面是一个可以生成和解析二维码的工具类,该类用到了zxing工具包,我通过Maven去下载的: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 <dependencies> <!-- JavaSE包依赖于Core包,因此Core包不需要直接依赖了 <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.1.0</version> </dependency> --> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.1.0</version> </dependency> </dependencies> 在网上搜索的时候我发现,有不少同学在使用maven的时候都同时导入了这两个包,但是我发现这个artifactId为javase的包依赖于core包,因此我们不需要再在pom.xml中声明对core包的依赖了。 下面这个类是一个工具类,该类可以生成一维码和二维码,也可以解析二维码: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 package com.abc.qrcode; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Image; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import javax.imageio.ImageIO; import com.google.zxing.BarcodeFormat; import com.google.zxing.Binarizer; import com.google.zxing.BinaryBitmap; import com.google.zxing.LuminanceSource; import com.google.zxing.MultiFormatReader; import com.google.zxing.MultiFormatWriter; import com.google.zxing.NotFoundException; import com.google.zxing.ReaderException; import com.google.zxing.Result; import com.google.zxing.WriterException; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import com.google.zxing.common.BitMatrix; import com.google.zxing.common.HybridBinarizer; public class QRCodeUtil { // 这几项可以由其他调用的类设置,因此是public static的 public static int BARCODE_WIDTH = 80; public static int QRCODE_WIDTH = 200; public static String FORMAT = "jpg";// 生成的图片格式 public static int BLACK = 0x000000;// 编码的颜色 public static int WHITE = 0xFFFFFF;// 空白的颜色 // 二维码中间的图像配置。注意,由于二维码的容错率有限,因此中间遮挡的面积不要太大,否则可能解析不出来。 private static int ICON_WIDTH = (int)(QRCODE_WIDTH / 6); private static int HALF_ICON_WIDTH = ICON_WIDTH / 2; private static int FRAME_WIDTH = 2;// Icon四周的边框宽度 // 二维码读码器和写码器 private static final MultiFormatWriter WRITER = new MultiFormatWriter(); private static final MultiFormatReader READER = new MultiFormatReader(); // 测试 public static void main(String[] args) throws Exception { /** * 二维码测试。 */ String iconPath = "C:\\icon.jpg"; String content = "http://www.baidu.com"; File qrCode = new File("C:\\QRCode." + FORMAT); File qrCodeWithIcon = new File("C:\\QRCodeWithIcon." + FORMAT); // 生成二维码 writeToFile(createQRCode(content), qrCode); // 生成带图标的二维码 writeToFile(createQRCodeWithIcon(content, iconPath), qrCodeWithIcon); // 解析二维码 System.out.println(parseImage(qrCode)); // 解析带图标的二维码 System.out.println(parseImage(qrCodeWithIcon)); // 编码成字节数组 byte[] data = createQRCodeToBytes(content); String result = parseQRFromBytes(data); System.out.println(result); /** * 一维码测试。 */ String barCodeContent="6936983800013"; File barCode = new File("C:\\BarCode." + FORMAT); // 生成一维码 writeToFile(createBarCode(barCodeContent), barCode); // 解析一维码 System.out.println(parseImage(barCode)); } /** * 将String编码成二维码的图片后,使用字节数组表示,便于传输。 * * @param content * @return * @throws WriterException * @throws IOException */ public static byte[] createQRCodeToBytes(String content) throws WriterException, IOException { BufferedImage image = createQRCode(content); ByteArrayOutputStream os = new ByteArrayOutputStream(); ImageIO.write(image, FORMAT, os); return os.toByteArray(); } /** * 把一个String编码成二维码的BufferedImage. * * @param content * @return * @throws WriterException */ public static final BufferedImage createQRCode(String content) throws WriterException { // 长和宽一样,所以只需要定义一个SIZE即可 BitMatrix matrix = WRITER.encode( content, BarcodeFormat.QR_CODE, QRCODE_WIDTH, QRCODE_WIDTH); return toBufferedImage(matrix); } /** * 编码字符串为二维码,并在该二维码中央插入指定的图标。 * @param content * @param iconPath * @return * @throws WriterException */ public static final BufferedImage createQRCodeWithIcon( String content, String iconPath) throws WriterException { BitMatrix matrix = WRITER.encode( content, BarcodeFormat.QR_CODE, QRCODE_WIDTH, QRCODE_WIDTH); // 读取Icon图像 BufferedImage scaleImage = null; try { scaleImage = scaleImage(iconPath, ICON_WIDTH, ICON_WIDTH, true); } catch (IOException e) { e.printStackTrace(); } int[][] iconPixels = new int[ICON_WIDTH][ICON_WIDTH]; for (int i = 0; i < scaleImage.getWidth(); i++) { for (int j = 0; j < scaleImage.getHeight(); j++) { iconPixels[i][j] = scaleImage.getRGB(i, j); } } // 二维码的宽和高 int halfW = matrix.getWidth() / 2; int halfH = matrix.getHeight() / 2; // 计算图标的边界: int minX = halfW - HALF_ICON_WIDTH;//左 int maxX = halfW + HALF_ICON_WIDTH;//右 int minY = halfH - HALF_ICON_WIDTH;//上 int maxY = halfH + HALF_ICON_WIDTH;//下 int[] pixels = new int[QRCODE_WIDTH * QRCODE_WIDTH]; // 修改二维码的字节信息,替换掉一部分为图标的内容。 for (int y = 0; y < matrix.getHeight(); y++) { for (int x = 0; x < matrix.getWidth(); x++) { // 如果点在图标的位置,用图标的内容替换掉二维码的内容 if (x > minX && x < maxX && y > minY && y < maxY) { int indexX = x - halfW + HALF_ICON_WIDTH; int indexY = y - halfH + HALF_ICON_WIDTH; pixels[y * QRCODE_WIDTH + x] = iconPixels[indexX][indexY]; } // 在图片四周形成边框 else if ((x > minX - FRAME_WIDTH && x < minX + FRAME_WIDTH && y > minY - FRAME_WIDTH && y < maxY + FRAME_WIDTH) || (x > maxX - FRAME_WIDTH && x < maxX + FRAME_WIDTH && y > minY - FRAME_WIDTH && y < maxY + FRAME_WIDTH) || (x > minX - FRAME_WIDTH && x < maxX + FRAME_WIDTH && y > minY - FRAME_WIDTH && y < minY + FRAME_WIDTH) || (x > minX - FRAME_WIDTH && x < maxX + FRAME_WIDTH && y > maxY - FRAME_WIDTH && y < maxY + FRAME_WIDTH)) { pixels[y * QRCODE_WIDTH + x] = WHITE; } else { // 这里是其他不属于图标的内容。即为二维码没有被图标遮盖的内容,用矩阵的值来显示颜色。 pixels[y * QRCODE_WIDTH + x] = matrix.get(x, y) ? BLACK : WHITE; } } } // 用修改后的字节数组创建新的BufferedImage. BufferedImage image = new BufferedImage( QRCODE_WIDTH, QRCODE_WIDTH, BufferedImage.TYPE_INT_RGB); image.getRaster().setDataElements(0, 0, QRCODE_WIDTH, QRCODE_WIDTH, pixels); return image; } /** * 从一个二维码图片的字节信息解码出二维码中的内容。 * * @param data * @return * @throws ReaderException * @throws IOException */ public static String parseQRFromBytes(byte[] data) throws ReaderException, IOException { ByteArrayInputStream is = new ByteArrayInputStream(data); BufferedImage image = ImageIO.read(is); return parseImage(image); } /** * 从一个图片文件中解码出二维码中的内容。 * * @param file * @return 解析后的内容。 * @throws IOException * @throws ReaderException */ public static final String parseImage(File file) throws IOException, ReaderException { BufferedImage image = ImageIO.read(file); return parseImage(image); } /** * 将字符串编码成一维码(条形码)。 * @param content * @return * @throws WriterException * @throws IOException */ public static BufferedImage createBarCode(String content) throws WriterException, IOException { MultiFormatWriter writer = new MultiFormatWriter(); // 一维码的宽>高。这里我设置为 宽:高=2:1 BitMatrix matrix = writer.encode(content, BarcodeFormat.EAN_13, BARCODE_WIDTH * 3, BARCODE_WIDTH); return toBufferedImage(matrix); } /** * 从图片中解析出一维码或者二维码的内容。如果解析失败,则抛出NotFoundException。 * @param image * @return * @throws NotFoundException */ public static final String parseImage(BufferedImage image) throws NotFoundException { LuminanceSource source = new BufferedImageLuminanceSource(image); Binarizer binarizer = new HybridBinarizer(source); BinaryBitmap bitmap = new BinaryBitmap(binarizer); Result result = READER.decode(bitmap); // 这里丢掉了Result中其他一些数据 return result.getText(); } /** * 将BufferedImage对象输出到指定的文件中。 * * @param image * @param destFile * @throws IOException */ public static final void writeToFile(BufferedImage image, File destFile) throws IOException { ImageIO.write(image, FORMAT, destFile); } /** * 将一个BitMatrix对象转换成BufferedImage对象 * * @param matrix * @return */ private static BufferedImage toBufferedImage(BitMatrix matrix) { int width = matrix.getWidth(); int height = matrix.getHeight(); BufferedImage image = new BufferedImage( width, height, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE); } } return image; } /** * 把传入的原始图像按高度和宽度进行缩放,生成符合要求的图标。 * * @param srcImageFile 源文件地址 * @param height 目标高度 * @param width 目标宽度 * @param hasFiller 比例不对时是否需要补白:true为补白; false为不补白; * @throws IOException */ private static BufferedImage scaleImage(String srcImageFile, int height, int width, boolean hasFiller) throws IOException { double ratio = 0.0; // 缩放比例 File file = new File(srcImageFile); BufferedImage srcImage = ImageIO.read(file); Image destImage = srcImage.getScaledInstance( width, height, BufferedImage.SCALE_SMOOTH); // 计算比例 if ((srcImage.getHeight() > height) || (srcImage.getWidth() > width)) { if (srcImage.getHeight() > srcImage.getWidth()) { ratio = (new Integer(height)).doubleValue() / srcImage.getHeight(); } else { ratio = (new Integer(width)).doubleValue() / srcImage.getWidth(); } AffineTransformOp op = new AffineTransformOp( AffineTransform.getScaleInstance(ratio, ratio), null); destImage = op.filter(srcImage, null); } if (hasFiller) {// 补白 BufferedImage image = new BufferedImage( width, height, BufferedImage.TYPE_INT_RGB); Graphics2D graphic = image.createGraphics(); graphic.setColor(Color.white); graphic.fillRect(0, 0, width, height); if (width == destImage.getWidth(null)) { graphic.drawImage(destImage, 0, (height - destImage.getHeight(null)) / 2, destImage.getWidth(null), destImage.getHeight(null), Color.white, null); } else { graphic.drawImage(destImage, (width - destImage.getWidth(null)) / 2, 0, destImage.getWidth(null), destImage.getHeight(null), Color.white, null); } graphic.dispose(); destImage = image; } return (BufferedImage) destImage; } } 方法的作用和用法在文档中都写得很清楚啦,就不需要解释了。下面是执行结果: 下面是生成的二维码: 下面是生成的一维码: 大家可以用手机扫描试试看。
以下内容来自:http://blog.csdn.net/mosquitolxw/article/details/25290315 What is the difference between Service Provider Interface (SPI) and Application Programming Interface (API)? More specifically, for Java libraries, what makes them an API and/or SPI? the API is the description of classes/interfaces/methods/... that you call and use to achieve a goal the SPI is the description of classes/interfaces/methods/... that you extend and implement to achieve a goal Put differently, the API tells you what a specific class/method does for you and the SPI tells you what you must do to conform. Sometimes SPI and API overlap. For example in JDBC the Driver class is part of the SPI: If you simply want to use JDBC, you don't need to use it directly, but everyone who implements a JDBC driver must implement that class. The Connection interface on the other hand is both SPI and API: You use it routinely when you use a JDBC driver and it needs to be implemented by the developer of the JDBC driver. 以下内容来自:http://www.cnblogs.com/happyframework/p/3349087.html 背景 Java 中区分 Api 和 Spi,通俗的讲:Api 和 Spi 都是相对的概念,他们的差别只在语义上,Api 直接被应用开发人员使用,Spi 被框架扩张人员使用。 Java类库中的实例 ? 1 2 3 4 5 Class.forName("com.mysql.jdbc.Driver"); Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/test", "root", "123456"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("select * from Users"); 说明:java.sql.Driver 是 Spi,com.mysql.jdbc.Driver 是 Spi 实现,其它的都是 Api。 如何实现这种结构? ? 1 2 3 4 5 6 7 8 public class Program { public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Class.forName("SpiA"); Api api = new Api("a"); api.Send("ABC"); } } ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.util.*; public class Api { private static HashMap<String, Class<? extends Spi>> spis = new HashMap<String, Class<? extends Spi>>(); private String protocol; public Api(String protocol) { this.protocol = protocol; } public void Send(String msg) throws InstantiationException, IllegalAccessException { Spi spi = spis.get(protocol).newInstance(); spi.send("消息发送开始"); spi.send(msg); spi.send("消息发送结束"); } public static void Register(String protocol, Class<? extends Spi> cls) { spis.put(protocol, cls); } } ? 1 2 3 public interface Spi { void send(String msg); } ? 1 2 3 4 5 6 7 8 9 10 public class SpiA implements Spi { static { Api.Register("a", SpiA.class); } @Override public void send(String msg) { System.out.println("SpiA:" + msg); } } 说明:Spi 实现的加载可以使用很多种方式,文中是最基本的方式。
WebService是个好东西,话不多说,干净利落 服务器端 来看下服务器端的结构: 先定义一个接口,用于暴露: ? 1 2 3 4 5 6 7 package com.abc.webservice; /** * 对外暴露的接口。 */ public interface IWebService { public String hello(String who); } 再定义这个接口的实现类: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.abc.webservice.impl; import javax.jws.WebService; import com.abc.webservice.IWebService; /** * wsdl:portType: MyService * wsdl:service: MyWebService */ @WebService(name="MyService", serviceName="MyWebService", targetNamespace="http://www.abc.com") public class WebServiceImpl implements IWebService { @Override public String hello(String who) { return "Hello " + who + "!"; } } 注意这里的name,它表示 The name of the Web Service. Used as the name of the wsdl:portType when mapped to WSDL 1.1. serviceName,它表示 The service name of the Web Service. Used as the name of the wsdl:service when mapped to WSDL 1.1. targetNamespace,就是你为Java客户端生成的代码的包名啦,生成的包名会自动反过来写,比如上面的是www.abc.com,生成的包名则会为package com.abc.* 。 最后将WebService发布出去: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.abc.webservice; import javax.xml.ws.Endpoint; import com.abc.webservice.impl.WebServiceImpl; /** * 发布WebService */ public class Publisher { public static void main(String[] args) { System.out.println("Start publish service"); Endpoint.publish("http://localhost:8080/MyService", new WebServiceImpl()); System.out.println("End publish service"); } } 这之后,可以打开浏览器,输入刚刚发布的URL:http://localhost:8080/MyService,去看看效果了: 点击上图中的超链接,可以看到生成的wsdl,以下是生成的wsdl: ? 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 <?xml version="1.0" encoding="UTF-8"?> <definitions xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsp="http://www.w3.org/ns/ws-policy" xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://www.abc.com" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://www.abc.com" name="MyWebService"> <types> <xsd:schema> <xsd:import namespace="http://www.abc.com" schemaLocation="http://localhost:8080/MyService?xsd=1" /> </xsd:schema> </types> <message name="hello"> <part name="parameters" element="tns:hello" /> </message> <message name="helloResponse"> <part name="parameters" element="tns:helloResponse" /> </message> <portType name="MyService"> <operation name="hello"> <input wsam:Action="http://www.abc.com/MyService/helloRequest" message="tns:hello" /> <output wsam:Action="http://www.abc.com/MyService/helloResponse" message="tns:helloResponse" /> </operation> </portType> <binding name="MyServicePortBinding" type="tns:MyService"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document" /> <operation name="hello"> <soap:operation soapAction="" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding> <service name="MyWebService"> <port name="MyServicePort" binding="tns:MyServicePortBinding"> <soap:address location="http://localhost:8080/MyService" /> </port> </service> </definitions> 看不懂没关系啦,这是WSDL,属于另一个范畴了,需要了解的朋友可以去搜一搜相关的资料。这里只是想说明如何使用JDK自带的WebService啦。 Java客户端 当然,WebServices可以被Java客户端调用,也可以被非Java语言的程序调用,这里我们只看Java客户端是如何调用的。 新建一个Poject,用于模拟在另一台机器上的客户端,并打开命令行: ? 1 >cd D:\Workspace\WebServiceClient\src 使用JDK自带的wsimport命令,生成Java客户端(注意中间有个点,表示当前目录): ? 1 >wsimport -keep . http://localhost:8080/MyService?wsdl 这句话表示生成客户端代码,保存在当前文件夹下。 会生成以下结构的客户端代码(图中选中的部分,那个webservice包是自己建的),刚刚有提到生成的Java客户端代码会放在com.abc包下: 至于生成的这些类里面是什么东西,你们自己去看啦。 然后编写客户端代码(com.abc.webservice.WebServiceClient.java): ? 1 2 3 4 5 6 7 8 9 10 11 12 package com.abc.webservice; import com.abc.MyWebService; public class WebServiceClient { public static void main(String[] args) { MyWebService myWebService = new MyWebService(); // 注意下面这句 MyService myService = myWebService.getMyServiePort(); System.out.println(myService.hello("Alvis")); } } 这里的MyWebService类就是wsimport命令根据WebService的WSDL生成的类啦。下面是WSDL中的一段: ? 1 2 3 4 5 <service name="MyWebService"> <port name="MyServicePort" binding="tns:MyServicePortBinding"> <soap:address location="http://localhost:8080/MyService" /> </port> </service> 从WSDL中可以看出,有个<service>的name为MyWebService,里面包含了一个<port>,因此代码中的 ? 1 myWebService.getMyServicePort(); 这句话得到的实际上是得到了MyService这个类的实例了,这个类其实是远端WebService实现类的代理对象。可以看看这个生成的MyWebService类中这个方法的定义: ? 1 2 3 4 @WebEndpoint(name = "MyServicePort") public MyService getMyServicePort() { return super.getPort(new QName("http://www.abc.com", "MyServicePort"), MyService.class); } 得到这个MyService的实例后,就可以使用该实例调用远程端的方法啦: ? 1 myService.hello("Alvis") 咱们再来看看MyService类中都有哪些东东: ? 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 package com.abc; import javax.jws.WebMethod; import javax.jws.WebParam; import javax.jws.WebResult; import javax.jws.WebService; import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.ws.Action; import javax.xml.ws.RequestWrapper; import javax.xml.ws.ResponseWrapper; /** * This class was generated by the JAX-WS RI. * JAX-WS RI 2.2.4-b01 * Generated source version: 2.2 */ @WebService(name = "MyService", targetNamespace = "http://www.abc.com") @XmlSeeAlso({ ObjectFactory.class }) public interface MyService { /** * @param arg0 * @return * returns java.lang.String */ @WebMethod @WebResult(targetNamespace = "") @RequestWrapper(localName = "hello", targetNamespace = "http://www.abc.com", className = "com.abc.Hello") @ResponseWrapper(localName = "helloResponse", targetNamespace = "http://www.abc.com", className = "com.abc.HelloResponse") @Action(input = "http://www.abc.com/MyService/helloRequest", output = "http://www.abc.com/MyService/helloResponse") public String hello( @WebParam(name = "arg0", targetNamespace = "") String arg0); } 可以看到,MyService是一个接口,因为真正的实现在远端。其实里面就一个方法,就是我们在远端定义的hello啦。 运行客户端代码即可: 这里是项目源代码,供需要的朋友参考。
原文地址:http://www.blogjava.net/zhenyu33154/articles/320245.html RMI是什么 RMI全称是Remote Method Invocation-远程方法调用,Java RMI在JDK1.1中实现的,其威力就体现在它强大的开发分布式网络应用的能力上,是纯Java的网络分布式应用系统的核心解决方案之一。其实它可以被看作是RPC的Java版本。但是传统RPC并不能很好地应用于分布式对象系统。而Java RMI 则支持存储于不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。 RMI目前使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信。由于JRMP是专为Java对象制定的,Java RMI具有Java的"Write Once,Run Anywhere"的优点,是分布式应用系统的百分之百纯Java解决方案。用Java RMI开发的应用系统可以部署在任何支持JRE(Java Run Environment Java,运行环境)的平台上。但由于JRMP是专为Java对象制定的,因此,RMI对于用非Java语言开发的应用系统的支持不足。不能与用非Java语言书写的对象进行通信。 RMI可利用标准Java本机方法接口JNI与现有的和原有的系统相连接。RMI还可利用标准JDBC包与现有的关系数据库连接。RMI/JNI和RMI/JDBC相结合,可帮助您利用RMI与目前使用非Java语言的现有服务器进行通信,而且在您需要时可扩展Java在这些服务器上的使用。RMI可帮助您在扩展使用时充分利用Java的强大功能。 RMI的组成 一个正常工作的RMI系统由下面几个部分组成: 远程服务的接口定义 远程服务接口的具体实现 桩(Stub)和框架(Skeleton)文件 一个运行远程服务的服务器 一个RMI命名服务,它允许客户端去发现这个远程服务 类文件的提供者(一个HTTP或者FTP服务器) 一个需要这个远程服务的客户端程序 RMI的原理 方法调用从客户对象经占位程序(Stub)、远程引用层(Remote Reference Layer)和传输层(Transport Layer)向下,传递给主机,然后再次经传 输层,向上穿过远程调用层和骨干网(Skeleton),到达服务器对象。 占位程序扮演着远程服务器对象的代理的角色,使该对象可被客户激活。 远程引用层处理语义、管理单一或多重对象的通信,决定调用是应发往一个服务器还是多个。传输层管理实际的连接,并且追踪可以接受方法调用的远程对象。服务器端的骨干网完成对服务器对象实际的方法调用,并获取返回值。返回值向下经远程引用层、服务器端的传输层传递回客户端,再向上经传输层和远程调用层返回。最后,占位程序获得返回值。 要完成以上步骤需要有以下几个步骤: 1、 生成一个远程接口 2、 实现远程对象(服务器端程序) 3、 生成占位程序和骨干网(服务器端程序) 4、 编写服务器程序 5、 编写客户程序 6、 注册远程对象 7、 启动远程对象 RMI远程调用步骤 1. 客户对象调用客户端辅助对象上的方法 2. 客户端辅助对象打包调用信息(变量,方法名),通过网络发送给服务端辅助对象 3. 服务端辅助对象将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象 4. 调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象 5. 服务端辅助对象将结果打包,发送给客户端辅助对象 6. 客户端辅助对象将返回值解包,返回给客户对象 7. 客户对象获得返回值 对于客户对象来说,步骤2-6是完全透明的 RMI的优点 从最基本的角度看,RMI是Java的远程过程调用(RPC)机制。与传统的RPC系统相比,RMI具有若干优点,因为它是Java面向对象方法的一部分。传统的RPC系统采用中性语言,所以是最普通的系统--它们不能提供所有可能的目标平台所具有的功能。 RMI以Java为核心,可与采用本机方法与现有系统相连接。这就是说,RMI可采用自然、直接和功能全面的方式为您提供分布式计算技术,而这种技术可帮助您以不断递增和无缝的方式为整个系统添加Java功能。 RMI的主要优点如下: 面向对象:RMI可将完整的对象作为参数和返回值进行传递,而不仅仅是预定义的数据类型。也就是说,您可以将类似Java哈希表这样的复杂类型作为一个参数进行传递。而在目前的RPC系统中,您只能依靠客户机将此类对象分解成基本数据类型,然后传递这些数据类型,最后在服务器端重新创建哈希表。RMI则不需额外的客户程序代码(将对象分解成基本数据类型),直接跨网传递对象。 可移动属性:RMI可将属性(类实现程序)从客户机移动到服务器,或者从服务器移到客户机。这样就能具备最大的灵活性,因为政策改变时只需要您编写一个新的Java类,并将其在服务器主机上安装一次即可。 设计方式:对象传递功能使您可以在分布式计算中充分利用面向对象技术的强大功能,如二层和三层结构系统。如果您能够传递属性,那么您就可以在您的解决方案中使用面向对象的设计方式。所有面向对象的设计方式无不依靠不同的属性来发挥功能,如果不能传递完整的对象--包括实现和类型--就会失去设计方式上所提供的优点。 安全:RMI使用Java内置的安全机制保证下载执行程序时用户系统的安全。RMI使用专门为保护系统免遭恶意小应用程序侵害而设计的安全管理程序,可保护您的系统和网络免遭潜在的恶意下载程序的破坏。在情况严重时,服务器可拒绝下载任何执行程序。 便于编写和使用:RMI使得Java远程服务程序和访问这些服务程序的Java客户程序的编写工作变得轻松、简单。远程接口实际上就是Java接口。服务程序大约用三行指令宣布本身是服务程序,其它方面则与任何其它Java对象类似。这种简单方法便于快速编写完整的分布式对象系统的服务程序,并快速地制做软件的原型和早期版本,以便于进行测试和评估。因为RMI程序编写简单,所以维护也简单。 可连接现有/原有的系统:RMI可通过Java的本机方法接口JNI与现有系统进行进行交互。利用RMI和JNI,您就能用Java语言编写客户端程序,还能使用现有的服务器端程序。在使用RMI/JNI与现有服务器连接时,您可以有选择地用Java重新编写服务程序的任何部分,并使新的程序充分发挥Java的功能。类似地,RMI可利用JDBC、在不修改使用数据库的现有非Java源代码的前提下与现有关系数据库进行交互。 编写一次,到处运行:RMI是Java“编写一次,到处运行 ”方法的一部分。任何基于RMI的系统均可100%地移植到任何Java虚拟机上,RMI/JDBC系统也不例外。如果使用RMI/JNI与现有系统进行交互工作,则采用JNI编写的代码可与任何Java虚拟机进行编译、运行。 分布式垃圾收集:RMI采用其分布式垃圾收集功能收集不再被网络中任何客户程序所引用的远程服务对象。与Java 虚拟机内部的垃圾收集类似,分布式垃圾收集功能允许用户根据自己的需要定义服务器对象,并且明确这些对象在不再被客户机引用时会被删除。 并行计算:RMI采用多线程处理方法,可使您的服务器利用这些Java线程更好地并行处理客户端的请求。Java分布式计算解决方案:RMI从JDK 1.1开始就是Java平台的核心部分,因此,它存在于任何一台1.1 Java虚拟机中。所有RMI系统均采用相同的公开协议,所以,所有Java 系统均可直接相互对话,而不必事先对协议进行转换。 RMI的示例一 以下用一个最简单的Hello的示例来介绍RMI的应用。以下用一个最简单的Hello的示例来介绍RMI的应用。 1:定义一个远程接口(IHello.java) ? 1 2 3 4 5 import java.rmi.Remote; public interface IHello extends Remote { public String sayHello(String name) throws java.rmi.RemoteException; } 这个接口需要继承自Rmi的Remote接口。Remote 接口是一个标识接口,用于标识所包含的方法可以从非本地虚拟机上调用的接口,Remote接口本身不包含任何方法。官方文档中这样说: Any object that is a remote object must directly or indirectly implement this interface. Only those methods specified in a "remote interface", an interface that extends java.rmi.Remote are available remotely. 由于远程方法调用的本质依然是网络通信,只不过RMI隐藏了底层实现,网络通信是经常会出现异常的,所以接口的所有方法都必须抛出RemoteException以说明该方法是有风险的。 2. 实现远程接口(HelloImpl.java) ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class HelloImpl extends UnicastRemoteObject implements IHello { // 这个实现必须有一个显式的构造函数,并且要抛出一个RemoteException异常 protected HelloImpl() throws RemoteException { super(); } /** * 说明清楚此属性的业务含义 */ private static final long serialVersionUID = 4077329331699640331L; public String sayHello(String name) throws RemoteException { return "Hello " + name + " ^_^ "; } public static void main(String[] args) { try { IHello hello = new HelloImpl(); java.rmi.Naming.rebind("rmi://localhost:1099/hello", hello); System.out.print("Ready"); } catch (Exception e) { e.printStackTrace(); } } } 这个实现类使用了UnicastRemoteObject去联接RMI系统。这里我们继承了UnicastRemoteObject这个类,它的作用是 事实上并不一定要这样做,如果一个类不是从UnicastRmeoteObject上继承,那必须使用它的exportObject()方法去联接到RMI。 如果一个类继承自UnicastRemoteObject,那么它必须提供一个构造函数并且声明抛出一个RemoteException对象。当这个构造函数调用了super(),它将激活UnicastRemoteObject中的代码完成RMI的连接和远程对象的初始化。 另外,由于方法参数与返回值最终都将在网络上传输,故必须提供一个serializeVersionUID。 3. 新建RMI客户端调用程序(Hello_RMI_Client.java) ? 1 2 3 4 5 6 7 8 9 10 11 import java.rmi.Naming; public class Hello_RMI_Client { public static void main(String[] args) { try { IHello hello = (IHello) Naming.lookup("rmi://localhost:1099/hello"); System.out.println(hello.sayHello("zhangxianxin")); } catch (Exception e) { e.printStackTrace(); } } } 4. 编译并运行 4.1 用javac命令编译IHello.java、HelloImpl.java、Hello_RMI_Client.java ? 1 >javac *.java 4.2 用rmic命令和class文件生成存根(Stub)和框架(Skeleton)文件 ? 1 2 3 4 >cd /home/user/Test/bin >rmic HelloImpl 如果有包名,则使用 >rmic com.package.HelloImpl 成功执行完上面的命令可以发现生成一个HelloImpl_Stub.class文件,如果JDK是使用Java2SDK,那么还可以发现多出一个HelloImpl_Skel.class文件。如果服务端程序与客户端程序在同一台机器上并在同一目录中,则可以省略掉接口实现类生成的存根和框架文件,但这就失去了使用RMI的意义,而如果要在不同的JVM上运行时,客户端程序就必须得依靠服务端运程方法实现的存根和框架文件以及接口类。 4.3 运行注册程序RMIRegistry,必须在包含刚写的类的目录下运行这个注册程序。 ? 1 >rmiregistry 注册程序开始运行了,不要管他,现在切换到另外一个控制台运行服务器 4.4 运行服务器HelloImpl ? 1 >java HelloImpl 当启动成功出现Ready...... 这个服务器就开始工作了,把接口的实现加载到内存等待客户端的联接。现在切换到第三个控制台,启动我们的客户端。 4.5 启动客户端:为了在其他的机器运行客户端程序你需要一个远程接口(IHello.class) 和一个stub(HelloImpl_Stub.class)。 使用如下命令运行客户端 ? 1 >java Hello_RMI_Client 当运行成功会在控制台打印:Hello zhangxianxin ^_^ 备注:如果不想在控制台上开启RMI注册程序RMIRegistry的话,可在RMI服务类程序中添加LocateRegistry.createRegistry(1099); 如下所示: 修改后的HelloImpl.java代码如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.server.UnicastRemoteObject; public class HelloImpl extends UnicastRemoteObject implements IHello { // 这个实现必须有一个显式的构造函数,并且要抛出一个RemoteException异常 protected HelloImpl() throws RemoteException { super(); } private static final long serialVersionUID = 4077329331699640331L; public String sayHello(String name) throws RemoteException { return "Hello " + name + " ^_^ "; } public static void main(String[] args) { try { IHello hello = new HelloImpl(); ///////////////////////////////////////// // 加上此程序,就可以不要在控制台上开启RMI的注册程序,1099是RMI服务监视的默认端口 LocateRegistry.createRegistry(1099); //// ///////////////////////////////////////// java.rmi.Naming.rebind("rmi://localhost:1099/hello", hello); System.out.print("Ready"); } catch (Exception e) { e.printStackTrace(); } } } RMI的示例二 以下用一个文件交换程序来介绍RMI的应用。这个应用允许客户端从服务端交换(或下载)所有类型的文件。第一步是定义一个远程的接口,这个接口指定的签名方法将被服务端提供和被客户端调用。 1.定义一个远程接口 (IFileUtil.java) ? 1 2 3 4 5 6 import java.rmi.Remote; import java.rmi.RemoteException; public interface IFileUtil extends Remote { public byte[] downloadFile(String fileName) throws RemoteException; } 接口IFileDownload提供了一个downloadFile方法,然后返回一个相应的文件数据。 2.实现远程的接口 (FileUtilImpl.java) 类FileImpl继承于UnicastRemoteObject类。这显示出FileImpl类是用来创建一个单独的、不能复制的、远程的对象,这个对象使用RMI默认的基于TCP的通信方式。 ? 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 import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class FileUtilImpl extends UnicastRemoteObject implements IFileUtil { protected FileUtilImpl() throws RemoteException { super(); } private static final long serialVersionUID = 7594622080290821912L; public byte[] downloadFile(String fileName) throws RemoteException { File file = new File(fileName); byte buffer[] = new byte[(int) file.length()]; int size = buffer.length; System.out.println("download file size = " + size + "b"); if (size > 1024 * 1024 * 10) {// 限制文件大小不能超过10M,文件太大可能导制内存溢出! throw new RemoteException("Error:<The File is too big!>"); } try { BufferedInputStream input = new BufferedInputStream( new FileInputStream(fileName)); input.read(buffer, 0, buffer.length); input.close(); System.out.println("Info:<downloadFile() hed execute successful!>"); return buffer; } catch (Exception e) { System.out.println("FileUtilImpl: " + e.getMessage()); e.printStackTrace(); return null; } } } 3.编写服务端 (FileUtilServer.java) (1)创建并安装一个RMISecurityManager实例。 (2)创建一个远程对象的实例。 (3)使用RMI注册工具来注册这个对象。 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.rmi.Naming; import java.rmi.RMISecurityManager; public class FileUtilServer { public static void main(String argv[]) { try { IFileUtil file = new FileUtilImpl(); // LocateRegistry.createRegistry(1099); // //加上此程序,就可以不要在控制台上开启RMI的注册程序,1099是RMI服务监视的默认端口 Naming.rebind("rmi://127.0.0.1/FileUtilServer", file); System.out.print("Ready"); } catch (Exception e) { System.out.println("FileUtilServer: " + e.getMessage()); e.printStackTrace(); } } } 声明 ? 1 Naming.rebind("rmi://127.0.0.1/FileUtilServer", file) 中假定了RMI注册工具(RMI registry )使用并启动了1099端口。如果在其他端口运行了RMI注册工具,则必须在这个声明中定义。例如,如果RMI注册工具在4500端口运行,则声明应为: ? 1 Naming.rebind("rmi://127.0.0.1:4500/FileUtilServer", file) 另外我们已经同时假定了我们的服务端和RMI注册工具是运行在同一台机器上的。否则需要修改rebind方法中的地址。 4.编写客户端 (FileUtilClient.java) 客户端可以远程调用远程接口(FileInterface)中的任何一个方法。无论如何实现,客户端必须先从RMI注册工具中获取一个远程对象的引用。当引用获得后,方法downloadFile被调用。在执行过程中,客户端从命令行中获得两个参数,第一个是要下载的文件名,第二个是要下载的机器的地址,在对应地址的机器上运行服务端。 ? 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 import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.rmi.Naming; public class FileUtilClient { public static void main(String args[]) { if (args.length != 3) { System.out.println("第一个参数:RMI服务的IP地址"); System.out.println("第二个参数:要下载的文件名"); System.out.println("第三个参数:要文件保存位置"); System.exit(0); } try { String name = "rmi://" + args[0] + "/FileUtilServer"; IFileUtil fileUtil = (IFileUtil) Naming.lookup(name); byte[] filedata = fileUtil.downloadFile(args[1]); if (filedata == null) { System.out.println("Error:<filedata is null!>"); System.exit(0); } File file = new File(args[2]); System.out.println("file.getAbsolutePath() = " + file.getAbsolutePath()); BufferedOutputStream output = new BufferedOutputStream( new FileOutputStream(file.getAbsolutePath())); output.write(filedata, 0, filedata.length); output.flush(); output.close(); System.out.println("~~~~~End~~~~~"); } catch (Exception e) { System.err.println("FileUtilServer exception: " + e.getMessage()); e.printStackTrace(); } } } 5.运行程序 为了运行程序,我们必须使用rmic来编译生成stubs和skeletons: ? 1 >rmic FileUtilImpl 这将会生成FileUtilImpl_Stub.class和FileUtilImpl_Skel.class两个文件。Stub是客户端的代理,而Skeleton是服务端的框架。服务端和客户端采用javac来编译(如果服务端和客户端在两个不同的机器,则必须复制一个IFileUtil接口)。 使用rmiregistry或者start rmiregistry 命令来运行RMI注册工具到window系统默认的端口上: ? 1 > rmiregistry portNumber 此处的portNumber为端口,RMI注册工具运行之后,需要运行服务FileUtilServer,因为RMI的安全机制将在服务端发生作用,所以必须增加一条安全策略: grant{permission java.security.AllPermission "", "";}; 为了运行服务端,需要有除客户类(FileUtilClient.class)之外所有的类文件。确认安全策略在policy.txt文件之后,使用如下命令来运行服务器。 ? 1 > java -Djava.security.policy=policy.txt FileUtilServer 为了在其他的机器运行客户端程序,需要一个远程接口(IFileUtil.class)和一个stub(FileUtilImpl_Stub.class)。 使用如下命令运行客户端: ? 1 > java FileUtilClient fileName machineName savePath 这里fileName是要下载的文件名,machineName 是要下载的文件所在的机器(也是服务端所在的机器),savePath 是要将下载过来的文件保存的路径(带文件名)。如果全部通过的话,当客户端运行后,则这个文件将被下载到本地。 Spring对RMI的支持 1.使用RMI暴露服务 使用Spring的RMI支持,你可以通过RMI基础设施透明的暴露你的服务。设置好Spring的RMI支持后,你会看到一个和远程EJB接口类似的配置,只是没有对安全上下文传递和远程事务传递的标准支持。当使用RMI调用器时,Spring对这些额外的调用上下文提供了钩子,你可以在此插入安全框架或者定制的安全证书。 2. 使用 RmiServiceExporter 暴露服务 使用 RmiServiceExporter,我们可以把AccountService对象的接口暴露成RMI对象。可以使用 RmiProxyFactoryBean 或者在传统RMI服务中使用普通RMI来访问该接口。RmiServiceExporter 显式地支持使用RMI调用器暴露任何非RMI的服务。 当然,我们首先需要在Spring BeanFactory中设置我们的服务: ? 1 2 3 <bean id="accountService" class="example.AccountServiceImpl"> <!-- any additional properties, maybe a DAO? --> </bean> 然后,我们将使用 RmiServiceExporter 来暴露我们的服务: ? 1 2 3 4 5 6 7 8 <bean class="org.springframework.remoting.rmi.RmiServiceExporter"> <!-- does not necessarily have to be the same name as the bean to be exported --> <property name="serviceName" value="AccountService"/> <property name="service" ref="accountService"/> <property name="serviceInterface" value="example.AccountService"/> <!-- defaults to 1099 --> <property name="registryPort" value="1199"/> </bean> 正如你所见,我们覆盖了RMI注册的端口号。通常,你的应用服务也会维护RMI注册,最好不要和它冲突。更进一步来说,服务名是用来绑定下面的服务的。所以本例中,服务绑定在 rmi://HOST:1199/AccountService。在客户端我们将使用这个URL来链接到服务。 注意:我们省略了一个属性,就是 servicePort 属性,它的默认值为0。 这表示在服务通信时使用匿名端口。当然如果你愿意的话,也可以指定一个不同的端口。 3. 在客户端链接服务 我们的客户端是一个使用AccountService来管理account的简单对象: ? 1 2 3 4 5 6 public class SimpleObject { private AccountService accountService; public void setAccountService(AccountService accountService) { this.accountService = accountService; } } 为了把服务连接到客户端上,我们将创建另一个单独的bean工厂,它包含这个简单对象和服务链接配置位: ? 1 2 3 4 5 6 7 8 <bean class="example.SimpleObject"> <property name="accountService" ref="accountService"/> </bean> <bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> <property name="serviceUrl" value="rmi://HOST:1199/AccountService"/> <property name="serviceInterface" value="example.AccountService"/> </bean> 这就是我们在客户端为支持远程account服务所需要做的。Spring将透明的创建一个调用器并且通过RmiServiceExporter使得account服务支持远程服务。在客户端,我们用RmiProxyFactoryBean连接它。 Spring对RMI支持的实际应用实例 在OMAS系统中提供给业务系统的RMI客户反馈服务的实现服务暴露是通过Resource/modules/interfaces/spring-conf/serviceContext.xml配置文件实现的,而远程接口的实现类必须序列化(即实现Serializable接口)。 Resource/modules/interfaces/spring-conf/serviceContext.xml的内容如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd"> <beans default-autowire="byName" default-lazy-init="false"> <!-- service实现类的配置 --> <bean id="fbWebService" class="com.ce.omas.interfaces.service.impl.FeedbackWebServiceImpl" /> <bean class="org.springframework.remoting.rmi.RmiServiceExporter"> <!-- does not necessarily have to be the same name as the bean to be exported --> <property name="serviceName" value="FeedbackRMIService" /> <property name="service" ref="fbWebService" /> <property name="serviceInterface" value="com.ce.omas.interfaces.service.IFeedbackWebService" /> <!-- <property name="registryHost" value="rmi://192.168.100.7"/> --> <!-- defaults to 1099 --> <property name="registryPort" value="1199" /> </bean> </beans> 对应的暴露的服务接口如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 public interface IFeedbackWebService { /** * <b>方法用途和描述:</b> 客户反馈RMI服务端接口方法<br> * <b>方法的实现逻辑描述:</b> 通过RMI提供服务,Spring支持的RMI远程调用 * @param systemID : 业务系统的唯一标识 * @param feedbackType :用户反馈的类型(1-系统BUG、2-系统易用性、3-客服人员态度、4-运维人员态度、5-其他) * @param feedbackContent :用户反馈的正文内容 * @return 反馈是否成功 true | false */ public boolean setFeedback(String systemID, FeedbackType feedbackType, String feedbackContent) throws OMASServiceException ; } 暴露的服务接口实现如下: ? 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 public class FeedbackWebServiceImpl implements IFeedbackWebService, Serializable { private static Log _log = LogFactory.getLog(FeedbackWebServiceImpl.class); private static final long serialVersionUID = -5532505108644974594L; /** * 客户反馈服务接口 */ private IFeedbackOperationService feedbackService; /** * 方法用途和描述: 注入运营模块的添加客户反馈的服务 * @param feedbackWebService * 运营模块服务 */ public void setFeedbackService(IFeedbackOperationService feedbackWebService) { _log.info("注入运营模块的添加客户反馈的服务"); this.feedbackService = feedbackWebService; } /** * 方法用途和描述: 外部接口子系统中添加客户反馈的方法 * @param systemID * :业务系统ID * @param feedbackType * :反馈类型 * @param feedbackContent * :反馈内容 * @return 操作是否成功 ture or false * @throws ServiceException */ public boolean setFeedback(String systemID, FeedbackType feedbackType, String feedbackContent) throws OMASServiceException { _log.info("进入到外部接口的添加客户反馈的方法"); if (BlankUtil.isBlank(systemID) || BlankUtil.isBlank(feedbackType) || BlankUtil.isBlank(feedbackContent)) { _log.error("添加客户反馈的接口参数为空!"); throw new OMASServiceException("omas.interfaces.001");// 添加客户反馈的接口参数为空 } WebServiceFeedbackVO vo = new WebServiceFeedbackVO(); vo.setFeedbackType(String.valueOf(feedbackType.getValue())); vo.setFeedbackContent(feedbackContent); vo.setSystemID(systemID); _log.info("调用运营子系统的添加客户反馈的方法开始!"); try { if (feedbackService == null) { _log.error("运营模块服务为空"); // 运营模块服务为空 throw new OMASServiceException("omas.interfaces.002"); } feedbackService.addFeedbackOperation(vo); } catch (ServiceException e) { _log.error("调用运营子系统的添加客户反馈出现异常:" + e.getMessage()); if (ExceptionConstants.EXCEPTION_OMAS_FEEDBACK_VO.equals(e.getMsgKey())) { // 客户调用接口的对像为空 throw new OMASServiceException("omas.interfaces.003"); } if (ExceptionConstants.EXCEPTION_OMAS_FEEDBACK_SYSTEMID.equals(e.getMsgKey())) { // 业务系统标识ID为空 throw new OMASServiceException("omas.omasservice.010"); } if (ExceptionConstants.EXCEPTION_OMAS_FEEDBACK_SIZE.equals(e.getMsgKey())) { // 非法的业务系统唯一标识 throw new OMASServiceException("omas.interfaces.004"); } if (ExceptionConstants.EXCEPTION_OMAS_FEEDBACK_BASE.equals(e.getMsgKey())) { // 数据库访问出了一点小问题! throw new OMASServiceException("omas.interfaces.005"); } // 未捕获到的异常信息! throw new OMASServiceException("omas.omasservice.000"); } return true; } } 接口方法setFeedback(String, FeedbackType, String)的实现大家不用关心,其与RMI并无关系,只是一些纯业务处理逻辑而已,要注意的是接口实现类必须实现 IfeedbackWebService和Serializable接口。 在客户本地的omasservice.jar包中客户反馈的RMI客户端的配置如下: Resource/config/omasrmi-client.xml ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd"> <beans default-autowire="byName" default-lazy-init="true"> <bean id="fbWebServiceProxy" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> <property name="serviceUrl"> <value>rmi://127.0.0.1:1199/FeedbackRMIService</value> </property> <property name="serviceInterface"> <value>com.ce.omas.interfaces.service.IFeedbackWebService</value> </property> </bean> <bean class="com.ce.omas.omasservice.service.impl.FeedbackRMIClientImpl"> <property name="feedbackWebService" ref="fbWebServiceProxy" /> </bean> </beans> 客户端调用RMI服务的方法如下所示: ? 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 /** * 方法用途和描述: 客户反馈:通过RMI方法与OMAS通讯 方法的实现逻辑描述: * * @param feedbackType * @param feedbackContent * @return * @throws OMASServiceException */ public static boolean setFeedback_RMIClient(String systemID, FeedbackType feedbackType, String feedbackContent) throws OMASServiceException { if (systemID == null || "".equals(systemID)) { _log.error("业务系统标识<SystemID>为空或不是对象"); throw new OMASServiceException("omas.omasservice.010"); } String rmiClientConfigFilePath = PropertyReader.getValue(ConfigConstants.OMASSERVICE_CONFIG_PATH, ConfigConstants.RMI_CLIENT_CONFIG_FILEPATH); if (rmiClientConfigFilePath == null || "".equals(rmiClientConfigFilePath)) { _log.error("配置文件错误:Key<rmiClientConfigFile>为空或不存在"); throw new OMASServiceException("omas.omasservice.006"); } _log.info("rmiClientConfigPath = " + rmiClientConfigFilePath); ApplicationContext context = null; try { context = new ClassPathXmlApplicationContext(rmiClientConfigFilePath); } catch (Exception e) { _log.error("客户反馈:解析rmi-config.xml文件时出现异常:" + e); _log.info("rmi-config.xml文件路径:" + rmiClientConfigFilePath); throw new OMASServiceException("omas.omasservice.007"); } IFeedbackWebService service = null; try { service = (IFeedbackWebService) context.getBean("fbWebServiceProxy"); } catch (Exception e) { _log.error("从Spring的RMI客户端Bean配置文件获取服务对象时出现异常:" + e); throw new OMASServiceException("omas.omasservice.009"); } boolean bln = service.setFeedback(systemID, feedbackType, feedbackContent); _log.info("反馈操作是否成功[true|false]:" + bln); return bln; }
一、Spring HTTP Invoker简介 Spring HTTP invoker 是 spring 框架中的一个远程调用模型,执行基于 HTTP 的远程调用(意味着可以通过防火墙),并使用 java 的序列化机制在网络间传递对象。这需要在远端和本地都使用Spring才行。客户端可以很轻松的像调用本地对象一样调用远程服务器上的对象,这有点类似于 webservice ,但又不同于 webservice ,区别如下: WebService Http Invoker 跨平台,跨语言 只支持 java 语言 支持 SOAP ,提供 wsdl 不支持 结构庞大,依赖特定的 webservice 实现,如 xfire等 结构简单,只依赖于 spring 框架本身 说明: 1. 服务器端:通过 HTTP invoker 服务将服务接口的某个实现类提供为远程服务 2. 客户端:通过 HTTP invoker 代理向服务器端发送请求,远程调用服务接口的方法 3. 服务器端与客户端通信的数据均需要序列化 二、配置服务器端和客户端的步骤 配置服务器端 1. 添加 springJAR 文件 2. 创建相应的DTO(如果需要用到的话) 3. 创建服务接口 4. 创建服务接口的具体实现类 5. 公开服务 配置客户端 1. 添加 springJAR 文件 2. 创建相应的DTO(如果需要用到的话) 3. 创建服务接口 4. 访问服务 三、实例讲解 配置服务器端 先来个项目结构图: 1). 添加 springJAR 文件,这就不用说了,直接照着图片添加相应的类库。 2). 创建服务接口和相应的DTO(Data Transmission Object) 这里我们需要调用远端的服务来查询一个User对象,因此需要DTO啦。下面这个User类就是用于在网络中传输的POJO类,也就是DTO啦,因此需要实现Serializable接口: ? 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 package com.abc.invoke.bean; import java.io.Serializable; public class User implements Serializable { private static final long serialVersionUID = -6970967506712260305L; private String name; private int age; private String email; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } @Override public String toString() { return "User [name=" + name + ", age=" + age + ", email=" + email + "]"; } } 3). UserService是一个接口,里面定义了服务的方法,这里面的方法将会被客户端调用: ? 1 2 3 4 5 6 7 package com.abc.invoke.server.service; import com.abc.invoke.bean.User; public interface UserService { public User getUserbyName(String name); } 4). 创建服务接口的具体实现类。这里的UserServiceImpl是实现了UserService方法: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.abc.invoke.server.service.impl; import com.abc.invoke.bean.User; import com.abc.invoke.server.service.UserService; public class UserServiceImpl implements UserService { public User getUserbyName(String name) { User u = new User(); u.setName(name); u.setEmail("abc@abc.com"); u.setAge(20); return u; } } 这里面我没有写DAO等层面的东西,因为那些不是这篇文章要讲述的内容,因而我只是简单的将传给服务端的参数封装到对象里的一个字段就返回了。 5). 公开服务 下面是web.xml文件的内容: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <display-name>SpringInvoke</display-name> <servlet> <servlet-name>service</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:service-servlet.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>service</servlet-name> <url-pattern>/service/*</url-pattern> </servlet-mapping> <!-- 其实下面这个welcome-file-list没啥用,我留着只是为了在起好Tomcat后不会报一个404而已 --> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app> 这里我们使用/service作为service的前缀,那么客户端请求调用时需要加上这个前缀,比如: http://{host}:{port}/InvokeServer/service/{serviceName} 里面用到的service-servlet文件: ? 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 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- 这个Bean映射了当URL是/userService时,处理器为userServiceInvoker --> <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="mappings"> <props> <prop key="/userService">userServiceInvoker</prop> </props> </property> </bean> <!-- Announce that this interface is a HTTP invoker service. --> <bean id="userServiceInvoker" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"> <property name="service" ref="userServiceImpl" /> <property name="serviceInterface" value="com.abc.invoke.server.service.UserService" /> </bean> <bean id="userServiceImpl" class="com.abc.invoke.server.service.impl.UserServiceImpl" /> </beans> 注意: <prop key="/userService">userServiceInvoker</prop>中的/userService是请求的服务的URL中的一部分,就是说这样的URL会被userServiceInvoker处理 这里将com.abc.invoke.server.service.UserService映射给了com.abc.invoke.server.service.impl.UserServiceImpl类了。 到此为止,服务器算是配置好了,接下来开始配置客户端。 配置客户端 先来看看项目结构图: 1). 添加 springJAR 文件,这也不用说了,直接照着图片添加相应的类库。 2). 创建服务接口和相应的DTO。 特别注意:这个类和Server端声明的DTO要一样,包名和字段名都要一样才行。因为客户端发起请求查询User,服务端处理后先将User序列化后在返回给客户端,而客户端拿到这个User后需要将其反序列化。如果包名或者字段名不同,则会被认为是不同的对象,会反序列化失败,调用也就出错了。我之前就是将User类的包名写得不一样(User类的包名在服务端为com.abc.invoke.server.bean,而在客户端则为com.abc.invoke.client.bean),报了以下错误: ? 1 2 3 4 5 6 7 8 9 Exception in thread "main" org.springframework.remoting.RemoteAccessException: Could not deserialize result from HTTP invoker remote service [http://localhost:8080/InvokeServer/service/userService]; nested exception is java.lang.ClassNotFoundException: com.abc.invoke.server.bean.User at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.convertHttpInvokerAccessException(HttpInvokerClientInterceptor.java:208) at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:145) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202) at com.sun.proxy.$Proxy0.getUserbyName(Unknown Source) at com.abc.invoke.client.Test.main(Test.java:14) 很明显可以看出,Could not deserialize result from HTTP invoker remote service......,就是因为Server端与Client端的DTO的包名不同导致反序列化失败。 3). 创建服务接口 这也没啥好说的,接口和Server端定义的一样就行,不一样肯定报错。可以直接将DTO和接口定义的类拷贝到客户端即可。这个接口将会被看做是客户端和服务端通信的“契约”。 4). 访问服务 来看看application-context.xml: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- 客户端使用 HttpInvokerProxyFactoryBean 代理客户端向服务器端发送请求,请求接口为 UserService 的服务 --> <bean id="userService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean" > <property name="serviceUrl" value="http://localhost:8080/InvokeServer/service/userService"/> <property name="serviceInterface" value="com.abc.invoke.client.service.UserService" /> </bean> </beans> 这里使用了org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean这个类来作为一个service的代理类。注意到serviceUrl属性为http://localhost:8080/InvokeServer/service/userService (当然,我在本机启动的服务端并在本机通过main函数调用service,我在另一台机器上运行Test类的main函数,调用结果正确)。这个localhost:8080应改为实际的IP地址和端口。),这个URL的地址以/service开始,因此会被Server端拦截下来,而URL中的 /userService则为service路径,该路径与在Server端中service-servlet.xml中声明的 ? 1 <prop key="/userService">userServiceInvoker</prop> 路径一致,因此这个调用会被userServiceInvoker处理。 最后再写一个简单的测试类Test.java: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.abc.invoke.client; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.abc.invoke.bean.User; import com.abc.invoke.client.service.UserService; public class Test { public static void main(String[] args) { ApplicationContext ac = new ClassPathXmlApplicationContext( "classpath:application-context.xml"); UserService service = (UserService)ac.getBean("userService"); User u = service.getUserbyName("Alvis"); System.out.println(u); } } 这个类也很简单,就是从Spring的Context中取出了定义的userService这个Bean(这其实就是服务端service的一个代理类),然后直接调用该service的方法获得结果并打印。 到此为止,客户端配置完成。 四、启动服务并测试 直接在项目InvokeServer上启动Tomcat,可以看到路径/userService的处理者是userServiceInvoker: 下面是远程调用的执行结果: 从结果中可以看到,我代码里写的名字叫Alvis,用客户端调用服务端的service后,返回的对象中名字是客户端设置的名字,测试成功。 这里是项目源代码,供需要的朋友参考。 参考页面:http://hanqunfeng.iteye.com/blog/868210
分类 Android的notification有以下几种: 1>普通notification 图1 标题,通过NotificationCompat.Builder.setContentTitle(String title)来设置 大图标,通过NotificationCompat.Builder.setLargeIcon(Bitmap icon)来设置 内容,通过NotificationCompat.Builder.setContentText("ContentText")来设置 内容附加信息,通过NotificationCompat.Builder.setContentInfo("ContentInfo")来设置 小图标,通过NotificationCompat.Builder.setSmallIcon(int icon)来设置 时间,通过NotificationCompat.Builder.setWhen(when)来设置 注: 一个notification不必对上面所有的选项都进行设置,但有3项是必须的: 小图标, set by setSmallIcon() 标题, set by setContentTitle() 内容, set by setContentText() 2>大布局Notification 图2 大布局notification是在android4.1以后才增加的,大布局notification与小布局notification只在‘7'部分有区别,其它部分都一致。大布局notification只有在所有notification的最上面时才会显示大布局,其它情况下显示小布局。你也可以用手指将其扩展为大布局(前提是它是大布局)。如下图: 图3 大布局notification有三种类型:如图2为NotificationCompat.InboxStyle 类型。图3左部为NotificationCompat.BigTextStyle。图3右部 为:NotificationCompat.BigPictureStyle. InboxStyle类型的notification看起来和BigTextStyle类型的notification,那么他们有什么不同呢?对于InboxStyle类型的notification,图2的‘7’位置处每行都是很简短的,第一行和最后两行由于内容很长,则使用了省略号略去了过长的内容;而图3的左图中,BigTextStyle类型的notification则是将过长的内容分在了多行显示。 3>自定义布局notification 除了系统提供的notification,我们也可以自定义notification。如下图所示的一个音乐播放器控制notification: 图4 创建自定义的notification 1>实例化一个NotificationCompat.Builder对象;如builder 2>调用builder的相关方法对notification进行上面提到的各种设置 3>调用builder.build()方法此方法返回一个notification对象。 4>获取系统负责通知的NotificationManager;如:manager 5>调用manager的notify方法。 示例代码 示例程序截图: 图5 0>初始化部分代码 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class MainActivity extends Activity implements OnClickListener { private int[] btns = new int[] { R.id.normal, R.id.inboxStyle, R.id.bigTextStyle, R.id.bigPicStyle, R.id.customize, R.id.progress, R.id.cancelNotification }; private NotificationManager manager; private Bitmap icon = null; private static final int NOTIFICATION_ID_NORMAL = 1; private static final int NOTIFICATION_ID_INBOX = 2; private static final int NOTIFICATION_ID_BIGTEXT = 3; private static final int NOTIFICATION_ID_BIGPIC = 4; private static final int NOTIFICATION_ID_CUSTOMIZE = 5; private static final int NOTIFICATION_ID_PROGRESS = 6; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 获取系统的通知服务 manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); for (int btn : btns) { findViewById(btn).setOnClickListener(this); } } @Override public void onClick(View v) { switch (v.getId()) { case R.id.normal: showNormalNotification(); break; case R.id.inboxStyle: showInboxStyleNotification(); break; case R.id.bigTextStyle: showBigTextStyleNotification(); break; case R.id.bigPicStyle: showBigPicStyleNotification(); break; case R.id.customize: showCustomizeNotification(); break; case R.id.progress: showProgressBar(); break; case R.id.cancelNotification: cancelNotification(); break; default: break; } } } 1>普通notification ? 1 2 3 4 5 6 7 8 9 private void showNormalNotification() { Notification notification = new NotificationCompat.Builder(this) .setLargeIcon(icon).setSmallIcon(R.drawable.ic_launcher) .setTicker("NormalNotification").setContentInfo("ContentInfo") .setContentTitle("ContentTitle").setContentText("ContentText") .setAutoCancel(true).setDefaults(Notification.DEFAULT_ALL) .build(); manager.notify(NOTIFICATION_ID_NORMAL, notification); } 2>大布局Text类型notification ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void showBigTextStyleNotification() { NotificationCompat.BigTextStyle textStyle = new NotificationCompat.BigTextStyle(); textStyle.setBigContentTitle("BigContentTitle") .setSummaryText("SummaryText") .bigText("I am Big Texttttttttttttttttttttttttttttttttt" + "tttttttttttttttttttttttttttttttttttttttttttt" + "!!!!!!!!!!!!!!!!!!!......"); Notification notification = new NotificationCompat.Builder(this) .setLargeIcon(icon).setSmallIcon(R.drawable.ic_launcher) .setTicker("showBigTextStyleNotification").setContentInfo("contentInfo") .setContentTitle("ContentTitle").setContentText("ContentText") .setStyle(textStyle).setAutoCancel(false) .setShowWhen(false).setDefaults(Notification.DEFAULT_ALL) .build(); manager.notify(NOTIFICATION_ID_BIGTEXT, notification); } 3> 大布局Inbox类型notification ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private void showInboxStyleNotification() { String[] lines = new String[]{"line1", "line2", "line3"}; NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); inboxStyle.setBigContentTitle("BigContentTitle") .setSummaryText("SummaryText"); for (int i = 0; i < lines.length; i++) { inboxStyle.addLine(lines[i]); } Notification notification = new NotificationCompat.Builder(this) .setLargeIcon(icon).setSmallIcon(R.drawable.ic_launcher) .setTicker("showBigView_Inbox").setContentInfo("ContentInfo") .setContentTitle("ContentTitle").setContentText("ContentText") .setStyle(inboxStyle).setAutoCancel(true) .setDefaults(Notification.DEFAULT_ALL) .build(); manager.notify(NOTIFICATION_ID_INBOX, notification); } 4>大布局Picture类型notification ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 private void showBigPicStyleNotification() { NotificationCompat.BigPictureStyle pictureStyle = new NotificationCompat.BigPictureStyle(); pictureStyle.setBigContentTitle("BigContentTitle") .setSummaryText("SummaryText") .bigPicture(icon); Notification notification = new NotificationCompat.Builder(this) .setLargeIcon(icon).setSmallIcon(R.drawable.ic_launcher) .setTicker("showBigPicStyleNotification").setContentInfo("ContentInfo") .setContentTitle("ContentTitle").setContentText("ContentText") .setStyle(pictureStyle) .setAutoCancel(true).setDefaults(Notification.DEFAULT_ALL) .build(); manager.notify(NOTIFICATION_ID_BIGPIC, notification); } 5>自定义notification效果图: 图6 并对中间的播放按钮做了一个简单的点击处理事件: ? 1 2 3 4 5 6 7 8 9 10 11 private void showCustomizeNotification() { RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.custom_notification); Intent intent = new Intent(this, PlayMusicActivity.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0); remoteViews.setOnClickPendingIntent(R.id.paly_pause_music, pendingIntent); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setContent(remoteViews).setSmallIcon(R.drawable.ic_launcher) .setLargeIcon(icon).setOngoing(true) .setTicker("music is playing").setDefaults(Notification.DEFAULT_ALL); manager.notify(NOTIFICATION_ID_CUSTOMIZE, builder.build()); } 布局文件: ? 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 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_vertical" android:orientation="horizontal" > <ImageView android:id="@+id/singer_pic" android:layout_width="64dp" android:layout_height="64dp" android:src="@drawable/singer" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center_vertical" android:orientation="horizontal" > <ImageView android:id="@+id/last_music" android:layout_width="0dp" android:layout_height="48dp" android:layout_weight="1" android:src="@drawable/player_previous" /> <ImageView android:id="@+id/paly_pause_music" android:layout_width="0dp" android:layout_height="48dp" android:layout_weight="1" android:src="@drawable/player_pause" /> <ImageView android:id="@+id/next_music" android:layout_width="0dp" android:layout_height="48dp" android:layout_weight="1" android:src="@drawable/player_next" /> </LinearLayout> </LinearLayout> 带进度条的notification: ? 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 private void showProgressBar() { final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setLargeIcon(icon).setSmallIcon(R.drawable.ic_launcher) .setTicker("showProgressBar").setContentInfo("ContentInfo") .setOngoing(true).setContentTitle("Downloading...") .setContentText("ContentText"); new Thread(new Runnable() { @Override public void run() { int progress = 0; for (progress = 0; progress < 100; progress += 5) { //将setProgress的第三个参数设为true即可显示为无明确进度的进度条样式 //builder.setProgress(100, progress, true); builder.setProgress(100, progress, false); manager.notify(NOTIFICATION_ID_PROGRESS, builder.build()); try { // Sleep for 5 seconds Thread.sleep(2 * 1000); } catch (InterruptedException e) { } } builder.setContentTitle("Download complete") .setProgress(0, 0, false).setOngoing(false); manager.notify(NOTIFICATION_ID_PROGRESS, builder.build()); } }).start(); } 原文地址:http://www.jb51.net/article/36567.htm
ArrayList是java开发时非常常用的类,常碰到需要对ArrayList循环删除元素的情况。这时候大家都不会使用foreach循环的方式来遍历List,因为它会抛java.util.ConcurrentModificationException异常。比如下面的代码就会抛这个异常: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); list.add("5"); for (String item: list) { if (item.equals("3")) { list.remove(item); } } System.out.println(Arrays.toString(list.toArray())); 那是不是在foreach循环时删除元素一定会抛这个异常呢?答案是否定的。 见这个代码: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); list.add("5"); for (String item: list) { if (string.equals("4")) { list.remove(item); } } System.out.println(Arrays.toString(list.toArray())); 这段代码和上面的代码只是把要删除的元素的索引换成了4,这个代码就不会抛异常。为什么呢? 接下来先就这个代码做几个实验,把要删除的元素的索引号依次从1到5都试一遍,发现,除了删除4之外,删除其他元素都会抛异常。接着把list的元素个数增加到7试试,这时候可以发现规律是,只有删除倒数第二个元素的时候不会抛出异常,删除其他元素都会抛出异常。 好吧,规律知道了,可以从代码的角度来揭开谜底了。 首先java的foreach循环其实就是根据list对象创建一个Iterator迭代对象,用这个迭代对象来遍历list,相当于list对象中元素的遍历托管给了Iterator,你如果要对list进行增删操作,都必须经过Iterator,否则Iterator遍历时会乱,所以直接对list进行删除时,Iterator会抛出ConcurrentModificationException异常 其实,每次foreach迭代的时候都有两步操作(): iterator.hasNext() //判断是否有下个元素 item = iterator.next() //下个元素是什么,并赋值给上面例子中的item变量 next()方法的代码如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } 这时候你会发现这个异常是在next方法的checkForComodification中抛出的,抛出原因是modCount != expectedModCount modCount是指这个list对象从new出来到现在被修改次数,当调用List的add或者remove方法的时候,这个modCount都会增加; expectedModCount是Iterator类中特有的变量,指现在期望这个list被修改的次数是多少次,这个值在调用list.iterator()创建iterator的时候初始化为modCount,该值在iterator初始化直到使用结束期间不会改变。 iterator创建的时候modCount被赋值给了expectedModCount,但是调用list的add和remove方法的时候不会同时修改expectedModCount,这样就导致下次取值时检查到两个count不相等,从而抛出异常。 解决这个问题的一种方式是使用Iterator来操作列表: ? 1 2 3 4 5 6 Iterator<String> it = list.iterator(); while(it.hasNext()) { if (it.next().equals("3")) { it.remove(); } } 那么为什么这种方式不会抛出该异常呢?下面是ArrayList中内部类Itr的remove方法: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } 注意下面这句: ? 1 expectedModCount = modCount; 可以看出,在使用iterator()方法得到的Iterator对象后,通过iterator.remove方法是可以正确删除列表元素的,因为它保证了expectedModCount=modCount。 避免这个问题的另一种方法,是不使用foreach语句的for循环: ? 1 2 3 4 5 6 7 8 for (int i = 0; i < list.size(); ) { String s = list.get(i); if (s.equals("3")) { list.remove(i); continue; } i++; } 回到问题上来,在使用foreach迭代ArrayList时,是可以删除任何一个元素的,且只能删除一个,而且这只能发生在迭代到倒数第二个元素的时候。比如下面的代码不会有异常: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 List<String> list = new ArrayList<>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); list.add("5"); for (String item: list) { if (string.equals("4")) { list.remove("5"); //删除的不一定是当前元素 } } System.out.println(Arrays.toString(list.toArray())); 其真正的原因是remove("5")这一句之后,下一次foreach语句将调用iterator.hasNext()方法,如果此时返回false,这样就不会进到next()方法里了,也就不会调用checkForComodification而导致异常了。 疑问:当循环到倒数第二个元素时,如果再多删除一个会怎样呢?比如: ? 1 2 3 4 5 6 7 for (String item: list) { if (item.equals("4")) { list.remove("1"); list.remove("5"); } System.out.println(Arrays.toString(list.toArray())); } 这段代码中,list是可以被打印出来的,因为list.remove()方法可以正确执行,其结果也是正确的。但是执行完这次打印,进入下一次迭代时,又产生了checkForComodification异常,还没想明白为什么。如果哪位大牛知道,请留言。
java.util.List实现了java.lang.Iterable接口. jdk api文档中是这样描述Iterable接口的:实现这个接口允许对象成为 "foreach" 语句的目标。不过咋一看Iterable接口并没啥特别之处,只是定义了一个迭代器而已。 ? 1 2 3 4 5 6 7 8 public interface Iterable<T> { /** * Returns an iterator over a set of elements of type T. * * @return an Iterator. */ Iterator<T> iterator(); } 究竟是如何实现foreach的呢,想想可能是编译器做了优化,就看了下最终编译成的字节码: ? 1 2 3 4 5 6 7 8 public class Iterable_eros { List<String> strings; public void display(){ for(String s : strings){ System.out.println(s); } } } 相应的字节码为: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void display (){ line0 : aload_0 getfield java.util.List my.lang.Iterable_eros.strings invokeinterface java.util.Iterator java.util.List.iterator() 1 astore_2 goto line30 line13 : aload_2 invokeinterface java.lang.Object java.util.Iterator.next() 1 checkcast java.lang.String astore_1 line23 : getstatic java.io.PrintStream java.lang.System.out aload_1 line27 : invokevirtual void java.io.PrintStream.println(java.lang.String) line30 : aload_2 invokeinterface boolean java.util.Iterator.hasNext() 1 ifne line13 line39 : return 果然没猜错哈!可以看到,foreach语法最终被编译器转为了对Iterator.hasNext()和对Iterator.next()的调用。而作为使用者的我们, jdk并没用向我们暴露这些细节,我们甚至不需要知道Iterator的存在,认识到jdk的强大之处了吧。 为了证实自己的想法,又用Iterator写了个遍历List的方法查看了字节码,果然跟foreach的形式基本一样,当然这是后话~ ? 1 2 3 4 5 6 7 8 9 10 11 public void display(){ for(String s : strings){ System.out.println(s); } Iterator<String> iterator = strings.iterator(); while(iterator.hasNext()){ String s = iterator.next(); System.out.println(s); } } 用相应的字节码如下: ? 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 public void display (){ line0 : aload_0 getfield java.util.List my.lang.Iterable_eros.strings invokeinterface java.util.Iterator java.util.List.iterator() 1 astore_2 goto line30 line13 : aload_2 invokeinterface java.lang.Object java.util.Iterator.next() 1 checkcast java.lang.String astore_1 line23 : getstatic java.io.PrintStream java.lang.System.out aload_1 line27 : invokevirtual void java.io.PrintStream.println(java.lang.String) line30 : aload_2 invokeinterface boolean java.util.Iterator.hasNext() 1 ifne line13 aload_0 getfield java.util.List my.lang.Iterable_eros.strings invokeinterface java.util.Iterator java.util.List.iterator() 1 astore_1 line49 : goto line69 line52 : aload_1 invokeinterface java.lang.Object java.util.Iterator.next() 1 checkcast java.lang.String astore_2 line62 : getstatic java.io.PrintStream java.lang.System.out aload_2 line66 : invokevirtual void java.io.PrintStream.println(java.lang.String) line69 : aload_1 invokeinterface boolean java.util.Iterator.hasNext() 1 ifne line52 line78 : return 这边还发现一个比较有趣的现象:在取Iterator.next()之后并在把该值load进内容栈之前,编译器调用了checkcast java.lang.String方法来进行类型安全检查,jdk应该是采用这个来检测并抛出ClassCastException的。 原文地址:http://blog.csdn.net/a596620989/article/details/6930479
为了更好的理解Handler的工作原理,先介绍一下与Handler一起工作的几个组件。 Message: Handler接收和处理的消息对象 Looper:每个线程只能拥有一个Looper。它的loop方法负责读取MessageQueue中的消息,之后把消息交给发送该消息的Handler处理 MessageQueue:消息队列,使用先进先出的方式来管理Message。程序创建Looper对象时会在它的构造器中创建MessageQueue对象。下面是Looper的构造函数: ? 1 2 3 4 5 private Looper() { mQueue = new MessageQueue();//这里创建MessageQueue,这个Queue就负责管理消息 mRun = true; mThread = Thread.currentThread(); } Handler,它的作用有两个——发送消息和处理消息。程序使用Handler发送消息,被Handler发送的消息必须被送到指定的MessageQueue。也就是说,如果希望Handler正常工作,必须在当前线程中有一个MessageQueue,否则消息就没地方保存了。而MessageQueue是由Looper管理的,因此必须在当前线程中有一个Looper对象,可以分如下两种情况处理: 主UI线程中,系统已经初始化了一个Looper对象,因此程序直接创建Handler即可,然后就可以通过Handler来发送消息和处理消息了。 程序员自己启动的子线程中,必须自己创建一个Looper对象,并启动它。调用它的prepare()方法即可创建Looper对象。 prepare()方法保证每个线程最多只有一个Looper对象: ? 1 2 3 4 5 6 public static final void prepare() { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper());//这里创建Looper对象并放到ThreadLocal中 } 然后调用Looper的静态loop()方法来启动它。loop()方法使用一个死循环不断取出MessageQueue中的消息,并将其分发给对应的Handler进行处理: ? 1 2 3 4 5 6 7 8 9 10 for(;;) { Message msg = queue.next();//获取Queue中的下一个消息,如果没有,将会阻塞 if (msg == null) { //如果消息为null,表明MessageQueue正在退出 return; } ... msg.target.dispatchMessage(msg); ... } 在线程中使用Handler的步骤如下: 调用Looper.prepare()为当前线程创建Looper对象,这将自动创建与之配套的MessageQueue 创建Handler子类的实例,重写handleMessage(Message msg)分发,该方法负责处理来自其他线程的消息 调用Looper.loop()启动Looper 例:输入一个整数,单击“计算”按钮,计算小于这个整数的所有质数。Activity的代码如下: ? 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 public class CalPrime extends Activity { static final String UPPER_NUM = "upper"; EditText etNum; CalThread calThread; //定义一个线程类 class CalThread extends Thread { public Handler mHandler; public void run() { Looper.prepare(); //Step1.创建Looper对象 mHandler = new Handler() { //Step2.创建Handler对象 //重写处理Message的方法 @Override public void handleMessage(Message msg) { if (msg.what == 123) { int upper = msg.getData().getInt(UPPER_NUM); List<Integer> nums = new ArrayList<Integer>(); outer: for (int i = 2; i <= upper; i++) { //用i % (从2开始到i的平方根的所有整数) for (int j = 2; j <= Math.sqrt(i); j++) { // 如果可以整除,则不是质数 if (i != 2 && i % j == 0) { continue outer; } } nums.add(i); } Toast.makeText(CalPrime.this, nums.toString(), Toast.LENGTH_LONG).show(); } } }; Looper.loop(); //Step3.启动Looper } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); etNum = (EditText)findViewById(R.id.etNum); calThread = new CalThread(); calThread.start(); } //为按钮提供处理函数 public void cal(View source) { Message msg = new Message(); msg.what = 0x123; Bundle bundle = new Bundle(); bundle.putInt(UPPER_NUM, Integer.parseInt(etNum.getText().toString())); msg.setData(bundle); //向新线程中的Handler发送消息 calThread.mHandler.sendMessage(msg); } }
注意这是发的广播信息,同一网段中其它机器都会收到这个信息(只有特殊的监听这类消息的机器会做出回应): SendUDP.java ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.UnknownHostException; public class SendUDP { public static void main(String[] args) throws Exception { // Use this port to send broadcast packet @SuppressWarnings("resource") final DatagramSocket detectSocket = new DatagramSocket(8888); // Send packet thread new Thread(new Runnable() { @Override public void run() { System.out.println("Send thread started."); while (true) { try { byte[] buf = new byte[1024]; int packetPort = 9999; // Broadcast address InetAddress hostAddress = InetAddress.getByName("192.168.184.255"); BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in)); String outMessage = stdin.readLine(); if (outMessage.equals("bye")) break; buf = outMessage.getBytes(); System.out.println("Send " + outMessage + " to " + hostAddress); // Send packet to hostAddress:9999, server that listen // 9999 would reply this packet DatagramPacket out = new DatagramPacket(buf, buf.length, hostAddress, packetPort); detectSocket.send(out); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } }).start(); // Receive packet thread. new Thread(new Runnable() { @Override public void run() { System.out.println("Receive thread started."); while(true) { byte[] buf = new byte[1024]; DatagramPacket packet = new DatagramPacket(buf, buf.length); try { detectSocket.receive(packet); } catch (IOException e) { e.printStackTrace(); } String rcvd = "Received from " + packet.getSocketAddress() + ", Data=" + new String(packet.getData(), 0, packet.getLength()); System.out.println(rcvd); } } }).start(); } } ReceiveUDP.java ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.net.DatagramPacket; import java.net.DatagramSocket; public class ReceiveUDP { public static void main(String[] args) throws Exception { int listenPort = 9999; byte[] buf = new byte[1024]; DatagramPacket packet = new DatagramPacket(buf, buf.length); @SuppressWarnings("resource") DatagramSocket responseSocket = new DatagramSocket(listenPort); System.out.println("Server started, Listen port: " + listenPort); while (true) { responseSocket.receive(packet); String rcvd = "Received " + new String(packet.getData(), 0, packet.getLength()) + " from address: " + packet.getSocketAddress(); System.out.println(rcvd); // Send a response packet to sender String backData = "DCBA"; byte[] data = backData.getBytes(); System.out.println("Send " + backData + " to " + packet.getSocketAddress()); DatagramPacket backPacket = new DatagramPacket(data, 0, data.length, packet.getSocketAddress()); responseSocket.send(backPacket); } } } 下图是SendUDP端的执行截图,发送内容为Message: 在SendUDP端发送了消息后,UDP端会立即显示收到消息,如下图: 正如第一幅图看到的,我在同一子网下的两台机器上运行着ReceiveUDP,于是两台机器都做出了回应。 如果将这种方式移植到Android手机上,可以用来探测同一WiFi下的其它设备(前提是这些设备上运行着类似ReceiveUDP的),以获取它们的IP地址。此后可以建立TCP连接,做其他的事情。有人说可以用Ping网段的方式来发现其它设备,但对于Android来说,这个方式并不可靠。因为判定消息不可达的时间难以确定。