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

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


  • 首页

  • 归档

  • 搜索

哈希分治法 - 统计海量数据中出现次数最多的前10个IP 场

发表于 2017-11-27

场景

这是一个 ip 地址 127.0.0.1
假设有100亿个这样的 ip 地址存在文件中
这个文件大小大约是 100GB
问题:要统计出100亿个 ip 中,重复出现次数最多的前10个

分析

100GB 几乎不可能一次加载进内存进行操作,所以必须要拆分那么可以利用分治的思想,把规模大的问题化小,然后解决各个小的问题,最后得出结果。

实现思路

  • ipv4 地址是一个 32 位的整数,可以用 uint 保存。
  • 我先设计一个哈希函数,把100个G的文件分成10000份,每份大约是 10MB,可以加载进内存了。

例如:我设计一个简单的哈希函数是 f(ip) = ip % 10000,(ip 是个32位整数)
那么 5 % 10000 = 5,不管 5 在哪个地方 5 % 10000 的结果都是 5,这就保证了相同的 ip 会被放在同一个子文件中,方便统计,相同的元素经过同一个哈希函数,得出的哈希值是一样的。
那么我把100亿个 ip,都进行 ip % 10000 的操作,就完成了 100GB 文件分解成 10000个子文件的任务了。当然实际中哈希函数的选取很重要,尽量使得元素分布均匀,哈希冲突少的函数才是最好的。

记住,我把上面这个分解的过程叫做 Map,由一台叫 master 的计算机完成这个工作。

  • 10MB 的小文件加进内存,统计出出现次数最多的那个ip

10MB 的小文件里面存着很多 ip,他们虽然是乱序的,但是相同的 ip 会映射到同一个文件中来!那么可以用二叉树统计出现次数,二叉树节点保存(ip, count)的信息,把所有 ip 插入到二叉树中,如果这个 ip 不存在,那么新建一个节点, count 标记 1,如果有,那么把 count++,最终遍历一遍树,就能找出 count 最大的 ip 了。

我把这个过程叫做 Reduce,由很多台叫 worker 的计算机来完成。
每个 worker 至少要找出最大的前10个 ip 返回给 master,master 最后会收集到 10000 * 10 个 ip,大约 400KB,然后再找出最大的前 10 个 ip 就可以了。
最简单的遍历10遍,每次拿个最大值出来就可以了,或者用快速排序,堆排序,归并排序等等方法,找出最大前 k 个数也行。

MapReduce

我刚刚除了介绍了一种海量数据的哈希分治算法之外,还穿插了一个谷歌的 MapReduce 分布式并行编程模型,原理就是上面说的那些了,有兴趣的可以去详细了解。

哈希函数是什么?哈希函数是把大空间的元素映射到一个小空间里。

说完了原理,我已经根据上面的原理写了一个实验程序,有兴趣的可以去看看,地址在 这里
可以下载来看,代码是C++的,vs2008
编译环境。

本文转载自: 掘金

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

Ubuntu 1604 Hadoop-273全分布模式

发表于 2017-11-27

Ps1:主要答疑区在本帖最下方,疑点会标注出来。个人在配置过程中遇到的困难都会此列举。

Ps2:本帖也是我自己原创的,最近从CSDN搬家过来。原帖地址

实验介绍:

  本次实验主要介绍了Hadoop平台的两个核心工具,HDFS和Mapreduce,结合这两个核心在Linux下搭建基于YARN集群的全分布模式的Hadoop架构。

  实验案例,基于Hadoop平台下的Wordcount分词统计的试验

实验需求:

  1、PC机,局域网服务,Linux系统  

背景介绍:

  Hadoop实现了一个分布式文件系统,简称HDFS。  HDFS有高容错性的特点,并且设计用来部署在普PC机上,而且它提供高吞吐量来访问应用程序的数据,适合那些有着超大数据集的应用程序。  HDFS放宽了POSIX的要求,可以以流的形式访问文件系统中的数据。   Hadoop的框架最核心的设计就是:    HDFS和MapReduce。    HDFS为海量的数据提供了存储,则MapReduce为海量的数据提供了计算。    开发者在熟练掌握了hadoop的使用后轻松地在Hadoop上开发和运行处理海量数据的应用程序   ### NameNode

  NameNode 是一个通常在 HDFS 实例中的单独机器上运行的软件。  它负责管理文件系统名称空间和控制外部客户机的访问。NameNode 决定是否将文件映射到 DataNode 上的复制块上。  对于最常见的 3 个复制块,第一个复制块存储在同一机架的不同节点上,最后一个复制块存储在不同机架的某个节点上。  实际的 I/O事务并没有经过 NameNode,只有表示 DataNode 和块的文件映射的元数据经过 NameNode。  当外部客户机发送请求要求创建文件时,NameNode 会以块标识和该块的第一个副本的 DataNode IP 地址作为响应,这个 NameNode 还会通知其他将要接收该块的副本的 DataNode。   NameNode 在一个称为 FsImage 的文件中存储所有关于文件系统名称空间的信息。  这个文件和一个包含所有事务的记录文件(这里是 EditLog)将存储在 NameNode 的本地文件系统上。FsImage 和 EditLog 文件也需要复制副本,以防文件损坏或 NameNode 系统丢失。  NameNode本身不可避免地具有SPOF单点失效的风险,主备模式并不能解决这个问题,通过Hadoop Non-stop namenode才能实现100% uptime可用时间。### DataNode

  DataNode 也是一个通常在 HDFS实例中的单独机器上运行的软件。  Hadoop 集群包含一个 NameNode 和大量 DataNode。  DataNode 通常以机架的形式组织,机架通过一个交换机将所有系统连接起来。  Hadoop 的一个假设是:机架内部节点之间的传输速度快于机架间节点的传输速度。   DataNode 响应来自 HDFS 客户机的读写请求。它们还响应来自 NameNode 的创建、删除和复制块的命令。  NameNode 依赖来自每个 DataNode 的定期心跳(heartbeat)消息。每条消息都包含一个块报告,NameNode 可以根据这个报告验证块映射和其他文件系统元数据。  如果 DataNode 不能发送心跳消息,NameNode 将采取修复措施,重新复制在该节点上丢失的块。

实验步骤及结果:

1.搭建平台(全分布式hadoop + eclipse Neon.1 + JDK1.8)

  

  集群搭建:

  主机两台(可拓展):

  (1)两个主机系统均为Ubuntu 16.04 LTS

    详情:

      master 192.168.:103.26(虚拟机)

      slave2 192.168.103.22(物理机)

      

      注:

        (1)slave1是在同学的笔记本上,因为他的笔记本总是飘忽不定,所以这次博客上就先不写他的ip地址

        (2)master是虚拟机的理由就是第一次尝试怕配错环境,导致崩溃,所以用了VMware为master,方便拯救平台

  (2)hadoop平台版本都为最新稳定版2.7.3(解压及安装hadoop)      

      下载地址:Hadoop官网 hadoop.apache.org/releases.ht…

      

      步骤1:点开网页以后,点击红色箭头所指的链接

      步骤2:点开后如下图

      

      步骤3:选择一个链接下载(个人推荐最后一个 tsinghua.edu.cn 清华大学链接源比较好)

      步骤4:下载完后打开文件管理器,选择Downloads文件夹(如果修改主要文件夹名字为中文的,应选择“下载”)

      

      步骤5:解压到指定路径

      步骤5.1:在当前文件夹下右键 - 在终端打开 键入su root命令

      

      步骤5.2:输入root用户密码后,如下图所示

      

      步骤5.3:键入解压命令

1
复制代码sudo tar zxvf hadoop-2.7.3.tar.gz -C /usr/local/hadoop

        (注意:如果提示hadoop文件夹不存在的,可以在root用户下用cd命令到 /usr/local路径下 键入 sudo mkdir /hadoop 创建夹)

      步骤5.4:解压后如下图所示

        (注意:路径满足如图所示即可,或自行定义)

    至此hadoop前期下载准备工作已经完成。接下准备java环境的配置

 

  (3)JDK版本为java8-oracle(配置java环境)

     (环境:系统稳定联网状态下)

      步骤1:打开终端键入命令(root用户模式可以不用加sudo前缀)

1
复制代码sudo add-apt-repository ppa:webupd8team/java

      步骤2:出现一段文字后按回车继续

      步骤3:继续键入命令

1
复制代码sudo apt-get update

      步骤4:待系统加载完所有下载源

      步骤5:键入安装命令

1
复制代码sudo apt-get install oracle-java8-installer

      步骤6:等待下载结束(过程稍微有点漫长)

      

      这个版本的java默认安装在 /usr/lib/jvm文件夹下

      安装结束后配置环境变量

      

      终端输入:

1
复制代码sudo gedit /etc/profile

      步骤7:配置完后,按 ctrl + s 保存

      步骤8:在终端中输入

1
复制代码sudo source /etc/profile

      使配置的环境变量生效

      步骤9:和在Windows下配置一样,在终端测试java和javac命令是否生效,在linux下可以多测试下jps命令看java进程号

      

      至此java环境变量配置完毕

  (4)SSH免密配置

    SSH 是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。SSH最初是UNIX系统上的一个程序,后来又迅速扩展到其他操作平台。

    SSH在正确使用时可弥补网络中的漏洞。SSH客户端适用于多种平台。

      Ubuntu Linux下配置免密登录主要依靠 ssh localhost的命令

      !!注意,如果改过 /etc/hosts 下的内容需要重新配置(下图是我的例子)

    

    由于后期为了避免hadoop的一些端口和IP错误,所以我把localhost的名字改了,顺带把 /etc/hostname 的名字也改了。

    改了上述的 hosts 和 hostname的名字后,记得重启电脑或者虚拟机

    

    192.168.91.45是我虚拟的IP的地址 名字叫master 相当于 没有改变配置文件之前的 127.0.0.1 localhost

    所以配置ssh免密的时候是键入 ssh master 而不是 ssh localhost

    

    话不多说!

    步骤0:SSH需要安装OpenSSH-server(如果已经安装则无需理会)

1
复制代码sudo apt-get install openssh-server

    步骤1:在非root用户模式下打开终端键入ssh localhost(或者是定义的用户名)

    步骤2:提示输入密码,输入你的ssh密码(自己记得住就好)

    步骤3:输入完以后,测试一下ssh localhost(或是自定义名字),输入密码后是否如下图弹出一些信息

    

    步骤4:如果下午所示后,则创建ssh成功

    步骤5:创建免密登录(不需要关闭终端),键入如下命令

1
复制代码ssh-keygen -t rsa

    

    步骤6:一直按回车直至出现RSA窗口即可

    步骤7:键入命令

1
复制代码sudo cp .ssh/id_rsa.pub .ssh/authorized_keys

    步骤8:验证免密登录,输入ssh localhost(或者自定义的名字),是否还需要输入密码登录

    root用户下:

    步骤1:进入root用户模式(用户模式下在终端键盘入:su root,输入root密码即可)

    步骤2:进入ssh配置文件

1
复制代码gedit /etc/ssh/sshd_config

    

    步骤3:把PermitRootLogin的字段改成 yes(原来的好像是Prohibit xxxx的),有点忘记了。总之改成yes就可以了

    步骤4:保存退出终端

    步骤5:打开新的终端键入命令

1
复制代码sudo service ssh restart

    重启ssh服务之后,打开终端

    

    步骤6:进入root用户模式下,键入 ssh localhost(或是你的自定义名字)

    步骤7:输入自定义ssh密码后,与用户模式下的类似

    步骤8:键入 ssh-keygen -t rsa 创建RSA密钥

    步骤9:一直回车直至出现RSA密钥图,(如果提示Overwrite 输入 y 即可)

    步骤10:键入配置免密的命令

1
复制代码cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

    步骤11:完成后,在root用户模式输入 ssh localhost(或自定义名字)后,如下图所示即可。

        

    

    至此,root用户和普通用户模式的ssh免密配置完成。

    

  (4.1)SSH免密配置(节点篇)

      需求:如果每个节点都需要下载安装hadoop ,则大量耗费人力物力。

      解决:所以需要一个SSH来远程发送hadoop包分发给每个节点。

      

      接下来来讲解master打通每个节点的连接方式(单节点和多节点一样,只要配置好就可以进行连接)

      步骤1:在hosts文件中配置好各子节点的ip地址以及名称(如下图)

      

      步骤2: 编辑好hosts文件保存并关闭,(root用户模式下)打开终端输入

1
2
复制代码ssh-copy-id -i ~/.ssh/id_rsa.pub root@slave* 
(星号代表子节点号码,或者把slave*换成自定义的名称)

      步骤3:提示输入,子节点的登录密码,输入完成后,等待命令完成

      步骤4:在终端中输入 ssh slave*(或者自定义名字),如下图:

     步骤5:ssh打通master和子节点的通道,可以通过scp命令传输数据了。

     至此,完成对于子节点的ssh免密访问配置。

  (5)hadoop平台版本都为最新稳定版2.7.3(解压及安装hadoop)

    hadoop配置下主要注意配置文件路径的问题

    主要包括:hadoop根目录下 /etc/hadoop 里面的xml配置文件

      例:hadoop-env.sh , hdfs-site.xml, mapred-site.xml , core-site.xml , yarn-site.xml

      

      注:mapred-site.xml需要复制出来到本路径,原本是mapred-site.xml.template 需要用 cp 命令复制并改名字

        或者可以通过 gedit 命令创建一个新的mapred-site.xml,把模板内的内容复制过去,然后再进行配置

     

      配置文件1:hadoop-env.sh(配置环境变量,让hadoop识别)

      配置文件2:core-site.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
复制代码<configuration>
<property>
<name>fs.default.name</name>
<value>hdfs://master:8020</value>
</property>
<property>
<name>io.file.buffer.size</name>
<value>131072</value>
</property>
<property>
<name>hadoop.tmp.dir</name>
<value>/home/hadoop/tmp</value>
</property>
<property>
<name>hadoop.proxyuser.hadoop.hosts</name>
<value>*</value>
</property>
<property>
<name>hadoop.proxyuser.hadoop.group</name>
<value>*</value>
</property>
<property>
<name>dfs.permissions</name>
<value>false</value>
</property>
</configuration>

      配置文件3:hdfs-site.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码<configuration>
<property>
<name>dfs.namenode.secondary.http-address</name>
<value>master:9000</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>file:/home/hadoop/dfs/name</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>file:/home/hadoop/dfs/data</value>
</property>
<property>
<name>dfs.replication</name>
<value>3</value>
</property>
<property>
<name>dfs.webhdfs.enabled</name>
<value>true</value>
</property>
</configuration>

      配置文件4: mapred-site.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
<property>
<name>mapred.job.tracker</name>
<value>master:9001</value>
</property>
<property>
<name>mapred.job.tracker.http.address</name>
<value>master:50030</value>
</property>
<property>
<name>mapreduce.jobhistory.address</name>
<value>master:10020</value>
</property>
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>master:19888</value>
</property>
</configuration>

      配置文件5:yarn-site.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
复制代码<configuration>
<property>
<name>yarn.nodemanager.aux-services</name>
<value>mapreduce_shuffle</value>
</property>
<property>
<name>yarn.nodemanager.aux-services.mapreduce.shuffle.class</name>
<value>org.apache.hadoop.mapred.ShuffleHandler</value>
</property>
<property>
<name>yarn.resourcemanager.address</name>
<value>master:8032</value>
</property>
<property>
<name>yarn.resourcemanager.scheduler.address</name>
<value>master:8030</value>
</property>
<property>
<name>yarn.resourcemanager.resource-tracker.address</name>
<value>master:8031</value>
</property>
<property>
<name>yarn.resourcemanager.admin.address</name>
<value>master:8033</value>
</property>
<property>
<name>yarn.resourcemanager.webapp.address</name>
<value>master:8088</value>
</property>
</configuration>

      !!!注:如果是master节点(即服务器)需要添加多一个slaves文件指定slave

      配置文件6:slaves(选)

1
复制代码slave2 192.168.90.33

     

    最后步骤:以上配置文件配置完毕后打开终端窗口,输入

1
复制代码hadoop namenode -format

    出现如下结果,没有JAVA报错即可

    初始化hadoop namenode节点成功!

    

    打开终端利用 cd 命令进入hadoop启动命令文件下

1
复制代码cd /usr/local/hadoop/hadoop-2.7.3/sbin

    

    键入如下命令启动hadoop(root用户模式下)     

1
复制代码./start-all.sh

    关闭hadoop则键入命令关闭

1
复制代码./stop-all.sh

    

    输入jps在master节点测试,如果如上图所示则测试成功

    在ssh slave2 节点输入jps测试

    

    通过hadoop 自带命令

1
复制代码hadoop dfsadmin -report

    如上图所示输出Live Datanodes,说明有存活节点,死节点为空。

    证明集群配置成功!

  (6)集群安装hadoop(完成Master节点的hadoop安装以及SSH的搭建)    

    构建好master与各个slave之间的ssh通信,如下图所示

    步骤1:测试ssh命令与各节点间的通信

    步骤2:确认本机的hadoop安装地址

    步骤3:

1
复制代码scp –r /usr/local/hadoop/ root@slaver2:/usr/local/hadoop

    把master上的hadoop分发给slave2节点(其他节点依次类推,只要搭好ssh就可以传输)。

    传输过程有点久,耐心等候。

    步骤4:在slave节点上配置环境变量

1
复制代码HADOOP_HOME=/usr/local/Hadoop  PATH=$PATH:${HADOOP_HOME}/bin:${HADOOP_HOME}/sbin

    步骤5:在master启动hadoop进行测试

Namenode界面 50070端口

hadoop管理界面 8088端口

hadoop SecondaryNamenode 管理界面 端口9000

  (7)hadoop Wordcount测试(完成eclipse和eclipse hadoop插件安装)

    步骤0:安装eclipse和eclipse hadoop插件

      步骤0.1:安装eclipse

      

    下载后,解压到自定义路径,解压后如所示

      在此给出eclipse hadoop插件下载(pan.baidu.com/s/1mi6UP5I)

      下载后,把jar放到eclipse根目录的dropins的目录

      在根目录进入终端,进入root用户模式,输入

1
复制代码./eclipse

      进入eclipse界面,完成安装。

      

    步骤1:启动hadoop完成上述集群测试

    步骤2:通过终端把测试数据 test.txt上传到hdfs中 (test.txt为hadoop跟目录下的NOTICE.txt)

      步骤2.1:在hdfs目录下创建input文件夹

1
2
复制代码hadoop fs -mkdir /input
hadoop fs -put test.txt /input

    如图所示,则上传成功。

    如果权限不对的话可以修改权限

1
复制代码hadoop fs -chmod -R 777 /input/test.txt

    

    步骤3:打开eclipse,并完成mapreduce的wordcount代码,完成eclipse hadoop的配置

   步骤4:确保左上角的DFS Location能够显示hdfs中的文件目录

WordCount代码:

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
复制代码package org.apache.hadoop.examples;

import java.io.IOException;
import java.util.StringTokenizer;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class WordCount {

public static class TokenizerMapper
extends Mapper<Object, Text, Text, IntWritable>{

private final static IntWritable one = new IntWritable(1);
private Text word = new Text();

public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
StringTokenizer itr = new StringTokenizer(value.toString());
while (itr.hasMoreTokens())
{
word.set(itr.nextToken());
context.write(word, one);
}
}
}

public static class IntSumReducer extends Reducer<Text,IntWritable,Text,IntWritable>
{
private IntWritable result = new IntWritable();

public void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException
{
int sum = 0;
for (IntWritable val : values)
{
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}

public static void main(String[] args) throws Exception
{
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCount.class);
job.setMapperClass(TokenizerMapper.class);
job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}

log4j日志文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码log4j.rootLogger=debug, stdout, R 
#log4j.rootLogger=stdout, R
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
#log4j.appender.stdout.layout.ConversionPattern=%5p - %m%n
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=log4j.log
log4j.appender.R.MaxFileSize=100KB
log4j.appender.R.MaxBackupIndex=1
log4j.appender.R.layout=org.apache.log4j.PatternLayout
#log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n
log4j.appender.R.layout.ConversionPattern=%d %p [%c] - %m%n
#log4j.logger.com.codefutures=DEBUG

    步骤5:配置Run Configuration

    步骤6:右键Run As - Run On Hadoop(日志我选用了DEBUG模式测试,所以会很长,但是方便测试)

    

    此时,master hdfs多出一个文件夹存放分词结果

    下列图为结果部分截图:

    

    至此,从安装到mapreduce Wordcount测试全部结束了。

    hadoop2.7.6全模式下,结合eclipse hadoop插件配置,完成Wordcount测试。

实验结果分析:

1、Wordcount项目代码是结合Map-reduce的核心思想,以及对于Java输入输出流的认识所编写,也参考了一下”大牛”博客编写的,能够基本实现分词-词频统计。

2、小项目的分词的效果显然没有Python Jieba分词来的精确,但是基于Hadoop Mapreduce的运算,分词一篇词汇众多的文档只需要5秒。(如需查看请点开。文档来源:Hadoop LICENSE.txt)

测试文档

心得体会:

    1、实验完成结果到达预期目标,在搭建平台的过程耗费了很多学习成本,主要花在安装包的下载以及对于Linux系统的理解和hadoop配置文件的理解。

    2、实验完成的过程中与小组成员分工合作,在搭建过程中自学了linux的命令操作以及linux系统的一些工作原理。

    3、在搭建hadoop平台时,遇到很多匪夷所思的问题,通过hadoop平台自带的log文件,查看日志文件,百度搜索或者看国外网站的配置方式,再通过自己的尝试,解决问题。

    4、在搭建过程体会最深的就是hadoop对于端口的使用很谨慎,第一次在尝试的时候没有仔细看清楚官网文档的端口设置,配置出错,导致进度耽误几天,最后才发现是端口的问题。

    5、在搭建完后对于linux系统也有深刻的体会,对于linux的权限设置,SSH,以及基本的文件操作命令等有基本的掌握经验。

    6、小组成员在第一次冲刺后决定更改软件工程项目,主要是为了适应目前的学习任务以及工作任务。小组成员目前在分析 学校历年学生体质测试数据 以及 网络招聘岗位数据对应学校各二级学院的专业核心技能

      Python Django项目属于python后端项目,初期小组成员定题是为了学习除java后端以外的另外一直后端开发。但是后期因为繁重的分析任务以及报告,所以决定开始寻找新的出路,也顺利在第三次冲刺前几天完成实验。

      虽然可能与软件工程的项目关系不太大,但是在搭建平台的过程,小组成员也深刻体会到团队合作的意义。以及对于大数据平台的理解,不再是觉得深不可测,改变对于大数据平台以及云计算的看法。

展望:

    1、希望在接下来的寒假或者未来的时间点,完善自己的hadoop平台,通过hadoop平台提交小组的数据分析项目,利用Mapreduce并行化算法以及YARN集群分布式计算,提高数据分析的效率。

    2、以及写一个基于hadoop平台的分布式爬虫,提高大数据的读取时间。

    3、目前也在学习Spark,掌握与Mapreduce相类似的并行化运算框架,也希望在日后的使用中,结合HBase,Mapreduce/Spark搭建一个云计算平台项目。

    4、在未来的时间,花更多时间从理解hadoop的核心架构,到理解hadoop的外沿,学习Spark,HBase,Pig,Mahout,Hive等核心工具的使用。

    5、最近时间关注大数据方向注意到关联数据RDF的应用,也希望能尝试利用Sqoop读取关联数据,进行数据分析。

本文转载自: 掘金

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

Spring Boot 入门之 Web 篇(二)

发表于 2017-11-27

一、前言

上一篇《Spring Boot 入门之基础篇(一)》介绍了 Spring Boot 的环境搭建以及项目启动打包等基础内容,本篇继续深入介绍 Spring Boot 与 Web 开发相关的知识。

二、整合模板引擎

由于 jsp 不被 SpringBoot 推荐使用,所以模板引擎主要介绍 Freemarker 和 Thymeleaf。

2.1 整合 Freemarker

2.1.1 添加 Freemarker 依赖

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

2.1.2 添加 Freemarker 模板配置

在 application.properties 中添加如下内容:

1
2
3
4
5
6
7
8
9
10
ini复制代码spring.freemarker.allow-request-override=false
spring.freemarker.cache=true
spring.freemarker.check-template-location=true
spring.freemarker.charset=UTF-8
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.expose-spring-macro-helpers=false
spring.freemarker.prefix=
spring.freemarker.suffix=.ftl

上述配置都是默认值。

2.1.3 Freemarker 案例演示

在 controller 包中创建 FreemarkerController:

1
2
3
4
5
6
7
8
9
10
typescript复制代码@Controller
@RequestMapping("freemarker")
public class FreemarkerController {
@RequestMapping("hello")
public String hello(Map<String,Object> map) {

map.put("msg", "Hello Freemarker");
return "hello";
}
}

在 templates 目录中创建名为 hello.ftl 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
dust复制代码<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link href="/css/index.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h2>${msg}</h2>
</div>
</body>
</html>

结果如下:

image

2.2 整合 Thymeleaf

2.2.1 添加 Thymeleaf 依赖

在 pom.xml 文件中添加:

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2.2.2 添加 Thymeleaf 模板配置

在 application.properties 中添加如下内容:

1
2
3
4
5
6
ini复制代码spring.thymeleaf.cache=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html

上述配置都是默认值。

2.2.3 Thymeleaf 案例演示

在 controller 包中创建 ThymeleafController:

1
2
3
4
5
6
7
8
9
typescript复制代码@Controller
@RequestMapping("thymeleaf")
public class ThymeleafController {
@RequestMapping("hello")
public String hello(Map<String,Object> map) {
map.put("msg", "Hello Thymeleaf");
return "hello";
}
}

在 template 目录下创建名为 hello.html 的文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
dust复制代码<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link href="/css/index.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h2 th:text="${msg}"></h2>
</div>
</body>
</html>

结果如下:

image

三、整合 Fastjson

3.1 添加依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.35</version>
</dependency>

3.2 整合 Fastjson

创建一个配置管理类 WebConfig ,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Configuration
public class WebConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);

fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);

HttpMessageConverter<?> converter = fastJsonHttpMessageConverter;

return new HttpMessageConverters(converter);
}
}

3.3 演示案例:

创建一个实体类 User:

1
2
3
4
5
6
7
8
9
haxe复制代码public class User {
private Integer id;

private String username;

private String password;

private Date birthday;
}

getter 和 setter 此处省略。

创建控制器类 FastjsonController :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pgsql复制代码@Controller
@RequestMapping("fastjson")
public class FastJsonController {
@RequestMapping("/test")
@ResponseBody
public User test() {
User user = new User();

user.setId(1);
user.setUsername("jack");
user.setPassword("jack123");
user.setBirthday(new Date());

return user;
}
}

打开浏览器,访问 http://localhost:8080/fastjson/test,结果如下图:

image

此时,还不能看出 Fastjson 是否正常工作,我们在 User 类中使用 Fastjson 的注解,如下内容:

1
2
kotlin复制代码@JSONField(format="yyyy-MM-dd")
private Date birthday;

再次访问 http://localhost:8080/fastjson/test,结果如下图:

image

日期格式与我们修改的内容格式一致,说明 Fastjson 整合成功。

四、自定义 Servlet

4.1 编写 Servlet

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码public class ServletTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("自定义 Servlet");
}

}

4.2 注册 Servlet

将 Servelt 注册成 Bean。在上文创建的 WebConfig 类中添加如下代码:

1
2
3
4
typescript复制代码@Bean
public ServletRegistrationBean servletRegistrationBean() {
return new ServletRegistrationBean(new ServletTest(),"/servletTest");
}

结果如下:

image

五、自定义过滤器/第三方过滤器

5.1 编写过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pgsql复制代码public class TimeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("=======初始化过滤器=========");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
long start = System.currentTimeMillis();
filterChain.doFilter(request, response);
System.out.println("filter 耗时:" + (System.currentTimeMillis() - start));
}
@Override
public void destroy() {
System.out.println("=======销毁过滤器=========");
}
}

5.2 注册过滤器

要是该过滤器生效,有两种方式:

1) 使用 @Component 注解

2) 添加到过滤器链中,此方式适用于使用第三方的过滤器。将过滤器写到 WebConfig 类中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
reasonml复制代码@Bean
public FilterRegistrationBean timeFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();

TimeFilter timeFilter = new TimeFilter();
registrationBean.setFilter(timeFilter);

List<String> urls = new ArrayList<>();
urls.add("/*");
registrationBean.setUrlPatterns(urls);

return registrationBean;
}

结果如下:

image

六、自定义监听器

6.1 编写监听器

1
2
3
4
5
6
7
8
9
typescript复制代码public class ListenerTest implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("监听器初始化...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}

6.2 注册监听器

注册监听器为 Bean,在 WebConfig 配置类中添加如下代码:

1
2
3
4
csharp复制代码@Bean
public ServletListenerRegistrationBean<ListenerTest> servletListenerRegistrationBean() {
return new ServletListenerRegistrationBean<ListenerTest>(new ListenerTest());
}

当启动容器时,结果如下:

image

针对自定义 Servlet、Filter 和 Listener 的配置,还有另一种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
aspectj复制代码@SpringBootApplication
public class SpringbootWebApplication implements ServletContextInitializer {

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// 配置 Servlet
servletContext.addServlet("servletTest",new ServletTest())
.addMapping("/servletTest");
// 配置过滤器
servletContext.addFilter("timeFilter",new TimeFilter())
.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),true,"/*");
// 配置监听器
servletContext.addListener(new ListenerTest());
}
public static void main(String[] args) {
SpringApplication.run(SpringbootWebApplication.class, args);
}
}

七、自定义拦截器

7.1 编写拦截器

使用 @Component 让 Spring 管理其生命周期:

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
pgsql复制代码@Component
public class TimeInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

System.out.println("========preHandle=========");
System.out.println(((HandlerMethod)handler).getBean().getClass().getName());
System.out.println(((HandlerMethod)handler).getMethod().getName());

request.setAttribute("startTime", System.currentTimeMillis());

return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
System.out.println("========postHandle=========");
Long start = (Long) request.getAttribute("startTime");
System.out.println("耗时:"+(System.currentTimeMillis() - start));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception)
throws Exception {
System.out.println("========afterCompletion=========");
Long start = (Long) request.getAttribute("startTime");
System.out.println("耗时:"+(System.currentTimeMillis() - start));

System.out.println(exception);
}
}

7.2 注册拦截器

编写拦截器后,我们还需要将其注册到拦截器链中,如下配置:

1
2
3
4
5
6
7
8
9
10
11
scala复制代码@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{

@Autowired
private TimeInterceptor timeInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timeInterceptor);
}
}

请求一个 controller ,结果如下:

image

八、配置 AOP 切面

8.1 添加依赖

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

8.2 编写切面类

使用 @Component,@Aspect 标记到切面类上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pgsql复制代码@Aspect
@Component
public class TimeAspect {
@Around("execution(* com.light.springboot.controller.FastJsonController..*(..))")
public Object method(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("=====Aspect处理=======");
Object[] args = pjp.getArgs();
for (Object arg : args) {
System.out.println("参数为:" + arg);
}
long start = System.currentTimeMillis();
Object object = pjp.proceed();
System.out.println("Aspect 耗时:" + (System.currentTimeMillis() - start));
return object;
}
}

请求 FastJsonController 控制器的方法,结果如下:

image

九、错误处理

9.1 友好页面

先演示非友好页面,修改 FastJsonController 类中的 test 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pgsql复制代码@RestController
@RequestMapping("fastjson")
public class FastJsonController {
@RequestMapping("/test")
public User test() {
User user = new User();

user.setId(1);
user.setUsername("jack");
user.setPassword("jack123");
user.setBirthday(new Date());

// 模拟异常
int i = 1/0;

return user;
}
}

浏览器请求:http://localhost:8080/fastjson/test,结果如下:

image

当系统报错时,返回到页面的内容通常是一些杂乱的代码段,这种显示对用户来说不友好,因此我们需要自定义一个友好的提示系统异常的页面。

在 src/main/resources 下创建 /public/error,在该目录下再创建一个名为 5xx.html 文件,该页面的内容就是当系统报错时返回给用户浏览的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>系统错误</title>
<link href="/css/index.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h2>系统内部错误</h2>
</div>
</body>
</html>

路径时固定的,Spring Boot 会在系统报错时将返回视图指向该目录下的文件。

如下图:

image

上边处理的 5xx 状态码的问题,接下来解决 404 状态码的问题。

当出现 404 的情况时,用户浏览的页面也不够友好,因此我们也需要自定义一个友好的页面给用户展示。

在 /public/error 目录下再创建一个名为 404.html 的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>访问异常</title>
<link href="/css/index.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h2>找不到页面</h2>
</div>
</body>
</html>

我们请求一个不存在的资源,如:http://localhost:8080/fastjson/test2,结果如下图:

image

9.2 全局异常捕获

如果项目前后端是通过 JSON 进行数据通信,则当出现异常时可以常用如下方式处理异常信息。

编写一个类充当全局异常的处理类,需要使用 @ControllerAdvice 和 @ExceptionHandler 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
livescript复制代码@ControllerAdvice
public class GlobalDefaultExceptionHandler {
/**
* 处理 Exception 类型的异常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Map<String,Object> defaultExceptionHandler(Exception e) {

Map<String,Object> map = new HashMap<String,Object>();
map.put("code", 500);
map.put("msg", e.getMessage());
return map;
}
}

其中,方法名为任意名,入参一般使用 Exception 异常类,方法返回值可自定义。

启动项目,访问 http://localhost:8080/fastjson/test,结果如下图:

image

我们还可以自定义异常,在全局异常的处理类中捕获和判断,从而对不同的异常做出不同的处理。

十、文件上传和下载

10.1 添加依赖

1
2
3
4
5
6
xml复制代码<!-- 工具 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>

10.2 实现

编写一个实体类,用于封装返回信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码public class FileInfo {
private String path;

public FileInfo(String path) {
this.path = path;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}

}

编写 Controller,用于处理文件上传下载:

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
reasonml复制代码@RestController
@RequestMapping("/file")
public class FileController {
private String path = "d:\\";
@PostMapping
public FileInfo upload(MultipartFile file) throws Exception {
System.out.println(file.getName());
System.out.println(file.getOriginalFilename());
System.out.println(file.getSize());
File localFile = new File(path, file.getOriginalFilename());
file.transferTo(localFile);
return new FileInfo(localFile.getAbsolutePath());
}
@GetMapping("/{id}")
public void download(@PathVariable String id, HttpServletRequest request, HttpServletResponse response) {
try (InputStream inputStream = new FileInputStream(new File(path, id + ".jpg"));
OutputStream outputStream = response.getOutputStream();) {
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment;filename=" + id + ".jpg");
IOUtils.copy(inputStream, outputStream);
} catch (Exception e) {
e.printStackTrace();
}
}
}

基本上都是在学习 javaweb 时用到的 API。

文件上传测试结果如下图:

image

十一、CORS 支持

前端页面:

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
xml复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>跨域测试</title>
</head>
<body>
<button id="test">测试</button>
<script type="text/javascript" src="jquery-1.12.3.min.js"></script>
<script type="text/javascript">
$(function() {
$("#test").on("click", function() {
$.ajax({
"url": "http://localhost:8080/fastjson/test",
"type": "get",
"dataType": "json",
"success": function(data) {
console.log(data);
}
})
});
});
</script>
</body>
</html>

通过 http 容器启动前端页面代码,笔者使用 Sublime Text 的插件启动的,测试结果如下:

image

从图中可知,前端服务器启动端口为 8088 与后端服务器 8080 不同源,因此出现跨域的问题。

现在开始解决跨域问题,可以两种维度控制客户端请求。

粗粒度控制:

方式一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typescript复制代码@Configuration
public class WebConfig {

@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/fastjson/**")
.allowedOrigins("http://localhost:8088");// 允许 8088 端口访问
}
};
}
}

方式二

1
2
3
4
5
6
7
8
9
scala复制代码@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/fastjson/**")
.allowedOrigins("http://localhost:8088");// 允许 8088 端口访问
}
}

配置后,重新发送请求,结果如下:

image

细粒度控制:

在 FastJsonController 类中的方法上添加 @CrossOrigin(origins=”xx”) 注解:

1
2
3
4
5
6
7
8
9
10
11
12
pgsql复制代码@RequestMapping("/test")
@CrossOrigin(origins="http://localhost:8088")
public User test() {
User user = new User();

user.setId(1);
user.setUsername("jack");
user.setPassword("jack123");
user.setBirthday(new Date());

return user;
}

在使用该注解时,需要注意 @RequestMapping 使用的请求方式类型,即 GET 或 POST。

十二、整合 WebSocket

12.1 添加依赖

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

12.2 实现方式

方式一:

该方式只适用于通过 jar 包直接运行项目的情况。

WebSocket 配置类:

1
2
3
4
5
6
7
8
typescript复制代码@Configuration
public class WebSocketConfig {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

WebSocket 处理类:

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
typescript复制代码@ServerEndpoint(value = "/webSocketServer/{userName}")
@Component
public class WebSocketServer {

private static final Set<WebSocketServer> connections = new CopyOnWriteArraySet<>();
private String nickname;
private Session session;
private static String getDatetime(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(date);
}
@OnOpen
public void start(@PathParam("userName") String userName, Session session) {
this.nickname = userName;
this.session = session;
connections.add(this);
String message = String.format("* %s %s", nickname, "加入聊天!");
broadcast(message);
}
@OnClose
public void end() {
connections.remove(this);
String message = String.format("* %s %s", nickname, "退出聊天!");
broadcast(message);
}
@OnMessage
public void pushMsg(String message) {
broadcast("【" + this.nickname + "】" + getDatetime(new Date()) + " : " + message);
}
@OnError
public void onError(Throwable t) throws Throwable {
}
private static void broadcast(String msg) {
// 广播形式发送消息
for (WebSocketServer client : connections) {
try {
synchronized (client) {
client.session.getBasicRemote().sendText(msg);
}
} catch (IOException e) {
connections.remove(client);
try {
client.session.close();
} catch (IOException e1) {
e.printStackTrace();
}
String message = String.format("* %s %s", client.nickname, "断开连接");
broadcast(message);
}
}
}
}

前端页面:

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
xml复制代码<!DOCTYPE html>
<html>
<head lang="zh">
<meta charset="UTF-8">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/bootstrap-theme.min.css">
<script src="js/jquery-1.12.3.min.js"></script>
<script src="js/bootstrap.js"></script>
<style type="text/css">
#msg {
height: 400px;
overflow-y: auto;
}
#userName {
width: 200px;
}
#logout {
display: none;
}
</style>
<title>webSocket测试</title>
</head>
<body>
<div class="container">
<div class="page-header" id="tou">webSocket及时聊天Demo程序</div>
<p class="text-right" id="logout">
<button class="btn btn-danger" id="logout-btn">退出</button>
</p>
<div class="well" id="msg"></div>
<div class="col-lg">
<div class="input-group">
<input type="text" class="form-control" placeholder="发送信息..." id="message"> <span class="input-group-btn">
<button class="btn btn-default" type="button" id="send"
disabled="disabled">发送</button>
</span>
</div>
<div class="input-group">
<input id="userName" type="text" class="form-control" name="userName" placeholder="输入您的用户名" />
<button class="btn btn-default" type="button" id="connection-btn">建立连接</button>
</div>
<!-- /input-group -->
</div>
<!-- /.col-lg-6 -->
</div>
<!-- /.row -->
</div>
<script type="text/javascript">
$(function() {
var websocket;
$("#connection-btn").bind("click", function() {
var userName = $("#userName").val();
if (userName == null || userName == "") {
alert("请输入您的用户名");
return;
}
connection(userName);
});
function connection(userName) {
var host = window.location.host;
if ('WebSocket' in window) {
websocket = new WebSocket("ws://" + host +
"/webSocketServer/" + userName);
} else if ('MozWebSocket' in window) {
websocket = new MozWebSocket("ws://" + host +
"/webSocketServer/" + userName);
}
websocket.onopen = function(evnt) {
$("#tou").html("链接服务器成功!")
$("#send").prop("disabled", "");
$("#connection-btn").prop("disabled", "disabled");
$("#logout").show();
};
websocket.onmessage = function(evnt) {
$("#msg").html($("#msg").html() + "<br/>" + evnt.data);
};
websocket.onerror = function(evnt) {
$("#tou").html("报错!")
};
websocket.onclose = function(evnt) {
$("#tou").html("与服务器断开了链接!");
$("#send").prop("disabled", "disabled");
$("#connection-btn").prop("disabled", "");
$("#logout").hide();
}
}
function send() {
if (websocket != null) {
var $message = $("#message");
var data = $message.val();
if (data == null || data == "") {
return;
}
websocket.send(data);
$message.val("");
} else {
alert('未与服务器链接.');
}
}
$('#send').bind('click', function() {
send();
});
$(document).on("keypress", function(event) {
if (event.keyCode == "13") {
send();
}
});
$("#logout-btn").on("click", function() {
websocket.close(); //关闭TCP连接
});
});
</script>
</body>
</html>

演示图如下:

image

如果使用该方式实现 WebSocket 功能并打包成 war 运行会报错:

1
mipsasm复制代码javax.websocket.DeploymentException: Multiple Endpoints may not be deployed to the same path

方式二:

该方式适用于 jar 包方式运行和 war 方式运行。

WebSocket 配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码@Configuration  
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketServer(), "/webSocketServer/*");
}

@Bean
public WebSocketHandler webSocketServer() {
return new WebSocketServer();
}
}

WebSocket 处理类:

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
java复制代码public class WebSocketServer extends TextWebSocketHandler {
private static final Map<WebSocketSession, String> connections = new ConcurrentHashMap<>();
private static String getDatetime(Date date) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(date);
}

/**
* 建立连接
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String uri = session.getUri().toString();
String userName = uri.substring(uri.lastIndexOf("/") + 1);

String nickname = URLDecoder.decode(userName, "utf-8");
connections.put(session, nickname);
String message = String.format("* %s %s", nickname, "加入聊天!");
broadcast(new TextMessage(message));
}
/**
* 断开连接
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String nickname = connections.remove(session);
String message = String.format("* %s %s", nickname, "退出聊天!");

broadcast(new TextMessage(message));
}
/**
* 处理消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String msg = "【" + connections.get(session) + "】" + getDatetime(new Date()) + " : " + message.getPayload();

broadcast(new TextMessage(msg));
}
private static void broadcast(TextMessage msg) {
// 广播形式发送消息
for (WebSocketSession session : connections.keySet()) {
try {
synchronized (session) {
session.sendMessage(msg);
}
} catch (Exception e) {
connections.remove(session);
try {
session.close();
} catch (Exception e2) {
e2.printStackTrace();
}
String message = String.format("* %s %s", connections.get(session), "断开连接");
broadcast(new TextMessage(message));
}
}
}
}

运行结果与上图一致。

十三、整合 Swagger2

13.1 添加依赖

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>

13.2 配置

重新创建一个配置类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
less复制代码@Configuration
@EnableSwagger2
public class Swagger2Configuration {

@Bean
public Docket accessToken() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("api")// 定义组
.select() // 选择那些路径和 api 会生成 document
.apis(RequestHandlerSelectors.basePackage("com.light.springboot.controller")) // 拦截的包路径
.paths(PathSelectors.regex("/*/.*"))// 拦截的接口路径
.build() // 创建
.apiInfo(apiInfo()); // 配置说明
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()//
.title("Spring Boot 之 Web 篇")// 标题
.description("spring boot Web 相关内容")// 描述
.termsOfServiceUrl("http://www.extlight.com")//
.contact(new Contact("moonlightL", "http://www.extlight.com", "445847261@qq.com"))// 联系
.version("1.0")// 版本
.build();
}
}

为了能更好的说明接口信息,我们还可以在 Controller 类上使用 Swagger2 相关注解说明信息。

我们以 FastJsonController 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
reasonml复制代码@Api(value = "FastJson测试", tags = { "测试接口" })
@RestController
@RequestMapping("fastjson")
public class FastJsonController {
@ApiOperation("获取用户信息")
@ApiImplicitParam(name = "name", value = "用户名", dataType = "string", paramType = "query")
@GetMapping("/test/{name}")
public User test(@PathVariable("name") String name) {
User user = new User();
user.setId(1);
user.setUsername(name);
user.setPassword("jack123");
user.setBirthday(new Date());
return user;
}
}

注意,上边的方法是用 @GetMapping 注解,如果只是使用 @RequestMapping 注解,不配置 method 属性,那么 API 文档会生成 7 种请求方式。

启动项目,打开浏览器访问 http://localhost:8080/swagger-ui.html。结果如下图:

image

本文转载自: 掘金

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

spring security免登录动态配置方案2

发表于 2017-11-27

序

之前有篇文章讲了怎么进行免登录动态配置的方案,动用了反射去实现,有点黑魔法的味道,这里再介绍另外一种方案

permitAll

spring-security-config-4.2.3.RELEASE-sources.jar!/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.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
复制代码public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
extends
AbstractInterceptUrlConfigurer<ExpressionUrlAuthorizationConfigurer<H>, H> {
static final String permitAll = "permitAll";
private static final String denyAll = "denyAll";
private static final String anonymous = "anonymous";
private static final String authenticated = "authenticated";
private static final String fullyAuthenticated = "fullyAuthenticated";
private static final String rememberMe = "rememberMe";

private final ExpressionInterceptUrlRegistry REGISTRY;

//......
/**
* Specify that URLs are allowed by anyone.
*
* @return the {@link ExpressionUrlAuthorizationConfigurer} for further
* customization
*/
public ExpressionInterceptUrlRegistry permitAll() {
return access(permitAll);
}

public ExpressionInterceptUrlRegistry access(String attribute) {
if (not) {
attribute = "!" + attribute;
}
interceptUrl(requestMatchers, SecurityConfig.createList(attribute));
return ExpressionUrlAuthorizationConfigurer.this.REGISTRY;
}

private void interceptUrl(Iterable<? extends RequestMatcher> requestMatchers,
Collection<ConfigAttribute> configAttributes) {
for (RequestMatcher requestMatcher : requestMatchers) {
REGISTRY.addMapping(new AbstractConfigAttributeRequestMatcherRegistry.UrlMapping(
requestMatcher, configAttributes));
}
}
}

permitAll操作将“permitAll”这个attribute以及对应的requestMatchers添加到REGISTRY

思路

1
2
3
4
5
6
7
8
9
10
复制代码@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**", "/js/**","/fonts/**").permitAll()
.anyRequest().authenticated();
}
}

这里重点注意这个anyRequest().authenticated(),可以看到没有配置permitAll的请求,都要求authenticated这个级别的,而AnonymousAuthenticationFilter设置的匿名级别只是anonymous。

于是我们的思路就来了,新建一个filter,插入在AnonymousAuthenticationFilter之前,对于免登录的设置为authenticated

DemoFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
复制代码public class DemoFilter extends GenericFilterBean {

private Object principal = "annoUser";

private List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_ANNO");

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
private final Map<String,HttpMethod[]> annoPatternMap = new HashMap<String,HttpMethod[]>(){{
//for demo, you can change it and read from db or else
put("/index/demo",new HttpMethod[]{HttpMethod.GET});
}};

String uri = ((HttpServletRequest) servletRequest).getRequestURI();
if(annoPatternMap.containsKey(uri)){
if(SecurityContextHolder.getContext().getAuthentication() == null){
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) servletRequest));
}
}else{
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println(auth == null);
if(auth != null && auth instanceof UsernamePasswordAuthenticationToken){
if(principal.toString().equals(auth.getPrincipal().toString())){
SecurityContextHolder.getContext().setAuthentication(null);
}
}
}
filterChain.doFilter(servletRequest, servletResponse);
}

protected Authentication createAuthentication(HttpServletRequest request) {
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
principal, "N/A", authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));

return auth;
}
}

这里创建了一个伪造的UsernamePasswordAuthenticationToken

这里有一点要注意一下,就在判断不是配置的允许匿名访问的url的时候,如果之前的token是我们设置的,则需要重新清空,防止一旦访问匿名url之后获取session再去越权访问其他没有配置的url。

配置filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(new DemoFilter(),AnonymousAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/css/**", "/js/**","/fonts/**").permitAll()
.anyRequest().authenticated();
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("admin").roles("USER");
}
}

在AnonymousAuthenticationFilter之前提前设置好SecurityContextHolder里头的authentication。

小结

这样基本就大功告成了,不过有几点需要注意:

  • 自定义的filter,可能存在执行两遍的问题,这点后面的文章来讲
  • 获取到的uri无法处理pathvariable的情况,需要根据url pattern来处理,这点后面再讲述一下

doc

  • spring security运行时配置ignore url

本文转载自: 掘金

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

解决spring security自定义filter重复执行

发表于 2017-11-27

序

本文讲一个spring security自定义filter非常容易出现的一个问题,那就是filter被执行两遍。

复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public DemoFilter demoFilter(){
return new DemoFilter();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(demoFilter(),AnonymousAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/login","/css/**", "/js/**","/fonts/**").permitAll()
.anyRequest().authenticated();
}
}

其中DemoFilter如下

1
2
3
4
5
6
7
8
复制代码public class DemoFilter extends GenericFilterBean {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//do something
filterChain.doFilter(servletRequest, servletResponse);
}
}

原因

在spring容器托管的GenericFilterBean的bean,都会自动加入到servlet的filter chain,而上面的定义,还额外把filter加入到了spring security的
AnonymousAuthenticationFilter之前。而spring security也是一系列的filter,在mvc的filter之前执行。因此在鉴权通过的情况下,就会先后各执行一次。

解决方案

方案1

不把filter托管给spring,直接new,比如

1
2
3
4
5
6
7
8
9
10
11
复制代码@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(new DemoFilter(),AnonymousAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/login","/css/**", "/js/**","/fonts/**").permitAll()
.anyRequest().authenticated();
}
}

方案2

有时候filter需要访问spring容器的资源,托管给容器可能好些,那么这个时候,就可以像FilterSecurityInterceptor做个标记FILTER_APPLIED

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class DemoFilter extends GenericFilterBean {

private static final String FILTER_APPLIED = "__spring_security_demoFilter_filterApplied";

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest.getAttribute(FILTER_APPLIED) != null) {
filterChain.doFilter(servletRequest, servletResponse);
return ;
}
//do something
servletRequest.setAttribute(FILTER_APPLIED,true);
filterChain.doFilter(servletRequest, servletResponse);
}
}

doc

  • Spring security custom filter called multiple times

本文转载自: 掘金

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

记使用Kotlin进行服务端开发所遇到的问题(一)

发表于 2017-11-27

以下是在使用Kotlin进行服务端开发以来所遇到的问题记录。

1. fastjson不能正确的对数据类(data class)进行序列化及反序列化。

首先先来看下相关的代码:

1
2
3
4
复制代码data class User(
val id: Long?,
val username: String
)

如上的一个实体类,在使用fastjson进行序列化时,会报找不到无参构造方法的错误。

原因:
Kotlin的语法中User之后的大括号表示User类的主构造方法,同时data修饰的class必须在主构造方法中定义所有的属性字段,因此编译过后只会存在指定参数的构造方法。

解决方法:

  1. 修改数据类的定义,对每个属性字段设置默认值(不推荐)
1
2
3
4
复制代码data class User(
val id: Long? = null,
val username: String = ""
)

利用Kotlin通过支持参数默认值来实现方法重载的特性使该类在进行序列化时可以找到重载的无参构造方法。

  1. 使用1.2.36+版本的fastjson(推荐)
1
2
3
4
5
复制代码<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.36</version>
</dependency>
  1. Kotlin编译插件中开启noarg的支持(推荐)
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
复制代码<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<plugin>no-arg</plugin>
</compilerPlugins>
<jvmTarget>1.8</jvmTarget>
<experimentalCoroutines>enable</experimentalCoroutines>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
2. Mybatis/JPA中实体类使用数据类(data class)定义时,会报找不到无参构造方法的错误。

原因:
同上一个问题。

解决方法:

  1. 修改数据类的定义,对每个属性字段设置默认值(不推荐)
1
2
3
4
复制代码data class User(
val id: Long? = null,
val username: String = ""
)
  1. Kotlin编译插件中开启noarg的支持(推荐)
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
复制代码<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<plugin>no-arg</plugin>
</compilerPlugins>
<jvmTarget>1.8</jvmTarget>
<experimentalCoroutines>enable</experimentalCoroutines>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
3. SpringMVC中validator的注解不生效的问题。

通常在Java Web应用中,使用SpringMVC支持的参数验证器的步骤为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码//DTO
public class LoginDTO {
@Size(min=6, max=12, message="{username.condition}")
private String username;
@Size(min=6, max=12, message="{password.condition}")
private String password;
// getter and setter
}

// Request Handle Method
...
public ResponseEntity login(@Valiadted LoginDTO loginDTO,
BindingResult result) {
}
...

在表现层的数据传输对象中使用validtor相关的约束注解标注字段的校验条件,同时在controller的请求处理方法参数上使用@Validated注解标注要进行校验的参数,这样在配置好校验器之后,SpringMVC就能自动的在请求参数解析时进行参数校验,从而提高请求处理的健壮性。 但将上述代码转换成Kotlin后, 其数据转换对象如下:

1
2
3
4
复制代码data class LoginDTO(
val username: String,
val password: String
)

这时使用validator的约束注解进行标注如下:

1
2
3
4
5
6
复制代码data class LoginDTO(
@Size(min=6, max=12, message="{username.condition}")
val username: String,
@Size(min=6, max=12, message="{password.condition}")
val password: String
)

再启动程序进行请求,会发现参数校验失效。通过断点调试会发现,在进行校验时,校验器无法获取到字段上的约束集合,从而导致参数校验的失效。

原因:
在Java中validator的约束注解实际上是作用于属性的getter方法,而在Kotlin的数据类中所有的属性均为不可变属性,且必须在初始化时指定或在构造方法中设置默认值,因此数据类作为一个安全的数据承载对象,可以直接使用.属性名的方式去访问属性的值,因而也不存在getter方法,最终导致了约束注解的失效。

解决方法:
使用@field:修饰约束注解,可使之生效

1
2
3
4
5
6
复制代码data class LoginDTO(
@field:Size(min=6, max=12, message="{username.condition}")
val username: String,
@field:Size(min=6, max=12, message="{password.condition}")
val password: String,
)
4. Spring Bean注入的问题。

通常我们在Java Web应用中使用Spring时,最常用的两种注入方式1. 构造器注入;2. 属性值注入,例如:

1
2
3
4
5
6
7
8
9
10
复制代码@Service
public class UserServcieImpl implements UserService {
@Autowired
private UserRepo userRepo;
private MessageSource messageSource;

public UserServiceImpl(MessageSource messageSource) {
this.messgeSource = messageSource
}
}

而在Kotlin中,由于类的定义方式的改变,通常更倾向于使用构造器注入的方式,如:

1
2
3
4
5
复制代码@Service
class UserServiceImpl(
val userRepo: UserRepo,
val messageSource: MessageSource
): UserService

同时Kotlin中也支持属性值注入:

1
2
3
4
5
复制代码@Service
class userServiceImpl: UserService {
@Autowired
lateinit var userRepo: UserRepo
}

但是在使用Kotlin编写的Junit单元测试类中,将只能使用属性值注入的方式:

1
2
3
4
5
6
7
复制代码class UserServiceTest : ServiceTest() {

@Autowired
lateinit var userRepo: UserRepo

@Test
fun baseJPAQueryTest() {}

因为Junit测试类要求,测试类不能有含参的构造方法。


未完待续。。。

PS:

  1. 感谢您的阅读,希望我的记录对您有所帮助。
  2. 欢迎各种提问,在相互探讨中共同提高。
  3. 如果筒子们有遇到过其他的问题,也欢迎留言给我提供研究的素材。

本文转载自: 掘金

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

POI读取文件的最佳实践

发表于 2017-11-27

POI是 Apache 旗下一款读写微软家文档声名显赫的类库。应该很多人在做报表的导出,或者创建 word 文档以及读取之类的都是用过 POI。POI 也的确对于这些操作带来很大的遍历性。我最近做的一个工具就是读取计算机中的 word 以及 excel 文件。下面我就两方面讲解以下遇到的一些坑:

word 篇

对于 word 文件,我需要的就是提取文件中正文的文字。所以可以创建一个方法来读取 doc 或者 docx 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码    private static String readDoc(String filePath, InputStream is) {
String text= "";
try {
if (filePath.endsWith("doc")) {
WordExtractor ex = new WordExtractor(is);
text = ex.getText();
ex.close();
is.close();
} else if(filePath.endsWith("docx")) {
XWPFDocument doc = new XWPFDocument(is);
XWPFWordExtractor extractor = new XWPFWordExtractor(doc);
text = extractor.getText();
extractor.close();
is.close();
}
} catch (Exception e) {
logger.error(filePath, e);
} finally {
if (is != null) {
is.close();
}
}
return text;
}

理论上来说,这段代码应该对于读取大多数 doc 或者 docx 文件都是有效的。但是!!!!我发现了一个奇怪的问题,就是我的代码在读取某些 doc 文件的时候,经常会给出这样的一个异常:

1
复制代码org.apache.poi.poifs.filesystem.OfficeXmlFileException: The supplied data appears to be in the Office 2007+ XML. You are calling the part of POI that deals with OLE2 Office Documents.

这个异常的意思是什么呢,通俗的来讲,就是你打开的文件并不是一个 doc 文件,你应该使用读取 docx 的方法去读取。但是我们明明打开的就是一个后缀是 doc 的文件啊!

其实 doc 和 docx 的本质不同的,doc 是 OLE2 类型,而 docx 而是 OOXML 类型。如果你用压缩文件打开一个 docx 文件,你会发现一些文件夹:

本质上 docx 文件就是一个 zip 文件,里面包含了一些 xml 文件。所以,一些 docx 文件虽然大小不大,但是其内部的 xml 文件确实比较大的,这也是为什么在读取某些看起来不是很大的 docx 文件的时候却耗费了大量的内存。

然后我使用压缩文件打开这个 doc 文件,果不其然,其内部正是如上图,所以本质上我们可以认为它是一个 docx 文件。可能是因为它是以某种兼容模式保存从而导致如此坑爹的问题。所以,现在我们根据后缀名来判断一个文件是 doc 或者 docx 就是不可靠的了。

老实说,我觉得这应该不是一个很少见的问题。但是我在谷歌上并没有找到任何关于此的信息。how to know whether a file is .docx or .doc format from Apache POI 这个例子是通过 ZipInputStream
来判断文件是否是 docx 文件:

1
复制代码boolean isZip = new ZipInputStream( fileStream ).getNextEntry() != null;

但我并不觉得这是一个很好的方法,因为我得去构建一个ZipInpuStream,这很显然不好。另外,这个操作貌似会影响到 InputStream,所以你在读取正常的 doc 文件会有问题。或者你使用 File 对象去判断是否是一个 zip 文件。但这也不是一个好方法,因为我还需要在压缩文件中读取 doc 或者 docx 文件,所以我的输入必须是 Inputstream,所以这个选项也是不可以的。 我在 stackoverflow 上和一帮老外扯了大半天,有时候我真的很怀疑这帮老外的理解能力,不过最终还是有一个大佬给出了一个让我欣喜若狂的解决方案,
FileMagic。这个是一个 POI 3.17新增加的一个特性:

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
复制代码public enum FileMagic {
/** OLE2 / BIFF8+ stream used for Office 97 and higher documents */
OLE2(HeaderBlockConstants._signature),
/** OOXML / ZIP stream */
OOXML(OOXML_FILE_HEADER),
/** XML file */
XML(RAW_XML_FILE_HEADER),
/** BIFF2 raw stream - for Excel 2 */
BIFF2(new byte[]{
0x09, 0x00, // sid=0x0009
0x04, 0x00, // size=0x0004
0x00, 0x00, // unused
0x70, 0x00 // 0x70 = multiple values
}),
/** BIFF3 raw stream - for Excel 3 */
BIFF3(new byte[]{
0x09, 0x02, // sid=0x0209
0x06, 0x00, // size=0x0006
0x00, 0x00, // unused
0x70, 0x00 // 0x70 = multiple values
}),
/** BIFF4 raw stream - for Excel 4 */
BIFF4(new byte[]{
0x09, 0x04, // sid=0x0409
0x06, 0x00, // size=0x0006
0x00, 0x00, // unused
0x70, 0x00 // 0x70 = multiple values
},new byte[]{
0x09, 0x04, // sid=0x0409
0x06, 0x00, // size=0x0006
0x00, 0x00, // unused
0x00, 0x01
}),
/** Old MS Write raw stream */
MSWRITE(
new byte[]{0x31, (byte)0xbe, 0x00, 0x00 },
new byte[]{0x32, (byte)0xbe, 0x00, 0x00 }),
/** RTF document */
RTF("{\\rtf"),
/** PDF document */
PDF("%PDF"),
// keep UNKNOWN always as last enum!
/** UNKNOWN magic */
UNKNOWN(new byte[0]);

final byte[][] magic;

FileMagic(long magic) {
this.magic = new byte[1][8];
LittleEndian.putLong(this.magic[0], 0, magic);
}

FileMagic(byte[]... magic) {
this.magic = magic;
}

FileMagic(String magic) {
this(magic.getBytes(LocaleUtil.CHARSET_1252));
}

public static FileMagic valueOf(byte[] magic) {
for (FileMagic fm : values()) {
int i=0;
boolean found = true;
for (byte[] ma : fm.magic) {
for (byte m : ma) {
byte d = magic[i++];
if (!(d == m || (m == 0x70 && (d == 0x10 || d == 0x20 || d == 0x40)))) {
found = false;
break;
}
}
if (found) {
return fm;
}
}
}
return UNKNOWN;
}

/**
* Get the file magic of the supplied InputStream (which MUST
* support mark and reset).<p>
*
* If unsure if your InputStream does support mark / reset,
* use {@link #prepareToCheckMagic(InputStream)} to wrap it and make
* sure to always use that, and not the original!<p>
*
* Even if this method returns {@link FileMagic#UNKNOWN} it could potentially mean,
* that the ZIP stream has leading junk bytes
*
* @param inp An InputStream which supports either mark/reset
*/
public static FileMagic valueOf(InputStream inp) throws IOException {
if (!inp.markSupported()) {
throw new IOException("getFileMagic() only operates on streams which support mark(int)");
}

// Grab the first 8 bytes
byte[] data = IOUtils.peekFirst8Bytes(inp);

return FileMagic.valueOf(data);
}


/**
* Checks if an {@link InputStream} can be reseted (i.e. used for checking the header magic) and wraps it if not
*
* @param stream stream to be checked for wrapping
* @return a mark enabled stream
*/
public static InputStream prepareToCheckMagic(InputStream stream) {
if (stream.markSupported()) {
return stream;
}
// we used to process the data via a PushbackInputStream, but user code could provide a too small one
// so we use a BufferedInputStream instead now
return new BufferedInputStream(stream);
}
}

在这给出主要的代码,其主要就是根据 InputStream 前 8 个字节来判断文件的类型,毫无以为这就是最优雅的解决方式。一开始,其实我也是在想对于压缩文件的前几个字节似乎是由不同的定义的,magicmumber。因为 FileMagic 的依赖和3.16 版本是兼容的,所以我只需要加入这个类就可以了,因此我们现在读取 word 文件的正确做法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码    private static String readDoc (String filePath, InputStream is) {
String text= "";
is = FileMagic.prepareToCheckMagic(is);
try {
if (FileMagic.valueOf(is) == FileMagic.OLE2) {
WordExtractor ex = new WordExtractor(is);
text = ex.getText();
ex.close();
} else if(FileMagic.valueOf(is) == FileMagic.OOXML) {
XWPFDocument doc = new XWPFDocument(is);
XWPFWordExtractor extractor = new XWPFWordExtractor(doc);
text = extractor.getText();
extractor.close();
}
} catch (Exception e) {
logger.error("for file " + filePath, e);
} finally {
if (is != null) {
is.close();
}
}
return text;
}

excel 篇

对于 excel 篇,我也就不去找之前的方案和现在的方案的对比了。就给出我现在的最佳做法了:

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
复制代码    @SuppressWarnings("deprecation" )
private static String readExcel(String filePath, InputStream inp) throws Exception {
Workbook wb;
StringBuilder sb = new StringBuilder();
try {
if (filePath.endsWith(".xls")) {
wb = new HSSFWorkbook(inp);
} else {
wb = StreamingReader.builder()
.rowCacheSize(1000) // number of rows to keep in memory (defaults to 10)
.bufferSize(4096) // buffer size to use when reading InputStream to file (defaults to 1024)
.open(inp); // InputStream or File for XLSX file (required)
}
sb = readSheet(wb, sb, filePath.endsWith(".xls"));
wb.close();
} catch (OLE2NotOfficeXmlFileException e) {
logger.error(filePath, e);
} finally {
if (inp != null) {
inp.close();
}
}
return sb.toString();
}

private static String readExcelByFile(String filepath, File file) {
Workbook wb;
StringBuilder sb = new StringBuilder();
try {
if (filepath.endsWith(".xls")) {
wb = WorkbookFactory.create(file);
} else {
wb = StreamingReader.builder()
.rowCacheSize(1000) // number of rows to keep in memory (defaults to 10)
.bufferSize(4096) // buffer size to use when reading InputStream to file (defaults to 1024)
.open(file); // InputStream or File for XLSX file (required)
}
sb = readSheet(wb, sb, filepath.endsWith(".xls"));
wb.close();
} catch (Exception e) {
logger.error(filepath, e);
}
return sb.toString();
}

private static StringBuilder readSheet(Workbook wb, StringBuilder sb, boolean isXls) throws Exception {
for (Sheet sheet: wb) {
for (Row r: sheet) {
for (Cell cell: r) {
if (cell.getCellType() == Cell.CELL_TYPE_STRING) {
sb.append(cell.getStringCellValue());
sb.append(" ");
} else if (cell.getCellType() == Cell.CELL_TYPE_NUMERIC) {
if (isXls) {
DataFormatter formatter = new DataFormatter();
sb.append(formatter.formatCellValue(cell));
} else {
sb.append(cell.getStringCellValue());
}
sb.append(" ");
}
}
}
}
return sb;
}

其实,对于 excel 读取,我的工具面临的最大问题就是内存溢出。经常在读取某些特别大的 excel 文件的时候都会带来一个内存溢出的问题。后来我终于找到一个优秀的工具 excel-streaming-reader,它可以流式的读取 xlsx 文件,将一些特别大的文件拆分成小的文件去读。

另外一个做的优化就是,对于可以使用 File 对象的场景下,我是去使用 File 对象去读取文件而不是使用 InputStream 去读取,因为使用 InputStream 需要把它全部加载到内存中,所以这样是非常占用内存的。

最后,我的一点小技巧就是使用 cell.getCellType 去减少一些数据量,因为我只需要获取一些文字以及数字的字符串内容就可以了。

以上,就是我在使用 POI 读取文件的一些探索和发现,希望对你能有所帮助。上面的这些例子也是在我的一款工具 everywhere 中的应用(这款工具主要是可以帮助你在电脑中进行内容的全文搜索),感兴趣的可以看看,欢迎 star 或者 pr。

本文转载自: 掘金

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

aria2 及 DLNA 服务 Docker 部署 -(3)

发表于 2017-11-27

tv

前两篇谈到了 aria2 服务,Web 页面,以及从百度云下载和 BT 种子 tracker list 的更新。这次我们说说下载文件的多终端共享。

Samba

说到文件共享,最先想到的 Samba。Samba 是一款实现 SMB(Server Messages Block)协议的免费软件,用来为局域网的不同操作系统的计算机提供文件及打印机等资源的共享服务。我开始就是用 Samba 用来共享下载下来的视频,并且简单的设置了目录和用户权限,用户密码等基本配置。使用了 joebiellik/samba-server 这个镜像,很容易就实现了局域网文件共享。

docker-compose.yml 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码version: '3.1'
services:
samba:
image: joebiellik/samba-server:latest
container_name: samba
restart: always
network_mode: 'host'
volumes:
- ./data:/mnt
- ./conf.d/smb.conf:/config/smb.conf
- /etc/localtime:/etc/localtime:ro
environment:
- USERNAME=samba
- PASSWORD=samba
logging:
driver: "json-file"
options:
max-size: "1m"

局域网内的手机,电视和 iPad 很容易就搜到了这个服务,并且浏览和播放视频资源。用了一段时间,感觉还可以,平常都播放一些几百兆不是很大的美剧,没觉得什么问题。但是后来下载一些几个 G 的高清电影的时候,比较吃力了,基本上加载不动,感觉可能就需要流服务了。🤗

MiniDLNA

MiniDLNA 是一个服务端软件,旨在为兼容 DLNA/UPnP 的客户端提供媒体文件(音乐、图片和视频)。MiniDLNA 功能简单,轻量级,没有管理页面来配置和管理,只能用配置文件来配置。并且不能像 Samba
一样配置用户名和密码等,设置访问权限。但是对我们来说,已经足够了。

MiniDLNA 使用的是 vladgh/minidlna 这个镜像,配置简单,docker-compose.yml 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码version: '3.1'
services:
minidlna:
image: vladgh/minidlna
container_name: minidlna
restart: always
network_mode: 'host'
environment:
- MINIDLNA_MEDIA_DIR=/media
- MINIDLNA_FRIENDLY_NAME=MiniDLNA
- MINIDLNA_INOTIFY=yes
- MINIDLNA_NOTIFY_INTERVAL=3
volumes:
- ./cache/minidlna:/var/lib/minidlna
- ./data:/media
logging:
driver: "json-file"
options:
max-size: "1m"

其中 network_mode 设置的 host ,一定要设定为 host ,如果以 bridge 的方式,会出一些网络相关的问题。inotify 配置为 yes ,是使用 inotify 服务,发现文件变更的时候,会刷新 MiniDLNA 数据库;如果设置为 no 的话,有新下载下来的文件的时候,还得手动重启服务才能更新数据库,较为麻烦。然后打开防火墙的
1900/udp 和 8200/tcp 端口。

刚开始配置完,启动服务,电视上使用的 Moli Player 立即就搜索到了 MiniDLNA 服务,并且可以浏览目录,播放视频文件,速度飞快,快进无感。但是过了一会儿,就再也找不到 MiniDLNA 这个服务了。

然后搜索问题,看到官方 Troubleshooting 里面有写:

Server not visible on Wireless behind a router

On some network configurations when the machine hosting MiniDLNA server is connected to the router through Ethernet, there may be problems accessing MiniDLNA server on WiFi (same router). To solve this, make sure that “Multicast Isolation” is
turned off on the router. For example, on ADB / Pirelli P.RG EA4202N router, connect to the configuration page, then Settings->Bridge and VLAN->Bridge List->click edit on Bridge Ethernet WiFi->set Multicast Isolation to No->Apply.

哦,看来是无线路由器的问题,无线路由器的多播隔离,影响到了 MiniDLNA 的多播。但是一想,也不对啊,服务刚启动的时候,是能发现的,是过了一会儿,才搜索不到服务的。如果是这个问题的话,一开始就不可能发现服务。

接着 Troubleshooting 往下看:

DLNA server stops being visible after some time when

being shared on a bridge device

If you are using ReadyMedia to “broadcast” on a bridged device (such as an OpenVPN device bridged to an Ethernet device), the server may stop being seen by the clients after some time (which may vary from a few seconds to half a day). In order
to solve this you need to disable ‘multicast snooping’. You can do it instantly with the following command:

1
2
3
> 复制代码# echo 0 >> /sys/devices/virtual/net/br0/bridge/multicast_snooping
>
>

这个标题描述的很对症啊,但是细看描述,又不太像。前面我们着重说了 network_mode 使用的是 host 模式,相当于在宿主机上直接跑的进程,并没有涉及到桥接设备,看来也不是这个原因。那该如何是好?😓

启动服务前,我们配置防火墙打开了 1900/udp 端口,这个端口就是 SSDP 的服务端口,MiniDLNA 就是使用了 SSDP 协议提供服务的发现。那解决问题前,我们先了解下 SSDP 的协议的实现:

按照协议的规定,当一个控制点(客户端)接入网络的时候,它可以向一个特定的多播地址的 SSDP 端口使用 M-SEARCH 方法发送 “ssdp:discover” 消息。当设备监听到这个保留的多播地址上由控制点发送的消息的时候,设备会分析控制点请求的服务,如果自身提供了控制点请求的服务,设备将通过单播的方式直接响应控制点的请求。

类似的,当一个设备接入网络的时候,它应当向一个特定的多播地址的 SSDP 端口使用 NOTIFY 方法发送 “ssdp:alive” 消息。控制点根据自己的策略,处理监听到的消息。考虑到设备可能在没有通知的情况下停止服务或者从网络上卸载,“ssdp:alive” 消息必须在 HTTP 协议头 CACHE-CONTROL 里面指定超时值,设备必须在约定的超时值到达以前重发 “ssdp:alive” 消息。如果控制点在指定的超时值内没有再次收到设备发送的 “ssdp:alive” 消息,控制点将认为设备已经失效。

当一个设备计划从网络上卸载的时候,它也应当向一个特定的多播地址的 SSDP 端口使用 NOTIFY 方法发送 “ssdp:byebye” 消息。但是,即使没有发送 “ssdp:byebye” 消息,控制点也会根据 “ssdp:alive” 消息指定的超时值,将超时并且没有再次收到的 “ssdp:alive” 消息对应的设备认为是失效的设备。

通过上面的描述可知,我们的 MiniDLNA 在刚接入网络的时候,NOTIFY 发送的 “ssdp:alive” 消息是成功的,并且客户端也是通过 “ssdp:discover” 消息发现了 MiniDLNA。但是后续的不清楚是 MiniDLNA 在超时值之前没有重发 “ssdp:alive” 消息,还是由于 Troubleshotting 里面所说的网络问题,客户端没有接到来自 MiniDLNA 的存活消息。但是不管怎么样,先能用了再说。

notify_interval 是用来配置 MiniDLNA 服务的 SSDP NOTIFY 时间间隔,默认时间是 895 秒。那我减小这个间隔,在超时值范围内就可以被客户端一直显示这个共享服务的存在。这里我配置了 3s,虽然有点小,但是可以接受(对路由器的网络拥堵影响不是很大)。基本上在客户端可以一直看到 MiniDLNA 的存在,问题算是从表面上解决了。后面有时间再研究下具体原因。

玩得开心 🤠

参考链接

  1. ReadyMedia
  2. Samba
  3. MiniDLNA
  4. minidlna.conf
  5. ReadyMedia Repo

本文转载自: 掘金

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

我们需要什么样的主存储

发表于 2017-11-27

解决什么问题

数据库的问题从来就不是数据库自身的问题。从能存取数据的角度,所有的数据库都是合格的。关键的问题是,我们用数据库去满足什么样的业务需求。不同的数据模型,会对上层的业务代码的风格产生非常深远的影响。从开发效率,开发体验的角度,从运行时性能的角度,从数据正确性的角度,从数据安全性的角度,可以解读出非常多元和丰富的信息。对一个数据库来说不能简单地用好或者不好来评价。另外过去的经验也会成为一种束缚,从解决的业务问题本身,到实际使用MySQL,中间是有很多种可能的。当我们习惯了一种现有的开发风格之后,思路很容易被限制在原有的领域里。如果仅仅从问题的本质出发去推导一个解决方案,会发现更广阔的可能性。


所有的信息系统的起点都是数据的写入。无论是一个电子商务系统,需要交易多方进行信息的录入。还是一个简单的内容管理系统需要编辑上传内容。所有的业务系统的起点必然是交易数据的产生或者数据的录入。所以我们会有一个类型的存储叫“主存储”,它保存了source system of record。

主存储的作用是协作的沟通桥梁。多个用户角色在一个系统上进行协作,通过把过去达成的协议记录成一条主存储的记录,从而影响后续的行为的履约。我们可以把主存储的数据想象成线下多方交易里的纸质单据。做一次存储就是相当于线下一次合同的签署。这个步骤签署的协议,决定了后续多方的行为。这里有两种细微地不同地主存储:

  • 交易发生在线下:比如线下已经签约了合同了。线上的电子化档案只是一份信息的拷贝。这个时候过去发生了什么,就应该是什么。扩大一点范围,对自然现象的采集,比如温度传感器,也是类似的。某时某分是多少度,这个是自然发生的,采集虽然是数字化的第一手数据,但是采集本身并不能决定这个值是什么。
  • 线上交易:签约的行为放在了线上。比如审批一个用户是否合法,比如发单。这些操作是否能够通过,也就是操作的结果是由线上系统来决定的。这种人与人之间,人与平台(其实也是人,只是平台代表了具体的人格)之间完全通过线上进行协作,以数字化记录的方式签订“合同”的方式,对于主存储就提出了非常高的要求。

主存储的职责


所以我们可以归纳出,如果主存储要支持好用户们在线上进行协议,需要具备四个基本职责:

  • 支持线上交易,主要的体现就是强制执行业务规则,让参与协作的主体对接下来要发生的事情达成一致,用于指导后续的行为(线上或者线下的行为)。简单的可能只是一个非空的校验,复杂的牵涉到并发情况下状态机的约束保证。
  • 处理并发:这个其实还是强制执行业务规则,只是实现难度更高一点,被单独提出来。当多人同时操作的时候,我们要做到言而有信。答应了的事情(比如请求的响应是success),就要做到(比如实际的数据库里的记录不对)。这种不一致,可以理解为商业上的违约行为。
  • 保存过去真实发生的事情,保存多方什么时候达成过什么样的协议。这个主要体现为单据可以查询上。甚至是可以按历史时间进行回溯查询。
  • 做为其他视图的数据源。比如搜索,可以查询到商家上架的商品。并不代表商品上架要直接写入到搜索引擎里,而是先做为一个商业上的契约落到主存储里。再做为数据源复制到搜索引擎。当然如果主存储也能支持各种各样的检索,承担部分视图的功能也是可以的。但是能够支持与其他存储系统集成,应该是主存储的一个基本职责。

接下来我们以扣费业务为例,来看一下不同的数据库当主存储这个角色的时候,遇到的一些挑战

MySQL

扣费的业务需求如下:

  • 扣费不能把余额扣成负数
  • 同一个单号的扣费不能重复扣。第一次扣成功了,第二次再扣还是返回成功,但是余额不再变化。
  • 要能够让用户查看账户的今日不同类型交易引起的余额变化情况
  • 扣费不单单是直接把钱扣了,得记录为什么订单扣的等其他信息,以用于后续流程的开展,以及对账需求。即使帐这边不记录,订单系统那边也得记录,要不然流程串 不起来。

MySQL 在处理这些需求的时候,遇到哪些挑战呢?

  • 并发修改余额的时候,要保证余额不变成负数。需要把业务逻辑写成SQL,通过单条SQL的原子性来保证并发下的“规则检查”与“实际扣费”这两个操作是原子的。也就是需要把业务逻辑下推到数据库去做。然后数据库的查询语言的能力就限制了业务逻辑的表达能力了。
  • 同一个单号不能重复扣,这个就必须把扣款单给保存起来了。但是余额也要存啊,因为前面还有第一条的业务规则摆在那呢。当需要既更新余额,又保存扣款单的情况下,怎么保证这两个的更新是原子的呢?就必须使用mysql的多表事务了,一方面降低了吞吐,另外一方面也给分库分表带来了挑战。
  • 需要统计今日的余额变化,还要分类型,这个拿主表来做显然是不合适的。但是引入其他的存储去做实时聚合,又会有数据库之间的同步机制问题,可靠性问题和延迟问题。
  • 因为扣费单不仅仅是有扣费相关的字段,还得附加其他的信息,这些信息和上游的交易流程还有关系。这样三天两头就要加字段。而每次加字段都要在半夜避开业务高峰期进行。当分库分表比较多的时候,需要加字段的实例多了,这个过程就更加漫长。导致新需求轻易是不加字段,能用“其他存储”解决就用“其他存储”解决。这样就会导致很多应该进主存储的信息,被放到了其他的地方。给数据的一致性,安全性和可靠性都带来了风险。

另外因为MySQL提供了开放性的读写接口。为了保证数据安全,一般还要再封装一层数据服务。让底层数据库只能由数据服务读写(比如ip白名单),然后把业务规则写到这个数据服务里。再由这个数据服务去提供给更多样化的业务系统使用。这种数据服务的封装往往引起了重复代码,以及多了一层延迟。但是开发者相比存储过程更喜欢自己封装一层。因为数据服务自身无状态,方便扩容。另外可以拿任意自己喜欢的语言来写,不一定要用SQL。

MongoDB

和 MySQL 的情况是非常类似的

  • 余额不能为负通过把操作下推到db来解决。需要组合findAndModify与$inc来完成。同样需要使用数据库的查询语言来表达业务逻辑。同时因为MongoDB相对小众,使得这个限制变得更加痛苦。
  • 不能重复扣这个MongoDB里没有直接的解法,因为不支持多行事务。如果直接把流水和余额存到一个document里来保证一致性,在流水不断增长的情况就会超过上限。
  • 数据同步方面MongoDB还不如MySQL。MySQL基于binlog,canal和kafka也算有一套相对完善的开源方案。相比之下基于 oplog 的方案没有那么成熟。
  • 加字段方面,MongoDB比MySQL强。因为MySQL改表结构要重写已有的行,而MongoDB加字段不需要动到现有的数据。

本文转载自: 掘金

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

JVM菜鸟进阶高手之路十四:分析篇

发表于 2017-11-27

转载请注明原创出处,谢谢!

题目回顾

JVM菜鸟进阶高手之路十三,问题现象就是相同的代码,jvm参数不一样,表现的现象不一样。

1
2
3
4
5
6
7
8
9
复制代码private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception {
byte[] all1 = new byte[2 * _1MB];
byte[] all2 = new byte[2 * _1MB];
byte[] all3 = new byte[2 * _1MB];
byte[] all4 = new byte[7 * _1MB];
System.in.read();
}

jvm参数配置如下:

1
2
3
4
5
6
7
复制代码-Xmx20m
-Xms20m
-Xmn10m
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=75

通过jstat命令,查看结果如下:

关于jstat命令详情可以参考:
docs.oracle.com/javase/8/do…
jvm参数调整如下:

1
2
3
4
复制代码-Xmx20m
-Xms20m
-Xmn10m
-XX:+UseParNewGC

通过jstat命令,查看结果如下:

说明

上面的题目仅仅是一个切入点而已,希望通过一个切入点把jvm的一些基础知识刚刚好说明下,顺便解答下上面的现象。

内存相关简单说明

自己画的图,见谅!

自己画的图,见谅!

图中参数:
-Xms设置最小堆空间大小(一般建议和-Xmx一样)。
-Xmx设置最大堆空间大小。
-Xmn设置新生代大小。
-XX:MetaspaceSize设置最小元数据空间大小。
-XX:MaxMetaspaceSize设置最大元数据空间大小。
-Xss设置每个线程的堆栈大小(这里有个故事,3年前用正则表达式,后续有空正则表达式再说)。

备注:tenured空间就用减法操作即可明白,堆空间大小减去年轻代大小就可以了。

说到这里,下面这个几个参数应该明白了。

1
2
3
复制代码-Xmx20m
-Xms20m
-Xmn10m

备注:参数-XX:SurvivorRatio用来表示s0、s1、eden之间的比例,默认情况下-XX:SurvivorRatio=8表示 s0:s1:eden=1:1:8。

得出结论:eden=8M,s0=1M,s1=1M,tenured=10M。

JVM垃圾回收期组合

还有一个问题需要解决,jvm垃圾回收器方面,下面这个图,我是我的JVM菜鸟进阶高手之路八(一些细节),里面的,当时依稀记得这个图应该是飞哥发给我的。

JVM垃圾回收期组合

JVM垃圾回收期组合

由于那个时候jdk9还没有出来,可以去看看我的JVM菜鸟进阶高手之路十二(jdk9、JVM方面变化, 蹭热度),虽然有些有些稍微去掉了,但是整体的组合还是影响不大。

取截取于JVM菜鸟进阶高手之路十二

取截取于JVM菜鸟进阶高手之路十二

由于上面的2个jvm参数都是基于分代收集算法的(先不考虑G1)

  • 依据对象的存活周期进行分为新生代,老年代。
  • 根据不同代的特点,选取合适的收集算法
    • 新生代,适合复制算法
    • 老年代,适合标记清理或者标记压缩

复制算法

  • 将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
  • 不适用于存活对象较多的场合 如老年代。(年轻代对象基本都是朝生夕灭所以特别适合,由于那样的话复制就少,如果类似老年代有大量存活对象,那么进行复制算法性能就不是特别好了)

    备注:使用复制算法的优点:每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况了,使用复制算法的缺点:对空间有一定浪费,所以复制空间一般不会特别大。

标记清除
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段,首先先找出根对象,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

备注:java根对象:

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性实体引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。
    等等。
    标记清除算法缺点:标记清除会产生不连续的内存碎片,如果空间内存碎片过多会导致,当程序在运行过程中需要分配空间时找不到足够的连续空间而不得不提前触发一次垃圾收集动作(根据算法不一样效果也不一样)。

标记压缩
标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。

备注:这样带来的好处就是不会产生内存碎片问题了。

上面已经说明了这么多了,我们可以继续说明上题中JVM的其他参数了。

1
2
复制代码-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC

-XX:+UseParNewGC 表示新生代使用ParNew并行收集器,-XX:+UseConcMarkSweepGC 表示老年代使用CMS回收器(CMS收集器是基于“标记-清除”算法实现的,特别提醒由于CMS是标记清除算法实现的所以是存在碎片问题的)。

可以去看看我的JVM菜鸟进阶高手之路六(JVM每隔一小时执行一次Full GC)、以及JVM菜鸟进阶高手之路七(tomcat调优以及tomcat7、8性能对比)图片就取的这两篇里面的。

备注:通过jstat -gcutil pid 查看的FGC这列的时候,CMS gc通常都是+2一次的,由于CMS-initial-mark和CMS-remark会stop-the-world。

所以看到这个图的FGC应该没有什么问题了吧。

1
2
复制代码-XX:+UseCMSInitiatingOccupancyOnly 
-XX:CMSInitiatingOccupancyFraction=75

还有这2个参数关于cms的,-XX:+UseCMSInitiatingOccupancyOnly表示JVM不基于运行时收集的数据来启动CMS垃圾收集周期通过CMSInitiatingOccupancyFraction的值进行每一次CMS收集,-XX:CMSInitiatingOccupancyFraction=75 表示当老年代的使用率达到阈值75%时会触发CMS GC。

备注:jstat -gcutil可以看出上图的老年代的使用率才60.02%

还有最后一个参数解释:

1
复制代码-XX:+UseParNewGC

-XX:+UseParNewGC 表示新生代使用ParNew并行收集器,那么老年代呢?
可以让同样参数修改代码执行一次old gc即可看日志有类似[Tenured:说明老年代使用的是Serial Old

备注:Serial Old使用的是标记压缩算法。

解题

1
2
3
4
5
6
7
8
9
复制代码private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception {
byte[] all1 = new byte[2 * _1MB];
byte[] all2 = new byte[2 * _1MB];
byte[] all3 = new byte[2 * _1MB];
byte[] all4 = new byte[7 * _1MB];
System.in.read();
}

说明:最后System.in.read();这句可以忽略,只是为了让程序阻塞在那里,不结束,这样好看日志,好看现象而已。

聪明如你一下子应该可以看到问题本质:同一份代码,jvm参数堆设置啥的都一样,年轻代gc参数也一样,唯一不同的就在于老年代gc使用上面,而jstat -gcutil图表中FGC没变的应该是正常结果,变了的CMS那个就是意外结果,所以关键点就在CMS上面了。

先来说说all1 、all12、all3、对象实例化开辟空间之后,eden空间都够,他们都在eden空间中,当all4过来的时候,eden空间不够了,需要执行ygc了。
下面有2个问题需要说明,1、如果s0能存的下,可以看看JVM菜鸟进阶高手之路三:MaxTenuringThreshold新生代的对象正常情况下最多经过多少次YGC的过程会晋升到老生代(CMS情况下默认为6),说到这里可能还需要提一个参数:-XX:TargetSurvivorRatio,可以参考飞哥的:JVM Survivor行为一探究竟 2、如果s0存不下,就是我们这里的情况(由于我们这里s0就是1M而已)所以直接进入到old空间了,所以可以看出来jstat -gcutil 里面的老年代的比例都是60%几了吧。
ygc执行完成之后,all4就还可以在eden分配(空间够),所以可以看出来jstat -gcutil 里面的eden的比例都是89%几了吧

备注:-XX:PretenureSizeThreshold参数来设置多大的对象直接进入老年代(这个参数其实只对串行回收器和ParNew有效,对ParallelGC无效)。

如果是-Xmx20m -Xms20m -Xmn10m -XX:+UseParNewGC 这套参数,那么结果就是如图可以解释了,并且每个参数比例啥的都可以理解了。

下面来好好解释下这个现象:

聪明如你一下子应该可以看到一个问题,那么就是时间间隔是每隔2s执行一次,没错就是2s执行一次。需要说道-XX:CMSWaitDuration(Time in milliseconds that CMS thread waits for young GC)默认值是2s,我们修改为-XX:CMSWaitDuration=5000看看效果:
CMSWaitDuration变化

CMSWaitDuration变化

看到了吧,修改为5s就是5s执行一次变化了。那么至于为什么会执行呢??执行的CMS GC的3中情况

执行的CMS GC的3中情况

本题就是当前新生代的对象是否能够全部顺利的晋升到老年代,如果不能,会触发CMS GC。
具体可以看看我比较崇拜的狼哥的分析,一个比我牛逼并且比我努力的大牛。一个有意思的频繁CMS问题

本人其他JVM菜鸟进阶高手之路相关文章或者其他系列文章可以关注公众号【匠心零度】获取更多!!!

如果读完觉得有收获的话,欢迎点赞、关注、加公众号【匠心零度】。


个人公众号,欢迎关注,查阅更多精彩历史!!!

匠心零度公众号

匠心零度公众号

本文转载自: 掘金

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

1…931932933…956

开发者博客

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