用 Cactus 来测试 J2ee 应用

    Junit 是当前最流行的测试框架,它能够让开发人员很方便的编写测试单元,可以使他们"放心"地开发。但是现在很多的应用都是基于 j2ee 的,代码都是在服务器端的容器里面运行,这个使测试带来了一些麻烦。对于普通的 jsp,servlet 用 Junit 来测试好像已经不是那么方便,对于 EJB 来说,特别是 2.0 版本,很多接口都是 Local Interface,没有办法进行分布式的测试。

    那么我们如何进行这些代码的测试呢? Apache 为我们提供了一个强大的工具 Cactus !它是一套简单,易于使用的服务器端测试框架,可以使开发人员很轻松的测试服务器端的程序,他们会说:"哦,就是这么简单"。

    Cactus 是 Junit 的一个扩展,但是它又和 Junit 有一些不同。Cactus 的测试分为三种不同的测试类别,JspTestCase,ServletTestCase,FilterTestCase, 而不是像 Junit 就一种 TestCase。Cactus 的测试代码有服务器端和客户端两个部分,他们协同工作。那我们为什么不用 Junit 来做测试呢?主要有一下几个理由:

  1. EJB2.0 中的 Local interface , 不允许远程调用。用 Junit 不好测试,而 Cactus 的 redirector 位于服务器端,可以和 EJB 运行在一个容器中,这使得它可以直接访问 Local Interface。
  2. 一般 EJB 或者 servlet,jsp 都是运行在服务器上,如果你使用 junit 测试的话,你的测试是在客户端,这使的运行环境和测试环境处于不同的系统环境中,这个有时候会不同的测试结果。
  3. 在一个 EJB 的应用中,一般都会有一些前端应用来访问 EJB,例如 :jsp,servlet,javabean。这就意味着你需要一个测试框架来测试这些前端的组件。Cactus 提供了所有这些组件的测试方法。哦,太棒了。
  4. Cactus 和 ant 很好的结合在一起,可以很容易的完成自动化测试,减少了很多工作量。当然,junit 也提供这样的支持。

    前面是对 Cactus 作了一个大致的介绍,接下来我们用一个实际的例子来运用一下这个强大的测试框架。首先我们需要一个被测试的对象,在这里我们选用 EJB2.0 CMP. 我们做一个简单的用户管理。一下就一些主要的代码,来进行一些分析。

 

 UserHome.java 
 package usersystem; 
 import javax.ejb.*; 
 import java.util.*; 
 public interface UserHome extends javax.ejb.EJBLocalHome { 
  public User create(String name, String password) throws CreateException; 
  public Collection findAll() throws FinderException; 
    public User findByPrimaryKey(String name) throws FinderException; 
 } 
 User.java 
 package usersystem; 
 import javax.ejb.*; 
 import java.util.*; 
 public interface User extends javax.ejb.EJBLocalObject { 
  public String getName(); 
  public void setPassword(String password); 
  public String getPassword(); 
  public void setUserInfo(UserInfo userInfo); 
  public UserInfo getUserInfo(); 
  public void setName(String name); 
 } 
 UserInfoHome.java 
 package usersystem; 
 import javax.ejb.*; 
 import java.util.*; 
 public interface UserInfoHome extends javax.ejb.EJBLocalHome { 
    public UserInfo create(String name, String email, String address, String tel) throws 
 CreateException; 
    public UserInfo findByPrimaryKey(String name) throws FinderException; 
 } 

    这里有两个 Entity Bean 用来创建用户信息。他们之间的关系在 xml 部署描述文件中描述,他们是 1 对 1 的关系。

 

 UserManagerLocal.java 
 package usersystem; 
 import javax.ejb.*; 
 import java.util.*; 
 public interface UserManagerLocal extends javax.ejb.EJBLocalObject { 
    public void addUser(String name, String password, String email, 
        String address, String tel); 
    public Collection findAll() ; 
    public void delAll(); 
    public void delByName(String name); 
    public User findByName(String name) ; 
 } 
 UserManagerBean.java 
 package usersystem; 
 import javax.ejb.*; 
 import javax.rmi.PortableRemoteObject; 
 import javax.naming.*; 
 import java.util.*; 
 public class UserManagerBean implements SessionBean { 
  SessionContext sessionContext; 
  public void ejbCreate() throws CreateException { 
    /**@todo Complete this method*/ 
  } 
  public void ejbRemove() { 
    /**@todo Complete this method*/ 
  } 
  public void ejbActivate() { 
    /**@todo Complete this method*/ 
  } 
  public void ejbPassivate() { 
    /**@todo Complete this method*/ 
  } 
  public void setSessionContext(SessionContext sessionContext) { 
    this.sessionContext = sessionContext; 
  } 
  /** 
   * 添加用户
   * @param name 用户姓名
   * @param password 密码
   * @param email 电子邮件
   * @param address 地址
   * @param tel 电话
   */ 
  public void addUser(String name, String password, String email, 
          String address, String tel) { 
      try{ 
          UserHome userHome=getUserHome(); 
          User user=userHome.create(name,password)  ; //create user entity 
          UserInfoHome userInfoHome=getUserInfoHome(); 
          //create userinfo entity 
     UserInfo userInfo=userInfoHome.create(name,email,address,tel);
          user.setUserInfo(userInfo) ; 
      }catch(Exception e){ 
          throw new javax.ejb.EJBException (e.toString()); 
      } 
  } 
  /** 
   * 返回 UserHome 接口
   * @return userHome 
   */ 
  private UserHome getUserHome(){ 
    try { 
        javax.naming.InitialContext ctx=new javax.naming.InitialContext (); 
          Object ref = ctx.lookup("User"); 
          //cast to Home interface 
          UserHome userHome = (UserHome) PortableRemoteObject.narrow(ref
             , UserHome.class); 
          return userHome; 
    } 
    catch (ClassCastException ex) { 
        ex.printStackTrace() ; 
        return null; 
    }catch (NamingException ex) { 
        ex.printStackTrace() ; 
        return null; 
    } 
  } 
  /** 
   * 返回 UserInfoHome 接口
   * @return 
   */ 
  private UserInfoHome getUserInfoHome(){ 
    try { 
        javax.naming.InitialContext ctx=new javax.naming.InitialContext (); 
          Object ref = ctx.lookup("UserInfo"); 
          //cast to Home interface 
          UserInfoHome userInfoHome = (UserInfoHome) PortableRemoteObject.narrow(ref,
               UserInfoHome.class); 
          return userInfoHome; 
    } 
    catch (ClassCastException ex) { 
        throw new EJBException(); 
    }catch (NamingException ex) { 
        throw new EJBException(ex.toString()); 
    } 
  } 
  /** 
   * 返回所有用户记录
   * @return c 
   * @throws javax.ejb.FinderException 
   */ 
  public java.util.Collection findAll() { 
    Collection c = null; 
    try { 
        UserHome uh=this.getUserHome() ; 
          c=uh.findAll() ; 
    } 
    catch (FinderException ex) { 
            throw new javax.ejb.EJBException (); 
    } 
      return c; 
  } 
    /** 
     * 删除所有记录
     */ 
    public void delAll(){ 
        try { 
            UserHome u=getUserHome(); 
            java.util.Collection c=u.findAll() ; 
            java.util.Iterator i=c.iterator() ; 
            while(i.hasNext() ){ 
                u.remove(((User)i.next()).getName()) ; 
            } 
        } 
        catch (Exception ex) { 
            throw new EJBException(ex.toString()); 
        } 
    } 
    /** 
     * 根据用户名删除记录
     * @param name 
     */ 
    public void delByName(String name) { 
        try { 
            User user=findByName(name); 
            UserHome uh=getUserHome(); 
            uh.remove(user.getName()) ; 
        } 
        catch (Exception ex) { 
            throw new javax.ejb.EJBException (ex.toString()); 
        } 
    } 
    /** 
     * 通过用户名查找用户记录
     * @param name 
     * @return 
     */ 
    public User findByName(String name) { 
        try { 
            UserHome uh=this.getUserHome() ; 
            User user=(User)uh.findByPrimaryKey(name) ; 
            UserHome u=this.getUserHome() ; 
            User uu=u.findByPrimaryKey(name)  ; 
            return user; 
        } 
        catch (FinderException ex) { 
            throw new EJBException(ex.toString()); 
        } 
    } 
 }

    UserManagerBean 是一个 session bean , 它主要是对 user 的管理,和客户端通讯,其实就是 session facade 模式。代码里面有注释,这里就不多叙述了。

 

ejb-jar.xml 部署文件描述

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems,Inc.//DTD Enterprise JavaBeans 2.0//EN"
"http://java.sun.com/dtd/ejb-jar_2_0.dtd"> 
 <ejb-jar> 
    <enterprise-beans> 
        <session> 
            <display-name>UserManager</display-name> 
            <ejb-name>UserManager</ejb-name> 
            <local-home>usersystem.UserManagerLocalHome</local-home> 
            <local>usersystem.UserManagerLocal</local> 
            <ejb-class>usersystem.UserManagerBean</ejb-class> 
            <session-type>Stateless</session-type> 
            <transaction-type>Container</transaction-type> 
            <ejb-local-ref> 
                <description /> 
                <ejb-ref-name>User</ejb-ref-name> 
                <ejb-ref-type>Entity</ejb-ref-type> 
                <local-home>usersystem.UserHome</local-home> 
                <local>usersystem.User</local> 
                <ejb-link>User</ejb-link> 
            </ejb-local-ref> 
            <ejb-local-ref> 
                <description /> 
                <ejb-ref-name>UserInfo</ejb-ref-name> 
                <ejb-ref-type>Entity</ejb-ref-type> 
                <local-home>usersystem.UserInfoHome</local-home> 
                <local>usersystem.UserInfo</local> 
                <ejb-link>UserInfo</ejb-link> 
            </ejb-local-ref> 
        </session> 
        <entity> 
            <display-name>User</display-name> 
            <ejb-name>User</ejb-name> 
            <local-home>usersystem.UserHome</local-home> 
            <local>usersystem.User</local> 
            <ejb-class>usersystem.UserBean</ejb-class> 
            <persistence-type>Container</persistence-type> 
            <prim-key-class>java.lang.String</prim-key-class> 
            <reentrant>False</reentrant> 
            <cmp-version>2.x</cmp-version> 
            <abstract-schema-name>User</abstract-schema-name> 
            <cmp-field> 
                <field-name>name</field-name> 
            </cmp-field> 
            <cmp-field> 
                <field-name>password</field-name> 
            </cmp-field> 
            <primkey-field>name</primkey-field> 
            <query> 
                <query-method> 
                    <method-name>findAll</method-name> 
                    <method-params /> 
                </query-method> 
                <ejb-ql>select Object(theUser) from User as theUser
                </ejb-ql>
            </query> 
        </entity> 
        <entity> 
            <display-name>UserInfo</display-name> 
            <ejb-name>UserInfo</ejb-name> 
            <local-home>usersystem.UserInfoHome</local-home> 
            <local>usersystem.UserInfo</local> 
            <ejb-class>usersystem.UserInfoBean</ejb-class> 
            <persistence-type>Container</persistence-type> 
            <prim-key-class>java.lang.String</prim-key-class> 
            <reentrant>False</reentrant> 
            <cmp-version>2.x</cmp-version> 
            <abstract-schema-name>UserInfo</abstract-schema-name> 
            <cmp-field> 
                <field-name>name</field-name> 
            </cmp-field> 
            <cmp-field> 
                <field-name>email</field-name> 
            </cmp-field> 
            <cmp-field> 
                <field-name>address</field-name> 
            </cmp-field> 
            <cmp-field> 
                <field-name>tel</field-name> 
            </cmp-field> 
            <primkey-field>name</primkey-field> 
        </entity> 
    </enterprise-beans> 
    <relationships> 
        <ejb-relation> 
            <ejb-relation-name>userInfo-user</ejb-relation-name> 
            <ejb-relationship-role> 
                <description>userInfo</description> 
                <ejb-relationship-role-name>UserInfoRelationshipRole
                </ejb-relationship-role-name> 
                <multiplicity>One</multiplicity> 
                <cascade-delete /> 
                <relationship-role-source> 
                    <description>userInfo</description> 
                    <ejb-name>UserInfo</ejb-name> 
                </relationship-role-source> 
                <cmr-field> 
                    <description>user</description> 
                    <cmr-field-name>user</cmr-field-name> 
                </cmr-field> 
            </ejb-relationship-role> 
            <ejb-relationship-role> 
                <description>user</description> 
                <ejb-relationship-role-name>UserRelationshipRole
                </ejb-relationship-role-name> 
                <multiplicity>One</multiplicity> 
                <relationship-role-source> 
                    <description>user</description> 
                    <ejb-name>User</ejb-name> 
                </relationship-role-source> 
                <cmr-field> 
                    <description>userInfo</description> 
                    <cmr-field-name>userInfo</cmr-field-name> 
                </cmr-field> 
            </ejb-relationship-role> 
        </ejb-relation> 
    </relationships> 
    <assembly-descriptor> 
        <container-transaction> 
            <method> 
                <ejb-name>User</ejb-name> 
                <method-name>*</method-name> 
            </method> 
            <trans-attribute>Required</trans-attribute> 
        </container-transaction> 
        <container-transaction> 
            <method> 
                <ejb-name>UserManager</ejb-name> 
                <method-name>*</method-name> 
            </method> 
            <trans-attribute>Required</trans-attribute> 
        </container-transaction> 
        <container-transaction> 
            <method> 
                <ejb-name>UserInfo</ejb-name> 
                <method-name>*</method-name> 
            </method> 
            <trans-attribute>Required</trans-attribute> 
        </container-transaction> 
    </assembly-descriptor> 
 </ejb-jar> 

接下来是访问 EJB 的客户端,我们用了一个 servlet.

 

 ManaServlet.java 
 package usersystem.servlet; 
 import javax.servlet.*; 
 import javax.servlet.http.*; 
 import java.io.*; 
 import java.util.*; 
 import usersystem.*; 
 import javax.naming.*; 
 import javax.ejb.*; 
 import javax.ejb.*; 
 import javax.ejb.*; 
 /** 
 * <p>Title: </p> 
 * <p>Description: </p> 
 * <p>Copyright: Copyright (c) 2002</p> 
 * <p>Company: </p> 
 * @author unascribed 
 * @version 1.0 
 */ 
 public class ManaServlet extends HttpServlet { 
    static final private String CONTENT_TYPE = "text/html; charset=GBK"; 
    private UserManagerLocalHome h=null; 
    private UserManagerLocal uml=null; 
    public void init() throws ServletException{ 
        try { 
            h=getHome(); 
            uml=h.create() ; 
        } 
        catch (CreateException ex) { 
            ex.printStackTrace() ; 
        } 
    } 
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws 
 ServletException, IOException { 
    } 
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws 
 ServletException, IOException { 
    } 
    public void addUser(HttpServletRequest request, HttpServletResponse response) throws 
 javax.ejb.EJBException  { 
        String name=request.getParameter("name") ; 
        String tel=request.getParameter("tel") ; 
        String address=request.getParameter("address") ; 
        String email=request.getParameter("email") ; 
        String pass=request.getParameter("pass") ; 
        uml.addUser(name,pass,email,address,tel) ; 
    } 
    public User findByName(String name) throws javax.ejb.EJBException { 
        User u = null; 
        u=uml.findByName(name) ; 
        return u; 
    } 
    public java.util.Iterator findAll() throws javax.ejb.EJBException { 
        java.util.Collection c=uml.findAll() ; 
        return c.iterator() ; 
    } 
    public void delAll() throws javax.ejb.EJBException { 
        uml.delAll() ; 
    } 
    public void delUser(String name) throws javax.ejb.EJBException  { 
            uml.delByName(name) ; 
    } 
    public UserManagerLocalHome getHome() { 
        UserManagerLocalHome home = null; 
        try { 
            javax.naming.InitialContext ctx=new javax.naming.InitialContext (); 
            home=(UserManagerLocalHome)ctx.lookup("UserManagerLocal") ; 
        } 
        catch (NamingException ex) { 
            ex.printStackTrace() ; 
            return null; 
        } 
        return home; 
    } 
    public void destroy() { 
    } 
 } 

这个 servlet 在 doGet,doPost 没有实现任何方法,这个不影响我们测试,我们要测试的只是这些 public method. 我们的测试代码如下:

 

 package usersystem.test; 
 /** 
 * <p>Title: </p> 
 * <p>Description: </p> 
 * <p>Copyright: Copyright (c) 2002</p> 
 * <p>Company: </p> 
 * @author unascribed 
 * @version 1.0 
 */ 
 import usersystem.servlet.*; 
 import java.io.IOException; 
 import java.net.URLDecoder; 
 import java.util.Hashtable; 
 import junit.framework.Test; 
 import junit.framework.TestSuite; 
 import org.apache.cactus.Cookie; 
 import org.apache.cactus.ServletTestCase; 
 import org.apache.cactus.WebRequest; 
 import org.apache.cactus.WebResponse; 
 import javax.ejb.*; 
 import javax.servlet.*; 
 import usersystem.*; 
 public class ManaServletTest  extends ServletTestCase{ 
    ManaServlet servlet=new ManaServlet(); 
    public ManaServletTest(String theName) { 
        super(theName); 
    } 
    public void setUp(){ 
        try { 
            servlet.init() ; 
        } 
        catch (ServletException ex) { 
            ex.printStackTrace() ; 
            this.fail() ; 
        } 
    } 
    public void tearDown(){ 
    } 
    public void beginAddUser(WebRequest theRequest) 
    { 
        theRequest.addParameter("name", "nameValue"); 
        theRequest.addParameter("pass","passValue") ; 
        theRequest.addParameter("tel","telValue") ; 
        theRequest.addParameter("address","addressValue") ; 
        theRequest.addParameter("email","emailValue"); 
    } 
    public void testAddUser() throws javax.ejb.EJBException{ 
        servlet.addUser(request,response) ; 
    } 
    public void testFindAll(){ 
        java.util.Iterator i=servlet.findAll() ; 
        //assertEquals(null,i); 
        boolean ok=false; 
        while(i.hasNext() ){ 
            if(((User)i.next()).getName().equals("nameValue")) { 
                ok=true; 
            }; 
        } 
        this.assertTrue(ok) ; 
    } 
    public void testFindByName() throws javax.ejb.EJBException { 
        User u=servlet.findByName("nameValue") ; 
        UserInfo ui=u.getUserInfo() ; 
        this.assertEquals("email",ui.getEmail()) ; 
        this.assertEquals("tel",ui.getTel()) ; 
        this.assertEquals("nameValue",u.getName()) ; 
        this.assertEquals("passValue",u.getPassword()) ; 
    } 
    public void testDel() throws javax.ejb.EJBException { 
            servlet.delUser("nameValue8") ; 
    } 
    public void testDelAll() throws javax.ejb.EJBException { 
        servlet.delAll() ; 
    } 
    public static void main(String[] theArgs) 
    { 
        junit.textui.TestRunner.main(new String[]{ 
        ManaServletTest.class.getName()}); 
    } 
    public static Test suite() 
    { 
        return new TestSuite(ManaServletTest.class); 
    } 
 } 

public class ManaServletTest extends ServletTestCase 我们要测试的是一个 servlet, 所以我们继承 ServletTestCase, 如果你测试 jsp 的话,就继承 JspTestCase.

 

   public ManaServletTest(String theName) { 
        super(theName); 
    } 

和 junit 一下,ServletTestCase 不允许使用默认的构造函数,所以必须使用一个带参数的构造函数,并且调用父类的构造函数。

 

   public void setUp(){ 
        try { 
            servlet.init() ; 
        } 
        catch (ServletException ex) { 
            ex.printStackTrace() ; 
            this.fail() ; 
        } 
    } 
    public void tearDown(){ 
    } 

setUp 是在测试类运行时候首先被调用的办法,在这里可以进行一些数据初始化之类的工作。在这里我们调用了 servlet.init().

 

在测试类运行的时候需要显式的调用 servlet 的 init() 方法。因为 cactus 在测试 servlet 的时候是实例化一个 ser vlet 的,不会调用 inti(), 而 servlet enginer 在调用的时候是会自动调用 servlet 的 init() 方法的。tearDown 方法在测试完成的时候运行,进行一些必要的数据处理,比如删除一些测试数据等,这里我们没有做任何工作。

 

   public void beginAddUser(WebRequest theRequest) 
    { 
        theRequest.addParameter("name", "nameValue"); 
        theRequest.addParameter("pass","passValue") ; 
        theRequest.addParameter("tel","telValue") ; 
        theRequest.addParameter("address","addressValue") ; 
        theRequest.addParameter("email","emailValue"); 
    } 
    public void testAddUser() throws javax.ejb.EJBException{ 
        servlet.addUser(request,response) ; 
    } 

在 Cactus 中,你需要用 testXXX 来命名你的方法,这样 Cactus 会自动调用这个方法进行测。而 BeingXXX 则是在调用 test 方法之前调用,也就是说在一个功能测试之前运行。这里我们现在 beginAddUser 中添加一些必要的参数。WebRequest 是 Cactus 提供的一个类,它允许你设置一些 Http 参数,如果你使用了 theRequest.addParameter("name","nameValue"), 那么在 servlet 中你就可以用 request.getParameter("name") 来取得 name 的值。当然还可以设置 Cookie,Http Head 参数。在 testAddUser() 方法中我们测试 addUser 方法,如果测试有异常,则会产生 EJBException, 得到一个测试失败。

 

   public void testFindByName() throws javax.ejb.EJBException { 
        User u=servlet.findByName("nameValue") ; 
        UserInfo ui=u.getUserInfo() ; 
        this.assertEquals("email",ui.getEmail()) ; 
        this.assertEquals("tel",ui.getTel()) ; 
        this.assertEquals("nameValue",u.getName()) ; 
        this.assertEquals("passValue",u.getPassword()) ; 
    } 

这个测试是测试根据用户名查找用户,之后你可以用 assertEquals 方法来测试返回的值是否正确。

   public static void main(String[] theArgs) 
    { 
        junit.textui.TestRunner.main(new String[]{ 
        ManaServletTest.class.getName()}); 
    } 

这里我们使用 textui 来运行我们的测试类,提供文本的测试信息,还有一个 Swing 的测试方法,一共一个界面,但是没有什么太大的意义。

 

到此我们介绍了所有的主要方法。最后我们谈谈如何运行这个测试。

  1. 首先下载 Cactus。
  2. 把 lib/ 下的 jar 文件加入到 web app 的 lib 下。以及你客户端的 classpath 中,这是最保险的,虽然不是所有的 jar 都用的着。
  3. 设置你的 Cactus. 找到 cactus.properties 文件,把它加入到客户端的 classpath 中。
  4. 修改 cactus.properties 文件,把 http://localhost:8080/test 改成你相应的设置,test 是你 web 应用的名称。其他设置可以不变。
  5. 修改服务器端 web 应用的配置 , 在 web.xml 中加入:
     <?xml version="1.0" encoding="ISO-8859-1"?> 
        <filter> 
            <filter-name>FilterRedirector</filter-name> 
            <filter-class>org.apache.cactus.server.FilterTestRedirector</filter-class> 
        </filter> 
        <filter-mapping> 
            <filter-name>FilterRedirector</filter-name> 
            <url-pattern>/FilterRedirector</url-pattern> 
        </filter-mapping> 
        <servlet> 
            <servlet-name>ServletRedirector</servlet-name> 
            <servlet-class>org.apache.cactus.server.ServletTestRedirector</servlet-class> 
        </servlet> 
        <servlet> 
            <servlet-name>JspRedirector</servlet-name> 
            <jsp-file>/jspRedirector.jsp</jsp-file> 
        </servlet> 
        <servlet-mapping> 
            <servlet-name>ServletRedirector</servlet-name> 
            <url-pattern>/ServletRedirector</url-pattern> 
        </servlet-mapping> 
        <servlet-mapping> 
            <servlet-name>JspRedirector</servlet-name> 
            <url-pattern>/JspRedirector</url-pattern> 
        </servlet-mapping>
  6. 编译 ejb 和 servlet, 把 EJB 文件的 jar, 和 servlet 的 war 文件打包成 ear 文件。
  7. 发布你的 ear 文件到 web application.
  8. 运行本地的测试文件 ManaServletTest.class

哈哈~~,终于完成了所有的工作,我们可以看看运行结果,"哦,不",居然出现了一个 Error, 那就是你的程序出现了问题,仔细看看吧,测试是不会骗你的 :) 。以上代码在 win2000+JBOSS3.0+MySql MAX 3.24+Cactus1.3 上运行成功。

关于作者

韩伟,任北京某公司系统分析员,主要从事j2ee发面的开发,对设计模式,Java,软件工程很感兴趣。您可以通过email: java_cn@21cn.com跟他取得联系。

 


如果给你带来帮助,欢迎微信或支付宝扫一扫,赞一下。