2009年3月18日
HibernateShard
多數據庫水平分區解決方案。
1. 簡介
Hibernate 的一個擴展,用于處理多數據庫水平分區架構。
由google工程師 2007年 捐獻給 Hibernate社區。
http://www.hibernate.org/414.html
目前版本: 3.0.0 beta2, 未發GA版。
條件:Hibernate Core 3.2, JDK 5.0
2. 水平分區原理
一個庫表如 Order 存在于多個數據庫實例上。按特定的分區邏輯,將該庫表的數據存儲在這些實例中,一條記錄的主鍵 PK,在所有實例中不得重復。
水平分區在大型網站,大型企業應用中經常采用。 像
www.sina.com.cn ,www.163.com
www.bt285.cn www.guihua.org
目的出于海量數據分散存儲,分散操作,分散查詢以便提高數據處理量和整體數據處理性能。
使用:
google工程師的設計還是非常好的,完全兼容 Hibernate本身的主要接口。
- org.hibernate.Session
- org.hibernate.SessionFactory
- org.hibernate.Criteria
- org.hibernate.Query
org.hibernate.Session
org.hibernate.SessionFactory
org.hibernate.Criteria
org.hibernate.Query
因此程序員開發變化不大,甚至不需要關心后臺使用了分區數據庫。程序遷移問題不大。而且配置上比較簡明。
3. 三種策略:
1) ShardAccessStrategy, 查詢操作時,到那個分區執行。
默認提供兩個實現:
順序策略:SequentialShardAccessStrategy, 每個query按順序在所有分區上執行。
平行策略:ParallelShardAccessStrategy, 每個query以多線程方式并發平行的在所有分區上執行。 此策略下,需要使用線程池機制滿足特定的性能需要,java.util.concurrent.ThreadPoolExecutor。
2) ShardSelectionStrategy, 新增對象時,存儲到哪個分區。
框架默認提供了一個輪詢選擇策略 RoundRobinShardSelectionStrategy, 但一般不這樣使用。
通常采用“attribute-based sharding”機制,基于屬性分區。一般是用戶根據表自己實現一個基于屬性分區的策略類ShardSelectionStrategy ,例如,以下WeatherReport基于continent屬性選擇分區:
- public class WeatherReportShardSelectionStrategy implements ShardSelectionStrategy {
- public ShardId selectShardIdForNewObject(Object obj) {
- if(obj instanceof WeatherReport) {
- return ((WeatherReport)obj).getContinent().getShardId();
- }
- throw new IllegalArgumentException();
- }
public class WeatherReportShardSelectionStrategy implements ShardSelectionStrategy {
public ShardId selectShardIdForNewObject(Object obj) {
if(obj instanceof WeatherReport) {
return ((WeatherReport)obj).getContinent().getShardId();
}
throw new IllegalArgumentException();
}
}
3) ShardResolutionStrategy, 該策略用于查找單個對象時,判斷它在哪個或哪幾個分區上。
默認使用 AllShardsShardResolutionStrategy ,可以自定義例如:
- public class WeatherReportShardResolutionStrategy extends AllShardsShardResolutionStrategy {
- public WeatherReportShardResolutionStrategy(List<ShardId> shardIds) {
- super(shardIds);
- }
-
- public List<ShardId> selectShardIdsFromShardResolutionStrategyData(
- ShardResolutionStrategyData srsd) {
- if(srsd.getEntityName().equals(WeatherReport.class.getName())) {
- return Continent.getContinentByReportId(srsd.getId()).getShardId();
- }
- return super.selectShardIdsFromShardResolutionStrategyData(srsd);
- }
- }
public class WeatherReportShardResolutionStrategy extends AllShardsShardResolutionStrategy {
public WeatherReportShardResolutionStrategy(List<ShardId> shardIds) {
super(shardIds);
}
public List<ShardId> selectShardIdsFromShardResolutionStrategyData(
ShardResolutionStrategyData srsd) {
if(srsd.getEntityName().equals(WeatherReport.class.getName())) {
return Continent.getContinentByReportId(srsd.getId()).getShardId();
}
return super.selectShardIdsFromShardResolutionStrategyData(srsd);
}
}
4. 水平分區下的查詢
對于簡單查詢 HibernateShard 可以滿足。
水平分區下多庫查詢是一個挑戰。主要存在于以下三種操作:
1) distinct
因為需要遍歷所有shard分區,并進行合并判斷重復記錄。
2) order by
類似 1)
3) aggregation
count,sim,avg等聚合操作先分散到分區執行,再進行匯總。
是不是有點類似于 MapReduce ? 呵呵。
目前 HibernateShard 不支持 1), 2), 對 3) 部分支持
HibernateShard 目前通過 Criteria 接口的實現對 聚合提供了較好的支持, 因為 Criteria 以API接口指定了 Projection 操作,邏輯相對簡單。
而HQL,原生 SQL 還不支持此類操作。
5. 再分區和虛擬分區
當數據庫規模增大,需要調整分區邏輯和數據存儲時, 需要再分區。
兩種方式: 1)數據庫數據遷移其他分區; 2) 改變記錄和分區映射關系。這兩種方式都比較麻煩。尤其“改變記錄和分區映射關系”,需要調整 ShardResolutionStrategy。
HibernateShard 提供了一種虛擬分區層。當需要調整分區策略時,只需要調整虛擬分區和物理分區映射關系即可。以下是使用虛擬分區時的配置創建過程:
-
- Map<Integer, Integer> virtualShardMap = new HashMap<Integer, Integer>();
- virtualShardMap.put(0, 0);
- virtualShardMap.put(1, 0);
- virtualShardMap.put(2, 1);
- virtualShardMap.put(3, 1);
- ShardedConfiguration shardedConfig =
- new ShardedConfiguration(
- prototypeConfiguration,
- configurations,
- strategyFactory,
- virtualShardMap);
- return shardedConfig.buildShardedSessionFactory();
Map<Integer, Integer> virtualShardMap = new HashMap<Integer, Integer>();
virtualShardMap.put(0, 0);
virtualShardMap.put(1, 0);
virtualShardMap.put(2, 1);
virtualShardMap.put(3, 1);
ShardedConfiguration shardedConfig =
new ShardedConfiguration(
prototypeConfiguration,
configurations,
strategyFactory,
virtualShardMap);
return shardedConfig.buildShardedSessionFactory();
6. 局限:
1)HibernateShard 不支持垂直分區, 垂直+水平混合分區。
2) 水平分區下 查詢功能受到一定限制,有些功能不支持。實踐中,需要在應用層面對水平分區算法進行更多的考慮。
3) 不支持跨分區的 關系 操作。例如:刪除A分區上的 s 表,B分區上的關聯子表 t的記錄無法進行參照完整性約束檢查。 (其實這個相對 跨分區查詢的挑戰應該說小的多,也許google工程師下個版本會支持,呵呵)
4) 解析策略接口似乎和對象ID全局唯一性有些自相矛盾,
AllShardsShardResolutionStrategy 的接口返回的是給定對象ID所在的 shard ID集合,按理應該是明確的一個 shard ID.
參考資料:HibernateShard 參考指南。
現在正開發的定位模塊用到的定位設置是塞格車圣導航設備,發送指令返回的經緯度需要轉換成十進制,再到GIS系統獲取地理信息描述。以后需要要經常用到這方面的知識,隨筆寫下。
將經緯度轉換成十進制
公式:
Decimal Degrees = Degrees + minutes/60 + seconds/3600
例:57°55'56.6" =57+55/60+56.6/3600=57.9323888888888
如把經緯度 (longitude,latitude) (205.395583333332,57.9323888888888)轉換據成坐標(Degrees,minutes,seconds)(205°23'44.1",57°55'56.6")。
步驟如下:
1、 直接讀取"度":205
2、(205.395583333332-205)*60=23.734999999920 得到"分":23
3、(23.734999999920-23)*60=44.099999995200 得到"秒":44.1
發送定位指令,終端返回的經緯度信息如下:
(ONE072457A3641.2220N11706.2569E000.000240309C0000400)
按照協議解析

獲得信息體的經緯度是主要,其它不要管,直接用String類的substring()方法截掉,獲取的經緯度
3641.2220N11706.2569E http://www.bt285.cn
- package com.tdt.test;
-
- import com.tdt.api.gis.LocationInfo;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- public class LonlatConversion {
-
-
-
-
-
-
-
- public static String xypase(String dms, String type) {
- if (dms == null || dms.equals("")) {
- return "0.0";
- }
- double result = 0.0D;
- String temp = "";
-
- if (type.equals("E")) {
- String e1 = dms.substring(0, 3);
-
- String e2 = dms.substring(3, dms.length());
-
- result = Double.parseDouble(e1);
- result += (Double.parseDouble(e2) / 60.0D);
- temp = String.valueOf(result);
- if (temp.length() > 9) {
- temp = e1 + temp.substring(temp.indexOf("."), 9);
- }
- } else if (type.equals("N")) {
- String n1 = dms.substring(0, 2);
- String n2 = dms.substring(2, dms.length());
-
- result = Double.parseDouble(n1);
- result += Double.parseDouble(n2) / 60.0D;
- temp = String.valueOf(result);
- if (temp.length() > 8) {
- temp = n1 + temp.substring(temp.indexOf("."), 8);
- }
- }
- return temp;
- }
- public static void main(String[] args) {
- String info="(ONE072457A3641.2220N11706.2569E000.000240309C0000400)";
- info=info.substring(11,info.length()-13);
-
- String N = info.substring(0, info.indexOf("N"));
-
- String E = info.substring(info.indexOf("N")+1,info.indexOf("E"));
-
- double x = Double.parseDouble(CoordConversion.xypase(E,"E"));
- double y = Double.parseDouble(CoordConversion.xypase(N,"N"));
- String result =LocationInfo.getLocationInfo("test", x, y);
- System.out.println(result);
- }
- }
package com.tdt.test;
import com.tdt.api.gis.LocationInfo;
/**
* <p>Title:坐標轉換 </p>
*
* <p>Description:</p>
*
* <p>Copyright: Copyright (c) 2009</p>
*
* <p>Company:</p>
*
* @author sunnylocus
* @version 1.0 [2009-03-24]
*
*/
public class LonlatConversion {
/**
*
* @param dms 坐標
* @param type 坐標類型
* @return String 解析后的經緯度
*/
public static String xypase(String dms, String type) {
if (dms == null || dms.equals("")) {
return "0.0";
}
double result = 0.0D;
String temp = "";
if (type.equals("E")) {//經度
String e1 = dms.substring(0, 3);//截取3位數字,經度共3位,最多180度
//經度是一倫敦為點作南北兩極的線為0度,所有往西和往東各180度
String e2 = dms.substring(3, dms.length());//需要運算的小數
result = Double.parseDouble(e1);
result += (Double.parseDouble(e2) / 60.0D);
temp = String.valueOf(result);
if (temp.length() > 9) {
temp = e1 + temp.substring(temp.indexOf("."), 9);
}
} else if (type.equals("N")) { //緯度,緯度是以赤道為基準,相當于把地球分兩半,兩個半球面上的點和平面夾角0~90度
String n1 = dms.substring(0, 2);//截取2位,緯度共2位,最多90度
String n2 = dms.substring(2, dms.length());
result = Double.parseDouble(n1);
result += Double.parseDouble(n2) / 60.0D;
temp = String.valueOf(result);
if (temp.length() > 8) {
temp = n1 + temp.substring(temp.indexOf("."), 8);
}
}
return temp;
}
public static void main(String[] args) {
String info="(ONE072457A3641.2220N11706.2569E000.000240309C0000400 http://www.guihua.org )";
info=info.substring(11,info.length()-13);
//緯度
String N = info.substring(0, info.indexOf("N"));
//經度
String E = info.substring(info.indexOf("N")+1,info.indexOf("E"));
//請求gis,獲取地理信息描述
double x = Double.parseDouble(CoordConversion.xypase(E,"E"));
double y = Double.parseDouble(CoordConversion.xypase(N,"N"));
String result =LocationInfo.getLocationInfo("test", x, y); //System.out.println("徑度:"+x+","+"緯度:"+y);
System.out.println(result);
}
}
運行結果
在濟南市,位于輕騎路和八澗堡路附近;在環保科技園國際商務中心和濟南市區賢文莊附近。
一、本圖片生成器具有以下功能特性:
1、可以設置圖片的寬度、高度、外框顏色、背景色;
2、可以設置圖片字體的大小、名稱、顏色;
3、可以設置輸出圖片的格式,如JPEG、GIF等;
4、可以將圖片存儲到一個文件或者存儲到一個輸出流;
5、可以為圖片增加若干條干擾線(在生成隨機碼圖片時可用此特性);
6、打印在圖片上的文字支持自動換行;
另外,本圖片生成器還用到了模板方法模式。
二、下面列出相關的源代碼
1、抽象類AbstractImageCreator的源代碼
- public abstract class AbstractImageCreator {
- private static Random rnd = new Random(new Date().getTime());
-
-
- private int width = 200;
-
-
- private int height = 80;
-
-
- private Color rectColor;
-
-
- private Color bgColor;
-
-
- private int lineNum = 0;
-
-
- private String formatName = "JPEG";
-
-
- private Color fontColor = new Color(0, 0, 0);
-
-
- private String fontName = "宋體";
-
-
- private int fontSize = 15;
-
-
-
-
-
-
-
-
- private void drawRandomLine(Graphics graph){
- for(int i=0;i<lineNum;i++){
-
- graph.setColor(getRandomColor(100, 155));
-
-
- int x1 = rnd.nextInt(width);
- int y1 = rnd.nextInt(height);
-
- int x2 = rnd.nextInt(width);
- int y2 = rnd.nextInt(height);
-
-
- graph.drawLine(x1, y1, x2, y2);
- }
- }
-
-
-
-
- private Color getRandomColor(int base, int range){
- if((base + range) > 255) range = 255 - base;
-
- int red = base + rnd.nextInt(range);
- int green = base + rnd.nextInt(range);
- int blue = base + rnd.nextInt(range);
-
- return new Color(red, green, blue);
- }
-
-
- public void drawImage(String text)throws IOException{
- BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
-
- if(rectColor == null) rectColor = new Color(0, 0, 0);
- if(bgColor == null) bgColor = new Color(240, 251, 200);
-
-
- Graphics graph = image.getGraphics();
-
-
- graph.setColor(bgColor);
- graph.fillRect(0, 0, width, height);
-
-
- graph.setColor(rectColor);
- graph.drawRect(0, 0, width-1, height-1);
-
-
- drawRandomLine(graph);
-
-
- drawString(graph, text);
-
-
- graph.dispose();
-
-
- saveImage(image);
- }
-
- protected abstract void drawString(Graphics graph, String text);
-
- protected abstract void saveImage(BufferedImage image)throws IOException;
-
- }
public abstract class AbstractImageCreator {
private static Random rnd = new Random(new Date().getTime());
//圖片寬度
private int width = 200;
//圖片高度
private int height = 80;
//外框顏色
private Color rectColor;
//背景色
private Color bgColor;
//干擾線數目
private int lineNum = 0;
//圖片格式
private String formatName = "JPEG";
//字體顏色
private Color fontColor = new Color(0, 0, 0);
//字體名稱
private String fontName = "宋體";
//字體大小
private int fontSize = 15;
//##### 這里省略成員變臉的get、set方法 #####
/**
* 畫干擾線
*/
private void drawRandomLine(Graphics graph){
for(int i=0;i<lineNum;i++){
//線條的顏色
graph.setColor(getRandomColor(100, 155));
//線條兩端坐標值
int x1 = rnd.nextInt(width);
int y1 = rnd.nextInt(height);
int x2 = rnd.nextInt(width);
int y2 = rnd.nextInt(height);
//畫線條
graph.drawLine(x1, y1, x2, y2);
}
}
/**
* 隨機獲取顏色對象
*/
private Color getRandomColor(int base, int range){
if((base + range) > 255) range = 255 - base;
int red = base + rnd.nextInt(range);
int green = base + rnd.nextInt(range);
int blue = base + rnd.nextInt(range);
return new Color(red, green, blue);
}
//該方法內應用了模板方法模式
public void drawImage(String text)throws IOException{
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
if(rectColor == null) rectColor = new Color(0, 0, 0);
if(bgColor == null) bgColor = new Color(240, 251, 200);
//獲取畫布
Graphics graph = image.getGraphics();
//畫長方形
graph.setColor(bgColor);
graph.fillRect(0, 0, width, height);
//外框
graph.setColor(rectColor);
graph.drawRect(0, 0, width-1, height-1);
//畫干擾線
drawRandomLine(graph);
//畫字符串
drawString(graph, text);
//執行
graph.dispose();
//輸出圖片結果
saveImage(image);
}
protected abstract void drawString(Graphics graph, String text);
protected abstract void saveImage(BufferedImage image)throws IOException;
}
2、類DefaultImageCreator的源代碼
該類將生成的圖片存儲到一個文件中,需要設置outputFilePath成員變量值,該成員變量值表示圖片的存儲全路徑。
- public class DefaultImageCreator extends AbstractImageCreator {
- private String outputFilePath;
-
- public String getOutputFilePath() {
- return outputFilePath;
- }
-
- public void setOutputFilePath(String outputFilePath) {
- this.outputFilePath = outputFilePath;
- }
-
- public DefaultImageCreator(){
-
- }
-
- public DefaultImageCreator(String outputFilePath){
- this.outputFilePath = outputFilePath;
- }
-
- @Override
- protected void drawString(Graphics graph, String text) {
- graph.setColor(getFontColor());
- Font font = new Font(getFontName(), Font.PLAIN, getFontSize());
- graph.setFont(font);
-
- FontMetrics fm = graph.getFontMetrics(font);
- int fontHeight = fm.getHeight();
-
- int offsetLeft = 0;
- int rowIndex = 1;
- for(int i=0;i<text.length();i++){
- char c = text.charAt(i);
- int charWidth = fm.charWidth(c);
-
-
- if(Character.isISOControl(c) || offsetLeft >= (getWidth()-charWidth)){
- rowIndex++;
- offsetLeft = 0;
- }
-
- graph.drawString(String.valueOf(c), offsetLeft, rowIndex * fontHeight);
- offsetLeft += charWidth;
- }
- }
-
- @Override
- protected void saveImage(BufferedImage image)throws IOException{
- ImageIO.write(image, getFormatName(), new File(outputFilePath));
- }
-
- }
public class DefaultImageCreator extends AbstractImageCreator {
private String outputFilePath;
public String getOutputFilePath() {
return outputFilePath;
}
public void setOutputFilePath(String outputFilePath) {
this.outputFilePath = outputFilePath;
}
public DefaultImageCreator(){
}
public DefaultImageCreator(String outputFilePath){
this.outputFilePath = outputFilePath;
}
@Override
protected void drawString(Graphics graph, String text) {
graph.setColor(getFontColor());
Font font = new Font(getFontName(), Font.PLAIN, getFontSize());
graph.setFont(font);
FontMetrics fm = graph.getFontMetrics(font);
int fontHeight = fm.getHeight(); //字符的高度
int offsetLeft = 0;
int rowIndex = 1;
for(int i=0;i<text.length();i++){
char c = text.charAt(i);
int charWidth = fm.charWidth(c); //字符的寬度
//另起一行
if(Character.isISOControl(c) || offsetLeft >= (getWidth()-charWidth)){
rowIndex++;
offsetLeft = 0;
}
graph.drawString(String.valueOf(c), offsetLeft, rowIndex * fontHeight);
offsetLeft += charWidth;
}
}
@Override
protected void saveImage(BufferedImage image)throws IOException{
ImageIO.write(image, getFormatName(), new File(outputFilePath));
}
}
3、類OutputStreamImageCreator的源代碼
該類將生成的圖片存儲到一個輸出流中,需要設置out成員變量值。
- public class OutputStreamImageCreator extends DefaultImageCreator {
- private OutputStream out ;
-
- public OutputStream getOut() {
- return out;
- }
-
- public void setOut(OutputStream out) {
- this.out = out;
- }
-
- public OutputStreamImageCreator(){
-
- }
-
- public OutputStreamImageCreator(OutputStream out){
- this.out = out;
- }
-
- @Override
- public String getOutputFilePath() {
- return null;
- }
-
- @Override
- public void setOutputFilePath(String outputFilePath) {
- outputFilePath = null;
- }
-
- @Override
- protected void saveImage(BufferedImage image) throws IOException {
- if(out!=null) ImageIO.write(image, getFontName(), out);
- }
-
- }
public class OutputStreamImageCreator extends DefaultImageCreator {
private OutputStream out ;
public OutputStream getOut() {
return out;
}
public void setOut(OutputStream out) {
this.out = out;
}
public OutputStreamImageCreator(){
}
public OutputStreamImageCreator(OutputStream out){
this.out = out;
}
@Override
public String getOutputFilePath() {
return null;
}
@Override
public void setOutputFilePath(String outputFilePath) {
outputFilePath = null;
}
@Override
protected void saveImage(BufferedImage image) throws IOException {
if(out!=null) ImageIO.write(image, getFontName(), out);
}
}
三、實例代碼
1、圖片存儲到文件
StringBuffer sb = new StringBuffer();
- sb.append("中華人民共和國\n");
- sb.append("中華人民共和國\n");
-
- DefaultImageCreator creator = new DefaultImageCreator("c:\\img.jpeg");
- creator.setWidth(150);
- creator.setHeight(100);
- creator.setLineNum(60);
- creator.setFontSize(20);
- creator.drawImage(sb.toString());
StringBuffer sb = new StringBuffer();
sb.append("中華人民共和國\n");
sb.append("中華人民共和國\n");
DefaultImageCreator creator = new DefaultImageCreator("c:\\img.jpeg");
creator.setWidth(150);
creator.setHeight(100);
creator.setLineNum(60);
creator.setFontSize(20);
creator.drawImage(sb.toString());

Java程序的源代碼很容易被別人偷看,只要有一個反編譯器,任何人都可以分析別人的代碼。本文討論如何在不修改原有程序的情況下,通過加密技術保護源代碼。
一、為什么要加密?
對于傳統的C或C++之類的語言來說,要在Web上保護源代碼是很容易的,只要不發布它就可以。遺憾的是,Java程序的源代碼很容易被別人偷看。只要有一個反編譯器,任何人都可以分析別人的代碼。Java的靈活性使得源代碼很容易被竊取,但與此同時,它也使通過加密保護代碼變得相對容易,我們唯一需要了解的就是Java的ClassLoader對象。當然,在加密過程中,有關Java Cryptography Extension(JCE)的知識也是必不可少的。
有幾種技術可以“模糊”Java類文件,使得反編譯器處理類文件的效果大打折扣。然而,修改反編譯器使之能夠處理這些經過模糊處理的類文件并不是什么難事,所以不能簡單地依賴模糊技術來保證源代碼的安全。
我們可以用流行的加密工具加密應用,比如PGP(Pretty Good Privacy)或GPG(GNU Privacy Guard)。這時,最終用戶在運行應用之前必須先進行解密。但解密之后,最終用戶就有了一份不加密的類文件,這和事先不進行加密沒有什么差別。
Java運行時裝入字節碼的機制隱含地意味著可以對字節碼進行修改。JVM每次裝入類文件時都需要一個稱為ClassLoader的對象,這個對象負責把新的類裝入正在運行的JVM。JVM給ClassLoader一個包含了待裝入類(比如java.lang.Object)名字的字符串,然后由ClassLoader負責找到類文件,裝入原始數據,并把它轉換成一個Class對象。
我們可以通過定制ClassLoader,在類文件執行之前修改它。這種技術的應用非常廣泛??在這里,它的用途是在類文件裝入之時進行解密,因此可以看成是一種即時解密器。由于解密后的字節碼文件永遠不會保存到文件系統,所以竊密者很難得到解密后的代碼。
由于把原始字節碼轉換成Class對象的過程完全由系統負責,所以創建定制ClassLoader對象其實并不困難,只需先獲得原始數據,接著就可以進行包含解密在內的任何轉換。
Java 2在一定程度上簡化了定制ClassLoader的構建。在Java 2中,loadClass的缺省實現仍舊負責處理所有必需的步驟,但為了顧及各種定制的類裝入過程,它還調用一個新的findClass方法。
這為我們編寫定制的ClassLoader提供了一條捷徑,減少了麻煩:只需覆蓋findClass,而不是覆蓋loadClass。這種方法避免了重復所有裝入器必需執行的公共步驟,因為這一切由loadClass負責。
不過,本文的定制ClassLoader并不使用這種方法。原因很簡單。如果由默認的ClassLoader先尋找經過加密的類文件,它可以找到;但由于類文件已經加密,所以它不會認可這個類文件,裝入過程將失敗。因此,我們必須自己實現loadClass,稍微增加了一些工作量。
二、定制類裝入器
每一個運行著的JVM已經擁有一個ClassLoader。這個默認的ClassLoader根據CLASSPATH環境變量的值,在本地文件系統中尋找合適的字節碼文件。
應用定制ClassLoader要求對這個過程有較為深入的認識。我們首先必須創建一個定制ClassLoader類的實例,然后顯式地要求它裝入另外一個類。這就強制JVM把該類以及所有它所需要的類關聯到定制的ClassLoader。Listing 1顯示了如何用定制ClassLoader裝入類文件。
【Listing 1:利用定制的ClassLoader裝入類文件】
以下是引用片段:
// 首先創建一個ClassLoader對象 如 http://www.bt285.cn
ClassLoader myClassLoader = new myClassLoader();
// 利用定制ClassLoader對象裝入類文件
// 并把它轉換成Class對象
Class myClass = myClassLoader.loadClass( "mypackage.MyClass" );
// 最后,創建該類的一個實例
Object newInstance = myClass.newInstance();
// 注意,MyClass所需要的所有其他類,都將通過
// 定制的ClassLoader自動裝入 |
如前所述,定制ClassLoader只需先獲取類文件的數據,然后把字節碼傳遞給運行時系統,由后者完成余下的任務。
ClassLoader有幾個重要的方法。創建定制的ClassLoader時,我們只需覆蓋其中的一個,即loadClass,提供獲取原始類文件數據的代碼。這個方法有兩個參數:類的名字,以及一個表示JVM是否要求解析類名字的標記(即是否同時裝入有依賴關系的類)。如果這個標記是true,我們只需在返回JVM之前調用resolveClass。
【Listing 2:ClassLoader.loadClass()的一個簡單實現】
以下是引用片段:
public Class loadClass( String name, boolean resolve ) 如:http://www.5a520.cn
throws ClassNotFoundException {
try {
// 我們要創建的Class對象
Class clasz = null;
// 必需的步驟1:如果類已經在系統緩沖之中,
// 我們不必再次裝入它
clasz = findLoadedClass( name );
if (clasz != null)
return clasz;
// 下面是定制部分
byte classData[] = /* 通過某種方法獲取字節碼數據 */;
if (classData != null) {
// 成功讀取字節碼數據,現在把它轉換成一個Class對象
clasz = defineClass( name, classData, 0, classData.length );
}
// 必需的步驟2:如果上面沒有成功,
// 我們嘗試用默認的ClassLoader裝入它
if (clasz == null)
clasz = findSystemClass( name );
// 必需的步驟3:如有必要,則裝入相關的類
if (resolve && clasz != null)
resolveClass( clasz );
// 把類返回給調用者
return clasz;
} catch( IOException ie ) {
throw new ClassNotFoundException( ie.toString() );
} catch( GeneralSecurityException gse ) {
throw new ClassNotFoundException( gse.toString() );
}
} |
Listing 2顯示了一個簡單的loadClass實現。代碼中的大部分對所有ClassLoader對象來說都一樣,但有一小部分(已通過注釋標記)是特有的。在處理過程中,ClassLoader對象要用到其他幾個輔助方法:
findLoadedClass:用來進行檢查,以便確認被請求的類當前還不存在。loadClass方法應該首先調用它。
defineClass:獲得原始類文件字節碼數據之后,調用defineClass把它轉換成一個Class對象。任何loadClass實現都必須調用這個方法。
findSystemClass:提供默認ClassLoader的支持。如果用來尋找類的定制方法不能找到指定的類(或者有意地不用定制方法),則可以調用該方法嘗試默認的裝入方式。這是很有用的,特別是從普通的JAR文件裝入標準Java類時。
resolveClass:當JVM想要裝入的不僅包括指定的類,而且還包括該類引用的所有其他類時,它會把loadClass的resolve參數設置成true。這時,我們必須在返回剛剛裝入的Class對象給調用者之前調用resolveClass。
三、加密、解密
Java加密擴展即Java Cryptography Extension,簡稱JCE。它是Sun的加密服務軟件,包含了加密和密匙生成功能。JCE是JCA(Java Cryptography Architecture)的一種擴展。
JCE沒有規定具體的加密算法,但提供了一個框架,加密算法的具體實現可以作為服務提供者加入。除了JCE框架之外,JCE軟件包還包含了SunJCE服務提供者,其中包括許多有用的加密算法,比如DES(Data Encryption Standard)和Blowfish。
為簡單計,在本文中我們將用DES算法加密和解密字節碼。下面是用JCE加密和解密數據必須遵循的基本步驟:
步驟1:生成一個安全密匙。在加密或解密任何數據之前需要有一個密匙。密匙是隨同被加密的應用一起發布的一小段數據,Listing 3顯示了如何生成一個密匙。 【Listing 3:生成一個密匙】
以下是引用片段:
// DES算法要求有一個可信任的隨機數源
SecureRandom sr = new SecureRandom();
// 為我們選擇的DES算法生成一個KeyGenerator對象
KeyGenerator kg = KeyGenerator.getInstance( "DES" );
kg.init( sr );
// 生成密匙
SecretKey key = kg.generateKey();
// 獲取密匙數據
byte rawKeyData[] = key.getEncoded();
/* 接下來就可以用密匙進行加密或解密,或者把它保存
為文件供以后使用 */
doSomething( rawKeyData ); |
步驟2:加密數據。得到密匙之后,接下來就可以用它加密數據。除了解密的ClassLoader之外,一般還要有一個加密待發布應用的獨立程序(見Listing 4)。 【Listing 4:用密匙加密原始數據】
以下是引用片段:
// DES算法要求有一個可信任的隨機數源
SecureRandom sr = new SecureRandom();
byte rawKeyData[] = /* 用某種方法獲得密匙數據 */;
// 從原始密匙數據創建DESKeySpec對象
DESKeySpec dks = new DESKeySpec( rawKeyData );
// 創建一個密匙工廠,然后用它把DESKeySpec轉換成
// 一個SecretKey對象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" );
SecretKey key = keyFactory.generateSecret( dks );
// Cipher對象實際完成加密操作
Cipher cipher = Cipher.getInstance( "DES" );
// 用密匙初始化Cipher對象
cipher.init( Cipher.ENCRYPT_MODE, key, sr );
// 現在,獲取數據并加密
byte data[] = /* 用某種方法獲取數據 */
// 正式執行加密操作
byte encryptedData[] = cipher.doFinal( data );
// 進一步處理加密后的數據
doSomething( encryptedData ); |
步驟3:解密數據。運行經過加密的應用時,ClassLoader分析并解密類文件。操作步驟如Listing 5所示。 【Listing 5:用密匙解密數據】
// DES算法要求有一個可信任的隨機數源
SecureRandom sr = new SecureRandom();
byte rawKeyData[] = /* 用某種方法獲取原始密匙數據 */;
// 從原始密匙數據創建一個DESKeySpec對象
DESKeySpec dks = new DESKeySpec( rawKeyData );
// 創建一個密匙工廠,然后用它把DESKeySpec對象轉換成
// 一個SecretKey對象
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" );
SecretKey key = keyFactory.generateSecret( dks );
// Cipher對象實際完成解密操作
Cipher cipher = Cipher.getInstance( "DES" );
// 用密匙初始化Cipher對象
cipher.init( Cipher.DECRYPT_MODE, key, sr );
// 現在,獲取數據并解密
byte encryptedData[] = /* 獲得經過加密的數據 */
// 正式執行解密操作
byte decryptedData[] = cipher.doFinal( encryptedData );
// 進一步處理解密后的數據
doSomething( decryptedData ); |
四、應用實例
前面介紹了如何加密和解密數據。要部署一個經過加密的應用,步驟如下:
步驟1:創建應用。我們的例子包含一個App主類,兩個輔助類(分別稱為Foo和Bar)。這個應用沒有什么實際功用,但只要我們能夠加密這個應用,加密其他應用也就不在話下。
步驟2:生成一個安全密匙。在命令行,利用GenerateKey工具(參見GenerateKey.java)把密匙寫入一個文件: % java GenerateKey key.data
步驟3:加密應用。在命令行,利用EncryptClasses工具(參見EncryptClasses.java)加密應用的類: % java EncryptClasses key.data App.class Foo.class Bar.class
該命令把每一個.class文件替換成它們各自的加密版本。
步驟4:運行經過加密的應用。用戶通過一個DecryptStart程序運行經過加密的應用。DecryptStart程序如Listing 6所示。 【Listing 6:DecryptStart.java,啟動被加密應用的程序】
以下是引用片段:
import java.io.*;
import java.security.*;
import java.lang.reflect.*;
import javax.crypto.*;
import javax.crypto.spec.*;
public class DecryptStart extends ClassLoader
{
// 這些對象在構造函數中設置,
// 以后loadClass()方法將利用它們解密類
private SecretKey key;
private Cipher cipher;
// 構造函數:設置解密所需要的對象
public DecryptStart( SecretKey key ) throws GeneralSecurityException,
IOException {
this.key = key;
String algorithm = "DES";
SecureRandom sr = new SecureRandom();
System.err.println( "[DecryptStart: creating cipher]" );
cipher = Cipher.getInstance( algorithm );
cipher.init( Cipher.DECRYPT_MODE, key, sr );
}
// main過程:我們要在這里讀入密匙,創建DecryptStart的
// 實例,它就是我們的定制ClassLoader。
// 設置好ClassLoader以后,我們用它裝入應用實例,
// 最后,我們通過Java Reflection API調用應用實例的main方法
static public void main( String args[] ) throws Exception {
String keyFilename = args[0];
String appName = args[1];
// 這些是傳遞給應用本身的參數
String realArgs[] = new String[args.length-2];
System.arraycopy( args, 2, realArgs, 0, args.length-2 );
// 讀取密匙
System.err.println( "[DecryptStart: reading key]" );
byte rawKey[] = Util.readFile( keyFilename );
DESKeySpec dks = new DESKeySpec( rawKey );
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" );
SecretKey key = keyFactory.generateSecret( dks );
// 創建解密的ClassLoader
DecryptStart dr = new DecryptStart( key );
// 創建應用主類的一個實例
// 通過ClassLoader裝入它
System.err.println( "[DecryptStart: loading "+appName+"]" );
Class clasz = dr.loadClass( appName );
// 最后,通過Reflection API調用應用實例
// 的main()方法
// 獲取一個對main()的引用
String proto[] = new String[1];
Class mainArgs[] = { (new String[1]).getClass() };
Method main = clasz.getMethod( "main", mainArgs );
// 創建一個包含main()方法參數的數組
Object argsArray[] = { realArgs };
System.err.println( "[DecryptStart: running "+appName+".main()]" );
// 調用main()
main.invoke( null, argsArray );
}
public Class loadClass( String name, boolean resolve )
throws ClassNotFoundException {
try {
// 我們要創建的Class對象
Class clasz = null;
// 必需的步驟1:如果類已經在系統緩沖之中
// 我們不必再次裝入它
clasz = findLoadedClass( name );
if (clasz != null)
return clasz;
// 下面是定制部分
try {
// 讀取經過加密的類文件
byte classData[] = Util.readFile( name+".class" );
if (classData != null) {
// 解密...
byte decryptedClassData[] = cipher.doFinal( classData );
// ... 再把它轉換成一個類
clasz = defineClass( name, decryptedClassData,
0, decryptedClassData.length );
System.err.println( "[DecryptStart: decrypting class "+name+"]" );
}
} catch( FileNotFoundException fnfe )
// 必需的步驟2:如果上面沒有成功
// 我們嘗試用默認的ClassLoader裝入它
if (clasz == null)
clasz = findSystemClass( name );
// 必需的步驟3:如有必要,則裝入相關的類
if (resolve && clasz != null)
resolveClass( clasz );
// 把類返回給調用者
return clasz;
} catch( IOException ie ) {
throw new ClassNotFoundException( ie.toString()
);
} catch( GeneralSecurityException gse ) {
throw new ClassNotFoundException( gse.toString()
);
}
}
} |
對于未經加密的應用,正常執行方式如下: % java App arg0 arg1 arg2
對于經過加密的應用,則相應的運行方式為: % java DecryptStart key.data App arg0 arg1 arg2
DecryptStart有兩個目的。一個DecryptStart的實例就是一個實施即時解密操作的定制ClassLoader;同時,DecryptStart還包含一個main過程,它創建解密器實例并用它裝入和運行應用。示例應用App的代碼包含在App.java、Foo.java和Bar.java內。Util.java是一個文件I/O工具,本文示例多處用到了它。完整的代碼請從本文最后下載。
五、注意事項
我們看到,要在不修改源代碼的情況下加密一個Java應用是很容易的。不過,世上沒有完全安全的系統。本文的加密方式提供了一定程度的源代碼保護,但對某些攻擊來說它是脆弱的。
雖然應用本身經過了加密,但啟動程序DecryptStart沒有加密。攻擊者可以反編譯啟動程序并修改它,把解密后的類文件保存到磁盤。降低這種風險的辦法之一是對啟動程序進行高質量的模糊處理。或者,啟動程序也可以采用直接編譯成機器語言的代碼,使得啟動程序具有傳統執行文件格式的安全性。
另外還要記住的是,大多數JVM本身并不安全。狡猾的黑客可能會修改JVM,從ClassLoader之外獲取解密后的代碼并保存到磁盤,從而繞過本文的加密技術。Java沒有為此提供真正有效的補救措施。
不過應該指出的是,所有這些可能的攻擊都有一個前提,這就是攻擊者可以得到密匙。如果沒有密匙,應用的安全性就完全取決于加密算法的安全性。雖然這種保護代碼的方法稱不上十全十美,但它仍不失為一種保護知識產權和敏感用戶數據的有效方案。
Java 流在處理上分為字符流和字節流。字符流處理的單元為 2 個字節的 Unicode 字符,分別操作字符、字符數組或字符串,而字節流處理單元為 1 個字節,操作字節和字節數組。
Java 內用 Unicode 編碼存儲字符,字符流處理類負責將外部的其他編碼的字符流和 java 內 Unicode 字符流之間的轉換。而類 InputStreamReader 和 OutputStreamWriter 處理字符流和字節流的轉換。字符流(一次可以處理一個緩沖區)一次操作比字節流(一次一個字節)效率高。
( 一 )以字節為導向的 stream------InputStream/OutputStream
InputStream 和 OutputStream 是兩個 abstact 類,對于字節為導向的 stream 都擴展這兩個雞肋(基類 ^_^ ) ;
1、 InputStream
1.1
ByteArrayInputStream -- 把內存中的一個緩沖區作為 InputStream 使用 . 如使用http://www.5a520.cn
construct---
(A)ByteArrayInputStream(byte[]) 創建一個新字節數組輸入流( ByteArrayInputStream ),它從指定字節數組中讀取數據( 使用 byte 作為其緩沖區數組)
(B)---ByteArrayInputStream(byte[], int, int) 創建一個新字節數組輸入流,它從指定字節數組中讀取數據。
---mark:: 該字節數組未被復制。
1.2
StringBufferInputStream -- 把一個 String 對象作為 InputStream .
construct---
StringBufferInputStream(String) 據指定串創建一個讀取數據的輸入流串。
注釋:不推薦使用 StringBufferInputStream 方法。 此類不能將字符正確的轉換為字節。
同 JDK 1.1 版中的類似,從一個串創建一個流的最佳方法是采用 StringReader 類。
1.3
FileInputStream -- 把一個文件作為 InputStream ,實現對文件的讀取操作
construct---
(A)FileInputStream(File name) 創建一個輸入文件流,從指定的 File 對象讀取數據。
(B)FileInputStream(FileDescriptor) 創建一個輸入文件流,從指定的文件描述器讀取數據。
(C)-FileInputStream(String name) 創建一個輸入文件流,從指定名稱的文件讀取數據。
method ---- read() 從當前輸入流中讀取一字節數據。
read(byte[]) 將當前輸入流中 b.length 個字節數據讀到一個字節數組中。
read(byte[], int, int) 將輸入流中 len 個字節數據讀入一個字節數組中。
1.4
PipedInputStream :實現了 pipe 的概念,主要在線程中使用 . 管道輸入流是指一個通訊管道的接收端。
一個線程通過管道輸出流發送數據,而另一個線程通過管道輸入流讀取數據,這樣可實現兩個線程間的通訊。
construct---
PipedInputStream() 創建一個管道輸入流,它還未與一個管道輸出流連接。
PipedInputStream(PipedOutputStream) 創建一個管道輸入流 , 它已連接到一個管道輸出流。
1.5
SequenceInputStream :把多個 InputStream 合并為一個 InputStream . “序列輸入流”類允許應用程序把幾個輸入流連續地合并起來,
并且使它們像單個輸入流一樣出現。每個輸入流依次被讀取,直到到達該流的末尾。
然后“序列輸入流”類關閉這個流并自動地切換到下一個輸入流。
construct---
SequenceInputStream(Enumeration) 創建一個新的序列輸入流,并用指定的輸入流的枚舉值初始化它。
SequenceInputStream(InputStream, InputStream) 創建一個新的序列輸入流,初始化為首先 讀輸入流 s1, 然后讀輸入流 s2 。
2、 OutputSteam http://www.bt285.cn
2.1
ByteArrayOutputStream : 把信息存入內存中的一個緩沖區中 . 該類實現一個以字節數組形式寫入數據的輸出流。
當數據寫入緩沖區時,它自動擴大。用 toByteArray() 和 toString() 能檢索數據。
constructor
(A)--- ByteArrayOutputStream() 創建一個新的字節數組輸出流。
(B)--- ByteArrayOutputStream() 創建一個新的字節數組輸出流。
(C)--- ByteArrayOutputStream(int) 創建一個新的字節數組輸出流,并帶有指定大小字節的緩沖區容量。
toString(String) 根據指定字符編碼將緩沖區內容轉換為字符串,并將字節轉換為字符。
write(byte[], int, int) 將指定字節數組中從偏移量 off 開始的 len 個字節寫入該字節數組輸出流。
write(int) 將指定字節寫入該字節數組輸出流。
writeTo(OutputStream) 用 out.write(buf, 0, count) 調用輸出流的寫方法將該字節數組輸出流的全部內容寫入指定的輸出流參數。
2.2
FileOutputStream: 文件輸出流是向 File 或 FileDescriptor 輸出數據的一個輸出流。
constructor
(A)FileOutputStream(File name) 創建一個文件輸出流,向指定的 File 對象輸出數據。
(B)FileOutputStream(FileDescriptor) 創建一個文件輸出流,向指定的文件描述器輸出數據。
(C)FileOutputStream(String name) 創建一個文件輸出流,向指定名稱的文件輸出數據。
(D)FileOutputStream(String, boolean) 用指定系統的文件名,創建一個輸出文件。
2.3
PipedOutputStream: 管道輸出流是指一個通訊管道的發送端。 一個線程通過管道輸出流發送數據,
而另一個線程通過管道輸入流讀取數據,這樣可實現兩個線程間的通訊。
constructor
(A)PipedOutputStream() 創建一個管道輸出流,它還未與一個管道輸入流連接。
(B)PipedOutputStream(PipedInputStream) 創建一個管道輸出流,它已連接到一個管道輸入流。
( 二 )以字符為導向的 stream Reader/Writer
以 Unicode 字符為導向的 stream ,表示以 Unicode 字符為單位從 stream 中讀取或往 stream 中寫入信息。
Reader/Writer 為 abstact 類
以 Unicode 字符為導向的 stream 包括下面幾種類型:
1. Reader
1.1
CharArrayReader :與 ByteArrayInputStream 對應此類實現一個可用作字符輸入流的字符緩沖區
constructor
CharArrayReader(char[]) 用指定字符數組創建一個 CharArrayReader 。
CharArrayReader(char[], int, int) 用指定字符數組創建一個 CharArrayReader
1.2
StringReader : 與 StringBufferInputStream 對應其源為一個字符串的字符流。
StringReader(String) 創建一新的串讀取者。
1.3
FileReader : 與 FileInputStream 對應
1.4
PipedReader :與 PipedInputStream 對應
2. Writer
2.1 CharArrayWrite : 與 ByteArrayOutputStream 對應
2.2 StringWrite :無與之對應的以字節為導向的 stream
2.3 FileWrite : 與 FileOutputStream 對應
2.4 PipedWrite :與 PipedOutputStream 對應
3、兩種不同導向的 stream 之間的轉換
3.1
InputStreamReader 和 OutputStreamReader :
把一個以字節為導向的 stream 轉換成一個以字符為導向的 stream 。
InputStreamReader 類是從字節流到字符流的橋梁:它讀入字節,并根據指定的編碼方式,將之轉換為字符流。
使用的編碼方式可能由名稱指定,或平臺可接受的缺省編碼方式。
InputStreamReader 的 read() 方法之一的每次調用,可能促使從基本字節輸入流中讀取一個或多個字節。
為了達到更高效率,考慮用 BufferedReader 封裝 InputStreamReader ,
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
例如: // 實現從鍵盤輸入一個整數
- String s = null;
- InputStreamReader re = new InputStreamReader(System.in);
- BufferedReader br = new BufferedReader(re);
- try {
- s = br.readLine();
- System.out.println("s= " + Integer.parseInt(s));
- br.close();
- }
- catch (IOException e)
- {
- e.printStackTrace();
- }
- catch (NumberFormatException e)
- {
- System.out.println(" 輸入的不是數字 ");
- }
InputStreamReader(InputStream) 用缺省的字符編碼方式,創建一個 InputStreamReader 。
InputStreamReader(InputStream, String) 用已命名的字符編碼方式,創建一個 InputStreamReader 。
OutputStreamWriter 將多個字符寫入到一個輸出流,根據指定的字符編碼將多個字符轉換為字節。
每個 OutputStreamWriter 合并它自己的 CharToByteConverter, 因而是從字符流到字節流的橋梁。
(三)Java IO 的一般使用原則 :
一、按數據來源(去向)分類:
1 、是文件: FileInputStream, FileOutputStream, ( 字節流 )FileReader, FileWriter( 字符 )
2 、是 byte[] : ByteArrayInputStream, ByteArrayOutputStream( 字節流 )
3 、是 Char[]: CharArrayReader, CharArrayWriter( 字符流 )
4 、是 String: StringBufferInputStream, StringBufferOuputStream ( 字節流 )StringReader, StringWriter( 字符流 )
5 、網絡數據流: InputStream, OutputStream,( 字節流 ) Reader, Writer( 字符流 )
二、按是否格式化輸出分:
1 、要格式化輸出: PrintStream, PrintWriter
三、按是否要緩沖分:
1 、要緩沖: BufferedInputStream, BufferedOutputStream,( 字節流 ) BufferedReader, BufferedWriter( 字符流 )
四、按數據格式分:
1 、二進制格式(只要不能確定是純文本的) : InputStream, OutputStream 及其所有帶 Stream 結束的子類
2 、純文本格式(含純英文與漢字或其他編碼方式); Reader, Writer 及其所有帶 Reader, Writer 的子類
五、按輸入輸出分:
1 、輸入: Reader, InputStream 類型的子類
2 、輸出: Writer, OutputStream 類型的子類
六、特殊需要:
1 、從 Stream 到 Reader,Writer 的轉換類: InputStreamReader, OutputStreamWriter
2 、對象輸入輸出: ObjectInputStream, ObjectOutputStream
3 、進程間通信: PipeInputStream, PipeOutputStream, PipeReader, PipeWriter
4 、合并輸入: SequenceInputStream
5 、更特殊的需要: PushbackInputStream, PushbackReader, LineNumberInputStream, LineNumberReader
決定使用哪個類以及它的構造進程的一般準則如下(不考慮特殊需要):
首先,考慮最原始的數據格式是什么: 原則四
第二,是輸入還是輸出:原則五
第三,是否需要轉換流:原則六第 1 點
第四,數據來源(去向)是什么:原則一
第五,是否要緩沖:原則三 (特別注明:一定要注意的是 readLine() 是否有定義,有什么比 read, write 更特殊的輸入或輸出方法)
第六,是否要格式化輸出:原則二