工作流产品模块化设计构想

  之前做过几年工作流产品的产品研发,最近正在的做的知识管理产品,需要新增文章审批功能,实际上就是一个简单的工作流。做完这个事情后,发现市面上单纯的开源工作流引擎居多,还没有一款全功能的开源工作流产品。顿时有了自己抽空余时间折腾一个的想法,权当把之前的工作经验与想法归纳总结一下。

  在我的构思中,为了最大限度的复用,这款工作流产品应当是模块化可拆分的。你差一个流程引擎那么你用引擎部分即可,差权限控制那么你把权限控制模块拿走即可,就是说能做到各个部分松耦合,可灵活替换。

  目前,这款工作流产品会包含如下几个部分:

  • 流程核心引擎
  • 流程可视化模块
  • 组织与人员模块
  • 权限模块
  • 消息模块
  • 菜单模块
  • 基础的表单开发组件
  • 流程流转统计分析模块

  默认会把这些模块组装成一个能够RUN起来的系统,用户喜欢那一部分可以单出抽出来用哪一部分,同样的,不喜欢哪一部分可以去掉自己开发。

  2018年就找这么一件事来丰富业余生活了。

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

读《恶意》有感

  实际上读完这本书已经有一段时间了,有几点感想需要尽快的记录下来,因为深知越往后拖就越没有记录下来的动力。

关于写作手法

  文章的写作手法是第一个让我惊奇的地方。全书并不是常见的从头到尾讲故事的叙述文,而是由犯罪嫌疑人和警察的笔记,以及心理活动穿插构成。笔记与心理活动相穿插,让故事与案件逻辑层层递进,不断的推进故事的发展。使得读者更容易的进入到故事当中,把笔记当作案件的记录与线索,把心理活动当作对案件的分析与不断的剥离出事实真相。这是我第一次看到这种写作手法,不由得赞叹:小说居然还能这么写!~

关于先入为主

  在故事的一开始,野野口修透露日高邦彦杀猫的情节给人一股深深的寒意,让读者一下子就觉得日高邦彦不会是什么好人。但是,事实却恰恰相反,这是野野口修故意设下的圈套。当得知最终的真相后,由于先入为主的观念,一个微不足道的小情节居然能带来如此巨大的影响,值得深思。

关于”恶意”

  “也没有什么特别的原因,就是看他不爽!”这就是野野口修杀害日高邦彦的犯罪动机。这么一句毫无逻辑,毫无道理的话,让人毛骨悚然,不寒而栗。
  回归到现实世界,在日常的生活与工作中,这样毫无缘由的恶意有时候也会毫无缘由的出现在你面前,当你感受到别人对你的这股恶意时,你会觉得很无力也很可怕。
  希望自己永远不要成为这样的人。

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

专研精神是技术人员成长的源动力

  前几天,项目中的开发人员向研发组提交了这样一个问题,发现在PC中使用搜狗输入法时,输入完成敲击回车按钮无法触发输入框的查询事件。同样,另外一个组件在IOS系统中,使用IOS原生的输入法也存在该问题。

  针对该问题我跟踪了一下,发现当使用搜狗输入法时,光标虽然在input框里,但是输入的内容却在输入法提供的浮动窗口中,输入结束,敲击空格或回车,此时才把输入的内容放入input框中。

  查看了该组件的实现代码,发现该组件是监听的keyup事件,以及监听了input的回车事件来实现输入完成后的自动查询。经过分析,使用搜狗输入法输入时,由于输入法有自己的弹出框,所有事件均在这个弹出框上,输入完成敲击回车或空格时同样也是,所以无法触发组件的input框事件。对于此种情况,考虑给input框绑定onchange事件来解决该问题,但是onchange事件必须要input框失去焦点才会触发。由此看起来,似乎已经没有什么好的解决办法了,于是乎就考虑到能否在input的输入框里加一个小的查询按钮来解决该问题。

  我把该分析过程以及产品组这边暂定的解决方案告诉了相关技术人员,让他们等候产品组这边的组件更新。本来想着这件事就告一段落,他们只需要等着产品组这边的更新即可了。但是,到了第二天下午,我收到一个amazing的消息,发现该问题的开发人员告诉我说已经自行解决了该问题,并且在产品组提供的组件上修改完成并做了有效性验证!让我这边再去看一下那样修改是否合适!

  最终是解决方案是使用HTML5的oninput与IE专属的onpropertychange来解决该问题。这篇帖子已经阐述得很详细了,这里就不在详细描述了。在这里,截图如下:

对于这件事有如下几点感想:

  • 技术人员的技术成长,不仅取决与多看书多看资料与文档,还取决于碰到问题不放弃的专研精神。同时,这股力量还会鞭策你多去了解你不懂的东西,弄懂之后发现更多的不懂,形成良好的正向循环。所以,毫不夸张的说专研精神是技术人员成长的源动力。
  • 懈怠、满于现有解决方案而不更深入的去探求更完美的方案,是我这边在这个问题上没有走得更远的根本原因。
  • 团队中需要更多这样的技术人员,思想的碰撞才能产生更多的火花。同时技术人员之间需要更多的技术分享与沟通,独立的思想注定会是孤独的

作者公众号:

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

人生中唯一的2017

  今天是2017的最后一天,转眼就将来到2018,很快就会迎来自己的30岁,三十而立。

重要事件

  今年是十分特别的一年,在今年升级做了父亲。在那一刻,除了激动还有感激,感激老婆怀胎十月的辛苦,生小孩儿的不容易。当升级做父亲的那一刻,突然间从心理上就感觉不一样了,这种不一样在于很多复杂的因素,能想到的词有:责任、家、顶梁柱、成熟等等。

收获的

  • 读完了《禅与摩托车维修指南》《暗时间》《人类简史》《遥远的救世主》《恶意》,技术方面《算法》第四版、《Think in Java》。茅盾文学奖获奖作品《无字》看到第四章,文字絮絮叨叨的实在是没有动力看下去了。
  • 单词由百词斩换到了Anki,麦克米伦7000还剩下80%,效果很不错,润物细无声。
  • 学会了如何带娃……

留下的

  • 完成用户中心产品的开发。
  • 今年基础开发框架产品差不多又升级了10来个版本,更加的稳定,同时也更加的陈旧。
  • 年中的时候,充分利用空挡,实现了知识管理产品的从0到1以及更多。年底公司内部正式上线,算是开始产生价值了。
  • 今年总共分享了37篇博客。博客的浏览量新增5W+。

2018

  • 好好带娃
  • 见缝插针多读书
  • 技术这条路坚持走下去
  • 满30之前搞定房子的装修

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

Tomcat启用HTTPS配置

获取证书

  可使用jdk自带的keytool开生成证书,此种方式与向第三方权威机构购买的证书的区别为,第一次请求时需要选择信任站点并继续访问,在浏览器的地址框里会显示不安全的红色提醒。

  使用keytool -genkey -alias tomcat -validity 3650 -keyalg RSA -keystore D://.keystore即可在D盘生成.keystore文件,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PS C:\Users\YiYing\Desktop> keytool -genkey -alias tomcat -keyalg RSA -keystore D://.keystore
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
[Unknown]: YI
您的组织单位名称是什么?
[Unknown]: CSS
您的组织名称是什么?
[Unknown]: CSS
您所在的城市或区域名称是什么?
[Unknown]: BJ
您所在的省/市/自治区名称是什么?
[Unknown]: BJ
该单位的双字母国家/地区代码是什么?
[Unknown]: CHINA
CN=YI, OU=CSS, O=CSS, L=BJ, ST=BJ, C=CHINA是否正确?
[否]: Y

输入 <tomcat> 的密钥口令
(如果和密钥库口令相同, 按回车):
PS C:\Users\YiYing\Desktop>

说明:

  • -alias为别名
  • -validity 3650有效期为10年
  • -keystore为生成的文件路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Option Defaults
Below are the defaults for various option values.
-alias "mykey"

-keyalg
"DSA" (when using -genkeypair)
"DES" (when using -genseckey)

-keysize
1024 (when using -genkeypair)
56 (when using -genseckey and -keyalg is "DES")
168 (when using -genseckey and -keyalg is "DESede")

-validity 90

-keystore the file named .keystore in the user's home directory

-storetype the value of the "keystore.type" property in the security properties file,
which is returned by the static getDefaultType method in java.security.KeyStore

-file stdin if reading, stdout if writing

-protected false

文档地址:
https://docs.oracle.com/javase/6/docs/technotes/tools/solaris/keytool.html

Tomcat配置

  需要修改tomcat的server.xml文件,如下所示:

1
2
3
4
5
6
7
<!-- Define a SSL HTTP/1.1 Connector on port 8443
This connector uses the BIO implementation that requires the JSSE
style configuration. When using the APR/native implementation, the
OpenSSL style configuration is required as described in the APR/native
documentation -->

<Connector SSLEnabled="true" clientAuth="false" keystoreFile="d://.keystore" keystorePass="111111" maxThreads="150" port="8443" protocol="org.apache.coyote.http11.Http11Protocol" scheme="https" secure="true" sslProtocol="TLS"/>

  本来这一段是注释掉的,去掉注释并增加keystoreFilekeystorePass参数即可。

  最后,启动服务器,使用https://localhost:8443/即可访问服务器。

文档地址:

  1. Tomcat7:https://tomcat.apache.org/tomcat-7.0-doc/ssl-howto.html
  2. Tomcat8:https://tomcat.apache.org/tomcat-8.0-doc/ssl-howto.html

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

字符解码时加号解码为空格问题探究

问题

  最近发现一个问题,后台Java代码使用new URL(url)方式发起一个POST请求,模拟表单提交操作,从后台提交数据到服务器端。当客户端的数据中带加号时,服务器端的Servlet使用request.getParameter('param')得到的数据加号被变为了空格。比如:客户端发送的数据为aa+aa,服务器接收到的数据变为了aa aa

简单写了几句代码验证一下:

1
2
3
String str = "aaa+aaa aaa%2B";
System.out.println("ENCODE="+URLEncoder.encode(str, "utf-8"));
System.out.println("DECODE="+URLDecoder.decode(str,"utf-8"));

运行结果如下:

1
2
ENCODE=aaa%2Baaa+aaa%252B
DECODE=aaa aaa aaa+

原因

  通过上面的代码可以看到,编码时,空格被编码成了+号,+号变成了%2b。当未经过编码的带+号的字符串解码时,+号直接转变为了空格。

翻了一下文档是这么说的(地址):

接着到application/x-www-form-urlencoded MIME format的说明处(地址):

再翻了一下servlet中request.getParameter('param')方法说明,如下:

接着看getParameter到底如何取数据的:

  从上图可以知道,当在servlet中使用getParameter方法来接收POST数据时,默认是会根据请求头的类型来解码的。这就证实了如果通过后端new URL(url)的方式模拟表单的POST,如果在POST前不对数据进行编码且数据中带有+号,那么服务器端收到的数据必然会是错误的(+号会被自动解码为空格)。

验证

  常规的表单提交content-type有两种:application/x-www-form-urlencodedmultipart/form-data,如果表单提交时不设置任何类型,默认以第一种方式提交数据;第二种属于带附件的表单提交,当表单中有附件时,必须设置表单的enctypemultipart/form-data.

  很多时候,我们用 Ajax 提交数据时,也是使用这种方式。例如 JQuery的 Ajax,Content-Type 默认值为application/x-www-form-urlencoded;charset=utf-8

前端代码

1
2
3
4
5
6
7
$.ajax({
url:getServer()+"/TestServlet",
data:{yiying:"aaa+aa a"},
type:'post',
success:function(response){

}})

HTTP请求情况

  由于在chrome的控制台中所显示的数据被优化显示了,所以在Form Date部分,需要点击后面的view source才能看到浏览器发送数据的真实格式是怎么样的。默认优化后的显示应当为yiying:aaa+aa a,

  从上图中可以看到,浏览器实际上的使用Content-Type:application/x-www-form-urlencoded; charset=UTF-8编码规则,实际发送的数据为yiying=aaa%2Baa+a,其中+号变为了%2b,空格被编码为+号。

后端情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public TestServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request,response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("==="+request.getParameter("yiying")+"===");
}
}
//输出值为
===aaa+aa a===

  从后端的输出值可以看到,后端的servlet自动解码为原始数据。

关于后端中文乱码问题的特别说明:

后端服务器(tomcat 的server.xml)如果没有设置编码规则(URIEncoding=”utf-8”),Servlet默认会以ISO-8859-1来解码中文字符(Servlet规范定义),而前端标识为utg-8,此种情况就会产生乱码,可以用如下3种办法来解决:

  1. 在server.xml设置编码规则为utf-8
  2. 在调用getParameter之前使用代码设置编码规则,request.setCharacterEncoding("utf-8");
  3. 定点处理,String name = new String(request.getParameter("name").getBytes("iso-8859-1"),"utf-8");

结论

  显而易见,如果需要在后端使用new java.net.URL(url)的方式POST提交数据,需要像浏览器那样先给数据编码处理,再发送数据。

作者公众号:

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

JSONP原理探究

介绍

  JSONP是一直种解决跨域问题的方案,实现的原理来自于页面中的<script>标签能够跨域请求资源。要通过JSONP实现跨域,需要服务器端做额外支持。

前端代码

  前端部分核心在于通过script标签的src告诉服务器端约定好的回调方法名。代码如下

1
2
3
4
5
6
7
8
var callbackName = 'callbackFunc';
window[callbackName] = function (response) {
// 对返回的数据做后续处理
console.log(response)
};
var script = document.createElement('script');
script.src = 'http://127.0.0.1:8080/sword-room/JSONPServlet?callback='+callbackName+'&param=JSONP';
document.body.appendChild(script);

后端代码

  服务端部分主要为接收前端发送过来的请求参数,核心在于约定好的方法名。代码如下

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.demo;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/JSONPServlet")
public class JSONPServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

public JSONPServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//请求参数
String param = request.getParameter("param");
//跟前端部分约定好的方法名
String callback = request.getParameter("callback");
PrintWriter out = response.getWriter();
//返回JSON字符串
response.setContentType("text/javascript; charset=UTF-8");
String jsonData = "{a:1,b:2,param:'"+param+"'}";
out.print(callback+"("+jsonData+");");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}

结果

下图为验证结果:

其它

1.前台部分jquery对jsonp的支持,只需设置dataType:"jsonp",样例如下

1
2
3
4
5
6
7
8
9
10
$.ajax({
url:"http://127.0.0.1:8080",
dataType:"jsonp",
data:{
userName:'管理员'
},
success:function(data){
console.log(data)
}
});

2.后台Java部分可使用fastjson来实现返回数据

1
2
3
4
5
6
7
8
 import com.alibaba.fastjson.JSONPObject;

JSONPObject OBJ = new JSONPObject();
//jquery ajax jsonp默认的方法参数名为callback,可使用jsonpCallback:'newcallback'自定义
OBJ.setFunction(request.getParameter("callback"));
//设置返回数据
OBJ.addParameter(result);
this.writeToPage(response, OBJ);

3.跨域情况下一般都牵涉到权限问题,可通过SSO或者服务器端开放匿名访问权限解决,或者token也行
4.使用Nginx的代理功能来解决跨域问题也是很好的一种办法

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

MySql启动报 write error: No space left on device问题解决

启动MySQL数据库时报如下错误:

1
2
3
4
5
[root@localhost redmine-3.1.1-1]# ./ctlscript.sh start
171023 09:36:32 mysqld_safe Logging to '/opt/redmine-3.1.1-1/mysql/data/mysqld.log'.
171023 09:36:32 mysqld_safe Starting mysqld.bin daemon with databases from /opt/redmine-3.1.1-1/mysql/data
/opt/redmine-3.1.1-1/mysql/bin/mysqld_safe: line 128: echo: write error: No space left on device
/opt/redmine-3.1.1-1/mysql/bin/mysqld_safe: line 165: 16298 Killed

其中,引人注目的就是No space left on device

使用df -h命令查看磁盘使用情况如下:

1
2
3
4
5
6
[root@localhost ~]# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 50G 50G 0 100% /
tmpfs 3.8G 100K 3.8G 1% /dev/shm
/dev/sda1 485M 54M 407M 12% /boot
/dev/sda5 404G 227M 383G 1% /home

可以看到磁盘空间已经被完全占满。

接着,使用du -sh *命令查看/dev/sda2中的磁盘占用具体情况,结果如下:

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
[root@localhost /]# du -sh *
8.8M bin
37M boot
344K dev
34M etc
29M home
122M lib
27M lib64
4.0K media
0 misc
4.0K mnt
0 net
42G opt
2.4G root
19M sbin
4.0K srv
164K tmp

[root@localhost /]# cd opt
[root@localhost opt]# du -sh *
15M apache-tomcat-9.0.0.M8
15M ksar
200M nexus-3.2.0-01
100M nexus-3.2.0-01-unix.tar.gz
904M redmine-3.1.1-1
40G sonatype-work

逐级往下找,发现是nexus的日志文件已经有30多G了,删掉,再次启动mysql问题得到解决。

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

HTTP协商缓存实践

  从浏览器的缓存类型上来讲分为强缓存和协商缓存,之前写过一篇文章《HTTP缓存详解》做了详细的整理。

  目前做的知识管理产品,为了首页能够更快的加载,需要对首页请求的资源做缓存整理。通过整理分析,决定对css、js做强缓存处理;对个人头像的请求做协商缓存处理;

  为什么要头像不做强缓存处理呢?原因为目前的头像都是通过人员的ID来获取,而不是通过头像ID来获取。这样做的好处为在后台获取相关数据时,比如排行榜以人员ID作为主键,如果要带出该人员的头像信息,还得去头像表里面联查一下来获取头像ID。因此,由于后端部分的设计造成头像做协商缓存才是最优解。

  目前的逻辑为,当用户第一次进入知识管理系统,请求个人头像的URL会返回200并从服务器下载个人头像,此时服务器会在response里面设置ETAG值为头像的最后修改日期;当刷新页面时,服务器会判断请求头中的If-None-Match是否与当前的最后更新日期匹配,如果匹配则直接返回304(不返回数据),如果不匹配则从磁盘中取出头像。

相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 协商缓存控制
* @param request
* @param response
* @param etag
* @return
*/
private boolean cacheControl(HttpServletRequest request,HttpServletResponse response,String etag){
String ifNoneMatch = request.getHeader("If-None-Match");
if(etag.equals(ifNoneMatch)){
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return false;
}
response.addHeader("ETag", etag);
return true;
}

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。

Nginx代理Redmine和Maven私服Nexus3.0配置

背景

  在这之前部门申请了一个独立IP,这个独立IP对应一台PC机,这台机器上安装了产品问题受理平台Redmine以及Maven私服。由于PC机性能有限,新找了一台双网卡的服务器接到独立IP上,为了不重新迁移环境,让PC机跟服务器组成了内网环境。在服务器上搭建Nginx来把请求转发到内网的PC机上。

Nginx配置

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
#user  nobody;
worker_processes 1;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;

sendfile on;

#nexus支持
proxy_send_timeout 120;
proxy_read_timeout 300;
proxy_buffering off;
keepalive_timeout 5 5;
tcp_nodelay on;

#代理到后台服务器
upstream ws_redmine {
server 192.168.100.100:8082;
}
upstream ws_nexus {
server 192.168.100.100:8083;
}
server {
listen 8082;
server_name localhost;

location / {
proxy_pass http://ws_redmine;
}

# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server {
listen 8083;
server_name localhost;

# nexus支持
# allow large uploads of files - refer to nginx documentation
client_max_body_size 1G;

location / {
proxy_pass http://ws_nexus;
# nexus支持
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}

}

说明:

  • 同时开启两个端口,分别对应redmine和nexus服务器
  • 配置nexus的转发时如果按照redmine的配置,会报400 bad request错误,之前以为是nginx的配置问题,最后翻nexus的官方文档,发现需要额外的配置。安装官方文档配置好后,发现转发时端口号缺失。查了一圈资料,原来是官方文档错误,需要修改proxy_set_header Host $host,变为proxy_set_header Host $host:$server_port.

其它记录

  • 查看Linux端口占用情况:lsof -i:80,80为端口号
  • 关闭占用端口的应用进程:kill -9 11071,11071为上一步中显示出来的端口占用进程号
  • Linux中启动Nginx:在nginx安装目录下运行nginx -c nginx.conf,不知道安装在哪儿可以使用whereis nginx查看
  • Linux中关闭Nginx:pkill -9 nginx

留言

欢迎交流想法。留言会通过 GitHub Issues 保存,首次使用需要登录 GitHub。