Java Socket连接池

Posted on

Java Socket连接池

1:SocketAdapter类,此类继承了socket,重载了socket类的close方法,目的是当用户关闭socket的时候,我们并不关闭它只是放在连接池内部。 package com.tarena.socketpool; import java.net./; import java.io.IOException; /// /

socket连接的简单实现

/

Description:

/

Copyright: Copyright Tarena(c) 2005

/

Company: Tarena

/
@author chengxing / @version 1.0 // public class ConnectionAdapter extends Socket{ /// / 连接状态 // private boolean status=true; /// / 默认的构造函数 // public ConnectionAdapter() { super(); } public ConnectionAdapter(String host,int port)throws UnknownHostException,IOException{ super(host,port); } /// / 判断此连接是否空闲 / @return boolean 空闲返回ture,否则false // public boolean isFree(){ return status; } /// / 当使用此连接的时候设置状态为false(忙碌) // public void setBusy(){ this.status=false; } /// / 当客户端关闭连接的时候状态设置为true(空闲) /*/ public void close(){ System.out.println(Close : set the status is free ); status=true; } public void destroy(){ //Close socket connection close(); // System.out.println(Close success ); } } 第二个类连接管理器。 package com.tarena.socketpool;

import java.lang.reflect./; import java.util.Properties; /// /

连接管理器

/

Copyright: Copyright Tarena(c) 2005

/

Company: Tarena

/ @author chengxing / @version 1.0 // public class ConnectionManager { //测试程序默认的连接池实现类 public static final String PROVIDER_CLASS=com.tarena.socketpool.MyConnectionProvider; //测试程序的默认ip public static final String HOST=127.0.0.1; //测试程序的默认端口号 public static final String PORT=9880; /// / 注册钩子程序的静态匿名块 // static { //增加钩子控制资源的释放周期 Runtime runtime = Runtime.getRuntime(); Class c = runtime.getClass(); try { Method m = c.getMethod(addShutdownHook, new Class[] { Thread.class } ); m.invoke(runtime, new Object[] { new ShutdownThread() }); } catch (NoSuchMethodException e) { e.printStackTrace(); // Ignore -- the user might not be running JDK 1.3 or later. } catch (Exception e) { e.printStackTrace(); } } /// / 默认的构造函数 // public ConnectionManager() { } /// / 得到并初始化一个连接池 / 连接池的实现类通过系统参数来传递进来,通过命令行-DConnectionProvider=YourImplClass / 如果没有指定的实现的话,则采用系统默认的实现类 / 通过命令行传入的参数列表如下 / 对方主机名-DHost=192.168.0.200 / 对方端口号 -DPort=9880 / 最小连接数 -DMax_style='font-size:10px'0 / 最大连结数 -DMin_style='font-size:14px'0 / 以上的值可以改变,但是参数不能改变, / 最大连结数和最小连接数可以省略,默认值分别为20和10 / @return ConnectionProvider /*/ public static ConnectionProvider getConnectionProvider()throws Exception{ String provider_class=System.getProperty(ConnectionProvider); if(provider_class==null)provider_class=PROVIDER_CLASS;

String host=System.getProperty(Host); if(host==null)host=HOST;

String port=System.getProperty(port); if(port==null)port=PORT;

String max_size=System.getProperty(Max_size); String min_size=System.getProperty(Min_size);

Properties pro=new Properties(); pro.setProperty(ConnectionProvider.SERVER_IP,host); pro.setProperty(ConnectionProvider.SERVER_PORT,port); if(max_size!=null)pro.setProperty(ConnectionProvider.MAX_SIZE,max_size); if(min_size!=null)pro.setProperty(ConnectionProvider.MIN_SIZE,min_size); //通过反射得到实现类 System.out.println(provider_class); System.out.flush(); Class provider_impl=Class.forName(provider_class); //由于是单子模式,采用静态方法回调 Method m=provider_impl.getMethod(newInstance,new Class[]{java.util.Properties.class}); ConnectionProvider provider=null; try{ provider = (ConnectionProvider) m.invoke(provider_impl, new Object[]{pro}); }catch(Exception e){ e.printStackTrace(); }

return provider; } /// / /

一个钩子的线程: 在程序结束的时候调用注销连接池

/

Description:

/

Copyright: Copyright Tarena(c) 2005

/

Company: Tarena

/
@author chengxing / @version 1.0 // private static class ShutdownThread extends Thread { public void run() { try{ ConnectionProvider provider = ConnectionManager.getConnectionProvider(); if (provider != null) { provider.destroy(); } }catch(Exception e){ e.printStackTrace(); } } }

}

第三个类,连接池的接口定义 package com.tarena.socketpool;

import java.net./; import java.util./; import java.io.IOException;

/// / /

定义的抽象类,所有的子类必须单子模式去实现, / 统一方法为public ConnectionProvider newInstance(); / 连接提供器的抽象接口,每一个实现它的子类最好都是JAVABEAN, / 这样它的方法就可以是被外界控制

/ @see JiveBeanInfo /

Copyright: Copyright Tarena(c) 2005

/

Company: Tarena

/ @author chengxing / @version 1.0 /*/ public interface ConnectionProvider { public static final String SERVER_IP = SERVER_IP_ADDRESS; public static final String SERVER_PORT = SERVER_IP_PORT; public static final String MAX_SIZE = MAX_SIZE; public static final String MIN_SIZE = MIN_SIZE;

/// /判断连接池内是否有连接 / @return true 有连接返回true,否则返回false /*/ public boolean isPooled();

/// / 当此方法被调用的时候提供一个 socket / @see Socket / @return Socket a Connection object. // public Socket getConnection() throws java.net.SocketException;

/// / 连接池初始化 // public void init() throws UnknownHostException, IOException;

/// / 连接池重新启动 // public void restart() throws UnknownHostException, IOException;

/// / 注销连接池 // public void destroy(); } 第四个类MyConnectionProvider,自己写的一个连接池的简单实现 package com.tarena.socketpool;

import java.util./; import java.net./; import java.net.SocketException; import java.io.IOException;

/// / /

这是一个连接管理器的简单实现

/

Description: implements the Interface ConnectionProvider

/

Copyright: Copyright Tarena(c) 2005

/

Company: Tarena

/
@author chengxing / @version 1.0 // public class MyConnectionProvider implements ConnectionProvider {

private Properties pro = null; private static ConnectionProvider provider = null; private static Object object_lock = new Object(); private String ip; private String port;

/// / 默认的最大连接数 // private int max_size = 20;

/// / 默认的最小连接数 // private int min_size = 10;

/// / Socket connection池数组 // private ConnectionAdapter[] socketpool = null;

/// / 构造对象的时候初始化连接池 / @throws UnknownHostException 未知的主机异常 / @throws IOException // private MyConnectionProvider(Properties pro) throws UnknownHostException, IOException { ip = pro.getProperty(SERVER_IP); port = pro.getProperty(SERVER_PORT); String max_size_s = pro.getProperty(MAX_SIZE); String min_size_s = pro.getProperty(MIN_SIZE); if (max_size_s != null) { max_size = Integer.parseInt(max_size_s); } if (min_size_s != null) { min_size = Integer.parseInt(min_size_s); }

init(); //构造对象的时候初始化连接池 }

/// / 判断是否已经池化 / @return boolean 如果池化返回ture,反之返回false /*/ public boolean isPooled() { if (socketpool != null) { return true; } else return false; }

/// /返回一个连接 / @return a Connection object. /*/ public Socket getConnection() { Socket s = null; for (int i = 0; i < socketpool.length; i++) { if (socketpool[i] != null) { //如果有空闲的连接,返回一个空闲连接,如果没有,继续循环 if (socketpool[i].isFree()) { s = socketpool[i]; return s; } else continue; } else { //如果连接为空,证明超过最小连接数,重新生成连接 try { s = socketpool[i] = new ConnectionAdapter(ip, Integer.parseInt(port)); } catch (Exception e) { //never throw } } } //如果连接仍旧为空的话,则超过了最大连接数 if (s == null) { try { //生成普通连接,由客户端自行关闭,释放资源,不再由连接池管理 s = new Socket(ip, Integer.parseInt(port)); } catch (Exception e) { //此异常永远不会抛出 } } return s; }

/// / 初始化连接池 / @throws UnknownHostException 主机ip找不到 / @throws IOException 此端口号上无server监听 // public void init() throws UnknownHostException, IOException {

socketpool = new ConnectionAdapter[max_size]; for (int i = 0; i < min_size; i++) { socketpool[i] = new ConnectionAdapter(ip, Integer.parseInt(port)); System.out.print( . ); } System.out.println(); System.out.println(System init success ....); }

/// / 重新启动连接池 / @throws UnknownHostException / @throws IOException // public void restart() throws UnknownHostException, IOException { destroy(); init(); }

/// / 注销此连接池 // public void destroy() { for (int i = 0; i < socketpool.length; i++) { if (socketpool[i] != null) { ConnectionAdapter adapter = (ConnectionAdapter) socketpool[i]; adapter.destroy(); System.out.print( . ); } } System.out.println(\ndestory success ....); } /// / 静态方法,生成此连接池实现的对象 / @param pro Properties 此连接池所需要的所有参数的封装 / @throws UnknownHostException 主机无法找到 / @throws IOException 与服务器无法建立连接 / @return ConnectionProvider 返回父类ConnectionProvider // public static ConnectionProvider newInstance(java.util.Properties pro) throws UnknownHostException, IOException { if (provider == null) { synchronized (object_lock) { if (provider == null) { provider = new MyConnectionProvider(pro); } } } return provider; } /// /设置系统属性 通过封装系统properties对象来封装所需要的不同值 / SERVER_IP,SERVER_PORT,MAX_SIZE,MIN_SIZE等父类定义的不同的参数 / @param pro Properties 传进来的系统属性 // public void setProperties(Properties pro) { this.pro = pro; } }

posted on 2008-04-26 16:51 马森 阅读(661) 评论(0) 编辑 收藏 网摘

使用Memory Analyzer tool(MAT)分析内存泄漏(二)

Posted on

使用Memory Analyzer tool(MAT)分析内存泄漏(二)

成都心情

关注 ODBMS noSQL RDBMS 还是把标题换回来

BlogJava :: 首页 :: :: 联系 :: 聚合 :: 管理 :: 87 随笔 :: 2 文章 :: 436 评论 :: 1 Trackbacks

公告

�0;0;0;6211;�0;0;0;8981;�0;0;0;5566;�0;0;0;514D;�0;0;0;8D39;�0;0;0;7EDF;�0;0;0;8BA1; Creative Commons License 本作品采用知识共享署名-相同方式共享 2.5 中国大陆许可协议进行许可。 Locations of visitors to this page

留言簿(4)

随笔分类(88)

随笔档案(88)

文章分类(2)

友情链接

积分与排名

  • 积分 - 243427
  • 排名 - 45

最新评论

阅读排行榜

评论排行榜

前言的前言 写blog就是好,在大前提下可以想说什么写什么,不像投稿那么字字斟酌。上周末回了趟成都办事,所以本文来迟了。K117从达州经由达成线往成都方向走的时候,发现铁路边有条河,尽管我现在也不知道其名字,但已被其深深的陶醉。河很宽且水流平缓,河边山丘森林密布,民房星星点点的分布在河边,河里偶尔些小船。当时我就在想,在这里生活是多么的惬意,夏天还可以下去畅游一番,闲来无事也可垂钓。唉,越来越讨厌北漂了。 前言使用Memory Analyzer tool(MAT)分析内存泄漏(一)中,我介绍了内存泄漏的前因后果。在本文中,将介绍MAT如何根据heap dump分析泄漏根源。由于测试范例可能过于简单,很容易找出问题,但我期待借此举一反三。 一开始不得不说说ClassLoader,本质上,它的工作就是把磁盘上的类文件读入内存,然后调用java.lang.ClassLoader.defineClass方法告诉系统把内存镜像处理成合法的字节码。Java提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。system class loader在没有指定装载器的情况下默认装载用户类,在Sun Java 1.5中既sun.misc.Launcher$AppClassLoader。更详细的内容请参看下面的资料。 准备heap dump 请看下面的Pilot类,没啥特殊的。 /// / Pilot class / @author rosen jiang /*/ package org.rosenjiang.bo; public class Pilot{

String name;
int age;

public Pilot(String a, int b){
    name = a;
    age = b;
}

} 然后再看OOMHeapTest类,它是如何撑破heap dump的。 /// / OOMHeapTest class / @author rosen jiang /*/ package org.rosenjiang.test; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.rosenjiang.bo.Pilot; public class OOMHeapTest { public static void main(String[] args){ oom(); }

private static void oom(){
    Map<String, Pilot> map = new HashMap<String, Pilot>();
    Object[] array = new Object[1000000];
    for(int i=0; i<1000000; i++){
        String d = new Date().toString();
        Pilot p = new Pilot(d, i);
        map.put(i+"rosen jiang", p);
        array[i]=p;
    }
}

} 是的,上面构造了很多的Pilot类实例,向数组和map中放。由于是Strong Ref,GC自然不会回收这些对象,一直放在heap中直到溢出。当然在运行前,先要在Eclipse中配置VM参数-XX:+HeapDumpOnOutOfMemoryError。好了,一会儿功夫内存溢出,控制台打出如下信息。 java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid3600.hprof Heap dump file created [78233961 bytes in 1.995 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space java_pid3600.hprof既是heap dump,可以在OOMHeapTest类所在的工程根目录下找到。 MAT安装 话分两头说,有了heap dump还得安装MAT。可以在http://www.eclipse.org/mat/downloads.php选择合适的方式安装。安装完成后切换到Memory Analyzer视图。在Eclipse的左上角有Open Heap Dump按钮,按照刚才说的路径找到java_pid3600.hprof文件并打开。解析hprof文件会花些时间,然后会弹出向导,直接Finish即可。稍后会看到下图所示的界面。 MAT工具分析了heap dump后在界面上非常直观的展示了一个饼图,该图深色区域被怀疑有内存泄漏,可以发现整个heap才64M内存,深色区域就占了99.5%。接下来是一个简短的描述,告诉我们main线程占用了大量内存,并且明确指出system class loader加载的"java.lang.Thread"实例有内存聚集,并建议用关键字"java.lang.Thread"进行检查。所以,MAT通过简单的两句话就说明了问题所在,就算使用者没什么处理内存问题的经验。在下面还有一个"Details"链接,在点开之前不妨考虑一个问题:为何对象实例会聚集在内存中,为何存活(而未被GC)?是的——Strong Ref,那么再走近一些吧。 点击了"Details"链接之后,除了在上一页看到的描述外,还有Shortest Paths To the Accumulation Point和Accumulated Objects部分,这里说明了从GC root到聚集点的最短路径,以及完整的reference chain。观察Accumulated Objects部分,java.util.HashMap和java.lang.Object[1000000]实例的retained heap(size)最大,在上一篇文章中我们知道retained heap代表从该类实例沿着reference chain往下所能收集到的其他类实例的shallow heap(size)总和,所以明显类实例都聚集在HashMap和Object数组中了。这里我们发现一个有趣的现象,既Object数组的shallow heap和retained heap竟然一样,通过Shallow and retained sizes一文可知,数组的shallow heap和一般对象(非数组)不同,依赖于数组的长度和里面的元素的类型,对数组求shallow heap,也就是求数组集合内所有对象的shallow heap之和。好,再来看org.rosenjiang.bo.Pilot对象实例的shallow heap为何是16,因为对象头是8字节,成员变量int是4字节、String引用是4字节,故总共16字节。 接着往下看,来到了Accumulated Objects by Class区域,顾名思义,这里能找到被聚集的对象实例的类名。org.rosenjiang.bo.Pilot类上头条了,被实例化了290,325次,再返回去看程序,我承认是故意这么干的。还有很多有用的报告可用来协助分析问题,只是本文中的例子太简单,也用不上。以后如有用到,一定撰文详细叙述。 又是perm gen 我们在上一篇文章中知道,perm gen是个异类,里面存储了类和方法数据(与class loader有关)以及interned strings(字符串驻留)。在heap dump中没有包含太多的perm gen信息。那么我们就用这些少量的信息来解决问题吧。 看下面的代码,利用interned strings把perm gen撑破了。 /// / OOMPermTest class / @author rosen jiang /*/ package org.rosenjiang.test; public class OOMPermTest { public static void main(String[] args){ oom(); }

private static void oom(){
    Object[] array = new Object[10000000];
    for(int i=0; i<10000000; i++){
        String d = String.valueOf(i).intern();
        array[i]=d;
    }
}

} 控制台打印如下的信息,然后把java_pid1824.hprof文件导入到MAT。其实在MAT里,看到的状况应该和“OutOfMemoryError: Java heap space”差不多(用了数组),因为heap dump并没有包含interned strings方面的任何信息。只是在这里需要强调,使用intern()方法的时候应该多加注意。 java.lang.OutOfMemoryError: PermGen space Dumping heap to java_pid1824.hprof Heap dump file created [121273334 bytes in 2.845 secs] Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 倒是在思考如何把class loader撑破废了些心思。经过尝试,发现使用ASM来动态生成类才能达到目的。ASM(http://asm.objectweb.org)的主要作用是处理已编译类(compiled class),能对已编译类进行生成、转换、分析(功能之一是实现动态代理),而且它运行起来足够的快和小巧,文档也全面,实属居家必备之良品。ASM提供了core API和tree API,前者是基于事件的方式,后者是基于对象的方式,类似于XML的SAX、DOM解析,但是使用tree API性能会有损失。既然下面要用到ASM,这里不得不啰嗦下已编译类的结构,包括: 1、修饰符(例如public、private)、类名、父类名、接口和annotation部分。 2、类成员变量声明,包括每个成员的修饰符、名字、类型和annotation。 3、方法和构造函数描述,包括修饰符、名字、返回和传入参数类型,以及annotation。当然还包括这些方法或构造函数的具体Java字节码。 4、常量池(constant pool)部分,constant pool是一个包含类中出现的数字、字符串、类型常量的数组。 已编译类和原来的类源码区别在于,已编译类只包含类本身,内部类不会在已编译类中出现,而是生成另外一个已编译类文件;其二,已编译类中没有注释;其三,已编译类没有package和import部分。 这里还得说说已编译类对Java类型的描述,对于原始类型由单个大写字母表示,Z代表boolean、C代表char、B代表byte、S代表short、I代表int、F代表float、J代表long、D代表double;而对类类型的描述使用内部名(internal name)外加前缀L和后面的分号共同表示来表示,所谓内部名就是带全包路径的表示法,例如String的内部名是java/lang/String;对于数组类型,使用单方括号加上数据元素类型的方式描述。最后对于方法的描述,用圆括号来表示,如果返回是void用V表示,具体参考下图。 下面的代码中会使用ASM core API,注意接口ClassVisitor是核心,FieldVisitor、MethodVisitor都是辅助接口。ClassVisitor应该按照这样的方式来调用:visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )/( visitInnerClass | visitField | visitMethod )/ visitEnd。就是说visit方法必须首先调用,再调用最多一次的visitSource,再调用最多一次的visitOuterClass方法,接下来再多次调用visitAnnotation和visitAttribute方法,最后是多次调用visitInnerClass、visitField和visitMethod方法。调用完后再调用visitEnd方法作为结尾。 注意ClassWriter类,该类实现了ClassVisitor接口,通过toByteArray方法可以把已编译类直接构建成二进制形式。由于我们要动态生成子类,所以这里只对ClassWriter感兴趣。首先是抽象类原型: /// / @author rosen jiang / MyAbsClass class // package org.rosenjiang.test; public abstract class MyAbsClass { int LESS = -1; int EQUAL = 0; int GREATER = 1; abstract int absTo(Object o); } 其次是自定义类加载器,实在没法,ClassLoader的defineClass方法都是protected的,要加载字节数组形式(因为toByteArray了)的类只有继承一下自己再实现。 /// / @author rosen jiang / MyClassLoader class // package org.rosenjiang.test; public class MyClassLoader extends ClassLoader { public Class defineClass(String name, byte[] b) { return defineClass(name, b, 0, b.length); } } 最后是测试类。 /// / @author rosen jiang / OOMPermTest class // package org.rosenjiang.test; import java.util.ArrayList; import java.util.List; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; public class OOMPermTest { public static void main(String[] args) { OOMPermTest o = new OOMPermTest(); o.oom(); } private void oom() { try { ClassWriter cw = new ClassWriter(0); cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "org/rosenjiang/test/MyAbsClass", null, "java/lang/Object", new String[] {}); cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "LESS", "I", null, new Integer(-1)).visitEnd(); cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "EQUAL", "I", null, new Integer(0)).visitEnd(); cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "GREATER", "I", null, new Integer(1)).visitEnd(); cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "absTo", "(Ljava/lang/Object;)I", null, null).visitEnd(); cw.visitEnd(); byte[] b = cw.toByteArray(); List classLoaders = new ArrayList(); while (true) { MyClassLoader classLoader = new MyClassLoader(); classLoader.defineClass("org.rosenjiang.test.MyAbsClass", b); classLoaders.add(classLoader); } } catch (Exception e) { e.printStackTrace(); } } } 不一会儿,控制台就报错了。 java.lang.OutOfMemoryError: PermGen space Dumping heap to java_pid3023.hprof Heap dump file created [92593641 bytes in 2.405 secs] Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 打开java_pid3023.hprof文件,注意看下图的Classes: 88.1k和Class Loader: 87.7k部分,从这点可看出class loader加载了大量的类。 更进一步分析,点击上图中红框线圈起来的按钮,选择Java Basics——Class Loader Explorer功能。打开后能看到下图所示的界面,第一列是class loader名字;第二列是class loader已定义类(defined classes)的个数,这里要说一下已定义类和已加载类(loaded classes)了,当需要加载类的时候,相应的class loader会首先把请求委派给父class loader,只有当父class loader加载失败后,该class loader才会自己定义并加载类,这就是Java自己的“双亲委派加载链”结构;第三列是class loader所加载的类的实例数目。 在Class Loader Explorer这里,能发现class loader是否加载了过多的类。另外,还有Duplicate Classes功能,也能协助分析重复加载的类,在此就不再截图了,可以肯定的是MyAbsClass被重复加载了N多次。 最后 其实MAT工具非常的强大,上面故弄玄虚的范例代码根本用不上MAT的其他分析功能,所以就不再描述了。其实对于OOM不只我列举的两种溢出错误,还有多种其他错误,但我想说的是,对于perm gen,如果实在找不出问题所在,建议使用JVM的-verbose参数,该参数会在后台打印出日志,可以用来查看哪个class loader加载了什么类,例:“[Loaded org.rosenjiang.test.MyAbsClass from org.rosenjiang.test.MyClassLoader]”。 全文完。 *参考资料 memoryanalyzer Blog java类加载器体系结构 ClassLoader

请注意!引用、转贴本文应注明原作者:Rosen Jiang 以及出处:http://www.blogjava.net/rosen posted on 2010-06-13 16:13 Rosen 阅读(1184) 评论(4) 编辑 收藏 所属分类: Java 基础

[

评论

/# re: 使用Memory Analyzer tool(MAT)分析内存泄漏(二)[未登录] 2010-06-13 16:31 Jet

好文章啊,支持一直写下去~~ 回复 更多评论 ]() /# re: 使用Memory Analyzer tool(MAT)分析内存泄漏(二) 2010-06-14 14:11 滴水

很棒,几种分类测试让人更加深刻的理解了JVM的内存分配机制。 回复 更多评论 /# re: 使用Memory Analyzer tool(MAT)分析内存泄漏(二) 2010-06-14 14:50 BeanSoft

经典文章! 回复 更多评论 /# re: 使用Memory Analyzer tool(MAT)分析内存泄漏(二) 2010-06-14 18:12 Rosen

@Jet @滴水 @BeanSoft 感谢各位的关注!非常感谢。 回复 更多评论 新用户注册 刷新评论列表

推荐链接: 众里寻你千百度,百度期待您的加盟 软件研发团队管理年会(7.10-7.11) 博客园2010T恤正式发布

IT新闻: · 富士通东芝同意合并手机业务 10月建合资公司 · 微软拼音输入法15周年 · 苹果设备占美国移动上网流量58.8% · 微软改变Office营销策略:允许厂商捆绑 · 木马来袭:Linux IRC服务器软件发现后门 博客园首页 学英语 IT新闻 知识库 Java程序员招聘 标题 请输入标题 姓名 请输入你的姓名 主页 请输入验证码 验证码 /* 内容(请不要发表任何与政治相关的内容) 请输入评论内容 Remember Me? 登录 [使用Ctrl+Enter键可以直接提交] 每天10分钟,轻松学英语 推荐职位: · Web前端研发工程师 (百度) · 数据挖掘工程师(百度) · PHP研发工程师(百度) · 急聘进销存软件开发经理(北京拜思腾信息) · 高级.NET开发工程师(盛大网络) · Coolite VS2008 开发工程师 (深圳创新通软) · 分布式系统开发高级工程师(深圳创新科) · Linux开发工程师(深圳创新科)

博客园首页随笔: · WinForm下PictureBox和Panel控件的On_Paint事件有何区别 · ChartPart 图表显示 · 随手写个Repeater分页控件,奉上自己源码,欢迎大虾挑刺 · 浅谈AnkhSvn · Android小项目之--前台界面与用户交互的对接 进度条与拖动条(附源码) 知识库: · 关于Page Fault的一些整理 · 我在南大的七年 · 编写跨浏览器兼容的 CSS 代码的金科玉律 · 程序员应知——首先检查自己的问题 · 程序员的语言“艳遇史”(五)——办公室秘书Smalltalk 网站导航:

博客园 IT新闻 个人主页 博客生活 IT博客网 C++博客 博客园社区 管理 相关文章:

Powered by: BlogJava Copyright © Rosen

设计文档模板

Posted on

设计文档模板 - 我的文章 - 崔超的思想备忘录

博客首页 注册 建议与交流 排行榜 加入友情链接 推荐 投诉 搜索: 帮助 崔超的思想备忘录

思想有重量吗? cuichaox.cublog.cn

xx系统,xx模块

设计文档v0.1

部门:xx

作者:崔超[cuichao@boco.com.cn](mailto:cuichao@boco.com.cn)

版权说明: ××拥有本文档的全部版权,没有经过明确的书面说明,任何人不能复制,转载本文档的所有内容.

可以把上面的内容放在一个好看的封面页上.

文档更新记录 版本

说明

完成日期

修改人 0.1

创建文档

崔超

1.概述

1.1.术语表

编号

缩写

全写

定义 1 2

1.3引用文档

编号

文档名称

作者

说明 1 2

1.4**文档说明**

本设计文档作为XX模块的设计文档,为编码的依据。也作为代码的说明,在代码开发过程中应该保持本文档的更新。

1.5项目背景

对项目的背景进行介绍

1.3**系统说明**

对整个系统的情况进行介绍。列举参考的文档

1.4**模块说明**

从功能角度模块进行简单的说明

2.**工具和方法论**

对使用的设计方法,关键的概念和工具进行说明。比如:是否使用面向对象的方法,组件技术?是否使用到某种开发框架?最终使用哪种编码语言?

3.**分析**

3.1.**本模块在整个系统中的地位**

最好使用结构图的形式并补充详细的说明。

3.2.**功能说明**

使用列表或描述的形式对功能进行详细的说明。对强调数据处理类型的模块,这里可以使用数据流图进行说明。对强调交互过程的模块,可以使用用例图进行说明。

3.3.**非功能需求**

性能的要求,从异常中恢复的要求等非功能的要求.

4.**概要设计**

4.1**外部接口**

4.1.1接口要求

对外部接口进行精确的描述。如果更上层的文档已经有了这个模块的接口要求,对文档进行引用.

4.1.2界面设计

如果存在界面设计

4.1.3配置文件

如果这个模块有自己的配置文件

4.2**模块划分**

使用软件结构图(结构化设计)。或使用包图,类图(面向对象设计)作为说明的主体。并且补充详细的文字说明.关键要说明每个子模块的(或类)的功能和职责。(对面向对象的设计,应该更多使用职责这样的词汇)。如果结构明显的呈现出层次,要对每个层次进行说明。

5.**详细设计**

对每个模块,或者每一个类,进行详细的说明.说明内容包括(1)接口定义 (2)关键算法。可能使用到,类图,流程图,顺序图,交互图,等.如果是多线程程序,还可能堆同步模型进行说明。

5.1**子模块/**1

... ...

5.2**子模块/**2

... ...

6.**测试考虑**

这节内容主要提供给测试人员使用,主要说明测试的重点。

7.**总结**

开发完成后,自己有什么体会,对近一步的改进有什么好的想法,好好的写一下总结。对软件今后改进,扩展.对自己进步都有好处.

update 2007-3-16 修改错字 发表于: 2007-03-15,修改于: 2007-03-16 13:41 已浏览4413次,有评论1条 推荐 投诉 网友评论 本站网友 时间:2009-04-10 19:56:31 IP地址:59.108.199.★ ** 很好,很强大,很个性 发表评论** Copyright © 2001-2010 ChinaUnix.net All Rights Reserved

感谢所有关心和支持过ChinaUnix的朋友们 页面生成时间:0.014

京ICP证041476号

DynamicReports(Getting started)

Posted on

DynamicReports(Getting started)

Getting started

Table of contents

  1. Overview
  2. Simple report

Overview

Creating a report with DynamicReports is very easy, take a look at the example below 01

import

static

net.sf.dynamicreports.report.builder.DynamicReports./*;

02

public

class

Report { 03

04

private

void

build() { 05

try

{

06

report()

//create new report design 07

.columns(...)

//adds columns

08

.groupBy(...)

//adds groups 09

.subtotalsAtSummary(...)

//adds subtotals

10

... 11

//set datasource

12

.setDataSource(...) 13

//export report

14

.toPdf(...)

//export report to pdf 15

.toXls(...)

//export report to excel

16

... 17

//other outputs

18

.toJasperPrint()

//creates jasperprint object 19

.show()

//shows report

20

.print()

//prints report 21

...

22

}

catch

(DRException e) { 23

e.printStackTrace();

24

} 25

}

26

... 27

}

See ReportBuilder class for all available features and JasperReportBuilder class for all available exports and outputs

Simple report

Please note that this is a tutorial example and that this tutorial doesn't describe all features!

You will learn in this section how to create a simple report step by step. First we need to create an empty report desing, configure it and set a datasource. Afterwards the report will be ready to export into any format. It's important to remember the DynamicReports class, through which you will have available most of features. The class provides methods for building a particular piece of a report. Let's start!

Step 1 : Start

First define columns through DynamicReports.col.column(title, field name, datatype) (the field name must match with the name of a field contained in the datasource) and pass them into the report as follows: 1

.columns(

//add columns

2

// title, field name data type 3

col.column(

"Item"

,

"item"

, type.stringType()),

4

col.column(

"Quantity"

,

"quantity"

, type.integerType()), 5

col.column(

"Unit price"

,

"unitprice"

, type.bigDecimalType()))

now define some text at the title and number of pages at the page footer as follows:

1

.title(cmp.text(

"Getting started"

))

//shows report title

2

.pageFooter(cmp.pageXofY())

//shows number of page at page footer

DynamicReports.cmp.text(some text) - creates a component that shows a text DynamicReports.cmp.pageXofY() - creates a component that shows page X of Y Methods title(...) and pageFooter(...) allows to customize a particular report band by adding components to it

SimpleReport_Step01 pdfpreview SimpleReport_Step01

Step 2 : Styles

Each style can have a parent style from which it will inherit its properties. Empty style can be created by DynamicReports.stl.style() 01

StyleBuilder boldStyle = stl.style().bold();

02

StyleBuilder boldCenteredStyle = stl.style(boldStyle) 03

.setHorizontalAlignment(HorizontalAlignment.CENTER);

04

StyleBuilder columnTitleStyle = stl.style(boldCenteredStyle) 05

.setBorder(stl.pen1Point())

06

.setBackgroundColor(Color.LIGHT_GRAY); 07

report()

08

.setColumnTitleStyle(columnTitleStyle) 09

.highlightDetailEvenRows()

10

.title(cmp.text(

"Getting started"

).setStyle(boldCenteredStyle)) 11

.pageFooter(cmp.pageXofY().setStyle(boldCenteredStyle)) SimpleReport_Step02 pdfpreview SimpleReport_Step02

Step 3 : Additional columns

You can very easy multiply, divide, add or subtract column of numbers by another column of numbers or by a number value 1

//price = unitPrice /* quantity

2

TextColumnBuilder priceColumn = unitPriceColumn.multiply(quantityColumn) 3

.setTitle(

"Price"

);

Adding percentage of any column of numbers is simple DynamicReports.col.percentageColumn(title, column)

1

PercentageColumnBuilder pricePercColumn = col.percentageColumn(

"Price %"

, priceColumn);

DynamicReports.col.reportRowNumberColumn(title) creates a column that shows row number

1

TextColumnBuilder rowNumberColumn =

2

col.reportRowNumberColumn(

"No."

) 3

//sets the fixed width of a column, width = 2 /* character width

4

.setFixedColumns(

2

) 5

.setHorizontalAlignment(HorizontalAlignment.CENTER); SimpleReport_Step03 pdfpreview SimpleReport_Step03

Step 4 : Group

We continue by adding a group as shown below 1

.groupBy(itemColumn) SimpleReport_Step04 pdfpreview SimpleReport_Step04

Step 5 : Subtotals

Now we can try to sum a column of numbers. Subtotal of sum is created through DynamicReports.sbt.sum(column) 1

.subtotalsAtSummary(

2

sbt.sum(unitPriceColumn), sbt.sum(priceColumn)) 3

.subtotalsAtFirstGroupFooter(

4

sbt.sum(unitPriceColumn), sbt.sum(priceColumn))

Method subtotalsAtSummary(...) allows to add subtotals to the summary band Method subtotalsAtFirstGroupFooter(...) will find first defined group and add subtotals to the group footer band

SimpleReport_Step05 pdfpreview SimpleReport_Step05

Step 6 : Charts

DynamicReports.cht provide methods for building charts. Category and series are required. 01

Bar3DChartBuilder itemChart = cht.bar3DChart()

02

.setTitle(

"<a href="

http:

//www.dynamicreports.org/examples/sales" title="Sales">Sales by item") 03

.setCategory(itemColumn)

04

.addSerie( 05

cht.serie(unitPriceColumn), cht.serie(priceColumn));

06

Bar3DChartBuilder itemChart2 = cht.bar3DChart() 07

.setTitle(

"<a href="

http:

//www.dynamicreports.org/examples/sales" title="Sales">Sales by item")

08

.setCategory(itemColumn) 09

.setUseSeriesAsCategory(

true

)

10

.addSerie( 11

cht.serie(unitPriceColumn), cht.serie(priceColumn));

Chart is a component and can be placed into any report band.

1

.summary(itemChart, itemChart2) SimpleReport_Step06 pdfpreview SimpleReport_Step06

Step 7 : Column grid & Containers

Components inserted into the bands are arranged vertically, each component is below the previously added component. To arrange components horizontally it is needed to wrap these components with a horizontal container. Container is a component as well and therefore it can be added to any of the report bands. 1

.summary(

2

cmp.horizontalList(itemChart, itemChart2))

Columns layout can be modified by a column grid. The layout is applied to the columns title, details and subtotals.

1

.columnGrid(

2

rowNumberColumn, quantityColumn, unitPriceColumn, 3

grid.verticalColumnGridList(priceColumn, pricePercColumn)) SimpleReport_Step07 pdfpreview SimpleReport_Step07

Step 8 : Hide subtotal

Subtotal for the group notebook is unnecessary because contains only one row. We need to change the group declaration and set the boolean expression condition on which depends whether subtotal is printed. DynamicReports.exp.printWhenGroupHasMoreThanOneRow(itemGroup) creates a boolean condition which returns true when itemGroup has more than one row. 1

ColumnGroupBuilder itemGroup = grp.group(itemColumn);

2

itemGroup.setPrintSubtotalsWhenExpression( 3

exp.printWhenGroupHasMoreThanOneRow(itemGroup));

1

.groupBy(itemGroup) SimpleReport_Step08 pdfpreview SimpleReport_Step08

Step 9 : Title

First define a title style. 1

StyleBuilder titleStyle = stl.style(boldCenteredStyle)

2

.setVerticalAlignment(VerticalAlignment.MIDDLE) 3

.setFontSize(

15

);

DynamicReports.cmp.image() creates an image component DynamicReports.cmp.filler() creates an empty component

1

.title(

//shows report title

2

cmp.horizontalList() 3

.add(

4

cmp.image(getClass().getResourceAsStream(

"../images/dynamicreports.png"

)).setFixedDimension(

80

,

80

), 5

cmp.text(

"DynamicReports"

).setStyle(titleStyle).setHorizontalAlignment(HorizontalAlignment.LEFT),

6

cmp.text(

"Getting started"

).setStyle(titleStyle).setHorizontalAlignment(HorizontalAlignment.RIGHT)) 7

.newRow()

8

.add(cmp.filler().setStyle(stl.style().setTopBorder(stl.pen2Point())).setFixedHeight(

10

)))

The defined filler creates an additional blank space between the title and the column header. Setting top border of a filler draws the line at the bottom of the title. Horizontal list, as previously mentioned, arranges components horizontally in one row but by calling the method row() a new horizontal list is created which is located at the bottom of the previous horizontal list.

SimpleReport_Step09 pdfpreview SimpleReport_Step09

Step 10 : Currency data type

Unit price and price column are currency types. Showing currency is possible by setting pattern to both columns (via method setPattern()), but the best practice is to create a custom data type and apply it to the columns. The custom data type then can be used in other reports. 01

CurrencyType currencyType =

new

CurrencyType();

02

TextColumnBuilder unitPriceColumn = col.column(

"Unit price"

,

"unitprice"

, currencyType); 03

//price = unitPrice /* quantity

04

TextColumnBuilder priceColumn = unitPriceColumn.multiply(quantityColumn).setTitle(

"Price"

) 05

.setDataType(currencyType);

06

private

class

CurrencyType

extends

BigDecimalType { 07

private

static

final

long

serialVersionUID = 1L;

08

09

@Override

10

public

String getPattern() { 11

return

"$ /#,/#/#/#.00"

;

12

} 13

} SimpleReport_Step10 pdfpreview SimpleReport_Step10

Step 11 : Detail row highlighters

1

ConditionalStyleBuilder condition1 = stl.conditionalStyle(cnd.greater(priceColumn,

150

))

2

.setBackgroundColor(

new

Color(

210

,

255

,

210

)); 3

ConditionalStyleBuilder condition2 = stl.conditionalStyle(cnd.smaller(priceColumn,

30

))

4

.setBackgroundColor(

new

Color(

255

,

210

,

210

));

1

.detailRowHighlighters(

2

condition1, condition2)

Condition1 is applied only if price is greater than 150 and sets background color of a row to green. Condition2 is applied only if price is smaller than 30 and sets background color of a row to red. SimpleReport_Step11 pdfpreview SimpleReport_Step11

Step 12 : Conditional styles

1

ConditionalStyleBuilder condition3 = stl.conditionalStyle(cnd.greater(priceColumn,

200

))

2

.setBackgroundColor(

new

Color(

0

,

190

,

0

)) 3

.bold();

4

ConditionalStyleBuilder condition4 = stl.conditionalStyle(cnd.smaller(priceColumn,

20

)) 5

.setBackgroundColor(

new

Color(

190

,

0

,

0

))

6

.bold(); 7

StyleBuilder priceStyle = stl.style()

8

.conditionalStyles( 9

condition3, condition4);

1

priceColumn = unitPriceColumn.multiply(quantityColumn).setTitle(

"Price"

)

2

.setDataType(currencyType) 3

.setStyle(priceStyle);

Condition3 is applied only if price is greater than 200 and sets background color of a price to green. Condition4 is applied only if price is smaller than 20 and sets background color of a price to red. SimpleReport_Step12 pdfpreview SimpleReport_Step1

来源: [http://www.dynamicreports.org/getting-started](http://www.dynamicreports.org/getting-started)

浅谈Java开发中的设计模式

Posted on

浅谈Java开发中的设计模式

浅谈Java开发中的设计模式

1、工厂模式:客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时,工厂类也要做相应的修改。如:如何创建及如何向客户端提供。

2、建造模式:将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。

3、工厂方法模式:核心工厂类不再负责所有产品的创建,而是将具体创建的工作交给子类去做,成为一个抽象工厂角色,仅负责给出具体工厂类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。

4、原始模型模式:通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。缺点是每一个类都必须配备一个克隆方法。

5、单例模式:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。单例模式只应在有真正的“单一实例”的需求时才可使用。

6、适配器(变压器)模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。

7、桥梁模式:将抽象化与实现化脱耦,使得二者可以独立的变化,也就是说将他们之间的强关联变成弱关联,也就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以独立的变化。

8、合成模式:合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。合成模式就是一个处理对象的树结构的模式。合成模式把部分与整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由他们复合而成的合成对象同等看待。

9、装饰模式:装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,提供比继承更多的灵活性。动态给一个对象增加功能,这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。

10、门面模式:外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类。

11、享元模式:FLYWEIGHT在拳击比赛中指最轻量级。享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。外蕴状态不能影响内蕴状态,它们是相互独立的。将可以共享的状态和不可以共享的状态从常规类中区分开来,将不可以共享的状态从类里剔除出去。客户端不可以直接创建被共享的对象,而应当使用一个工厂对象负责创建被共享的对象。享元模式大幅度的降低内存中对象的数量。

12、代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。

13、责任链模式:在责任链模式中,很多对象由每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。

14、命令模式:命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。

15、解释器模式:给定一个语言后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器。客户端可以使用这个解释器来解释这个语言中的句子。解释器模式将描述怎样在有了一个简单的文法后,使用模式设计解释这些语句。在解释器模式里面提到的语言是指任何解释器对象能够解释的任何组合。在解释器模式中需要定义一个代表文法的命令类的等级结构,也就是一系列的组合规则。每一个命令对象都有一个解释方法,代表对命令对象的解释。命令对象的等级结构中的对象的任何排列组合都是一个语言。

16、迭代子模式:迭代子模式可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象。多个对象聚在一起形成的总体称之为聚集,聚集对象是能够包容一组对象的容器对象。迭代子模式将迭代逻辑封装到一个独立的子对象中,从而与聚集本身隔开。迭代子模式简化了聚集的界面。每一个聚集对象都可以有一个或一个以上的迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。迭代算法可以独立于聚集角色变化。

17、调停者模式:调停者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用。从而使他们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用。保证这些作用可以彼此独立的变化。调停者模式将多对多的相互作用转化为一对多的相互作用。调停者模式将对象的行为和协作抽象化,把对象在小尺度的行为上与其他对象的相互作用分开处理。

18、备忘录模式:备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。

19、观察者模式:观察者模式定义了一种一队多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使他们能够自动更新自己。

20、状态模式:状态模式允许一个对象在其内部状态改变的时候改变行为。这个对象看上去象是改变了它的类一样。状态模式把所研究的对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能取得的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类。

21、策略模式:策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。策略模式把行为和环境分开。环境类负责维持和查询行为类,各种算法在具体的策略类中提供。由于算法和环境独立开来,算法的增减,修改都不会影响到环境和客户端。

22、模板方法模式:模板方法模式准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。

23、访问者模式:访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。访问者模式使得增加新的操作变的很容易,就是增加一个新的访问者类。访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。当使用访问者模式时,要将尽可能多的对象浏览逻辑放在访问者类中,而不是放到它的子类中。访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员