2010年9月28日 星期二

Java 物件清除與 finalize() 函式使用

垃圾回收器(garbage collection,簡稱GC)回收不再被使用的物件的記憶體空間
這點是自  C\C++ 以來,高階程式語言很大的改變之一

使用 C\C++ 語言撰寫時,memory leak 是最常發生的嚴重錯誤之一
如果使用完的物件沒有被清除,記憶體空間的資源也無法被釋放
Java的垃圾回收器能回收不再被使用的記憶體空間
但是有些重點必須要注意,垃圾回收器只能回收經由new產生的物件
被占用的特殊記憶體,例如從外部的其他語言獲得的物件,清除方式就不能透過GC進行
針對這類的物件,Java提供了 filanlize() method來因應

class 檔中可以定義 finalize() method
當垃圾回收器要開始釋放被物件占用的記憶體時,會先呼叫 finalize()函式
接著在下一次的垃圾回收動作發生時才會回收該記憶體空間
所以一般常見 finalize() 的定義是在垃圾回收時先執行某些重要的清理工作

有個可能比較容易引起誤會的地方是 finalize() 並不是C++的 destructor
差異點在於C++物件一定會被摧毀,所以destrucor絕對會被喚起
但是Java物件卻不一定會被GC回收
GC的執行條件是記憶體接近用完
假如直到程式執行結束都沒有啟動GC,記憶體空間會在程式終止時一次歸還
GC的執行會給程式帶來額外的負擔,能不執行的話自然是最好
因此 finalize() 並不一定會被呼叫
如果有一定要執行的清除動作,必須自行撰寫函式處理
通常影像處理類的任務比較有這類需求
finalize()真正適合使用的時機就只有要清除 non-Java 的物件

2010年9月26日 星期日

JDBC 簡介與基礎使用

JDBC(Java Database Connectivity)是Java規範資料庫存取的API
資料庫廠商依據Java提供的介面進行實作,開發者則依照JDBC的標準進行操作
如此設計的因素是每家廠商提供用來操作的 API 並不相同
開發者透過JDBC的介面操作的好處是,若有更換資料庫的要求,可以避免大量修改程式碼
只需要替換實作廠商的資料庫驅動
也就是寫一個Java程式就能應付所有的資料庫
當然實作時,有時為了使用資料庫的特定功能,仍有修改程式碼的必要

廠商實作JDBC驅動程式的方式有四種類型
1.JDBC-ODBC Bridge Driver
   ODBC(Open DataBase Connectivity)是微軟所主導的資料庫連結標準
   所以也很常在 Microsoft 的系統上使用
   這類的驅動程式將JDBC的呼叫轉換成對ODBC的呼叫
   但因為兩者並非一對一的對應,所以在功能及存取速度上都有所限制

2.Native API Driver
   載入由廠商提供的 C\C++撰寫成的原生函式庫
   直接將JDBC的呼叫轉成資料庫中的相關 API 呼叫
   因為直接呼叫 API 的關係,操作速度比第一種快(但比不上後兩種)
   但因為驅動程式作用的對象只限制在原生資料庫
   沒有做到JDBC的跨資料庫的目標

3.JDBC-Net Driver
   這類型的驅動程式提供了用戶端網路API,JDBC驅動透過Socket呼叫伺服器上的中介元件
   由該元件將JDBC的呼叫轉換成 API 呼叫
   客戶端的驅動程式與中介元件間的協定是固定的,所以不需要載入廠商的函式庫
   也因此在更換資料庫時,只需修改中介元件即可
   此種方式也是相當有彈性的架構

4.Native Protocol Driver
   這類的驅動程式使用Socket,直接在用戶端和資料庫間通訊
   也因為這是最直接的實作方式,通常具有最快的存取速度
   但針對不同的資料庫需使用不同的驅動程式
   為不需要JDBC-Net Driver那般彈性時的選擇,也是最常見的驅動程式類型

接下來的操作範例以第四種為主,並以MySQL為操作範例
MySQL提供的JDBC驅動程式是 mysql-connector-java ,可在其官網中找到
首先來個範例:
SQLController.java=======

package DB;

import java.sql.*;

public class SQLController {
  private Connection con = null;      //Database objects
  private Statement stat = null;        //SQL code to execute
  private ResultSet res = null;         //result
  private PreparedStatement pst = null;

  public SQLController(){

  }
  public SQLController(String SQLType, String SQLDriver, String daatabase, String account, String password){

/*  範例裡傳入的參數
SQLType = "mysql"
SQLDriver = "com.mysql.jdbc.Driver"
其餘的則按資料庫的設定傳入
*/ 
    
      String conInfo = "jdbc:" + SQLType + "://localhost:3306/" + daatabase + "?useUnicode=true&characterEncoding=UTF-8";
     
      try {
          //register driver
          Class.forName(SQLDriver);
          con = DriverManager.getConnection(conInfo, account, password); 
      }
      catch(ClassNotFoundException e){
            System.out.println("DriverClassNotFound :"+e.toString());
      }
       //prevent SQL code error
      catch(SQLException x) {
            System.out.println("Exception :"+x.toString());
      }
  }

  //clear objects in order to close database
  private void Close()
  {
    try{       
      if(res!=null){  res.close();}
      if(stat!=null){ stat.close();}
      if(pst!=null){ pst.close(); }
      if(con!=null){ con.close(); }
    }
    catch(SQLException e){
      System.out.println("Close Exception :" + e.toString());
    }
  }

}

網路上可以找到寫成JavaBean格式的寫法,搭配EL也能正常地使用資料庫
範例將資料庫連線寫成Java物件,寫法依個人喜好而定

連結資料庫前必須先載入JDBC驅動程式
透過Class.forName(),範例程式動態載入 com.mysql.jdbc.Driver 類別至 DriverManager
類別會自動向 DriverManager 做註冊
生成連線時,DriverManager就會使用該驅動建立Connection實例

JDBC URL定義連線資料庫的協定、子協定、資料來源識別
型態是  協定:子協定:識別資料來源
使用MySQL的JDBC URL就會是  jdbc:mysql://localhost:3306/資料庫名稱?參數=值&...
識別資料裡的 localhost:3306 連結 MySQL的連結阜
使用中文時需附加參數 useUnicode=true&characterEncoding=UTF8,指定用UTF8編碼
建立Connection實例時我們必須提供JDBC URL
DriverManager.getConnection() method有兩種引入參數的版本
單一 參數的版本必須將資料庫的帳密資料附加在JDBC URL裡,再引入URL
另一個版本就是本例使用的 DriverManager.getConnection(conInfo, account, password);
將帳號、密碼當作呼叫時的參數傳入

SQLException是資料庫處理過程發生異常時會被丟出的例外
資料庫的使用過程中都必須做這個例外處理的準備

Connection是連接資料庫的代表物件
執行SQL還需要取得 java.sql.Statement 物件
下面的範例示範實際執行SQL碼的撰寫方式,並搭配DCBP與JNDI來進行:
DatabaseBean.java======

package tw.vencs;

import java.sql.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.DataSource;

public class DatabaseBean {
    private DataSource dataSource = null;

    public DatabaseBean(){

    }  
    public void setConSource(DataSource dataSource){
        this.dataSource = dataSource;
    }
    public Connection getConnection() throws SQLException{
        return dataSource.getConnection();
    }
}

class SignInBean extends DatabaseBean{
    public boolean isSignIn(String account, String password){
        Connection con = null;
        Statement sta = null;
        ResultSet res = null;
        boolean checkOK = false;
      
        try{
          con = getConnection();
          sta = con.createStatement();
          res = sta.executeQuery("SELECT * FROM `member` WHERE account = '"+ account
                                   +"' AND password = '"+ password +"' ");
        
          //如果有找到與輸入的帳密相同的資料,允許會員登入
          while(res.next()){
              checkOK = true;
          }
        }catch(SQLException ex){
            Logger.getLogger(SignInBean.class.getName()).log(Level.SEVERE, null, ex);
            throw new RuntimeException();
        }finally{
          try{  
            if(res != null){ res.close();}
            if(sta != null){ sta.close();}
            if(con != null){ con.close();}
          }catch(SQLException ex){
            Logger.getLogger(SignInBean.class.getName()).log(Level.SEVERE, null, ex);
            throw new RuntimeException();
          }
        }
        return checkOK;
    }
}

SignInConfirmer.java======

package tw.vencs;

import java.io.IOException;
import javax.servlet.http.*;
import javax.sql.DataSource;
import com.colid.DatabaseBean;

public class SignInConfirmer extends HttpServlet{
  public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException{
         String account = request.getParameter("account");
         String password = request.getParameter("password");
         String cookieMaker = request.getParameter("cookieMaker");
         //get DBCP connection resource from InitialListener
         DataSource dataSource = (DataSource)getServletContext().getAttribute("conData");

         SignInBean confirmer = new SignInBean();
         confirmer.setConSource(dataSource);
        
         if(confirmer.isSignIn(account, password)){
             HttpSession session = request.getSession();
             session.setAttribute("account", account);
            
             if(cookieMaker.equals("yes")){
                int life = 30*24*60*60;  //set Life of Cookie to 30 days
                Cookie cookie = new Cookie("account", account);
                cookie.setMaxAge(life);        
                response.addCookie(cookie);
             }
            
             //encode URL if user's COOKIE function is closed
             response.sendRedirect(response.encodeURL("index.jsp"));
         }
         else{
             getServletContext().setAttribute("warning", true);
             response.sendRedirect(response.encodeURL("login.jsp"));
         }

  }
}

範例是一隻處理登入狀態(會話管理)的 Servlet 程式
DBCP 與 JNDI 的部分可以去找那篇的筆記來看

執行SQL碼的 Statement 物件從連線物件中取得,程式碼如: con.createStatement()
Statement 物件常用的方法有 executeUpdate()、executeQuery()和execute()
executeUpdate()主要用來執行  CREATE、INSERT、 DROP等改變資料庫內容的SQL碼
執行完畢後回傳  int 數值,表示資料變動的筆數
executeQuery() 則如字面所示,用來得到 SELECT等SQL碼查詢到的結果
執行完畢後回傳 java.sql.ResultSet 物件

ResultSet 的next() method 會回傳 boolean值來表示是否有下一筆資料
如果確實有資料的話可以使用 getXXX()的方式取得該筆資料,XXX為資料型別
查詢結果會從1開始作為欄位的索引值
例如: int id = res.getINT(1)
execute() 這個method則用在無法事先得知要執行的SQL碼的場合
假使回傳的結果是 true,則可以用getResultSet() 取得 ResultSet 物件
回傳 false 則可以用 getUpdateCount() 得知更新資料筆數

最後提到上面的範例沒有展示到的 PreparedStatement
假使有動態資料的情況下
像上述範例以 + 運算子結合字串組成SQL碼會比較麻煩
字串每使用一次 + ,其實也是重新製作一個String物件
如此的字串使用也造成效能上的負擔
PreparedStatement 的使用與 Statement 很相似,如改寫範例後為:
PreparedStatement pre = con.prepareStatement("SELECT * FROM `member` WHERE
                                                                                       account =? AND password =?");
pre.setString(1, account);           //setString() 輸入的參數可以避免 Injection Attack
pre.setString(2, password);
pre.executeUpdate();
pre.clearParameters();

將SQL碼中要動態輸入的部分以 ? 運算子代替,然後設置所要輸入的變數
con.prepareStatement() 會建立一個預先編譯的SQL實例
它也同樣可以使用executeUpdate()、executeQuery() method
當查詢完畢之後,clearParameters() 清除所輸入的參數
留下來的實例卻可以填上別的參數繼續使用,而不用重新再建立SQL實例
會被頻繁使用的SQL碼實例適合用這種方式建立
setString() 輸入的參數會被視為純粹的String物件,最後在SQL碼內的形式是包覆在引號內
若是需要填入SQL碼內的是數字,則可以用 setInt() 以免出現SQL碼錯誤的情形
PreparedStatement 會是效能與安全性考量的好選擇

2010年9月14日 星期二

Tomcat 6.0 環境配置 DBCP 與 JNDI 使用

取得資料庫連線的過程必須經過網路連線、協定交換等多個步驟
過程中的耗費在單機使用者上也許感覺不太出來
但放大到使用者眾多的網路應用程式,開開關關的連線就是不可小看的浪費

改善連線效能的方法之一就是建立儲存了連線實例的連接池
盡量使用已經開啟的連線,重複使用以減低資源上的浪費
Tomcat的DBCP連接池機制提供了連接池建立的功能
JNDI 處理 Java EE 平台上資源的分散式運算的協調與資源上查詢
JNDI 更詳細的介紹可以在 SUN 的文件裡看到
接著搭配兩者來示範建立連接池

Java EE將資料庫處理的相關行為規範在 javax.sql.DataSource 介面
取得Connecion實例等的處理會由實作介面的物件來處理,我們只需取得DataSource實例即可
範例以建立一個能被封裝散佈的WAR檔的Web Application為例:
context.xml=========

<?xml version="1.0" encoding="UTF-8"?>
<!-- 應用程式目錄是Demo -->
<Context antiJARLocking="true" path="/Demo">
  <Resource name="jdbc/sql" auth="Container" type="javax.sql.DataSource"
            maxActive="60" maxIdle="20" maxWait="10000"
            username="***" password="***" driverClassName="com.mysql.jdbc.Driver"
            url="jdbc:mysql://localhost:3306/***?useUnicode=true&amp;characterEncoding=UTF8" />
</Context>

web.xml===========

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
  ....... 
  <resource-ref>
      <res-ref-name>jdbc/sql</res-ref-name>
      <res-type>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
      <res-sharing-scope>Shareable</res-sharing-scope>
  </resource-ref>
  ....

</web-app>

DatabaseBean.java====

package tw.vencs;

import java.sql.*;
import java.util.logging.*;
import javax.naming.*;
import javax.sql.DataSource;

public class DatabaseBean {
    private DataSource dataSource;
   
    public DatabaseBean(){
        try{
            Context initContext = new InitialContext();
            //java:comp/env 表示應用程式環境項目
            Context envContext = (Context)initContext.lookup("java:/comp/env");
            //查找 jdbc/sql 對應的 DataSource 物件
            dataSource = (DataSource)envContext.lookup("jdbc/sql");
        }catch(NamingException ex){
            Logger.getLogger(DatabaseBean.class.getName()).log(Level.SEVERE, null, ex);
            throw new RuntimeException();
        }
    }
   
    public boolean isConnectedOK(){
        boolean ok = false;
        Connection conn = null;
        try{
            // 透過 DataSource 物件取得連線
            conn = dataSource.getConnection();
            if(!conn.isClosed()){
                ok = true;
            }
        }catch(SQLException ex){
            Logger.getLogger(DatabaseBean.class.getName()).log(Level.SEVERE, null, ex);
        }finally{
            if(conn != null){
                try{
                    conn.close();
                }catch(SQLException ex){
                    Logger.getLogger(DatabaseBean.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return ok;
    }
}

conn.jsp========

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<jsp:useBean id="db" class="tw.vencs.DatabaseBean" />   
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>db Test</title>
</head>
<body>

<c:choose>
  <c:when test="${db.connectedOK }">連線成功</c:when>
  <c:otherwise>失敗</c:otherwise>
</c:choose>

</body>
</html>


context.xml需要放置在Web應用程式根目錄的META-INF目錄裡
<Context>裡的 path 填上相對於容器網路根目錄的應用程式目錄位置
<Resource>標籤裡有許多與連線設定相關的屬性
name                       存取辨識用的 JNDI 名稱
auth                         認證方式,一般為Container
type                         數據集的型別,使用標準的javax.sql.DataSource
maxActive               連接池中最多的Connection實例數量,0為不限制
maxIdle                   最大空閒連接數,也就是一個連接池裡最少要有的Connection實例數量
maxWait                  等待可用的Connection實例,最大的等待時間,-1為不限制
username                資料庫使用者名稱
password                 資料庫密碼
driverClassName     JDBC Driver 類別名稱
url                           JDBC URL,注意因資訊寫在xml裡,"&"需改成"&amp;"才符合XML規範

web.xml裡的設定提供 JNDI 查找時需要的環境資訊
減少物件建立所需要設置的參數的麻煩

DatabaseBean 物件裡不會看見資料庫連結的實體位置、連接阜等資訊
那些資訊由context.xml設定,只須給資料庫管理人員管理

程式部署後,Tomcat根據放在 META-INF 裡的context.xml設定,尋找指定的JDBC Driver
所以驅動程式也必須放在程式的Build path裡(如Tomcat的lib資料夾)
conn.jsp只提供了連線的訊息,資料庫的操作還須依照JDBC的規範來

2010年9月8日 星期三

Eclipse 環境下Tomcat 發生did not find a matching property 錯誤解決方法

環境配置:
eclipse-jee-helios-win32-x86_64.zip
apache-tomcat-6.0.29

錯誤訊息大致如下:
[SetPropertiesRule]{Server/Service/Engine/Host/Context} Setting property 'source' to 'org.eclipse.jst.j2ee.server:demo' did not find a matching property.

解決方法:
1.對 Servers 欄位右鍵取得 properties-> General
   點擊Switch Location 讓 Location = /Servers/Tomcat v6.0 Server at localhost.server
2.Project Explorer -> Servers 打開此目錄下的 Tomcat v6.0 Server at localhost.server
3.Server Options -> Check "Publish module contexts to separate XML files"

APR Tomcat Native library 擴充 Tomcat 性能

最近遇到 eclipse 跳出了個警告訊息出來的情況:
The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found ....

查了一下解決的方法,發現這個問題發生的原因是Tomcat
Tomcat從 5.5版本後增加了APR技術(Apache Portable Runtime)
這是個用C語言寫成的文件包,目的在於提高Tomcat的服務性能
使Tomcat將不僅僅擔任一個容器的功能,而能成為一個一般的web服務器
預設的Tomcat檔案並沒有這個文件包

解決的方法就是到官網下載這個文件包,在官網中的Download區裡選擇 Tomcat Native
至 Tomcat Native Connector 選擇 You may download them from HERE 下載適合的版本
將下載到的 tcnative-1.dll 放置在 JAVA_HOME 環境變數所設定的目錄底下
例如 C:\Program Files\Java\jdk1.7.0_09\bin
這樣問題就解決了

2010年9月3日 星期五

程式物件與記憶體的配置方式

物件在配置時能被儲存在五個地方:
1.暫存器(Registers)  2.堆疊(Stack)  3.堆積(Heap)
4.常數暫存空間(Constant storage)   5.Non-RAM儲存空間

以下對這五種方式做說明


1.暫存器(Registers):
   暫存器內建於CPU內,所以是速度最快的儲存位置
   但因為數量有限,所以這裡的物件配置都由編譯器來決定
   自由度比較高的C或C++也僅能向編譯器建議暫存器的配置

2.堆疊(Stack):
   堆疊的位置位於一般的RAM裡,處理器經由其指標(stack pointer)提供直接支援來運作
   當程式配置一塊記憶體時,stack指標會往後移動;釋放記憶體則會讓指標往前移回
   堆疊很快也很有效率,速度僅次於暫存器

   因為Java的編譯器必須自行建立移動stack pointer的程式碼
   所以必須要完全掌握存在stack裡的資料的實際大小與存活時間
   因為這個限制,儘管物件的reference可以儲存於其上,但一般的Java物件並不能如此

3.堆積(Heap):
   Heap是通用性質的記憶體儲存空間,也位在RAM裡
   與stack不同的是,它不需要事先知道必須配置於其上的物件大小與存活時間
   所以它有了相當的彈性

   Heap也是Java放置一般Java物件的地方
   每當在程式中執行 new 指令時,就會在heap上配置
   當然有彈性的另一面是配置的時間較stack來的長

4.常數暫存空間(Constant storage):
   因為常數值不會改變,所以有時候會被放在ROM(read-only memory)或內嵌式系統中
   內嵌式系統的例子舉Java的string pool,它是特殊的儲存空間

5.Non-RAM儲存空間:
   如果資料可以獨立於程式外,這類物件通常被儲存於磁碟裡
   這一類儲存空間的特色是,可以將物件轉換為可儲存於其他媒介的形式
   必要時也可以還原成儲存於RAM中的一般物件


我們稱呼在撰寫程式過程中常用到的類別為基本型別(primitive types)
要特別的處理它是因為透過 new 來配置這種極小、簡單的變數於heap上,效率不佳
Java採取C/C++的方式來處理,也就是不用 new 來配置其空間
這產生了"automatic"變數(不再使用reference型態)
這類變數直接存放資料值,並配置在stack上以取得較佳的效率