Website Logo. Upload to /source/logo.png ; disable in /source/_includes/logo.html

舞乐 VOLER

舞动我人生

关于IntegerCache

Jan 10, 2017

很久没有写文章了,今天以一段代码重新开始,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Integer a = 127;
Integer b = 127;
int c = 127;
System.out.println(a == b);
System.out.println(a == c);

a = 128;
b = 128;
c = 128;
System.out.println(a == b);
System.out.println(a == c);

a = new Integer(127);
b = new Integer(127);
System.out.println(a == b);

这里还给出了代码截图,可以看到这段代码在IDE中有多处warning提示,包括

Condition ‘a == c’ is always ‘true’
Unnecessary boxing ‘new Integer(127)’
Number objects are compared using ‘==’, not ‘equals()’

实际编码时应该按照提示修改warning。这里直接给出执行结果

true
true
false
true
false

了解自动拆箱的知识后,a == c的结果很好理解,包装类型a与原始类型c进行比较时,自动拆箱,相当于127 == 127以及128 == 128,所以c相关的结果都是true。再看一下最后一个a == b,了解equals与==的区别后,结果是false也好理解,这里还提示我们应该使用equals来比较包装类型a和b。本文主要关注前两个a == b,127时结果是true,128时结果是false。

了解自动装箱的知识后,明白这里Integer a = 127等价于Integer a = Integer.valueOf(127)。那现在就看一下valueOf这个方法做了什么。

1
2
3
4
5
6
public static Integer valueOf(int i) {
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

原来valueOfIntegerCache这个类相关,那再看一下IntegerCache这个类做了什么。

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
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
    }

    private IntegerCache() {}
}

IntegerCache中定义了一个Integer数组,初始化时,这个数组预先保存了[-128, high]之间的Integer实例。valueOf方法在自动装箱时会判断需要装箱的数是否在这个区间内,如果在则返回预先保存的实例,不在则新实例化。默认的区间范围是[-128, 127],第一次装箱127时,返回的是预先保存的同一个实例,所以结果为true;第二次装箱128时,返回的是新实例化的对象,所以结果为false。

除了Integer内建了IntegerCache,Byte、Short、Long也内建了相应的类。

Could Not Get the Value for Parameter compilerId for Plugin Execution

Apr 16, 2016

最近Eclipse下的很多Maven工程报错,导入其他的Maven工程时也同样报错,报错的日志类似如下,

CoreException: Could not get the value for parameter compilerId for plugin execution default-compile: PluginResolutionException: Plugin org.apache.maven.plugins:maven-compiler-plugin:3.1 or one of its dependencies could not be resolved: The following artifacts could not be resolved: org.apache.maven:maven-core:jar:2.0.9, org.apache.maven:maven-settings:jar:2.0.9, org.apache.maven:maven-plugin-parameter-documenter:jar:2.0.9, org.apache.maven:maven-profile:jar:2.0.9, org.apache.maven:maven-model:jar:2.0.9, org.apache.maven:maven-repository-metadata:jar:2.0.9, org.apache.maven:maven-error-diagnostics:jar:2.0.9, org.apache.maven:maven-project:jar:2.0.9, org.apache.maven:maven-plugin-registry:jar:2.0.9, org.apache.maven:maven-plugin-descriptor:jar:2.0.9, org.apache.maven:maven-artifact-manager:jar:2.0.9, org.apache.maven:maven-monitor:jar:2.0.9, org.apache.maven:maven-toolchain:jar:1.0, org.apache.maven.shared:maven-shared-utils:jar:0.1, com.google.code.findbugs:jsr305:jar:2.0.1, org.apache.maven.shared:maven-shared-incremental:jar:1.1, org.codehaus.plexus:plexus-component-annotations:jar:1.5.5, org.codehaus.plexus:plexus-compiler-api:jar:2.2, org.codehaus.plexus:plexus-compiler-manager:jar:2.2, org.codehaus.plexus:plexus-compiler-javac:jar:2.2, org.codehaus.plexus:plexus-container-default:jar:1.5.5, org.codehaus.plexus:plexus-classworlds:jar:2.2.2, org.apache.xbean:xbean-reflect:jar:3.4, log4j:log4j:jar:1.2.12, commons-logging:commons-logging-api:jar:1.1, com.google.collections:google-collections:jar:1.0: Failure to transfer org.apache.maven:maven-core:jar:2.0.9 from https://repo.maven.apache.org/maven2 was cached in the local repository, resolution will not be reattempted until the update interval of central has elapsed or updates are forced. Original error: Could not transfer artifact org.apache.maven:maven-core:jar:2.0.9 from/to central (https://repo.maven.apache.org/maven2): The operation was cancelled.

开始认为是org.apache.maven.plugins:maven-compiler-plugin:3.1插件下载失败,检查本地仓库时,发现该插件已下载成功。今天参考 CoreException: Could not get the value for parameter compilerId for plugin execution default-compile 继续向下看日志,关键的错误日志应该是,

Failure to transfer org.apache.maven:maven-core:jar:2.0.9 from https://repo.maven.apache.org/maven2 was cached in the local repository, resolution will not be reattempted until the update interval of central has elapsed or updates are forced.

确认本地仓库中的org.apache.maven:maven-core:jar:2.0.9已下载成功,仍然报错的原因是,

resolution will not be reattempted until the update interval of central has elapsed or updates are forced.

参考 Error in pom.xml Maven build,在Eclipse中使用Alt+F5快捷键,在弹出的Update Maven Project对话框中选择报错的Maven工程,勾选下图中的Force Update of Snapshots/Releases,更新结束后,问题解决。

alt + F5

利用后缀数组解决问题

Apr 03, 2016

在介绍后缀数组之前,以字符串"aabaaaab"为例建立两个字符串数组,

1
2
3
4
5
6
String[] before = new String[s.length()];
for(int i = 0; i < before.length; i++) {
  before[i] = s.substring(i);
}
java.util.Arrays.sort(before);
String[] after = before;

before数组的内容为,

1
{ "aabaaaab", "abaaaab", "baaaab", "aaaab", "aaab", "aab", "ab", "b" }

对before数组元素按字典序进行排序,得到after数组,

1
{ "aaaab", "aaab", "aab", "aabaaaab", "ab", "abaaaab", "b", "baaaab" }

后缀数组suffix的定义为,after[i]=before[suffix[i]-1],即排第几的元素是什么?

1
{ 4, 5, 6, 1, 7, 2, 8, 3 }

名次数组rank的定义为,before[i]=after[rank[i]-1],即该元素排第几?

1
{ 4, 6, 8, 1, 2, 3, 5, 7 }

height数组保存after数组中相邻元素之间的最长前缀,

1
2
3
4
5
6
7
8
9
10
11
String[] height = new String[after.length];
height[0] = "";
for(int i = 1; i < height.length; i++) {
  int x = 0, y = 0;
    for(; x < after[i - 1].length() && y < after[i].length(); x++, y++) {
      if(after[i - 1].charAt(x) != after[i].charAt(y)) {
          break;
        }
    }
  height[i] = after[i - 1].substring(0, x);
}
1
{ "", "aaa", "aa", "aab", "a", "ab", "", "b" }
可重叠最长重复子串

即为height数组中length最大的元素,"aaa""aab"

最长回文子串

将原字符串翻转后拼接到原字符串末尾,中间用特殊字符(如$)隔开,得到新的字符串"aabaaaab$baaaabaa",新字符串的height数组为,

1
{ "", "", "a", "aa", "aaaab", "aaa", "aaab", "aa", "aab", "aabaa", "a", "ab", "abaa", "", "b", "baa", "baaaab" }

最长回文子串即为height数组中length最大的元素,"baaaab"

最近公共祖先问题

Mar 28, 2016

从git merge说起

接着 使用Git协同工作 中的内容,使用伪代码描述git merge的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
找到目标分支current和被合并分支merge的共同祖先分支ancestor;
// 在fix_bug分支上执行git merge master
// master为目标分支,fix_bug为被合并分支
if(ancestor == merge) {
  return;
} else if(ancestor == current) {
  fast forward merge,分支current指向merge
} else {
  确定ancestor与merge的差异
  try {
      将差异合并到current
    } catch(合并出现矛盾) {
      添加矛盾标记,通知用户解决矛盾
        return;
    }
    根据current、merge创建新的子分支
    分支current指向新的子分支
}

Git的分支结构为树形结构,这里找到两个分支的共同祖先分支,即求树中两个节点的最近公共祖先

如果每个分支可以确定其父分支,即树中节点具有指向其父亲的parent指针,那问题可以转化为求两个链表的公共节点。两个节点分别使用parent指针遍历到根节点root,遍历步数之差即两节点到公共节点的步数之差,由步数较大的节点先遍历步数之差对应的步数,接着两节点同时使用parent指针遍历,当parent指针的指向一致时,其指向即所求的公共节点。可以进一步参考 两链表的第一个公共结点

如果树为平衡二叉树,根据BST的性质,从根节点root开始遍历,如果当前节点的值同时大于两节点的值,说明两节点都位于当前节点的左子树,接下来向左子树遍历;如果当前节点的值同时小于两节点的值,说明两节点都位于当前节点的右子树,接下来向右子树遍历;如果当前节点的值位于两节点的值之间,那当前节点即所求的公共节点。

下面介绍使用Tarjan离线算法解决上述问题,Tarjan算法基于深度遍历和并查集,先介绍并查集相关的内容。

并查集

并查集详解 (转) 非常生动地介绍了并查集相关的内容,这里给出并查集的Java实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class UnionFindSet {

  private int[] set;
  private static final int INIT_SET_SIZE = 8;
  /** 参数超出并查集下标范围,返回ERROR_INDEX */
  private static final int ERROR_INDEX = -1;

  public UnionFindSet() {
      this(INIT_SET_SIZE);
  }

  public UnionFindSet(int size) {
      set = new int[size];
      for (int i = 0; i < set.length; i++) {
          set[i] = i;
      }
  }

  /**
  * 等价于find(x, true)
  * 
  * @param x
  * @return
  */
  public int find(int x) {
      if (outOfLength(x)) {
          return ERROR_INDEX;
      }

      if (set[x] != x) {
          set[x] = find(set[x]);
      }
      return set[x];
  }

  /**
  * 
  * @param x
  * @param compress
  *            是否压缩路径
  * @return x超出并查集下标范围,返回ERROR_INDEX;否则返回x的根元素
  */
  public int find(int x, boolean compress) {
      if (outOfLength(x)) {
          return ERROR_INDEX;
      }
      if (compress) {
          return find(x);
      }

      int temp = x;
      while (set[temp] != temp) {
          temp = set[temp];
      }
      return temp;
  }

  /**
  * 
  * @param x
  * @param y
  * @return x、y的根元素是否相等
  */
  public boolean query(int x, int y) {
      if (outOfLength(x) || outOfLength(y)) {
          return false;
      }
      return find(x) == find(y);
  }

  /**
  * 将x、y合并到同一根元素下
  * 
  * @param x
  * @param y
  */
  public void join(int x, int y) {
      int prex = find(x);
      int prey = find(y);
      if (prex != ERROR_INDEX && prey != ERROR_INDEX && prex != prey) {
          set[prex] = prey;
      }
  }

  private boolean outOfLength(int index) {
      return index < 0 || index >= set.length;
  }

}
Tanjar离线算法

理解并查集之后,Tanjar算法即将并查集与深度遍历有效结合,伪代码描述原理为

1
2
3
4
5
6
7
8
9
Tarjan(u) {
  begin
      设置u号节点的祖先为u
        深度遍历u的子树
        访问每一条与u相关的询问u和v
          -若v已经被访问过则输出v当前的祖先t
        标记u为已被访问将所有u的子节点包括u本身的祖先改为u的父节点
    end
}

具体的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
/**
 *
 * @param node
 * @param queryList
 *            LCA查询条件列表,Query类的u、v属性为查询条件,即已知的两节点;ancestor属性保存查询结果,即公共节点
 */
public void tarjan(TreeNode node, ArrayList<Query> queryList) {
  node.ancestor = node;// 初始标记node的祖先为自己
  for (TreeNode child : node.children) {
      tarjan(child, queryList);// 深度遍历
      join(child, node);// 将子节点的祖先修改为node
  }

  node.searched = true;// 标记node已被访问
  // 遍历查询条件,如果查询条件与node相关且两节点已被访问,基于并查集查找公共节点
  for (Query query : queryList) {
      if (query.u == node) {
          if (query.v.searched) {
              query.ancestor = find(query.v);
          }
      } else if (query.v == node) {
          if (query.u.searched) {
              query.ancestor = find(query.u);
          }
      }
  }
}

对ConcurrentHashMap的解释

Feb 27, 2016

java.util.concurrent.ConcurrentHashMap整体为Segment数组,concurrentLevel为初始化参数,2sshift-1^ < concurrentLevel <= 2sshift^,Segment数组的大小ssize = 2sshift^,

1
2
3
4
5
6
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
  ++sshift;
  ssize <<= 1;
}

ssize * (c - 1) < initialCapacity <= ssize * c,每个数组元素Segment的容量cap与c的大小关系,和ssize与concurrentLevel的大小关系一致,

1
2
3
4
5
6
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
  ++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
  cap <<= 1;

初始化时,使用UNsafe.putOrderedObject将新建Segment存入数组的0下标处,

1
2
3
4
5
6
7
8
9
10
Class sc = Segment[].class;
SBASE = UNSAFE.arrayBaseOffset(sc);
ss = UNSAFE.arrayIndexScale(sc);
SSHIFT = 31 - Integer.numberOfLeadingZeros(ss);
// create segments and segments[0]
Segment<K,V> s0 =
  new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                  (HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

使用put方法添加新的键值对时,仍然先计算key的哈希值,hash方法与HashMap中的hash方法类似,添加了再哈希步骤,其目的是为了减少哈希冲突,使元素能够均匀的分布在不同的Segment上,从而提高容器的存取效率,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int hash(Object k) {
  int h = hashSeed;

    if ((0 != h) && (k instanceof String)) {
      return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // Spread bits to regularize both segment and index locations,
    // using variant of single-word Wang/Jenkins hash.
    h += (h <<  15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h <<   3);
    h ^= (h >>>  6);
    h += (h <<   2) + (h << 14);
    return h ^ (h >>> 16);
}

接着根据哈希值确定数组下标,这里的segmentShiftsegmentMask是初始化时根据sshiftssize确定的,

1
2
3
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
int j = (hash >>> segmentShift) & segmentMask;

反复确认j下标处的Segment是否为空,为空则依照0下标处的Segment新建Segment,使用UNSAFE.compareAndSwapObject更新null为新建Segment,

1
2
3
4
5
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
      == null) {
  if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
      break;
}

接下来的操作之前会尝试对该Segment加锁,Segment整体为HashEntry数组,每个数组元素为HashEntry链表,继续根据哈希值确定数组下标,遍历index下标处的HashEntry链表,

1
2
3
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);

与HashMap中的比较逻辑一致,确定key是否存在,

1
2
3
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
  //...
}

如果已存在,更新其对应的value;如果未存在,根据键值对新建HashEntry对象,使用UNsafe.putOrderedObject将新建HashEntry存入数组的index下标处。使用get方法获取键对应的值时,不需要对当前Segment加锁,类似地先确定Segment数组下标,再确定HashEntry数组下标,遍历链表,比较确定key是否存在,如果存在返回对应的value;不存在返回null

文章中表述不清的地方可以进一步参考聊聊并发(四)——深入分析ConcurrentHashMap,对于UNsafe类中的方法,可以参考Java中Unsafe类详解

对Hashtable和HashMap的补充解释

Feb 26, 2016

java.util.Hashtable使用synchronized关键字实现线程安全,value不可以为空,

1
2
3
if (value == null) {
  throw new NullPointerException();
}

key也不可为空,计算key的哈希值时会调用它的hashCode方法

1
2
3
4
private int hash(Object k) {
  // hashSeed will be zero if alternative hashing is disabled.
    return hashSeed ^ k.hashCode();
}

默认情况下hashSeed为0,即不使用alternative hashing,可以设置容量达到特定阀值时开始使用alternative hashing,这时,

1
hashSeed = sun.misc.Hashing.randomHashSeed(this);

Hashtable整体为数组tab,每个数组元素为Entry链表,使用put方法添加新的键值对时,首先计算key的哈希值,根据哈希值确定数组下标,

1
int index = (hash & 0x7FFFFFFF) % tab.length;

接着遍历index处的Entry链表,通过下面的比较逻辑确定key是否存在,

1
2
3
if ((e.hash == hash) && e.key.equals(key)) {
  //...
}

如果已存在,更新其对应的value;如果未存在,根据键值对新建Entry对象,插入到当前Entry链表的首位。使用get方法获取键对应的值时,类似地确定数组下标,遍历链表,比较确定key是否存在,如果存在返回对应的value;不存在返回null

容量大于设定的阀值时会调用rehash方法扩容,这时会重新计算每个键值对的数组下标及位置。

java.util.HashMapHashtable类似,区别在于无synchronized关键字,HashMap中key、value可以为空,key为null的键值对保存在数组的0下标处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final int hash(Object k) {
  int h = hashSeed;
  if (0 != h && k instanceof String) {
      return sun.misc.Hashing.stringHash32((String) k);
  }

  h ^= k.hashCode();

  // This function ensures that hashCodes that differ only by
  // constant multiples at each bit position have a bounded
  // number of collisions (approximately 8 at default load factor).
  h ^= (h >>> 20) ^ (h >>> 12);
  return h ^ (h >>> 7) ^ (h >>> 4);
}

HashMap中确定key是否存在的比较逻辑,

1
2
3
4
if (e.hash == hash &&
  ((k = e.key) == key || (key != null && key.equals(k)))) {
  //...
}

使用Git协同工作

Feb 18, 2016

本文没有深层次的分析Git的工作原理,而是从实用角度,关注常用命令,简单分析了执行命令时发生的具体动作。

建立本地仓库

从远端仓库克隆数据到本地仓库

1
git clone https://github.com/jemoii/Koenigspress.git Koenigspress

执行git clone命令,默认将远端仓库命名为origin

  • 拉取远端仓库的所有数据;
  • 创建一个指向远端仓库master分支的指针,并在本地将其命名为origin/master
  • 创建一个跟踪origin/master的本地master分支。

添加-o away选项,即执行

1
git clone -o away https://github.com/jemoii/Koenigspress.git Koenigspress

远端仓库被命名为away,相应的本地指向远端仓库master分支的指针被命名为away/master

回到上一步,可以通过git remote show origin查看远端仓库的相关信息,这里的origin即远端仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git remote
origin

$ git remote show origin
* remote origin
  Fetch URL: https://github.com/jemoii/Koenigspress.git
  Push  URL: https://github.com/jemoii/Koenigspress.git
  HEAD branch: master
  Remote branch:
    master tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

管理本地分支

现在执行git branch optimize_thememaster上新建optimize_theme分支,执行git checkout optimize_theme切换到optimize_theme分支上。

可以使用git checkout -b optimize_theme合并上述两步操作,执行git branch显示本地仓库分支列表,*标识当前位于optimize_theme分支

1
2
3
4
5
6
$ git checkout -b optimize_theme
Switched to a new branch 'optimize_theme'

$ git branch
  master
* optimize_theme

提交更新

修改文件后,执行git add .将修改内容添加到暂存区,接着执行git commit -m '提交主题优化'将修改内容提交到本地仓库

可以使用git commit -a -m '提交主题优化'合并上述两步操作。

现在使用git push origin optimize_theme将修改内容推送到远端仓库,远端仓库新建分支optimize保存修改内容

1
2
3
4
5
6
$ git push origin optimize_theme
Username for 'https://github.com': jemoii
Password for 'https://jemoii@github.com':
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/jemoii/Koenigspress.git
 * [new branch]      optimize_theme -> optimize_theme

现在执行git remote show origin查看远端仓库信息

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git remote show origin
* remote origin
  Fetch URL: https://github.com/jemoii/Koenigspress.git
  Push  URL: https://github.com/jemoii/Koenigspress.git
  HEAD branch: master
  Remote branches:
    master         tracked
    optimize_theme tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local refs configured for 'git push':
    master         pushes to master         (up to date)
    optimize_theme pushes to optimize_theme (up to date)

可以看到除了master分支,现在本地分支optimize_theme跟踪origin/optimize_theme

更新本地仓库

如果有另外一个协同者与我们一样克隆了远端仓库的数据到他的本地仓库,新建fix_bug分支,类似的将修改内容推送到远端仓库,且在我们执行git push前将修改内容合并到远端仓库的master分支,现在远端仓库master分支上的内容与我们执行git clone时相比发生了变化,我们再执行git push时会提示需要先将协同者的修改内容合并到我们的分支上。

首先执行git checkout master切换到master分支,执行git fetch origin

  • 从远端仓库获取本地仓库没有的数据,移动origin/master指针指向更新后的位置;
  • 创建一个指向远端仓库fix_bug分支的指针,并在本地将其命名为origin/fix_bug,本地不会新建fix_bug分支,只创建不可编辑的origin/fix_bug指针;

现在执行git merge origin/master将更新的数据合并到本地仓库的master分支,接着执行git checkout optimize_theme重新切换到optimize_theme分支,执行git merge master将更新的数据合并到本地仓库的optimize_theme分支。

现在执行git push origin optimize_theme可以将我们的修改内容推送到远端仓库。

回到上一步,在master分支上使用git pull可以合并git fetch origingit merge origin/master两步操作。

关于git merge

协同开发中创建工作分支的两种方式,第一种是前面提到的在本地仓库新建工作分支,执行git push时在远端仓库创建对应的工作分支;另一种是在远端仓库新建工作分支,执行新建/更新时在本地仓库创建指向远端仓库工作分支的指针,接着执行类似如下的命令,

1
git checkout -b fix_bug origin/fix_bug

在本地仓库创建fix_bug分支,跟踪origin/fix_bug,执行结束后切换到fix_bug分支。

让CAS服务端跑起来

Dec 11, 2015

参考资料:安装 CAS 服务器

注释

1、下载方式

http://central.maven.org/maven2/org/jasig/cas/cas-server-webapp/页面选择合适的版本,下载对应目录下的cas-server-webapp-version.war。

2、初始登录

在/WEB-INF/deployerConfigContext.xml中的

1
2
3
4
5
6
7
<bean id="primaryAuthenticationHandler" class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
    <property name="users">
        <map>
            <entry key="casuser" value="Mellon"/>
        </map>
    </property>
</bean>

配置了初始认证方式,这里的username/password即casuser/Mellon。

修改认证方式
配置CAS服务端通过查询数据库的方式认证用户。

首先与Spring应用一样配置数据库,这里使用的是PostgreSQL数据库。

1
2
3
4
5
6
7
8
9
<bean id="casDataSource" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="org.postgresql.Driver" />
    <property name="url" value="jdbc:postgresql://localhost:5432/jemoii" />
    <property name="username" value="postgres" />
    <property name="password" value="password" />
    <property name="maxActive" value="20" />
    <property name="maxIdle" value="10" />
    <property name="maxWait" value="-1" />
</bean>
建立新的认证方式jdbcAuthenticationHandler,
1
2
3
4
5
6
7
<bean id="jdbcAuthenticationHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
      <property name="dataSource" ref="casDataSource" />
      <property name="sql" value="select password from user_info where email = ?" />
      <property name="passwordEncoder">
          <bean class="me.voler.cas.AddSaltPasswordEncoder"/>
      </property>
</bean>
  • dataSource属性即前面配置的数据库;
  • sql属性是一条SQL语句,功能即根据登录使用的username查询password,根据实际的table做相应的修改;
  • passwordEncoder属性是实现了org.jasig.cas.authentication.handler.PasswordEncoder接口的Bean。

CAS本身提供了org.jasig.cas.authentication.handler.DefaultPasswordEncoder、org.jasig.cas.authentication.handler.PlainTextPasswordEncoder两种实现,这里根据实际的加密方式新建了me.voler.cas.AddSaltPasswordEncoder,即在rawPassword后面拼接自定义PASSWORD_SALT后再使用MD5加密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package me.voler.cas;

import org.jasig.cas.authentication.handler.DefaultPasswordEncoder;
import org.jasig.cas.authentication.handler.PasswordEncoder;

public class AddSaltPasswordEncoder implements PasswordEncoder {

    private static final String PASSWORD_SALT = "xx";

    @Override
    public String encode(String password) {
        DefaultPasswordEncoder encoder = new DefaultPasswordEncoder("MD5");
        return encoder.encode(password + PASSWORD_SALT);
    }

}
应用新的认证方式jdbcAuthenticationHandler

将deployerConfigContext.xml中id为authenticationManager的Bean的

1
2
3
4
5
6
<constructor-arg>
    <map>
        <entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
        <entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />
    </map>
</constructor-arg>

修改为

1
2
3
4
5
6
<constructor-arg>
    <map>
        <entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
        <entry key-ref="jdbcAuthenticationHandler" value-ref="primaryPrincipalResolver" />
    </map>
</constructor-arg>

添加修改认证方式依赖的jar,在/WEB-INF/lib目录下添加

  • commons-pool-version.jar
  • commons-dbcp-version.jar
  • cas-server-support-jdbc-version.jar

现在可以使用数据库中的username/password登录CAS服务端。

Apache POI使用示例

Dec 05, 2015

读取记事本(txt)中的内容时,一直出现乱码,原因在于记事本的默认编码是GBK,不是UTF-8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package me.voler.jechat.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.extractor.ExcelExtractor;
import org.apache.poi.xssf.extractor.XSSFExcelExtractor;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.xmlbeans.XmlException;

/**
 * 支持取.txt、.doc、.docx、.xls、.xlsx,5种格式文档的内容,以字符串的形式返回。
 * {@link http://poi.apache.org}
 * 
 */
public class DocumentExtraction {

    /**
     * 取微软记事本中的内容,以字符串的形式返回。记事本中内容的编码为GBK
     * 
     * @param file
     * @return
     */
    public static String txt2String(File file) {
        StringBuffer buffer = new StringBuffer();
        try {
            InputStreamReader input = new InputStreamReader(new FileInputStream(file), "GBK");
            BufferedReader reader = new BufferedReader(input);

            String line = null;
            while ((line = reader.readLine()) != null) {
                buffer.append(line).append('\n');
            }
            reader.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return buffer.toString();
    }

    /**
     * 取Excel xlsx中的内容,以字符串的形式返回。{@link http://poi.apache.org}
     * 
     * @param file
     * @return
     */
    @SuppressWarnings("resource")
    public static String xlsx2String(File file) {

        XSSFWorkbook xlsxwb = new XSSFWorkbook();
        try {
            OPCPackage pkg = OPCPackage.open(new FileInputStream(file));
            xlsxwb = new XSSFWorkbook(pkg);
        } catch (InvalidFormatException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        ExcelExtractor extractor = new XSSFExcelExtractor(xlsxwb);
        extractor.setFormulasNotResults(true);
        extractor.setIncludeSheetNames(false);

        return extractor.getText();
    }

    /**
     * 取Excel xls中的内容,以字符串的形式返回。{@link http://poi.apache.org}
     * 
     * @param file
     * @return
     */
    @SuppressWarnings("resource")
    public static String xls2String(File file) {

        HSSFWorkbook xlswb = new HSSFWorkbook();
        try {
            POIFSFileSystem fileSystem = new POIFSFileSystem(new FileInputStream(file));
            xlswb = new HSSFWorkbook(fileSystem);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        ExcelExtractor extractor = new org.apache.poi.hssf.extractor.ExcelExtractor(xlswb);
        extractor.setFormulasNotResults(true);
        extractor.setIncludeSheetNames(false);

        return extractor.getText();
    }

    /**
     * 取Word doc中的内容,以字符串的形式返回。{@link http://poi.apache.org}
     * 
     * @param file
     * @return
     */
    public static String doc2String(File file) {
        StringBuffer buffer = new StringBuffer();

        WordExtractor extractor = null;
        POIFSFileSystem fileSystem;
        try {
            fileSystem = new POIFSFileSystem(new FileInputStream(file));
            extractor = new WordExtractor(fileSystem);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        String[] paragraphText = extractor.getParagraphText();
        for (String paragraph : paragraphText) {
            buffer.append(paragraph);
        }

        return buffer.toString();
    }

    /**
     * 取Word docx中的内容,以字符串的形式返回。{@link http://poi.apache.org}
     * 
     * @param file
     * @return
     */
    public static String docx2String(File file) {

        XWPFWordExtractor extractor = null;
        try {
            OPCPackage pkg = OPCPackage.open(new FileInputStream(file));
            extractor = new XWPFWordExtractor(pkg);
        } catch (InvalidFormatException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (XmlException e) {
            e.printStackTrace();
        } catch (OpenXML4JException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return extractor.getText();
    }

}

pom.xml中的依赖包配置,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
    ...
  <!-- xls -->
  <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi</artifactId>
      <version>3.12</version>
  </dependency>
  <!-- doc -->
  <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-scratchpad</artifactId>
      <version>3.12</version>
  </dependency>
  <!-- xlsx docx -->
  <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>3.12</version>
  </dependency>
</dependencies>

使用Commons Email发送邮件以及与Spring的结合

Dec 04, 2015

使用Maven构建项目,在pom.xml中添加依赖包,

1
2
3
4
5
6
7
8
<dependencies>
    ...
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-email</artifactId>
        <version>1.3.3</version>
    </dependency>
</dependencies>

参考User guide,基于QQ邮箱发送文本邮件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
    Email email = new SimpleEmail();
    // 连接参数配置
    email.setHostName("smtp.qq.com");
    email.setSmtpPort(465);
    email.setAuthenticator(new DefaultAuthenticator("QQ No", "QQ Password");
    email.setSSLOnConnect(true);
    // 邮件相关内容
    try {
        email.setFrom("from@qq.com");
        email.setMsg("Hello, world!");
        email.addTo("to@example.com");
        email.send();
    } catch (EmailException e) {

    }
}

与使用JDBC连接数据库一样,前面的代码包含大量的模板操作,所以结合Spring,与JdbcTemplate类似,构建MailTemplate。在applicationContext-mail.xml中声明Bean,

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
<beans...>
    <bean
        class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:mail.properties</value>
            </list>
        </property>
    </bean>

    <bean id="simpleMail" class="org.apache.commons.mail.SimpleEmail">
        <property name="hostName" value="${me.mail.hostName}" />
        <property name="smtpPort" value="${me.mail.smtpPort}" />
        <property name="from" value="${me.mail.from}" />
        <property name="authenticator">
            <bean class="org.apache.commons.mail.DefaultAuthenticator">
                <constructor-arg value="${me.mail.authenticator.userName}" />
                <constructor-arg value="${me.mail.authenticator.password}" />
            </bean>
        </property>
    </bean>

    <bean id="mailTemplate" class="me.voler.jechat.core.MailTemplate">
        <property name="simpleMail" ref="simpleMail" />
    </bean>
</beans>

将连接参数添加到mail.properties,me.voler.jechat.core.MailTemplate即构建的MailTemplate,

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
package me.voler.jechat.core;

import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.SimpleEmail;

public class MailTemplate {

    private SimpleEmail simpleMail;

    public void send(String msg, String to) {
        this.send("【邮件】", msg, to);
    }

    /**
     * 
     * @param subject 邮件主题
     * @param msg 邮件正文
     * @param toList 收件人列表
     */
    public void send(String subject, String msg, String... toList) {

        try {
            simpleMail.setSubject(subject);
            simpleMail.setMsg(msg);
            simpleMail.addTo(toList);

            simpleMail.setSSLOnConnect(true);
            simpleMail.send();
        } catch (EmailException e) {
            e.printStackTrace();
        }

    }

    public SimpleEmail getSimpleMail() {
        return simpleMail;
    }

    public void setSimpleMail(SimpleEmail simpleMail) {
        this.simpleMail = simpleMail;
    }

}

使用构建的MailTemplate发送文本邮件,

1
2
3
4
5
6
7
@Autowired
@Qualifier("mailTemplate")
private MailTemplate mailTemplate;

public void sendEmptyMail() {
    mailTemplate.send("中文测试,English Test.", "to@example.com");
}

SimpleEmail的父类的属性sslOnConnect的set方法名为setSSLOnConnect,如果直接在applicationContext-mail.xml的simpleMailBean下配置<property name="sslOnConnect" value="${me.mail.sslOnConnect}" />会提示

Bean property is not writable or has an invalid setter method