Leo's Blog

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  13 随笔 :: 3 文章 :: 18 评论 :: 0 Trackbacks

2006年2月23日 #

    昨天读了一篇关于JDBC4.0设计与性能提高的文章,由于小弟英语翻译水准实在有限,就不在这里献丑了,就把原文给大家转载出来供大家阅读:

转载自:http://www.javaworld.com/javaworld/jw-05-2006/jw- 0501-jdbc .html

Design and performance improvements with JDBC 4.0

Effectively utilize JDBC'S features to get desired results with less code

Summary
Java Database Connectivity (JDBC) 4.0 is ready for release by mid 2006 as a part of Java Standard Edition 6.0. How can you leverage the new specification to improve the design and performance of database access and interactions in Java applications? This article discusses the new features of JDBC 4.0, illustrates its solutions to some existing problems, and presents its improvements in design and performance through examples. (2,100 words; May 1, 2006)
By Shashank Tiwari


Java Database Connectivity (JDBC), which has existed from the first public version of the core Java language, has evolved significantly over the last 10 years. In its current version, 4.0, which will be packaged with Java Standard Edition 6.0 (Java SE is Sun's new name for J2SE), it shows significant improvements in design and provides a richer API, with focus on ease of development and improvement in productivity.

This article discusses some of the important changes in the JDBC specification that either improve the design or facilitate better performance. The article does not enlist or survey every single change incorporated as a part of Java Specification Request 221, the JDBC 4.0 initiative.

After reading this article, you should be ready to leverage the new features in your next set of applications.

Annotations and the generic DataSet
I assume you are already aware of annotations and generics, which were introduced in Java with J2SE 5.0. JDBC 4.0 introduces annotations and the generic DataSet. This change aims to simplify execution of SQL queries (in scenarios that return a single result set) and SQL DML (data manipulation language) statements (that return either a row count or nothing).

The new API defines a set of Query and DataSet interfaces. The Query interface defines a set of methods decorated with the JDBC annotations. These decorated methods describe the SQL select and update statements, and specify how the result set should be bound to a DataSet. The DataSet interface is a parameterized type, as defined by generics. The DataSet interface provides a type-safe definition for the result set data.

All Query interfaces inherit from the BaseQuery interface. A concrete implementation of the interface can be instantiated using either the Connection.createQueryObject() or DataSource.createQueryObject() methods and passing a Query interface type as its parameter.

A DataSet interface inherits from java.util.List. A data class describing the columns of the result set data, returned by an annotated method of the Query interface, is its parameter type. A DataSet can be manipulated and operated upon both in a connected and disconnected mode. Thus, the DataSet is implemented either as a ResultSet or a CachedRowSet, depending on its operating mode: connected or disconnected. DataSet, being a sub-interface of the java.util.List, allows access of its data rows with the Iterator pattern, using the java.util.Iterator interface.

The data class or the user-defined class, which is a parameter type of the DataSet interface, can be specified in two ways: as a structure or as a JavaBeans object. Either method achieves the goal of binding result set data columns to user-defined class definitions, but the JavaBeans component model is more elegant and facilitates object definition reuse within other frameworks that support the JavaBeans model.

Listing 1 illustrates code snippets for a simple example to show how the new API is used to create and run SQL queries, define result set data using a user-defined class, and bind the returned result set to the user-defined specifications.

Listing 1. Employee user-defined type and employeeQueries

pubic class Employee {
   private int employeeId;
   private String firstName;
   private String lastName;

   public int getEmployeeId() {
      return employeeId;
   }
  
   public setEmployeeId(int employeeId) {
      this.employeeId = employeeId;
   }

   public String getFirstName() {
      return firstName;
   }

   public setFirstName(String firstName) {
      this.firstName = firstName;
   }

   pubic String lastName() {
      return lastName;
   }

   public setLastName(String lastName) {
      this.lastName = lastName;
   }
}


interface EmployeeQueries extends BaseQuery {
   @Select (sql="SELECT employeeId, firstName, lastName FROM employee")
   DataSet<Employee> getAllEmployees ();

   @Update (sql="delete from employee")
   int deleteAllEmployees ();
}


Connection con = ...

EmployeeQueries empQueries = con.createQueryObject (EmployeeQueries.class);

DataSet<Employee> empData = empQueries.getAllEmployees ();

Exception-handling enhancements
The exception-handling functionality in the JDBC API prior to version 4.0 is limited and often insufficient. SQLException is thrown for all types of errors. There is no classification of exceptions, and no hierarchy defines them. The only way to get some meaningful information is to retrieve and analyze the SQLState value. SQLState values and their corresponding meanings change from datasource to datasource; hence, getting to the root of the problem and efficiently handling exceptions proves to be a tedious task.

JDBC 4.0 has enhanced exception-handling capability and alleviates some of the mentioned problems. The key changes are as follows:

  • Classification of SQLException into transient and non-transient types
  • Support for chained exceptions
  • Implementation of the Iterable interface

The SQLTransientException is thrown where a previously failed operation may succeed on retrial. The SQLNonTransientException is thrown where retrial will not lead to a successful operation unless the cause of the SQLException is corrected.

Figure 1 illustrates the subclasses of SQLTransientException and SQLNonTransientException.


Figure 1. SQL exception classes: Transient and non-transient

Support for chained exceptions are now included. New constructors add extra parameters to capture possible causes for the exception. Multiple SQLExceptions could be iterated over in a loop, and getCause() could be called to determine the exception's possible cause. The getCause() method can return non-SQLExceptions if they are the underlying cause of the exceptions.

The SQLException class now implements the Iterable interface and supports the J2SE 5.0 for each loop for easier and more elegant looping.

Listing 2 depicts the usage of the new for-each-loop construct:

Listing 2. For each loop

catch(SQLException ex) {
   for(Throwable t : ex) {
      System.out.println("exception:" + t);
   }
}


SQL/XML
A large amount of data now exists in the XML format. Databases have extended support for the XML data type by defining a standard XML type in the SQL 2003 specification. Most database vendors have an implementation of the XML data type in their new releases. With the inclusion of such a type, an XML dataset or document could be one of the fields or column values in a row of a database table. Prior to JDBC 4.0, perhaps the best way to manipulate such data within the JDBC framework is to use proprietary extensions from the driver vendors or access it as a CLOB type.

JDBC 4.0 now defines SQLXML as the Java data type that maps the database SQL XML type. The API supports processing of an XML type as a string or as a StAX stream. Streaming API for XML, which for Java has been adopted via JSR 173, is based on the Iterator pattern, as opposed to the Simple API for XML Processing (SAX), which is based on the Observer pattern.

Invoking the Connection object's createSQLXML() method can create a SQLXML object. This is an empty object, so the data can be attached to it by either using the setString() method or by associating an XML stream using the createXMLStreamWriter() method with the object. Similarly, XML data can be retrieved from a SQLXML object using getString() or associating an XML stream using createXMLStreamReader() with the object.

The ResultSet, the PreparedStatement, and the CallableStatement interfaces have getSQLXML() methods for retrieving a SQLXML data type. PreparedStatement and CallableStatement also have setSQLXML() methods to add SQLXML objects as parameters.

The SQLXML resources can be released by calling their free() methods, which might prove pertinent where the objects are valid in long-running transactions. DatabaseMetaData's getTypeInfo() method can be called on a datasource to check if the database supports the SQLXML data type, since this method returns all the data types it supports.

Connections and Statements
The Connection interface definitions have been enhanced to analyze connection state and usage to facilitate efficiency.

Sometimes database connections are unusable though they may not necessarily be closed and garbage collected. In such situations, the database appears slow and unresponsive. In most of these circumstances, reinitializing the connections is perhaps the only way to resolve the problem. When using the JDBC API prior to version 4.0, there is no way to distinguish between a stale connection and a closed connection. The new API adds an isValid() method to the Connection interface to query if the connection is still valid.

Also, database connections are often shared among clients, and sometimes some clients tend to use more resources than others, which can lead to starvation-like situations. The Connection interface defines a setClientInfo() method to define client-specific properties, which could be utilized to analyze and monitor resource utilization by the clients.

RowId
The RowId in many databases is a unique way to identify a row in a table. Queries using RowId in the search criteria are often the fastest way to retrieve data, especially true in the case of the Oracle and DB2 databases. Since java.sql.RowId is now a built-in type in Java, you could utilize the performance benefits associated with its usage. RowIds are most useful in identifying unique and specific rows when duplicate data exists and some rows are identical. However, it is important to understand that RowIds often are unique only for a table and not for the entire database; they may change and are not supported by all databases. RowIds are typically not portable across datasources and thus should be used with caution when working with multiple datasources.

A RowId is valid for the lifetime defined by the datasource and as long as the row is not deleted. The DatabaseMetadata.getRowIdLifetime() method is called to determine the RowId's lifetime. The return type is an enumeration type as summarized in the table below.

RowIdLifetime enum type Definition
ROWID_UNSUPPORTED Datasource does not support RowId type
ROWID_VALID_OTHER Implementation-dependent lifetime
ROWID_VALID_TRANSACTION Lifetime is at least the containing transaction
ROWID_VALID_SESSION Lifetime is at least the containing session
ROWID_VALID_FOREVER Unlimited lifetime

The ROWID_VALID_TRANSACTION, ROWID_VALID_SESSION, and ROWID_VALID_FOREVER definitions are true as long as the row is not deleted. It is important to understand that a new RowId is assigned if a row is deleted and reinserted, which sometimes could happen transparently at the datasource. As an example, in Oracle, if the "enable row movement" clause is set on a partitioned table and an update of the partition key causes the row to move from one partition to another, the RowId will change. Even without the "enable row movement" flag with the "alter table table_name" move, the RowId could change.

Both the ResultSet and CallableStatement interfaces have been updated to include a method called getRowID(), which returns a javax.sql.RowId type.

Listing 3 shows how RowId could be retrieved from a ResultSet and from a CallableStatement.

Listing 3. Get RowId

//The method signatures to retrieve RowId from a ResultSet is as follows:
   RowId getRowId (int columnIndex)
   RowId getRowId (String columnName)
...

Statement stmt = con.createStatement ();

ResultSet rs = stmt. ExecuteQuery (…);

while (rs.next ()) {

...

java.sql.RowId rid = rs.getRowId (1);

...

}

//The method signatures to retrieve RowId from a CallableStatement is as follows:
   RowId getRowId (int parameterIndex)
   RowId getRowId (String parameterName)

Connection con;
...

CallableStatement cstmt = con.prepareCall (…);
...

cstmt.registerOutParameter (2, Types.ROWID);

...

cstmt.executeUpdate ();

...

java.sql.RowId rid = cstmt.getRowId (2);

The RowId can be used to refer uniquely to a row and thus can be used to retrieve the rows or update the row data. When fetching or updating using RowId references, it is important to know the validity of the RowId's lifetime to assure consistent results. It is also advisable to simultaneously use another reference, such as the primary key, to avoid inconsistent results in circumstances where the RowId could change transparently.

The RowId values can also be set or updated. In the case of an updateable ResultSet, the updateRowId() method could be used to update the RowId for a particular row in a table.

Both the PreparedStatement and the CallableStatement interfaces support a setRowId() method, with different signatures, to set the RowId as a parameter value. This value could be used to refer to data rows or to update the RowId value for a particular row in a table.

The facility to set or update the RowId provides the flexibility to control the unique row identifiers and could be used to make such identifiers unique across the tables used. Perhaps, portability of RowId across supporting datasources could also be achieved by explicitly setting consistent values across them. However, because system-generated RowIds are often efficient, and transparent tasks could alter RowIds, they are best used by an application as a read-only attribute.

Leverage nonstandard vendor implemented resources
The new JDBC API defines a java.sql.Wrapper interface. This interface provides the ability to access datasource-vendor-specific resources by retrieving the delegate instance using the corresponding wrapped proxy instance.

This Wrapper interface has 17 sub-interfaces as per the current specification and includes Connection, ResultSet, Statement, CallableStatement, PreparedStatement, DataSource, DatabaseMetaData, and ResultSetMetaData, among others in the list. This is an excellent design as it facilitates datasource-vedor-specific resource implementation at almost all stages of the query-creation and result-set-retrieval lifecycles.

The unwrap() method returns the object that implements the given interface to allow access to vendor-specific methods. The isWrapperFor() method returns a Boolean value. It returns true if it implements the interface, or if it directly or indirectly is a wrapper for the object.

As an example, when using Oracle, Oracle JDBC drivers provide update batching extensions that are better performing and more efficient as compared to the standard JDBC batch-updating mechanisms. For earlier JDBC versions, this implies using the Oracle-specific definitions, such as OraclePreparedStatement, in the code. This compromises code portability. With the new API, many such efficient implementations can be wrapped and exposed within the standard JDBC definitions.

Service provider mechanism for driver loading
In a nonmanaged or standalone program scenario, prior to JDBC 4.0, you would have to load the JDBC driver class explicitly by invoking the Class.forName method, as shown in Listing 4:

Listing 4. Class.forName

Class.forName ("com.driverprovider.jdbc.jdbcDriverImpl");

With JDBC 4.0, if the JDBC driver vendors package their drivers as services, defined under the server provider mechanism definitions as per the JAR specification, the DriverManager code would implicitly load the driver by searching for it in the classpath. The benefit of this mechanism is that the developer does not need to know about the specific driver class and can write a little less code when using JDBC. Also, since the driver class name is no longer in the code, a name change would not require recompilations. If multiple drivers are specified in the classpath, then DriverManger will try and establish a connection using the first driver it encounters in the classpath and iterate further if required.

Conclusion
In this article, I have discussed some of the new and improved features of JDBC 4.0. Many of these new features enhance a developer's productivity and facilitate development. Also, the specification does not eradicate the possible use of extra JDBC frameworks to provide templating facilities and advanced exception-handling capabilities. However, there is some criticism as well. Some believe that annotations effectively lead to hard-coding in code, which causes problems in code maintainability.

posted @ 2006-05-09 11:17 Leo 阅读(887) | 评论 (2)编辑 收藏

ANT安装、配置

内容摘要:
ant是一个基于JAVA的自动化脚本引擎,脚本格式为XML。除了做JAVA编译相关任务外,ANT还可以通过插件实现很多应用的调用。


ANT的基本概念:
ANT的安装:解包,设置路径
ANT的使用:最好的学习只不过是一个简单实用的例子起步……
ANT的基本概念:Java的Makefile
当一个代码项目大了以后,每次重新编译,打包,测试等都会变得非常复杂而且重复,因此c语言中有make脚本来帮助这些工作的批量完成。在Java 中应用是平台无关性的,当然不会用平台相关的make脚本来完成这些批处理任务了,ANT本身就是这样一个流程脚本引擎,用于自动化调用程序完成项目的编译,打包,测试等。除了基于JAVA是平台无关的外,脚本的格式是基于XML的,比make脚本来说还要好维护一些。


每个ant脚本(缺省叫build.xml)中设置了一系列任务(target):比如对于一个一般的项目可能需要有以下任务。

任务1:usage 打印本脚本的帮助信息(缺省)
任务2:clean <-- init 清空初始化环境
任务3:javadoc <-- build <-- init 生成JAVADOC
任务4:jar <-- build <-- init 生成JAR
任务5:all <-- jar + javadoc <-- build <-- init 完成以上所有任务:jar javadoc
而多个任务之间往往又包含了一定了依赖关系:比如把整个应用打包任务(jar)的这个依赖于编译任务(build),而编译任务又依赖于整个环境初始化任务(init)等。

注:我看到很多项目的ant脚本中的命名基本上都是一致的,比如:编译一般叫build或者compile;打包一般叫jar或war;生成文档一般命名为javadoc或javadocs;执行全部任务all。在每个任务的中,ANT会根据配置调用一些外部应用并配以相应参数执行。虽然ANT可调用的外部应用种类非常丰富,但其实最常用的就2,3个:比如javac javadoc jar等。
ANT的安装
解包后在系统可执行路径中加入指向ant的bin的路径就可以了,比如可以在GNU/Linux上把以下配置加入/etc/profile中:
export ANT_HOME=/home/ant
export JAVA_HOME=/usr/java/j2sdk1.4.1
export PATH=$PATH:$JAVA_HOME/bin:$ANT_HOME/bin

这样执行ant 后,如果不指定配置文件ant会缺省找build.xml这个配置文件,并根据配置文件执行任务,缺省的任务设置可以指向最常用的任务,比如: build,或指向打印帮助信息:usage,告诉用户有那些脚本选项可以使用。


ANT的使用

最好的学习过程就是看懂那些open source项目中的build.xml脚本,然后根据自己的需要简化成一个更简单的,ANT和APACHE上很多非常工程派的项目:简单易用,而且适应性非常强,因为这些项目的建立往往来源于开发人员日常最直接的需求。
以下是的一个WebLucene应用的例子:修改自JDOM的build.xml:

<project default="usage" basedir=".">

<!-- =================================================================== -->
<!-- Initialization target -->
<!-- =================================================================== -->
<target name="init">
<tstamp/>
<property file="${basedir}/build.properties" />
<property name="Name" value="ProjectFullName"/>
<property name="name" value="project_name"/>
<property name="version" value="0.2"/>
<property name="year" value="2003"/>

<echo message="----------- ${Name} ${version} [${year}] ------------"/>

<property name="debug" value="off"/>
<property name="optimize" value="on"/>
<property name="deprecation" value="on"/>

<property name="src.dir" value="./src/WEB-INF/src"/>
<property name="lib.dir" value="./src/WEB-INF/lib"/>
<property name="packages" value="com.chedong.*,org.apache.lucene.*"/>

<property name="build.src" value="./src/WEB-INF/build"/>
<property name="build.dest" value="./src/WEB-INF/classes"/>
<property name="build.javadocs" value="./src/doc"/>

<path id="classpath">
<pathelement path="${jsdk_jar}"/>
<fileset dir="${lib.dir}">
<include name="**/*.jar"/>
</fileset>
</path>

<filter token="year" value="${year}"/>
<filter token="version" value="${version}"/>
<filter token="date" value="${TODAY}"/>
<filter token="log" value="true"/>
<filter token="verbose" value="true"/>
</target>

<!-- =================================================================== -->
<!-- Help on usage -->
<!-- =================================================================== -->
<target name="usage" depends="init">
<echo message="${Name} Build file"/>
<echo message="-------------------------------------------------------------"/>
<echo message=""/>
<echo message=" available targets are:"/>
<echo message=""/>
<echo message=" jar --> generates the ${name}.jar file"/>
<echo message=" build --> compiles the source code"/>
<echo message=" javadoc --> generates the API documentation"/>
<echo message=" clean --> cleans up the directory"/>
<echo message=""/>
<echo message=" Please rename build.properties.default to build.properties"/>
<echo message=" and edit build.properties to specify JSDK 2.3 classpath."/>
<echo message=""/>
<echo message=" See the comments inside the build.xml file for more details."/>
<echo message="-------------------------------------------------------------"/>
<echo message=""/>
<echo message=""/>
</target>

<!-- =================================================================== -->
<!-- Prepares the source code -->
<!-- =================================================================== -->
<target name="prepare-src" depends="init">
<!-- create directories -->
<mkdir dir="${build.src}"/>
<mkdir dir="${build.dest}"/>

<!-- copy src files -->
<copy todir="${build.src}">
<fileset dir="${src.dir}"/>
</copy>
</target>

<!-- =================================================================== -->
<!-- Compiles the source directory -->
<!-- =================================================================== -->
<target name="build" depends="prepare-src">
<javac srcdir="${build.src}"
destdir="${build.dest}"
debug="${debug}"
optimize="${optimize}">
<classpath refid="classpath"/>
</javac>
</target>

<!-- =================================================================== -->
<!-- Creates the class package -->
<!-- =================================================================== -->
<target name="jar" depends="build">
<jar jarfile="${lib.dir}/${name}.jar"
basedir="${build.dest}"
includes="**"/>
</target>

<!-- =================================================================== -->
<!-- Creates the API documentation -->
<!-- =================================================================== -->
<target name="javadoc" depends="build">
<mkdir dir="${build.javadocs}"/>
<javadoc packagenames="${packages}"
sourcepath="${build.src}"
destdir="${build.javadocs}"
author="true"
version="true"
use="true"
splitindex="true"
windowtitle="${Name} API"
doctitle="${Name}">
<classpath refid="classpath"/>
</javadoc>
</target>

<!-- =================================================================== -->
<!-- Clean targets -->
<!-- =================================================================== -->
<target name="clean" depends="init">
<delete dir="${build.src}"/>
<delete dir="${build.dest}/org"/>
<delete dir="${build.dest}/com"/>
<delete>
<fileset dir="${build.dest}" includes="**/*.class"/>
</delete>
</target>
</project>
<!-- End of file -->

缺省任务:usage 打印帮助文档,告诉有那些任务选项:可用的有build, jar, javadoc和clean.

初始化环境变量:init
所有任务都基于一些基本环境变量的设置初始化完成,是后续其他任务的基础,在环境初始化过程中,有2点比较可以方便设置:

1 除了使用却缺省的property设置了JAVA源路径和输出路径外,引用了一个外部的build.properties文件中的设置,
<property file="${basedir}/build.properties" />
这样大部分简单配置用户只要会看懂build.properties就可以了,毕竟XML比起key value的属性文件还是要可读性差一些。用build.properties也可以方便其他用户从编译的细节中解放出来。

2 CLASSPATH设置:使用了其中的:
<path id="classpath">
<pathelement path="${jsdk_jar}"/>
<fileset dir="${lib.dir}">
<include name="**/*.jar"/>
</fileset>
</path>
则相当于设置了:CLASSPATH=/path/to/resin/lib/jsdk23.jar; /path/to/project/lib/*.jar;

文件复制:prepare-src
创建临时SRC存放目录和输出目录。
<!-- =================================================================== -->
<!-- Prepares the source code -->
<!-- =================================================================== -->
<target name="prepare-src" depends="init">
<!-- create directories -->
<mkdir dir="${build.src}"/>
<mkdir dir="${build.dest}"/>

<!-- copy src files -->
<copy todir="${build.src}">
<fileset dir="${src.dir}"/>
</copy>
</target>

编译任务:build
编译时的CLASSPATH环境通过一下方式找到引用一个path对象
<classpath refid="classpath"/>

打包任务:jar
对应用打包生成项目所写名的.jar文件
<!-- =================================================================== -->
<!-- Creates the class package -->
<!-- =================================================================== -->
<target name="jar" depends="build">
<jar jarfile="${lib.dir}/${name}.jar"
basedir="${build.dest}"
includes="**"/>
</target>

生成JAVADOC文档任务: javadoc
<!-- =================================================================== -->
<!-- Creates the API documentation -->
<!-- =================================================================== -->
<target name="javadoc" depends="build">
<mkdir dir="${build.javadocs}"/>
<javadoc packagenames="${packages}"
sourcepath="${build.src}"
destdir="${build.javadocs}"
author="true"
version="true"
use="true"
splitindex="true"
windowtitle="${Name} API"
doctitle="${Name}">
<classpath refid="classpath"/>
</javadoc>
</target>

清空临时编译文件:clean
<!-- =================================================================== -->
<!-- Clean targets -->
<!-- =================================================================== -->
<target name="clean" depends="init">
<delete dir="${build.src}"/>
<delete dir="${build.dest}/org"/>
<delete dir="${build.dest}/com"/>
<delete>
<fileset dir="${build.dest}" includes="**/*.class"/>
</delete>
</target>

TODO:
更多任务/扩展:(样例)

测试任务:JUnit测试
代码风格检查任务:CheckStyle,Jalopy等
邮件警报任务:可以把以上这些任务的输出警告发送到制定的用户列表中,这个任务可以设置每天自动运行。

参考资料:

Jakarta ANT:
http://ant.apache.org



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=707995

posted @ 2006-05-08 10:54 Leo 阅读(650) | 评论 (3)编辑 收藏

最近写程序已经很少直接用JDBC了,一直都是用ibaits, Hibernate等来招呼,因为现在的集成框架已经很稳定了。不过对JDBC的直接使用还是不可以忽略的,JDBC3.0提供的n多的新特征还是要熟悉了解的,以前学jdbc的时候就是上网找些demo和介绍来学,使用很单一,对JDBC3.0的好多新的特征都忽略了,比如下面一个例子:

Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM user WHERE username='aa'");
stmt.executeUpdate("UPDATE user SET lastdatetime=now() where username='aa'");

这是一个用户登录时,经常用到的代码,先是根据用户名aa查找该用户的详细信息,然后再更新该用户的最后登录时间(lastdatetime)。这这个里面,我们用了两个sql语句,这个是我一直用的方法,但是如果用JDBC2.0给我们提供的便利,我们只要写一条sql就够了,其他的都交给jdbc,看下面的代码:

Statement stmt2 = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE);
ResultSet rs2 = stmt.executeQuery("SELECT * FROM user WHERE username='aa'");
rs2.next();
rs2.updateDate("lastdatetime", new Date(Calendar.getInstance().getTimeInMillis()));
rs2.updateRow();

这里面最主要的特征就是ResultSet.TYPE_FORWARD_ONLY和ResultSet.CONCUR_UPDATABLE,通过初始化Statement时传不同的参数,可以对ResultSet进行不用的错作限制。con.createStatement的时候,有三种可以掉用的函数:

1、createStatement();
2、createStatement(int resultSetType, int resultSetConcurrency)
3、createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)

其中resultSetType可选值是:
   1、ResultSet.TYPE_FORWARD_ONLY  在ResultSet中只能先前移动游标,
   2、ResultSet.TYPE_SCROLL_INSENSITIVE 在ResultSet中可以随心所欲的先前向后移动游标,
   3、ResultSet.TYPE_SCROLL_SENSITIVE 在ResultSet中可以随心所欲的先前向后移动游标,同时ResultSet的值有所改变的时候,他可以得到改变后的最新的值
其中resultSetConcurrency可选值是:
   1、ResultSet.CONCUR_READ_ONLY  在ResultSet中的数据记录是只读的,可以修改
   2、ResultSet.CONCUR_UPDATABLE  在ResultSet中的数据记录可以任意修改,然后更新会数据库
其中resultSetHoldability可选值是:
   1、ResultSet.HOLD_CURSORS_OVER_COMMIT 表示修改提交时,不关闭ResultSet的游标
   2、ResultSet.CLOSE_CURSORS_AT_COMMIT  表示修改提交时,关闭ResultSet的游标

对于查询操作第一种初始化方法createStatement(),相当于第二种方法的createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY),第三种方法的createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT)

下面写一段demo的代码,我把一些特征函数都用出来,但是只是用来查考和说明名灵活性的。

 Statement stmt2 = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE);
 ResultSet rs2 = stmt.executeQuery("SELECT * FROM user");
 rs2.next();
 rs2.updateDate("lastdatetime", new Date(Calendar.getInstance().getTimeInMillis()));
 rs2.updateRow();
 rs2.afterLast();
 while(rs2.previous()){ /**....*/ }
 rs.beforeFirst();
 while(rs2.next()){  /**....*/ }
 rs.last();
 rs.first();
 rs.absolute(5); //游标移动到第5条
 rs.absolute(-1);  //游标移动到最后一条
 rs.relative(-5);  //游标向上移动5条
 rs.relative(2);   //游标向下移动2条
 rs.deleteRow(); //删除当前行
 rs.last();  //游标移动到最后
 rs.updateString("summary", "This is ..."); //设置更新的字段值
 rs.cancelRowUpdates();  //取消刚才输入的更新
 rs.getRow(); //得到当前行号
 rs.moveToInsertRow();  //游标移动到要新增的那条记录上
 rs.updateInt("id", 1);
 rs.updateString(2, "my name");
 rs.insertRow(); //插入新记录


JDBC2.0提供的还有一个功能就是数据库的批量操作

  con.setAutoCommit(false);
  Statement stmt3 = con.createStatement();
  stmt3.addBatch("insert .....");
  stmt3.addBatch("insert .....");
  int[] rows = stmt3.executeBatch();
  con.commit();

但是有一点要注意,stmt3.executeBatch()他不会自动给你回滚数据操作,当你有5条update语句的时候,如果第三条发生错误,那么将无法自动回滚前两条update语句的影响,所以一定要自己手工进行事务管理。

在您的事务中使用 Savepoint
JDBC3.0中最令人兴奋的附加特点就是 Savepoint 了。有时候需要的是对事务多一点的控制,而不是在当前的事务中简单地对每一个改变进行回滚。在JDBC3.0下,您就可以通过 Savepoint 获得这种控制。Savepoint 接口允许您将事务分割为各个逻辑断点,以控制有多少事务需要回滚。看下面的代码:

conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
Statement stmt = conn.createStatement();
int rows = stmt.executeUpdate( "INSERT INTO authors (first_name, last_name) valueS(′Lewis′, ′Carroll′)");
Savepoint svpt = conn.setSavepoint("NewAuthor");
try{
 rows = stmt.executeUpdate( "UPDATE authors set type = ′fiction′ WHERE last_name = ′Carroll′");
}catch(Exception e){
 conn.rollback(svpt);
 rows = stmt.executeUpdate( " update .......... other sql ");
}
conn.commit();

上面代码显示,当UPDATE authors失败的时候,系统事务回滚UPDATE authors的sql的影响,而INSERT INTO authors的sql仍然有效


检索自动产生的关键字
为了解决对获取自动产生的或自动增加的关键字的值的需求,JDBC 3.0现在将获取这种值变得很轻松。要确定任何所产生的关键字的值,只要简单地在语句的 execute() 方法中指定一个可选的标记,Statement.RETURN_GENERATED_KEYS和Statement.NO_GENERATED_KEYS。在执行这条语句后,所产生的关键字的值就会通过从 Statement 的实例方法 getGeneratedKeys() 来检索 ResultSet 而获得。ResultSet 包含了每个所产生的关键字的列。看下面代码:

Statement stmt = conn.createStatement();
stmt.executeUpdate("INSERT INTO authors (first_name, last_name) valueS (′George′, ′Orwell′)", Statement.RETURN_GENERATED_KEYS);
ResultSet rs = stmt.getGeneratedKeys();
if ( rs.next() ) {
 int key = rs.getInt();
}

 参考资料: http://java.sun.com/j2se/1.5.0/docs/api/java/sql/package-summary.html

posted @ 2006-05-08 10:49 Leo 阅读(1527) | 评论 (2)编辑 收藏

< script language = " javascript "  type = " text/javascript " >

function Hashtable()
{
    
this ._hash         =   new  Object();
    
this .add         =  function(key,value){
                        
if (typeof(key) != " undefined " ){
                            
if ( this .contains(key) == false ){
                                
this ._hash[key] = typeof(value) == " undefined " ? null :value;
                                
return   true ;
                            } 
else  {
                                
return   false ;
                            }
                        } 
else  {
                            
return   false ;
                        }
                    }
    
this .remove         =  function(key){delete  this ._hash[key];}
    
this .count         =  function(){var i = 0 ; for (var k in  this ._hash){i ++ ;}  return  i;}
    
this .items         =  function(key){ return   this ._hash[key];}
    
this .contains     =  function(key){  return  typeof( this ._hash[key]) != " undefined " ;}
    
this .clear         =  function(){ for (var k in  this ._hash){delete  this ._hash[k];}}

}

var a 
=   new  Hashtable();

a.add(
" aa " );
a.add(
" bb " , 2342 );
a.add(
" bb " , 2342 );

a.remove(
" aa " );

alert(a.count());

alert(a.contains(
" bb " ));

alert(a.contains(
" aa " ));

alert(a.items(
" bb " ));


</ script >
posted @ 2006-04-29 20:59 Leo 阅读(456) | 评论 (2)编辑 收藏

     摘要: < HTML > < HEAD > < TITLE > print </ TITLE > < meta  http-equiv =...  阅读全文
posted @ 2006-04-29 20:57 Leo 阅读(1325) | 评论 (1)编辑 收藏

     摘要: 事件源对象 event.srcElement.tagName event.srcElement.type 捕获释放 event.srcElement.setCapture();  event.srcElement.releaseCapture();  事件按键 ...  阅读全文
posted @ 2006-04-29 20:50 Leo 阅读(521) | 评论 (2)编辑 收藏

     摘要: 为 我们的项目写的一个轻量的分页API。目的在于将分页与数据查询的逻辑完全剥离。我以前看过robbin发的通过detachedCriteria实现的 分页那片贴子,里面把分页和数据查询结合在一起了。而我觉得分开更轻量,而且替换也比较容易。但是这个实现中有一个反模式,在逻辑中生成了代码,无奈之 选,为了简便。其中字符生成可以自己扩展i18n实现,应该非常容易。分页实现的接口:package c...  阅读全文
posted @ 2006-04-03 02:22 Leo 阅读(498) | 评论 (2)编辑 收藏


作者:朱骐

作者简介


朱骐,男,江苏南京人,讲师,主要研究方向:信息系统与集成技术。您可以通过qizhu003@msn.com和作者取得联系。

内容摘要


JMS面向Web的应用与面向桌面的应用相比,有特殊的用户环境要求:同一个消息必须能被若干未知的用户消费,因此在消息接收方必须有"接收而不确认"的提交机制;本文以CWNF校务系统为实现案例,讨论面向Web的JMS应用系统消息提交原理及采用的关键技术。

消息传递是一种在软件组件或应用之间进行分布式通信的松散耦合方法,与各种紧密耦合通信技术(如CORBA、Java RMI、COM/DCOM)相比,不同之处在于:①消息系统是一种对等实施,通信双方即消息的发送者和接受者都是该系统中的客户端,彼此不呈C/S关系; ②通信双方的工作是异步的;③基于消息格式一致,通信双方只需一个中介来存储并管理消息就可以实现通信,而紧密耦合技术则需要知道远程方法在本地的接口。 因自身特点,消息传递技术在企业中和企业间有较广泛的应用需求。

JMS(Java Message Service)是J2EE企业平台的Java消息服务,目前主流J2EE产品的JMS都实现了存储功能,JMS客户端通过JMS API创建,彼此间通过目的地(Destination)对象进行通信;可是JMS消息系统多见于桌面应用,而Web应用鲜见,本文以笔者开发的CWNF 校务系统为案例,讨论面向Web的JMS应用系统的实现原理及采用的关键技术。

1  面向Web的JMS应用系统实现原理

1.1  JMS应用系统消息传递原理


JMS应用系统有4个部分:①JMS提供者(JMS Provider),是一个逻辑数据存储体,并提供管理工具和控制特性;②JMS客户端,是用Java语言编写的发送或接收消息的组件或应用;③消息,是 JMS客户端间被传递的承载信息的对象;④被管理对象,是系统管理员为客户端预置的JMS对象,包括目的地对象和连接工厂对象,其中目的地对象是客户端间 的消息中介。这4个部分通过JNDI相关联:管理员通过管理工具把目的地对象和连接工厂对象绑定到一个JNDI API命名空间中,JMS客户端就可以在命名空间中查找这些对象,并通过JMS提供者建立与这些对象的逻辑连接,从而彼此之间实现通信(图1)。JMS支 持2种消息传递域:点到点、发布/订阅,与之相对应的消息目的地对象也有2种:队列、主题。

1.2  Web应用的消息提交机制


通常,无论是消息发送方还是接收方,桌面应用都不容许消息丢失或重复,JMS消息提交机制是基于这个要求的,它们从不同方面保证该要求的实现:①在 接收方控制消息的确认。通过确认保证一个接收者对一个消息只消费一次,在非事务性的会话中,消息确认方式取决于create×××Session方法第二 个参数的值;在事务性会话中,无论由Bean管理事务还是由Bean容器管理事务,消息确认都由Bean容器自动完成。②在发送方指定消息的提交模式和生 存期。提交模式有两种:PERSISTENT(稳定存储)和NON_PERSISTENT(非稳定存储),稳定存储保证在故障情况下消息不会丢失;生存期 决定一个消息在存储中介中的存在寿命,JMS提供者会自动摧毁到期的消息。③创建持久定阅的接收方。在发布/订阅系统中,持久订阅者可以接收到在订阅者关 闭阶段消息发送方发布的消息。

但是Web应用系统在消息接收方有Web特有的用户环境要求:①若干个用户共用一个JMS客户端组件,因此消息就应向一个消息接收者提交而不需确 认,具有容器自动确认功能的Bean是无法实现这一要求的;在一个组件内如果把会话设置成事务性的,而这个组件的容器又不具有事务管理能力,则这个组件就 能做到"接收而不确认",在Web应用系统中只有Servlet组件符合这一要求。②JMS客户端的消息接收者经常关闭,为了接收在关闭期间发送来的消 息,消息接收者必定是基于主题的持久定阅者,所以面向Web的JMS应用系统必定采用发布/订阅消息传递域。

2  CWNF校务系统模型


CWNF是一个面向Web的JMS校务系统,用于校园发布通知及征求意见等校务工作,通知分为2类:普通通知和征求意见性通知。

该系统用户分成3类,用户不同,处理模型也不同,基本情况如下:①发布用户,拥有通知发布权,向主题发布通知;②署名用户,查阅通知,也可发表对征求意见性通知的反馈意见;③匿名用户,只查阅通知。

2.1  数据与数据流模型


系统中的数据因此有2类:通知、反馈。接收方接收的数据将形成一个XML文档对象,以便发往Web浏览器显示;基于这样的要求,考察下面2个问题:①系统中各方之间的数据关系,②各方数据的形式。

主要的数据关系有3个:①通知发送方与通知接收方的数据关系,②反馈发送方与反馈接收方的数据关系,③通知接收方与反馈接收方的数据关系。(如图 2)在发送方,数据(通知或反馈)是一件一件的发送,在接收方,数据(通知或反馈)则是批接收,是对应发送方数据的集合,因此在发送方没有必要把数据直接 加工成XML文档对象形式,只要生成能构成XML文档对象的元素对象即可;而通知接收方与反馈接收方的数据关系则是:每一条征求意见性通知都有相关的一个 反馈集合。

系统的数据流模型如下:
①通知发送方:表单数据→XML元素(通知)→主题(存储)
②通知接收方:主题(存储)→XML元素(通知)→XML文档(通知)→XSL显示(含表单)
③通知接收方到反馈接收方: XSL显示(含表单)→主题(存储)
④反馈接收方:主题(存储)→XML元素(反馈)→XML文档(反馈)→XSL显示(含表单)
⑤反馈发送方:表单数据→XML元素(反馈)→主题(存储)

2.2  组件模型


系统组件模型如图3:主题CWNFTopic是消息传递中介,NoticerServlet组件向发布用户发送表单,并从表单接收数据,然后生成 XML元素对象,该元素对象和其它一些数据被作为参数调用PublisherBean组件方法,向主题发送以该元素对象为消息体的消息; ReaderServlet组件处理署名用户和匿名用户查阅通知的业务,它从表单获得用户将查阅什么方面通知的有关信息后,便使用receive方法限时 阻塞地从主题接收消息并对消息进行筛选,把筛选出的若干消息的消息体取出,然后加工成XML文档对象(根元素是通知集),最后输出。 FeedbackerPubServlet用于反馈发送方的业务处理,功能与NoticerServlet相似; FeedbackerSubServlet用于反馈接收方的业务处理,功能与ReaderServlet相似;PublisherBean组件被 NoticerServlet组件和FeedbackerPubServlet组件调用,用于发送消息,容器管理发送事务,具有很高的可靠性。

3  关键的实现技术

3.1  JDOM建立、输出XML文档


JDOM是一个开放源代码的纯Java树式API,用于分析、建立、处理和序列化XML文档。在数据流模型中,XML元素和XML文档都由JDOM API建立,在发送方,通过用户提交的表单取得名/值对若干,这些数据经过JDOM方法处理生成XML元素对象,元素对象被作为消息的消息体发往主题存 储;在接收方,持久订阅者接收到若干XML元素对象后,继续通过JDOM方法建立XML文档对象。且XML文档向Web浏览器输出也依赖于JDOM的 XMLOutputte对象方法:

XMLOutputter serializer=new XMLOutputter();

PrintWriter out
=response.getWriter();    // out 是ServletResponse的输出流对象
serializer.output(xmldoc,out);            //通过out把XML文档输出到页面


3.2  XSL定义XML文档显示样式


XSL是可扩展的样式单语言,通知集的XML文档和反馈集的XML文档都有相关的XSL文档决定其页面显示,如通知集XML文档的XSL样式定义如下:

<?xml version="1.0" encoding="GBK"?>
<xsl:stylesheet>
<xsl:template match="/">

<HTML>
<BODY>

<DIV><xsl:apply-templates select="通知集"/></DIV>    
</BODY>
</HTML>

</xsl:template>
<xsl:template match="通知集">
<xsl:for-each select="通知">

</xsl:for-each>
</xsl:template>
</xsl:stylesheet> 

3.3  Servlet间数据的传递

3.3.1  注册/登录


用户的一些处理工作需要注册/登录后才能进行,因此注册/登录的获准信息必须能在有关Servlet组件之间传递。ServletContext 对象可设置和读取属性,使不同Servlet之间相互通信,在系统中被用于有关组件对用户身份的验证。

3.3.2  通知与反馈的数据关联


每一条征求意见性通知都有一个相关联的反馈集合,关联可通过设置消息属性实现。JMS消息(包括通知类消息)都有系统级JMSMessageID属 性,其值是唯一的,可用于表征每一条征求意见性通知,因此对任何反馈消息也可以设置一个应用级属性(CWNF中是FeedbackSN),让它取与之相关 联的征求意见性通知的JMSMessageID属性值。这样就建立了两者间的数据关联。

因此数据流模型"③通知接收方到反馈接收方: XSL显示(含表单)→主题(存储)"的实现流程如下:用户在页面上选择一条征求意见性通知后,该通知的JMSMessageID属性值将被传递给 FeedbackerSubServlet组件,该组件将使用这个属性值去匹配从主题取出的反馈消息的FeedbackSN属性,从而筛选出相关联的反馈 消息。

那么一条征求意见性通知的JMSMessageID属性值又如何传递给FeedbackerSubServlet组件呢?通过 ServletContext对象只能传递可预知信息,CWNF的做法是:由XSL为每一条征求意见性通知设置一个独立的表单,并把该通知的 JMSMessageID属性值写在表单的TEXTAREA元素框内,这样用户在表单上选择一条征求意见性通知后,该通知的JMSMessageID属性 值就随表单一起提交给FeedbackerSubServlet组件。XSL有关代码如下:

<xsl:if test="string(意见反馈)='on'">
<FORM method="post" action="http://localhost:6888/Feedbacker/servlet
/FeedbackerSubServlet"
>
<BUTTON type="submit">意见反馈</BUTTON>
<TEXTAREA name="序列号" rows="1" cols="40">
<xsl:value-of select="序列号"/>
</TEXTAREA>
</FORM>
</xsl:if>

4  结束语


JMS应用系统与数据库系统有相似性,从数据方面看,JMS消息体的数据类型支持文本和对象,所以JMS更灵活,与XML集成应用的空间更大;但从 管理上看,JMS Provider向管理员提供的管理功能远远低于DBMS提供的管理功能,因此在面向Web的应用中,JMS宜作为中小流量、管理员参与度较低的信息系统 解决方案。

转载自:http://gceclub.sun.com.cn/yuanchuang/2004_Q4/jms.html

posted @ 2006-02-23 19:07 Leo 阅读(609) | 评论 (0)编辑 收藏

作者:刘学超

1  Java技术与Java虚拟机

说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系如下图所示:

图1  Java四个方面的关系

运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class 文件)。最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。从上图也可以看出 Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。 这个平台的结构如下图所示:

在Java平台的结构中, 可以看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。

那么到底什么是Java虚拟机(JVM)呢?通常我们谈论JVM时,我们的意思可能是:

  1. 对JVM规范的的比较抽象的说明;
  2. 对JVM的具体实现;
  3. 在程序运行期间所生成的一个JVM实例。

对JVM规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》(《Java虚拟机规范》)中被详细地描述了;对JVM的具体实现要么是软件,要么是软件和硬件的组合,它已经被许多生产厂 商所实现,并存在于多种平台之上;运行Java程序的任务由JVM的运行期实例单个承担。在本文中我们所讨论的Java虚拟机(JVM)主要针对第三种情 况而言。它可以被看成一个想象中的机器,在实际的计算机上通过软件模拟来实现,有自己想象中的硬件,如处理器、堆栈、寄存器等,还有自己相应的指令系统。

JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。下面我们从JVM的体系结构和它的运行过程这两个方面来对它进行比较深入的研究。

2  Java虚拟机的体系结构

刚才已经提到,JVM可以由不同的厂商来实现。由于厂商的不同必然导致JVM在实现上的一些不同,然而JVM还是可以实现跨平台的特性,这就要归功于设计JVM时的体系结构了。

我们知道,一个JVM实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这些部分,它们描 述了JVM的一个抽象的内部体系结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每个 JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。 每个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成的体系结构图为:

图3  JVM的体系结构

JVM的每个实例都有一个它自己的方法域和一个堆,运行于JVM内的所有的线程都共享这些区域;当虚拟机装载类文件 的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行的时候,JVM把程序初始化的所有对象置于堆上;而每个线程创建的时 候,都会拥有自己的程序计数器和Java栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的Java栈则存储为该线程调用Java方法的状态; 本地方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。

下面分别对这几个部分进行说明。

执行引擎处于JVM的核心位置,在Java虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规范很详 细地说明了当JVM执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java虚拟机支持大约248个字节码。每个字节码执行一种基本 的CPU运算,例如,把一个整数加到寄存器,子程序转移等。Java指令集相当于Java程序的汇编语言。

Java指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有0个或多个操作数,提供操作所需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。

虚拟机的内层循环的执行过程如下: 
do{
取一个操作符字节;
根据操作符的值执行一个动作;
}while(程序未结束)

由于指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提高执行的效率。指令中操作数的数量和大小是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个16位的参数存放时占用两个字节,其值为:

第一个字节*256+第二个字节字节码。

指令流一般只是字节对齐的。指令tableswitch和lookup是例外,在这两条指令内部要求强制的4字节边界对齐。

对于本地方法接口,实现JVM并不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接 口(JNI)是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替Sun公司的JNI。但是这些设计与实现是比较复杂的事情,需要确保垃圾回 收器不会将那些正在被本地方法调用的对象释放掉。

Java的堆是一个运行时数据区,类的实例(对象)从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式释放对象的能力。Java不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。

Java方法区与传统语言中的编译后代码或是Unix进程中的正文段类似。它保存方法代码(编译后的 java代码)和符号表。在当前的Java实现中,方法代码不包括在垃圾回收堆中,但计划在将来的版本中实现。每个类文件包含了一个Java类或一个 Java界面的编译后的代码。可以说类文件是Java语言的执行代码文件。为了保证类文件的平台无关性,Java虚拟机规范中对类文件的格式也作了详细的 说明。其具体细节请参考Sun公司的Java虚拟机规范。

Java虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似。Java虚拟机的寄存器有四种:

  1. pc: Java程序计数器;
  2. optop: 指向操作数栈顶端的指针;
  3. frame: 指向当前执行方法的执行环境的指针;。
  4. vars: 指向当前执行方法的局部变量区第一个变量的指针。

在上述体系结构图中,我们所说的是第一种,即程序计数器,每个线程一旦被创建就拥有了自己的程序计数器。当线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那么程序计数器的值就不会被定义。

Java虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。

局部变量区

每个Java方法使用一个固定大小的局部变量集。它们按照与vars寄存器的字偏移量来寻址。局部变量都是32位 的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具有索引n的局部变量,如果是一个双精度浮点数,那 么它实际占据了索引n和n+1所代表的存储空间)虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数 栈的指令,也提供了把操作数栈中的值写入局部变量的指令。

运行环境区

在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉。

动态链接

运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的class 文件代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释还没有定义的符号,并把变量 访问翻译成与这些变量运行时的存储结构相应的偏移地址。动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。

正常的方法返回

如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。

异常捕捉

异常情况在Java中被称作Error(错误)或Exception(异常),是Throwable类的子类,在程序中的原因是:①动态链接错,如无法找到所需的class文件。②运行时错,如对一个空指针的引用。程序使用了throw语句。

当异常发生时,Java虚拟机采取如下措施:

  • 检查与当前方法相联系的catch子句表。每个catch子句包含其有效指令范围,能够处理的异常类型,以及处理异常的代码块地址。
  • 与异常相匹配的catch子句应该符合下面的条件:造成异常的指令在其指令范围之内,发生的异常类型是其能处理的异常类型的子类型。如 果找到了匹配的catch子句,那么系统转移到指定的异常处理块处执行;如果没有找到异常处理块,重复寻找匹配的catch子句的过程,直到当前方法的所 有嵌套的catch子句都被检查过。
  • 由于虚拟机从第一个匹配的catch子句处继续执行,所以catch子句表中的顺序是很重要的。因为Java代码是结构化的,因此总 可以把某个方法的所有的异常处理器都按序排列到一个表中,对任意可能的程序计数器的值,都可以用线性的顺序找到合适的异常处理块,以处理在该程序计数器值 下发生的异常情况。
  • 如果找不到匹配的catch子句,那么当前方法得到一个"未截获异常"的结果并返回到当前方法的调用者,好像异常刚刚在其调用者中发 生一样。如果在调用者中仍然没有找到相应的异常处理块,那么这种错误将被传播下去。如果错误被传播到最顶层,那么系统将调用一个缺省的异常处理块。

操作数栈区

机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存器或非 通用寄存器的机器(如Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操 作的参数,并保存操作的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整 数将从堆栈弹出、相加,并把结果压回到操作数栈中。

每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long 和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个int类型的数,如果把它们当作是一个long类型的数则 是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不 考虑类型的。

本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问 虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。例如,本地栈是一个C语言的栈,那么当C程序调用C函数时,函数的参数以某种顺序被压入 栈,结果则返回给调用函数。在实现Java虚拟机时,本地方法接口使用的是C语言的模型栈,那么它的本地方法栈的调度与使用则完全与C语言的栈相同。

3  Java虚拟机的运行过程

上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。

虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。例如对于程序:

class HelloApp 
{
    
public static void main(String[] args) 
    {
        System.out.println(
"Hello World!"); 
        
for (int i = 0; i < args.length; i++ )
        {
            System.out.println(args[i]);
        }
    }
}

编译后在命令行模式下键入: java HelloApp run virtual machine

将通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、"virtual"、"machine"的数组。现在我们略述虚拟机在执行HelloApp时可能采取的步骤。

开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二 进制代表,于是虚拟机使用ClassLoader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用之 前,必须对类HelloApp与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接 口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数 和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如下:

图4:虚拟机的运行过程

4  结束语

本文通过对JVM的体系结构的深入研究以及一个Java程序执行时虚拟机的运行过程的详细分析,意在剖析清楚Java虚拟机的机理。

转载自:http://gceclub.sun.com.cn/staticcontent/html/2004-04-09/jvm.html

posted @ 2006-02-23 19:00 Leo 阅读(720) | 评论 (0)编辑 收藏

作者:盛戈歆

作者简介

盛戈歆,软件工程师,你可以通过shenggexin@topwaver.com与他联系。

正文

Java中类的查找与装载出现的问题总是会时不时出现在Java程序员面前,这并不是什么丢脸的事情,相信没有一个 Java程序员没遇到过ClassNotException,因此不要为被人瞅见自己也犯这样的错误而觉得不自然,但是在如果出现了 ClassNotFoundException后异常后一脸的茫然,那我想你该了解一下java的类装载的体制了,同时为了进行下面的关于类装载器之间的 隔离性的讨论,我们先简单介绍一下类装载的体系结构。

1. Java类装载体系结构

装载类的过程非常简单:查找类所在位置,并将找到的Java类的字节码装入内存,生成对应的Class对象。 Java的类装载器专门用来实现这样的过程,JVM并不止有一个类装载器,事实上,如果你愿意的话,你可以让JVM拥有无数个类装载器,当然这除了测试 JVM外,我想不出还有其他的用途。你应该已经发现到了这样一个问题,类装载器自身也是一个类,它也需要被装载到内存中来,那么这些类装载器由谁来装载 呢,总得有个根吧?没错,确实存在这样的根,它就是神龙见首不见尾的Bootstrap ClassLoader. 为什么说它神龙见首不见尾呢,因为你根本无法在Java代码中抓住哪怕是它的一点点的尾巴,尽管你能时时刻刻体会到它的存在,因为java的运行环境所需 要的所有类库,都由它来装载,而它本身是C++写的程序,可以独立运行,可以说是JVM的运行起点,伟大吧。在Bootstrap完成它的任务后,会生成 一个AppClassLoader(实际上之前系统还会使用扩展类装载器ExtClassLoader,它用于装载Java运行环境扩展包中的类),这个 类装载器才是我们经常使用的,可以调用ClassLoader.getSystemClassLoader() 来获得,我们假定程序中没有使用类装载器相关操作设定或者自定义新的类装载器,那么我们编写的所有java类通通会由它来装载,值得尊敬吧。 AppClassLoader查找类的区域就是耳熟能详的Classpath,也是初学者必须跨过的门槛,有没有灵光一闪的感觉,我们按照它的类查找范围 给它取名为类路径类装载器。还是先前假定的情况,当Java中出现新的类,AppClassLoader首先在类传递给它的父类类装载器,也就是 Extion ClassLoader,询问它是否能够装载该类,如果能,那AppClassLoader就不干这活了,同样Extion ClassLoader在装载时,也会先问问它的父类装载器。我们可以看出类装载器实际上是一个树状的结构图,每个类装载器有自己的父亲,类装载器在装载 类时,总是先让自己的父类装载器装载(多么尊敬长辈),如果父类装载器无法装载该类时,自己就会动手装载,如果它也装载不了,那么对不起,它会大喊一声: Exception,class not found。有必要提一句,当由直接使用类路径装载器装载类失败抛出的是NoClassDefFoundException异常。如果使用自定义的类装载 器loadClass方法或者ClassLoader的findSystemClass方法装载类,如果你不去刻意改变,那么抛出的是 ClassNotFoundException。

我们简短总结一下上面的讨论:

1.JVM类装载器的体系结构可以看作是树状结构。

2.父类装载器优先装载。在父类装载器装载失败的情况下再装载,如果都装载失败则抛出ClassNotFoundException或者NoClassDefFoundError异常。

那么我们的类在什么情况下被装载的呢?

2. 类如何被装载

在java2中,JVM是如何装载类的呢,可以分为两种类型,一种是隐式的类装载,一种式显式的类装载。

2.1 隐式的类装载

隐式的类装载是编码中最常用得方式:

A b = new A();

如果程序运行到这段代码时还没有A类,那么JVM会请求装载当前类的类装器来装载类。 问题来了,我把代码弄得复杂一点点,但依旧没有任何难度,请思考JVM得装载次序:

 1 package test;
 2 Public class A{
 3     public void static main(String args[]){
 4         B b = new B();
 5     }
 6 }
 7 
 8 class B{C c;}
 9 
10 class C{}

揭晓答案,类装载的次序为A->B,而类C根本不会被JVM理会,先不要惊讶,仔细想想,这不正是我们最需要 得到的结果。我们仔细了解一下JVM装载顺序。当使用Java A命令运行A类时,JVM会首先要求类路径类装载器(AppClassLoader)装载A类,但是这时只装载A,不会装载A中出现的其他类(B类),接 着它会调用A中的main函数,直到运行语句b = new B()时,JVM发现必须装载B类程序才能继续运行,于是类路径类装载器会去装载B类,虽然我们可以看到B中有有C类的声明,但是并不是实际的执行语句, 所以并不去装载C类,也就是说JVM按照运行时的有效执行语句,来决定是否需要装载新类,从而装载尽可能少的类,这一点和编译类是不相同的。

2.2 显式的类装载

使用显示的类装载方法很多,我们都装载类test.A为例。

使用Class类的forName方法。它可以指定装载器,也可以使用装载当前类的装载器。例如:

Class.forName("test.A");

它的效果和
Class.forName("test.A",true,this.getClass().getClassLoader());

是一样的。

使用类路径类装载装载.

ClassLoader.getSystemClassLoader().loadClass("test.A");

使用当前进程上下文的使用的类装载器进行装载,这种装载类的方法常常被有着复杂类装载体系结构的系统所使用。

Thread.currentThread().getContextClassLoader().loadClass("test.A")

使用自定义的类装载器装载类

1 public class MyClassLoader extends URLClassLoader{
2 public MyClassLoader() {
3         super(new URL[0]);
4     }
5 }
6 MyClassLoader myClassLoader = new MyClassLoader();
7 myClassLoader.loadClass("test.A");

MyClassLoader继承了URLClassLoader类,这是JDK核心包中的类装载器,在没有指定父类装载器的情况下,类路径类装载器就是它的父类装载器,MyClassLoader并没有增加类的查找范围,因此它和类路径装载器有相同的效果。

我们已经知道Java的类装载器体系结构为树状,多个类装载器可以指定同一个类装载器作为自己的父类,每个子类装载 器就是树状结构的一个分支,当然它们又可以个有子类装载器类装载器,类装载器也可以没有父类装载器,这时Bootstrap类装载器将作为它的隐含父类, 实际上Bootstrap类装载器是所有类装载器的祖先,也是树状结构的根。这种树状体系结构,以及父类装载器优先的机制,为我们编写自定义的类装载器提 供了便利,同时可以让程序按照我们希望的方式进行类的装载。例如某个程序的类装载器体系结构图如下:

图2:某个程序的类装载器的结构

解释一下上面的图,ClassLoaderA为自定义的类装载器,它的父类装载器为类路径装载器,它有两个子类装载 器ClassLoaderAA和ClassLaderAB,ClassLoaderB为程序使用的另外一个类装载器,它没有父类装载器,但有一个子类装载 器ClassLoaderBB。你可能会说,见鬼,我的程序怎么会使用这么复杂的类装载器结构。为了进行下面的讨论,暂且委屈一下。

3. 奇怪的隔离性

我们不难发现,图2中的类装载器AA和AB, AB和BB,AA和B等等位于不同分支下,他们之间没有父子关系,我不知道如何定义这种关系,姑且称他们位于不同分支下。两个位于不同分支的类装载器具有 隔离性,这种隔离性使得在分别使用它们装载同一个类,也会在内存中出现两个Class类的实例。因为被具有隔离性的类装载器装载的类不会共享内存空间,使 得使用一个类装载器不可能完成的任务变得可以轻而易举,例如类的静态变量可能同时拥有多个值(虽然好像作用不大),因为就算是被装载类的同一静态变量,它 们也将被保存不同的内存空间,又例如程序需要使用某些包,但又不希望被程序另外一些包所使用,很简单,编写自定义的类装载器。类装载器的这种隔离性在许多 大型的软件应用和服务程序得到了很好的应用。下面是同一个类静态变量为不同值的例子。

 1 package test;
 2 public class A {
 3   public static void main( String[] args ) {
 4     try {
 5       //定义两个类装载器
 6       MyClassLoader aa= new MyClassLoader();
 7       MyClassLoader bb = new MyClassLoader();
 8 
 9       //用类装载器aa装载testb.B类
10       Class clazz=aa.loadClass("testb. B");
11       Constructor constructor= 
12         clazz.getConstructor(new Class[]{Integer.class});
13       Object object = 
14         constructor.newInstance(new Object[]{new Integer(1)});
15       Method method = 
16         clazz.getDeclaredMethod("printB",new Class[0]);
17 
18       //用类装载器bb装载testb.B类
19       Class clazz2=bb.loadClass("testb. B");
20       Constructor constructor2 = 
21         clazz2.getConstructor(new Class[]{Integer.class});
22       Object object2 = 
23         constructor2.newInstance(new Object[]{new Integer(2)});
24       Method method2 = 
25         clazz2.getDeclaredMethod("printB",new Class[0]);
26 
27       //显示test.B中的静态变量的值 
28       method.invoke( object,new Object[0]);
29       method2.invoke( object2,new Object[0]);
30     } catch ( Exception e ) {
31       e.printStackTrace();
32     }
33   }
34 }
35 
36 
37 //Class B 必须位于MyClassLoader的查找范围内,
38 //而不应该在MyClassLoader的父类装载器的查找范围内。
39 package testb;
40 public class B {
41     static int b ;
42 
43     public B(Integer testb) {
44         b = testb.intValue();
45     }
46 
47     public void printB() {
48         System.out.print("my static field b is ", b);
49     }
50 }
51 
52 
53 public class MyClassLoader extends URLClassLoader{
54   private static File file = new File("c:\\classes ");
55   //该路径存放着class B,但是没有class A
56 
57   public MyClassLoader() {
58     super(getUrl());
59   }
60 
61   public static URL[] getUrl() {
62     try {
63       return new URL[]{file.toURL()};
64     } catch ( MalformedURLException e ) {
65       return new URL[0];
66     }
67   }
68 }

程序的运行结果为:
my static field b is 1
my static field b is 2

程序的结果非常有意思,从编程者的角度,我们甚至可以把不在同一个分支的类装载器看作不同的java虚拟机,因为它 们彼此觉察不到对方的存在。程序在使用具有分支的类装载的体系结构时要非常小心,弄清楚每个类装载器的类查找范围,尽量避免父类装载器和子类装载器的类查 找范围中有相同类名的类(包括包名和类名),下面这个例子就是用来说明这种情况可能带来的问题。

假设有相同名字却不同版本的接口 A,

版本 1:
package test;
Intefer Same{ 
public String getVersion(); }

版本 2:
Package test;
Intefer Same{ 
public String getName(); }

接口A两个版本的实现:

版本1的实现
package test;
public class Same1Impl implements Same {
public String getVersion(){ return "A version 1";}
}

版本2的实现
public class Same 2Impl implements Same {
public String getName(){ return "A version 2";}
}

我们依然使用图2的类装载器结构,首先将版本1的Same和Same的实现类Same1Impl打成包 same1.jar,将版本2的Same和Same的实现类Same1Impl打成包same2.jar。现在,做这样的事情,把same1.jar放入 类装载器ClassLoaderA的类查找范围中,把same2.jar放入类装器ClassLoaderAB的类查找范围中。当你兴冲冲的运行下面这个 看似正确的程序。

实际上这个错误的是由父类载器优先装载的机制造成,当类装载器ClassLoaderAB在装载Same2Impl 类时发现必须装载接口test.Same,于是按规定请求父类装载器装载,父类装载器发现了版本1的test.Same接口并兴冲冲的装载,但是却想不到 Same2Impl所希望的是版本2 的test.Same,后面的事情可想而知了,异常被抛出。

我们很难责怪Java中暂时并没有提供区分版本的机制,如果使用了比较复杂的类装载器体系结构,在出现了某个包或者类的多个版本时,应特别注意。

掌握和灵活运用Java的类装载器的体系结构,对程序的系统设计,程序的实现,已经程序的调试,都有相当大的帮助。希望以上的内容能够对您有所帮助.

转载自:http://gceclub.sun.com.cn/yuanchuang/week-9/classloader.html
posted @ 2006-02-23 18:43 Leo 阅读(467) | 评论 (0)编辑 收藏

时间:2005-11-04
作者:juxtapose


 如果大家对一般类的装载器熟悉的话,就知道在java中类的装载采用“代理机制”,即子装载器如果需要装载一个类文件,首先会将此任务提交给父 装载器,如果父装载器找不到此类文件,才有子装载器来装载类文件,如果子装载器也找不到,那么就会报告ClassNotFoundException异 常。下面简单谈一下我对weblogic server的类装载器原理的了解,希望能和大家分享。

1.Weblogic允许定制的类装载器,同时也有一个默认的类装载器。其默认的装载器的结构分层如下:

 

  当部署一个应用的时候,weblogic server会自动创建一个具有层次结构的类装载器。在图中,a.Application Classloader负责装载应用中的所有的EJB JAR文件;

  b.Web Application Classloader负责装载所有的Web application 中的WAR 文件(所有得jsp文件除外);

  c.Jsp Classloader 负责装载Web application 中的所有的jsp 文件;

  这样的分层结构有一个好处,就是在Jsp,Servlet中可以直接访问EJB的接口。这种上层装载EJB,下层装载servlet等,最下面装载jsp文件的结构,使得经常变动的jsp,servlet等可以被重新装载而不会涉及到EJB层。

在这种默认的类装载器结构下,有一点需要提出的是:

  a. 我们的应用必须打包成一个EAR文件,才会允许我们应用中的jsp和servlet文件直接访问ejb;如果将WAR与JAR文件分别打包。 Weblogic server会为他们分别生成一个类装载器,作为兄弟节点,这时如果需要在jsp或者servlet中使用ejb,就必须将EJB的Home接口与 remote接口打包到WAR中才可以。后面这种情况,适合用在将EJB的客户端和EJB部署在不同的JVM中;

  b.web application classloader中,不会装载jsp文件,jsp文件由web application classloader的子装载器Jsp classloader负责装载,因为jsp文件经常的变动,通过为jsp设立一个单独的classloader可以避免对jsp的装载影响到其他的 java class或者ejb;

默认装载器的优点:

  a. 调用ejb的时候可以采用call-by-referrence的方式;

  b. 允许web module独立的装载,不影响其它的web module;

  • 通过在将整个应用打包成一个EAR文件,可以方便的不用再web module中包含EJB的home和remote接口,就可以方便的通过call-by-referrence来调用ejb;

2. 定制classloader
  如果觉得默认的类装载器不能满足需要,weblogic server支持定制的类装载器。在weblogic的文档中指出,自定义的classloader多用于开发者使用,当应用发布之后,不推荐使用。自定 义的类装载器通过xml文件来描述。描述文件放在weblogic-application.xml中。Weblogic官方提供的DTD描述文件如下:

<classloader-structure> 
    
<module-ref> 
        
<module-uri>ejba.jar</module-uri> 
    
</module-ref> 
    
<module-ref> 
        
<module-uri>webc.war</module-uri> 
    
</module-ref>
    
<classloader-structure> 
        
<module-ref> 
            
<module-uri>weba.war</module-uri> 
        
</module-ref> 
    
</classloader-structure>
    
<classloader-structure> 
        
<module-ref> 
            
<module-uri>ejbc.jar</module-uri> 
        
</module-ref> 
        
<module-ref> 
            
<module-uri>webb.war</module-uri> 
        
</module-ref>
        
<classloader-structure> 
            
<module-ref> 
    
<module-uri>webd.war</module-uri> 
            
</module-ref> 
        
</classloader-structure> 
        
<classloader-structure> 
            
<module-ref> 
    
<module-uri>ejbb.jar</module-uri> 
            
</module-ref> 
        
</classloader-structure>
    
</classloader-structure>
</classloader-structure>

  通过我们给出的配置文件,我们自定义的classloader的层次结构如下图:

 

  在J2EE的规范中明确的指出,J2EE应用不应该依赖于任一个给定的类装载器。所以,我们自定义的类装载器,在开发过程中还是可以使用的,但一定不要应用于发布后的应用中。

自定义的类装载器有如下得限制:
  a.不能够装载servlet;

  b.嵌套的深度最大为3,也就是说,最多只能够嵌套三层;

  c.自定义装载器的module类型仅限于 Web和 EJB这两种;

  d.Jsp Classloader不受此自定义类装载器的影响,它永远都是web module的子类装载器;

  • 相同的类可能导致部署异常;
  • 在自定义的类装载器中,如果要使用EJB,就必须将EJB的home和remote接口打包到相应的web module中去;
  •  

3.Ejb的单独加载
  
有时候我们可能需要单独加载某个EJB,这个时候我们可以通过以下两种方法来实现:

  第一:将应用需要的jar文件放在APP-INF/lib中,或者将类文件放在APP-INF/classes中,这些类文件和JAR文件会被root classloader进行装载,可以被多个应用共享;

  第二:可以通过META-INF/MANIFEST.MF文件来指定需要的classes。通常的用法是在META-INF/MANIFEST.MF文件中增加Class-Path:一行。举例如下:

  Class-Path:/d:ejb/add.jar

  这样就会在当前的jar包中可以找到我们需要的add.jar文件。需要说明的是,在Class-Path:行的最后一定要有一个换行,否则会发生错误。还有,通过Class-Path只能指定本地的JAR文件。

  如果能对应用服务器的类装载原理有了较清楚地了解,会对我们的应用移植,在开发中避免不必要的类装载的错误会有很大的帮助。

转载自:http://dev2dev.bea.com.cn/bbsdoc/20051104121.html

posted @ 2006-02-23 18:37 Leo 阅读(386) | 评论 (0)编辑 收藏

作者:刘学超

一、引言

Java虚拟机(JVM)的类装载就是指将包含在类文件中的字节码装载到JVM中, 并使其成为JVM一部分的过程。JVM的类动态装载技术能够在运行时刻动态地加载或者替换系统的某些功能模块, 而不影响系统其他功能模块的正常运行。本文将分析JVM中的类装载系统,探讨JVM中类装载的原理、实现以及应用。

二、Java虚拟机的类装载实现与应用

2.1  装载过程简介

所谓装载就是寻找一个类或是一个接口的二进制形式并用该二进制形式来构造代表这个类或是这个接口的class对象的过程,其中类或接口的名称是给定了的。当然名称也可以通过计算得到,但是更常见的是通过搜索源代码经过编译器编译后所得到的二进制形式来构造。

在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:

  • 装载:查找和导入类或接口的二进制数据;
  • 链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
  • 校验:检查导入类或接口的二进制数据的正确性;
  • 准备:给类的静态变量分配并初始化存储空间;
  • 解析:将符号引用转成直接引用;
  • 初始化:激活类的静态变量的初始化Java代码和静态Java代码块。

至于在类装载和虚拟机启动的过程中的具体细节和可能会抛出的错误,请参看《Java虚拟机规范》以及《深入Java虚拟机》,它们在网络上面的资源地址是: http://java.sun.com/docs/books/vmspec/2nd-edition/html/Preface.doc.htmlhttp://www.artima.com/insidejvm/ed2/index.html。 由于本文的讨论重点不在此就不再多叙述。

2.2  装载的实现

JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。

在Java中,ClassLoader是一个抽象类,它在包java.lang中,可以这样说,只要了解了在 ClassLoader中的一些重要的方法,再结合上面所介绍的JVM中类装载的具体的过程,对动态装载类这项技术就有了一个比较大概的掌握,这些重要的 方法包括以下几个:

①loadCass方法  loadClass(String name ,boolean resolve)其中name参数指定了JVM需要的类的名称,该名称以包表示法表示,如Java.lang.Object;resolve参数告诉方法 是否需要解析类,在初始化类之前,应考虑类解析,并不是所有的类都需要解析,如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要解析。这个 方法是ClassLoader 的入口点。

②defineClass方法  这个方法接受类文件的字节数组并把它转换成Class对象。字节数组可以是从本地文件系统或网络装入的数据。它把字节码分析成运行时数据结构、校验有效性等等。

③findSystemClass方法  findSystemClass方法从本地文件系统装入文件。它在本地文 件系统中寻找类文件,如果存在,就使用defineClass将字节数组转换成Class对象,以将该文件转换成类。当运行Java应用程序时,这是 JVM 正常装入类的缺省机制。

④resolveClass方法  resolveClass(Class c)方法解析装入的类,如果该类已经被解析过那么将不做处理。当调用loadClass方法时,通过它的resolve 参数决定是否要进行解析。

⑤findLoadedClass方法  当调用loadClass方法装入类时,调用 findLoadedClass 方法来查看ClassLoader是否已装入这个类,如果已装入,那么返回Class对象,否则返回NULL。如果强行装载已存在的类,将会抛出链接错 误。

2.3  装载的应用

一般来说,我们使用虚拟机的类装载时需要继承抽象类java.lang.ClassLoader,其中必须实现的方 法是loadClass(),对于这个方法需要实现如下操作:(1) 确认类的名称;(2) 检查请求要装载的类是否已经被装载;(3) 检查请求加载的类是否是系统类;(4) 尝试从类装载器的存储区获取所请求的类;(5) 在虚拟机中定义所请求的类;(6) 解析所请求的类;(7) 返回所请求的类。

所有的Java 虚拟机都包括一个内置的类装载器,这个内置的类库装载器被称为根装载器(bootstrap ClassLoader)。根装载器的特殊之处是它只能够装载在设计时刻已知的类,因此虚拟机假定由根装载器所装载的类都是安全的、可信任的,可以不经过 安全认证而直接运行。当应用程序需要加载并不是设计时就知道的类时,必须使用用户自定义的装载器(user-defined ClassLoader)。下面我们举例说明它的应用。

 1 public abstract class MultiClassLoader extends ClassLoader{
 2     
 3     public synchronized Class loadClass(String s, boolean flag)
 4         throws ClassNotFoundException
 5     {
 6         /* 检查类s是否已经在本地内存*/
 7         Class class1 = (Class)classes.get(s);
 8 
 9         /* 类s已经在本地内存*/
10         if(class1 != null)  return class1; 
11         try/*用默认的ClassLoader 装入类*/  {
12             class1 = super.findSystemClass(s);
13             return class1;
14         }
15         catch(ClassNotFoundException _ex)  {
16             System.out.println(">> Not a system class.");
17         }
18 
19         /* 取得类s的字节数组*/
20         byte abyte0[] = loadClassBytes(s);
21         if(abyte0 == null)   throw new ClassNotFoundException();
22 
23         /* 将类字节数组转换为类*/
24         class1 = defineClass(null, abyte0, 0, abyte0.length);
25         if(class1 == nullthrow new ClassFormatError();
26         if(flag)   resolveClass(class1); /*解析类*/
27 
28         /* 将新加载的类放入本地内存*/
29         classes.put(s, class1);
30         System.out.println(">> Returning newly loaded class.");
31 
32         /* 返回已装载、解析的类*/
33         return class1;
34     }
35     
36 }

三、Java虚拟机的类装载原理

前面我们已经知道,一个Java应用程序使用两种类型的类装载器:根装载器(bootstrap)和用户定义的装载 器(user-defined)。根装载器是Java虚拟机实现的一部分,举个例子来说,如果一个Java虚拟机是在现在已经存在并且正在被使用的操作系 统的顶部用C程序来实现的,那么根装载器将是那些C程序的一部分。根装载器以某种默认的方式将类装入,包括那些Java API的类。在运行期间一个Java程序能安装用户自己定义的类装载器。根装载器是虚拟机固有的一部分,而用户定义的类装载器则不是,它是用Java语言 写的,被编译成class文件之后然后再被装入到虚拟机,并像其它的任何对象一样可以被实例化。 Java类装载器的体系结构如下所示:

图1  Java的类装载的体系结构

Java的类装载模型是一种代理(delegation)模型。当JVM 要求类装载器CL(ClassLoader)装载一个类时,CL首先将这个类装载请求转发给他的父装载器。只有当父装载器没有装载并无法装载这个类时, CL才获得装载这个类的机会。这样, 所有类装载器的代理关系构成了一种树状的关系。树的根是类的根装载器(bootstrap ClassLoader) , 在JVM 中它以"null"表示。除根装载器以外的类装载器有且仅有一个父装载器。在创建一个装载器时, 如果没有显式地给出父装载器, 那么JVM将默认系统装载器为其父装载器。Java的基本类装载器代理结构如图2所示:

图2  Java类装载的代理结构

下面针对各种类装载器分别进行详细的说明。

根(Bootstrap) 装载器:该装载器没有父装载器,它是JVM实现的一部分,从sun.boot.class.path装载运行时库的核心代码。

扩展(Extension) 装载器:继承的父装载器为根装载器,不像根装载器可能与运行时的操作系统有关,这个类装载器是用纯Java代码实现的,它从java.ext.dirs (扩展目录)中装载代码。

系统(System or Application) 装载器:装载器为扩展装载器,我们都知道在安装JDK的时候要设置环境变量(CLASSPATH ),这个类装载器就是从java.class.path(CLASSPATH 环境变量)中装载代码的,它也是用纯Java代码实现的,同时还是用户自定义类装载器的缺省父装载器。

小应用程序(Applet) 装载器: 装载器为系统装载器,它从用户指定的网络上的特定目录装载小应用程序代码。

在设计一个类装载器的时候,应该满足以下两个条件:

  1. 对于相同的类名,类装载器所返回的对象应该是同一个类对象
  2. 如果类装载器CL1将装载类C的请求转给类装载器CL2,那么对于以下的类或接口,CL1和CL2应该返回同一个类对象:a)S为C的 直接超类;b)S为C的直接超接口;c)S为C的成员变量的类型;d)S为C的成员方法或构建器的参数类型;e)S为C的成员方法的返回类型。

每个已经装载到JVM中的类都隐式含有装载它的类装载器的信息。类方法getClassLoader 可以得到装载这个类的类装载器。一个类装载器认识的类包括它的父装载器认识的类和它自己装载的类,可见类装载器认识的类是它自己装载的类的超集。注意我们 可以得到类装载器的有关的信息,但是已经装载到JVM中的类是不能更改它的类装载器的。

Java中的类的装载过程也就是代理装载的过程。比如:Web浏览器中的JVM需要装载一个小应用程序 TestApplet。JVM调用小应用程序装载器ACL(Applet ClassLoader)来完成装载。ACL首先请求它的父装载器, 即系统装载器装载TestApplet是否装载了这个类, 由于TestApplet不在系统装载器的装载路径中, 所以系统装载器没有找到这个类, 也就没有装载成功。接着ACL自己装载TestApplet。ACL通过网络成功地找到了TestApplet.class 文件并将它导入到了JVM中。在装载过程中, JVM发现TestAppet是从超类java.applet.Applet继承的。所以JVM再次调用ACL来装载 java.applet.Applet类。ACL又再次按上面的顺序装载Applet类, 结果ACL发现他的父装载器已经装载了这个类, 所以ACL就直接将这个已经装载的类返回给了JVM , 完成了Applet类的装载。接下来,Applet类的超类也一样处理。最后, TestApplet及所有有关的类都装载到了JVM中。

四、结论

类的动态装载机制是JVM的一项核心技术, 也是容易被忽视而引起很多误解的地方。本文介绍了JVM中类装载的原理、实现以及应用,尤其分析了ClassLoader的结构、用途以及如何利用自定义 的ClassLoader装载并执行Java类,希望能使读者对JVM中的类装载有一个比较深入的理解。

转载自:http://gceclub.sun.com.cn/yuanchuang/week-8/jvm.html

posted @ 2006-02-23 18:31 Leo 阅读(355) | 评论 (0)编辑 收藏