GeXiangDong

精通Java、SQL、Spring的拼写,擅长Linux、Windows的开关机

0%

在单页面应用中,为了利用浏览器的后退/前进按钮,可以通过改变地址栏为#foo 和 onhashchange 事件一起来控制。但这有个缺点:在一个树形网站结构中点来点去时,浏览器并不会按照我们需要的返回上一级,而只是返回前一个看的页面,特别是在提交表单后,点返回又到了表单页。

HTML5提供了一种解决办法,可以通过window.history.pushState等方法来实现。

下面是例子:

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
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width"/>
<style type="text/css">
#content div{text-align:center; padding-top:20px;}
</style>
<script>
var visitHistory = [];
var currentPage = null;

window.onload = function() {
var homePage = {"id":"index", params:{}}; //id是主要部分,不同类型页面不重复;params用于记录页面需要的参数,方面返回时重新描画页面用
window.history.pushState(visitHistory, homePage, "#");
gotoPage(homePage);
}

function gotoPage(page){
for(var i = 0; i < visitHistory.length; i++){
if(visitHistory[i].id == page.id){
//exists, delete items after this
while(visitHistory.length > i){
visitHistory.pop();
}
currentPage = null; //如果进入已存路径上页面,则当前页面不用保存了
break;
}
}

if(currentPage != null){
visitHistory.push(currentPage);
}
currentPage = page;

window.history.replaceState(visitHistory, page.id, "#");
showPage(page);
}

window.onpopstate = function() {
if (visitHistory.length > 0) {
currentPage = visitHistory.pop();
window.history.pushState(visitHistory, currentPage.id, "#"); // + pg);
showPage(currentPage);
} else {
//No "history" - let them exit or keep them in the app.
showMessage("no history, exit or leave with no action.");

}
}

/**
* 这个是具体的描画页面的函数,根据需要改写
* page参数类型时JSON,例如:{"id":"index", params:{}}
*/
function showPage(page){
var id = page.id;
document.getElementById("title").innerHTML = id;
var pathHTML = '<a href="javascript:gotoPageId(\'index\')">Home</a> &gt; ';
var contentHTML = '<div style="border:1px solid #999;background-color:#DDD;width:200px;height:50px;border-radius:5px;" onclick="gotoPageId(\'1\');">goto 1</div>';

if(id == "1"){
pathHTML += '<a href="javascript:gotoPageId(\'1\')">1</a> ';
contentHTML = '<div style="border:1px solid #999;background-color:#DDD;width:200px;height:50px;border-radius:5px;" onclick="gotoPageId(\'1.1\');">goto 1.1</div>';
}else if(id == '1.1'){
pathHTML += '<a href="javascript:gotoPageId(\'1\')">1</a> &gt; ';
pathHTML += '<a href="javascript:gotoPageId(\'1.1\')">1.1</a> ';
contentHTML = '<div style="border:1px solid #999;background-color:#DDD;width:200px;height:50px;border-radius:5px;" onclick="gotoPageId(\'1.1.1\');">goto 1.1.1</div>';
}else if(id == '1.1.1'){
pathHTML += '<a href="javascript:gotoPageId(\'1\')">1</a> &gt; ';
pathHTML += '<a href="javascript:gotoPageId(\'1.1\')">1.1</a> &gt; ';
pathHTML += '<a href="javascript:gotoPageId(\'1.1.1\')">1.1.1</a> ';
contentHTML = 'no child page.';
}

document.getElementById("path").innerHTML = pathHTML;
document.getElementById("content").innerHTML = contentHTML;

showMessage("showPage---" + id);
}

/**
* 这个函数是供showPage函数例子中链接用的,实际根据需要写或不写
*/
function gotoPageId(id){
gotoPage({"id":id, "params":{}});
}
/**
* window.history.pushState 不会触发onHashChange事件了,下面这个函数/事件无意义
*/
window.onhashchange = function(){
document.getElementById("addressbar").innerHTML = "hashchange:" + window.location.href;
}
/**
* 供测试用的函数
*/
function showMessage(msg){
document.getElementById("msg").innerHTML += "<br>" + msg;
}
</script>
</head>
<body>
<h1 id="title" style="text-align:center;">none</h1>
<div id="path"></div>

<div id="content" style="padding:20px;">
</div>

<div id="msg" style="border:1px solid #333;margin:10px; min-height:300px">
</div>
<div id="addressbar" style="border:1px solid #666">none</div>
</body>

IOS 9以上开始支持(IOS8不支持)

遇到一个Timer某些情况下会自动被cancel掉,再次schedule TimerTask时会抛出java.lang.IllegalStateException: Timer already cancelled. 程序中并无timer.cancel()的调用。最后发现是TimerTask执行时发生未被捕获的异常导致,例如下段代码肯定会出问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;

public class TestTimer{

public static void main(String[] argvs) throws Exception{
Timer timer = new Timer(true);
timer.schedule(new MyTimerTask(), 1); //1毫秒后执行 MyTimerTask

Thread.sleep(1000);
timer.schedule(new MyTimerTask(), 1); //这里会抛出 java.lang.IllegalStateException: Timer already cancelled.;因为执行上一个TimerTask时异常导致Timer结束了
}
}

class MyTimerTask extends TimerTask {

public void run() {
//下面这句会导致运行此TimerTask的Timer被cancel掉
throw new RuntimeException();
}
}

  1. 下载tomcat tgz文件
  2. 解压缩到某目录,例如 /web/tomcats/tomcat1
  3. 进入/lib/systemd/system 目录
  4. 创建一个 tomcat-1.service文件
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
[Unit]
Description=Apache Tomcat Web Application Container [tomcat """"""""1]
After=network.target

[Service]
Type=forking

Environment=JAVA_HOME=/usr/lib/jvm/java-8-oracle
Environment=CATALINA_PID=/web/tomcats/tomcat1/temp/tomcat.pid
Environment=CATALINA_HOME=/web/tomcats/tomcat1
Environment=CATALINA_BASE=/web/tomcats/tomcat1
Environment” =’CATALINA_OPTS=-Xms512M -Xmx2048M -server -XX:+UseParallelGC’
Environment=’JAVA_OPTS=-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom’

ExecStart=/web/tomcats/tomcat1/bin/startup.sh
ExecStop=/web/tomcats/tomcat1/bin/shutdown.sh

User=www-data
Group=www-data
UMask=0007
RestartSec=10
Restart=always

[Install]
WantedBy=multi-user.target
Alias=xxx #【注意,如果有这行,不要和其他的服务重复】

创建服务

执行下面的命令

1
suduo systemctl link /lib/systemd/system/tomcat-1.service

上面这句会在/etc/systemd/system下给前面编辑的文件创建一个连接

1
sudo systemctl enable tomcat1

上面这句设置此服务自动随机器启动而启动

重启systemd

1
sudo systemctl daemon-reload

启动tomcat

1
sudo systemctl start tomcat1

设置自动启动

1
sudo systemctl enable tomcat1

注意目录权限,注意server.xml配置端口不要重复

重启可以用如下命令

1
sudo systemctl restart tomcat1

今年开始在团队内使用新的版本管理策略,以便达到每周定期发布的模式。以前按功能发布,经常有大功能需要几周开发,并行的小功能需要几天,而导致小功能需要等大功能开发测试完毕才能发布,拖延了一些重要功能,不适合互联网开发模式。新的版本策略每周定期发布新版本,只发布已经测试完毕的功能,未经过测试的留待以后发布。

工具

  • 使用git做版本管理 【必须】
  • redmine作为开发+测试(不含测试用例管理)的日常工具 【可选】
  • pmwiki管理文档 【可选】

分支策略

下图是一个简单地分支模型,当多个项目有公共的部分时,master, prerelease分支要有对应的多份。

分支模型

  • develop分支,开发的主干分支,所有开发以此分支为基础且只有开发完毕的功能才会提交到此分支。每天会自动从develop取出程序发布到测试服务器,形成nightly version,供提前测试发现问题。

  • master 是发布到生产环境的版本。

  • prerelease仅仅在测试开始时才创建,测试完成后删除。

  • feature_x在开发新功能时创建,功能开发完毕后删除。

  • 当有新功能需要开发时:

    1. 创建一个feature_xx分支,xx以功能的名字命名(注意不要用中文,中文会导致redmine出问题)
    2. 其他开发人员checkout此分支,大家日常提交代码到此分支
    3. 开发人员每日从feature_xx拉取其他人(共同开发此feature_xx功能的同事)提交的代码,也要从远程develop分支合并到本地(图中#1,合并后不提交到远程服务器),这是为了避免最后合并冲突麻烦。
    4. 开发完成后,合并代码到develop分支,并删除feature_xx分支
  • 需要发布新版本前

    1. 确认需要发布的功能都已经提交到develop分支后,从develop分支创建prerelease分支
    2. 测试prerelease分支
    3. 如果有bug则在prerelease分支修改,改后合并到develop分支,注意千万不要在develop分支修改,改后合并到prerelease分支,这样会把其他人提交的东西也合并到prerelease分支;如果bug被允许在后续版本中修复,则bug另建feature分支修改
    4. 测试通过后,提交prerelease分支到master分支,并打TAG标记(标签采用 前缀+日期+序号 形式)。之后删除prerelease分支
      从master分支提取代码部署到生产环境
  • 当生产环境发现重要bug,需要紧急修复时

    1. 如上图#3红色节点
    2. 从master分支checkout出对应的代码,修复后测试通过提交到对应的master分支,并形成下一个TAG和生产环境发型版本。
    3. 提交修复到develop分支
    4. 如果存在prerelease分支,则提交到prerelease分支。(不存在prerelease分支时,不需要此步骤)
  • 测试组一般工作在prerelease分支上,开发组一般在feature(开发期间)和prerelease(测试期间)工作

  • 紧急修复时测试和开发都在hotfix上工作

Commit Message

  • 每次提交都必须写Commit Message,不要写无意义的信息。
  • 提交内容和redmine有关联时,要关联redmine上对应的问题编号,例如:
    • 修改了101号问题,耗时3个半小时,但没有全部完成则可按照如下格式写refs #101 @3h 30m {中文注释}
    • 如果101号问题修改完毕,则用fixed关键字开头 fixed #101 @2h {中文注释}
    • 时间只写此次提交内容花费的修改时间,redmine会自动累加。
    • 中文注释写解决问题的技术实现办法,不用描述问题本身,因为问题已经在redmine上有说明。

开发测试日常工作流程

  1. 周一确定一周开发内容
  2. 周一~周二测试上周发布到release分支的内容,并修改bug
  3. 周三发布到生产服务器
  4. 开发人员在周五完成本周开发内容后提交到develop分支,周五下班前提交到release分支。
  5. release log,每周从周一开始记录直至发布

注:上述流程看上去程序员会同时做很多事,开发、修改bug等等,其实大部分时间这些不是并行,因为每周要发布的内容中不是包含所有程序员的工作,可能一个程序员负责的部分2周甚至更长时间才有新功能发布一次。

glusterfs可以用来给同步两台机器(或更多)做文件镜像,而且是所有节点都在线可用得实时热备份。

安装

1
apt-get install glusterfs-server

即可完成安装,也有glusterfs-client可用。

配置Gluster节点

先在2个机器的hosts文件里加入类似的域名指向,以便后面使用
【以下方案仅限于2台主机同步,3台不可行】

1
2
192.168.1.10  node1
192.168.1.11 node2

注意:文件里不要有127.0.0.1 node1这样的行(如果node1和机器名重名会出现此现象)

在node1上运行

1
gluster peer probe node2

在node2上运行

1
gluster peer probe node1

之后可以使用下面的命令查看状态

1
gluster peer status

配置镜像的卷

实际使用中应该用一个单独的逻辑分区,此处仅仅是使用了一个目录
在两个机器上都分别运行下面命令创建一个目录。(不要使用已经存在的目录)

1
mkdir /home/gluster-test

然后在任意一台机器上运行

1
2
gluster volume create gv0 replica 2 node1:/home/gluster-test node2:/home/gluster-test force
gluster volume start gv0

注意force是因为使用root用户创建,gluster建议不用root创建。命令在任意一个机器上运行后,另外一个机器会自动同步。

如果出现

volume create: gv0: failed: /home/gluster-test or a prefix of it is already part of a volume

或类似的错误信息,可以用如下命令删除文件后,重新再运行 gluster volume create

1
2
3
setfattr -x trusted.glusterfs.volume-id /home/gluster-test
setfattr -x trusted.gfid /home/gluster-test
rm -rf /home/gluster-test/.glusterfs

如果没有setfattr,可以通过 apt-get install attr 来安装。

可以使用 gluster volume info 查看上述卷状态。

注意:卷创建成功后,不要去修改/home/gluster-test目录下地任何文件、目录等(如果是单独分区,不mount最好)

挂载gluster的卷

在需要使用文件的机器上执行:

1
mount -t glusterfs node1:/gv0 /mnt

此命令会把上一步骤创建的卷挂载到/mnt目录下,以后可以访问/mnt目录,读写文件。文件会自动同步到node1,node2的节点。

如果不需要访问,只供备份的节点可以不用此步骤。

在/etc/fstab文件里加入如下行,可以避免每次启动都要手工mount,可自动mount

1
node1:/gv0   /web    glusterfs     defaults,nobootwait,_netdev  0  0

注意一共6部分,空格区分,每部分之间不要有空格

参考

http://gluster.readthedocs.org/en/latest/Quick-Start-Guide/Quickstart/

安装redmine

1
apt-get install redmine redmine-pgsql apache2 libapache2-mod-passenger apparmor

如果安装过程后自动启动的配置出错,可以随时使用如下命令启动配置redmine的进程

1
dpkg-reconfigure -plow redmine 

安装完后配置remine,在Apache的000-default.conf中映射
在/web/htdocs(网站根目录)下创建一个连接

1
ln -s /usr/share/redmine/public redmine

在apache 0000-default.conf中加入(也可在其他部分)

1
2
3
4
<Directory /web/htdocs/redmine>
RailsBaseURI /redmine
PassengerResolveSymlinksInDocumentRoot on
</Directory>

之后可以用

1
service restart apache2

重启apache,并在浏览器输入 http://localhost/redmine/ 查看是否已经成功。

如果出现类似如下错误:

Web application could not be started
cannot load such file -- bundler/setup (LoadError)
  /usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
  /usr/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
  /usr/lib/ruby/vendor_ruby/phusion_passenger/loader_shared_helpers.rb:245:in `block in run_load_path_setup_code'
  /usr/lib/ruby/vendor_ruby/phusion_passenger/loader_shared_helpers.rb:348:in `running_bundler'
  /usr/lib/ruby/vendor_ruby/phusion_passenger/loader_shared_helpers.rb:243:in `run_load_path_setup_code'
  /usr/share/passenger/helper-scripts/rack-preloader.rb:100:in `preload_app'
  /usr/share/passenger/helper-scripts/rack-preloader.rb:158:in `<module:App>'
  /usr/share/passenger/helper-scripts/rack-preloader.rb:29:in `<module:PhusionPassenger>'
  /usr/share/passenger/helper-scripts/rack-preloader.rb:28:in `<main>'
Application root
/usr/share/redmine
...

可能是ruby 没有安装bundle所致,用如下命令安装

1
gem install bundle

安装好后,要把目录权限修改下

1
chown -R nobody:nogroup /usr/share/redmine

注意上面的nobody nogroup是apache用户和组

安装 git

1
apt-get install git-core

配置

创建redmine用的仓库镜像

在/var/lib/redmine下创建一个repos目录,也可以放其他地方。

1
2
3
4
cd /var/lib/redmine
mkdir repos
git clone --mirror /home/git/projects/test.git
chown -R git:git test.git

注意:不需要单独创建项目的.git目录,git clone会自动创建。

修改mirror里地权限设置

1
2
3
cd /var/lib/redmine/repos/test.git
chmod a+rX -R ./
git config --add core.sharedRepository 644

以上设置是为了确保redmine程序对这里有读取的权限。

如果redmine访问时出现「404 版本库中不存在该条目和(或)其修订版本。」提示,那么可能是上述配置没有加好。

在redmine中登记仓库镜像位置

进入redmine对应的项目->配置->版本库,选择新增版本库
SCM选择Git,库路径处输入 /var/lib/redmine/repos/test.git,并设置其他项目最后保存。(注意:如果用的docker redmine,这里要填写的路径是docker内的路径,不要写成宿主机的路径,一般是存在宿主机上,通过docker -v参数映射给docker访问,这是就是映射的目录+仓库目录了)

从redmine里版本库标签看到的是master分支的源码,可以前后到其他标签,如果默认想看其他标签的,例如develop分支

1
2
cd /var/lib/redmine/repos/test.git
git symbolic-ref HEAD refs/heads/develop

这样默认就是develop分支了

git commit message 和 redmine issue 关联

以后push到git server的更新,commit message中有 #101 字样的提交会被自动关联到101号问题。

例如:

fixed bug #101 @1h30m 

如果需要自动更改问题状态,请在管理->配置->版本库处设置关键字。

设置git仓库,收到提交后自动更新redmine用镜像

通过设置hooks 来实现

1
cd /home/git/projects/test.git/hooks

创建post-receive文件,文件内容

1
2
3
#!/bin/bash
/usr/bin/git push --mirror /var/lib/redmine/repos/test.git
curl "http://127.0.0.1/redmine/sys/fetch_changesets?key={apikey}" &

apikey在redmine 管理->配置->版本库下面,选中“启用用于版本库管理的WebService”,然后生成一个API key。并把生成的key拷贝到这里。

1
2
chmod 755 post-receive
chown git:git post-receive

登记耗时不成功的可能原因

如果在 git commit message 中写的 耗时不被记载入redmine,可能是以下几个原因

  1. 在版本库配置「在提交信息中引用和解决问题」处,记录的活动选择了「默认」,而枚举值设置中,「活动(时间跟踪)」没有设置默认值
  2. 提交的用户和项目中设置的开发人员不匹配,git提交的用户,用户名和邮箱需要和项目中开发人员对应的账号匹配

在Mac OS上 Java 8, Eclipse从CVS上取回的中文文件名(目录)乱码,导致无法和windows下的eclipse共享。

可以通过修改 eclipse.ini(在Eclipse.app/Content/MacOS目录下)在最后增加一行

1
-Dfile.encoding=GBK

来解决。

我估计在windows的eclipse的eclipse.ini中增加
-Dfile.encoding=UTF-8
也可以解决,只要两边保持一致即可。

解决这个问题涉及到已经在CVS里存的目录、文件是什么编码的。

最后:增加-Dfile.encoding=GBK后,eclipse 默认的workspace encoding就不是UTF-8而是GBK了,需要去eclipse->preferences->General->Workspace->Text file encoding中修改一下。

部分浏览器支持直接从剪贴板粘贴图片到HTML编辑器内,不同浏览器处理方式有差异。

chrome

1
2
3
4
5
6
7
8
9
10
document.onpaste = function(event){
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
console.log(JSON.stringify(items)); // will give you the mime types
var blob = items[0].getAsFile();
var reader = new FileReader();
reader.onload = function(event){
console.log(event.target.result)}; // data url!
reader.readAsDataURL(blob);
//upload to the server here.....................
}

Firefox

firefox粘贴比较简单,直接贴出来的是base64后的代码, 例如:

1
<img  src="data:image/png;base64,iVBORw0KGgoAAAAN..................................." />

Eclipse 提示Warning “Nested weights are bad for performance”

原因很简单:父子元素都使用了 android:weight=1属性。

最好的解决办法,不要嵌套使用android:weight属性。可以通过RelativeLayout布局替代LinearLayout布局来改善原有代码。 RelativeLayout的layout_toRightOf , layouttoLeftOf等一起用可以达到充满剩余空间的效果。

如果使用RelativeLayout仍旧无法达到效果,想不显示waring可以:

add to the root of your layout: xmlns:tools=”http://schemas.android.com/tools”. Then in your buttons add tools:ignore=”NestedWeights”

另外tools:ignore=”UselessParent”可以去掉提示两级元素多余(This LinearLayout layout or its FrameLayout parent is possibly useless; transfer the background attribute to the other view)的警告。

Reference: http://stackoverflow.com/questions/19636323/nested-weights-are-bad-for-performance-in-my-xml-code

使用SystemUiHider时,全屏的Layout必须加android:keepScreenOn=”true”属性,非全屏模式下的空间增加android:fitsSystemWindows=”true”不可颠倒,错误可能会出现非全屏模式下控件布局占用android占用系统虚拟键位置导致被遮挡的现象

用google ZXing生成二维码、一维码的小例子

pom 依赖

增加依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>

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
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import java.io.FileOutputStream;
import java.util.Hashtable;

/**
* 创建二维码、一维码的例子
*/
public class Qrcode {

public static void main(String[] args) throws Exception {
createBarCode("012341");
createQrCode("http://www.163.com");
}

/**
* 创建二维码
* @param content
* @throws Exception
*/
private static void createQrCode(String content) throws Exception{
String format = "png";
MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);// 设置二维码排错率,可选L(7%)、M(15%)、Q(25%)、H(30%)
hints.put(EncodeHintType.MARGIN, 10);
BitMatrix bitMatrix = multiFormatWriter.encode(content, BarcodeFormat.QR_CODE, 300, 300, hints);

FileOutputStream fos = new FileOutputStream("target/qrcode.png");
MatrixToImageWriter.writeToStream(bitMatrix, format, fos);
fos.close();
}



/**
* 一维条码
* @param text
* @throws Exception
*/
private static void createBarCode(String text) throws Exception{
String format = "png";
Hashtable hints= new Hashtable();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");

String content = "http://www.163.com";
BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.CODE_128, 300, 80, hints);

FileOutputStream fos = new FileOutputStream("target/barcode.png");
MatrixToImageWriter.writeToStream(bitMatrix, format, fos);
fos.close();
}
}