|
2006年8月3日
1.Oracle 8i 下使用最新的oracle thin driver时用DatabaseMetaData获取主键等信息时,需要将 connection.getMetaData().getPrimaryKeys(connection.getCatalog(),null,tableName); 中的tableName转为大写,否则无法得到数据。 2.正则表达式中,需要以","分割字符串,但是要分割的字串中含有","号,为了避免冲突,引入前置转义字符"\",这样的正则怎么写呢? 例如: String txt = "STATE_COUNTY=kj\\\\,,ADDR_LINE1=l=j,ADDR_LINE2=mj\n\n,ADDR_LINE3=n\\,o,\n\nADDR_LINE4=\np"; 需要把键值对切分出来: Pattern.compile("[^\\\\],)"); 这个是不行的,会将","号前一个字符消耗掉。 Pattern.compile("(?![\\\\]),)"); 也不行 Pattern p = Pattern.compile,",(?![\\\\])"); 倒是可以,但是把转义字符放后面似乎有点诡异。 找了一个折衷办法,不切割使用正则获取"键=值"子串: Pattern p = Pattern.compile("\\w+\\s*=.*?[,]*.*?(?=,|$)",Pattern.DOTALL); 但是还是带来了子串中不能含有"="的问题。 最后查了一个JDK1.4 DOC,发现了一个反向的非匹配串写法: Pattern p = Pattern.compile("(?<!\\\\),\\s*"); 这样一来就解决了以上问题。
2006年7月4日
乱弹权限系统续一
原文在这:http://www.blogjava.net/RongHao/archive/2006/07/03/56258.html
仔细分析一,二,三,四权限背后的实质可以发现: 一系统权限的概念有一些冗余,很难想象这样一种情况:你已经有了子系统下的很多权限,结果因为没有模块权限而使得无法使用该模块进行任何操作,分配权限的人要非常小心才行.这个世界已经够复杂了,不要再给开发,部署人员增加复杂度了.很明白的,这个权限是不需要资源的权限 二数据库操作权限的概念,有一点疑惑,不知道为什么要建立这样的一个概念,和行级权限有什么区别呢? 从你的上下文理解来看,似乎是这样子的:有操作X表的业务,如果用户有增加权限,则可以任意增加数据,如果用户有编辑权限,则可以编辑任意数据.实际上对应标准权限模型为:不需要限定资源的操作,即不需要资源标识的权限. 三行级数据权限,这个概念很直白,对应标准权限模型就是: 资源(行数据)+操作 四列级数据权限,由于不是针对某特定行数据,所以它也是无资源型权限 就这样,所有的权限最终可划为需要资源标识和不需要资源标识,换句话说,所有权限可划分为控制某些集合的权限和控制单体的权限两种,在某些时候,也称之为 功能权限和数据权限
谈到把权限分给别人,很自然的就是如何控制权限的权限的问题了,很拗口,是吧?仔细想想,这样很直观,也没有什么后遗症,权限自递归控制和自解释,真是一个完美的循环. 有爱思考的同学想深了,会觉得非常麻烦,难实现.当然,概念上一回事,具体实现上可以是另一回事,可以做很多的变通来达到目的.只要保持概念上的简单性,就足以使得非常多的人得以解脱了。
另外,作为架构设计者,非常非常不赞成动辄就把很底层的概念扯进高层设计中(例如行级,数据库什么的),很容易把自己和别人搞胡涂。 可以最近状态不好,要不好好blog一篇,8过,有句话怎么说来着:“都素那浮云而已。。。”
2006年6月8日
摘要: 在本篇文章中,作者在一个系统的构建中深度地被各种配置逻辑所困扰,由此发现了IOC工具(如Spring,Nuts等)的又一个发展方向。 阅读全文
2006年5月17日
需求: 某机构体系下,机构类型分为子公司,部门,人员等,以后可能在某机构或者其子孙机构下可能会再分出其他子机构类型,希望在增加新类型过程中,尽可能的避免修改已有代码。
情况:子公司,部分,人员等已完成所有编码(界面,商业逻辑,数据逻辑) 变化:需要把这个机构体系组成为一颗树状结构 策略:鉴于除了树结构外的其他部分代码已经完成,那么应该首先保持这些代码不予改动。复用修改的优先级从高到低的顺序如下: 界面×JSP,Action层 商业逻辑 Service层 数据逻辑层 数据物理层 有经验的人知道,大部分情况下,越是下层的改动,越是影响越广泛(注意不是修改难度),所以我们只有在无计可施的情况下,才进行低层的修改。
分析: 回到我们的需求,从功能上看,维护一个组织机构的需求,已经涵盖了每一个子结构的维护需求,以部门的建立为例,在新建一个部门时,同时也必须建立机构树上的节点, 这样,如果需要直接使用原有的创建部门的所有代码,需要在其上加上创建组织机构所需要的父节点,以及当前节点名称信息(在这里department的增加界 面JSP是需要修改的,不过实际上我没有修改该文件,而是利用DHTML来动态加入需要新增加的信息),然后提交给原创建部门的URI (departmentSave.action)和组织机构创建URI(orgCreate.action),在这里我们利用ww提供的action chain功能来完成这两个操作。 这里需要修改department.action的配置,拦截save方法使其执行完后跳过原来的relist结果页面转向组织结构的创建orgCreate.action: <action name="unitSave" class="com.wolfsquare.ibase.org.action.UnitAction" method="save"> <result name="input">/org/unit/input.jsp</result> <result name="relist" type="chain"> <param name="actionName">orgCreate</param> <param name="namespace">/org</param> </result> <result name="xxx" type="redirect">/org/unit.action?start=${start}</result> <interceptor-ref name="validationStack"/> </action> 可能有同学看到这里会问:创建组织节点时应该还需要关联前面创建的部门对象啊,这个操作是如何实现的?信息是如何传递的? 在这里,由于整个架构体系并没有支持这种信息传递的功能,所以只好以一种比较”脏“的方式实现: 在department.action类里增加了一个方法getModel()返回刚刚创建的部门对象,然后在org.action类中增加一个接收的方法setModel(object o)这样在整action chain执行的时候,ww会自动将getModel后的数据填入setModel中,这样做的后果是以后增加新的机构类型的功能时,action必须也照这样的语意设置getModel方法。(如果要解决这个问题,这能需要使用一个特定的Context,然后拦截指定Service的创建方法,把创建结果放入Context,不过这又带来如何清除Context的问题,于是又要求助与ww的interspector,专门写一个拦截器来擦屁股,够麻烦。。。)
就这样,我们完成了新增,修改组织机构的功能合成,虽然有点拖沓,但是还是达到了复用,少修改原有代码,而且扩展性也很好的目标。这上篇说的是两个简单业务的功能揉合问题,下篇我们来看看稍微复杂点的情况,看看还能不能继续依葫芦画瓢来完成功能合的成 (未完待续)
2006年4月20日
虽然以前一直在用log4j,但是对其配置不甚了了,突然间因为需解决某些问题,要理解log4j的配置, 然而用google搜了一下,却发现网上没有一个简单直观的说明,于是只好看log4j的官方介绍,终于 理解了log4j的配置用法,以下是我对log4j配置的一点认识,如有谬误还请不吝赐教.
首先我们搞清楚log4j能干什么,简单来说就是提供一个记录不同级别信息内容的日志工具, 可以把不同级别,不同包路径的信息,以指定格式输出到多种设备(控制台,文件等) 在程序中,可以以以下方式来使用 Log log = org.apache.commons.logging.LogFactory.LogFactory.getLog(yourClassName.class); log.debug("debug message -------------------"); log.info("info message ******************"); log.warn("warn message +++++++++++++++"); log.error("error msg================="); 本文主要讲的是如何配置log4j,先让我们先看看一个典型的log4j配置: ==========log4j.properties================== log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{MM-dd HH\:mm\:ss.SSS} %-5p [%F\:%L]%x %m%n log4j.appender.fileout=org.apache.log4j.RollingFileAppender log4j.appender.fileout.File=D:/workspace/log4jtest/log/application.log log4j.appender.fileout.MaxFileSize=10000KB log4j.appender.fileout.MaxBackupIndex=10 log4j.appender.fileout.layout=org.apache.log4j.PatternLayout log4j.appender.fileout.layout.ConversionPattern=%d{MM-dd HH:mm:ss.SSS}[%24F:%-3L:%-5p]%x %m%n log4j.rootCategory=INFO, stdout, fileout log4j.logger.com.wolfsquare.log2=DEBUG,stdout =================================== 这个文件可以划为三小块 ===========第一块定义了一个名为 stdout 的appender和layout (appender,layout的概念后面再解释,目前先记着有这样两个名词): log4j.appender.stdout=org.apache.log4j.ConsoleAppender 定义stdout的实际输出实现类,从这个appender实现类名可以猜到,这个类是负责控制台输出的。 log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 定义stdout的输出装饰器 log4j.appender.stdout.layout.ConversionPattern=%d{MM-dd HH\:mm\:ss.SSS} %-5p [%F\:%L]%x %m%n 装饰器参数配置 ============第二块定义了一个名为 fileout 的appender和layout: log4j.appender.fileout=org.apache.log4j.RollingFileAppender 同理可猜这个实现类是输出到文件的 log4j.appender.fileout.File=D:/workspace/log4jtest/log/application.log log4j.appender.fileout.MaxFileSize=10000KB log4j.appender.fileout.MaxBackupIndex=10 log4j.appender.fileout.layout=org.apache.log4j.PatternLayout log4j.appender.fileout.layout.ConversionPattern=%d{MM-dd HH:mm:ss.SSS}[%24F:%-3L:%-5p]%x %m%n
============第三块定义了名字分别为rootCategory,log4j.logger.com.wolfsquare.log2的两个logger log4j.rootCategory=INFO, stdout, fileout log4j.logger.com.wolfsquare.log2=DEBUG,stdout rootCategory logger是缺省的logger,记录所有的包的信息输出。 第二个logger是只输出指定包com.wolfsquare.log2下的日志信息。 那么INFO,DEBUG又是什么意思呢,他们是信息的分级标识,通过继承实现这个实现自定义级别的分级。 第三块配置两句的意思是这样的: rootCategory 把所有类的INFO级别以上的信息输出到stdout和fileout两个appender中, logger.com.wolfsquare.log2,把com.wolfsquare.log2包中的所有类(包括子包)DEBUG级别(含)以上的信息输出到stdout 中 一个logger可以输出到很多个设备中(appender),如果需要增加输出设备则用分号分隔开appender名称即可。 输出信息的分类级别是DEBUG > INFO > WARN > ERROR,信息细节由细到粗,指定输出某一级别的信息时, 过细的信息输出将会被忽略 如果一个配置中有多个logger,他们之间会有什么关系呢?答案是,在输出上,他们没有任何关系,都是独立运作的, 不相关的,但是在配置上,父包的配置会传给子包,如果子包没有另外定义配置的话。 例如上面配置文件中的两个logger: log4j.logger.com.wolfsquare log4j.logger.com.wolfsquare.log2 这里认为 log4j.logger.com.wolfsquare.log2 继承自 log4j.logger.com.wolfsquare,他们的配置声明如下: log4j.rootCategory=INFO, stdout, fileout log4j.logger.com.wolfsquare.log2=,stdout 注意第二句没有指定输出级别,那么根据配置继承规则会继承父logger的配置,在这里就是INFO。 同时需要强调的是,如果两个logger有继承关系,且输出到同一个appender,根据输出独立原则,那么将会出现两行一样的信息, 例如上面的两个logger定义会导致这样的情况。 最后以一幅图来概括:
2006年2月7日
问题:Spring+Hibernate的应用中,定义了两个业务Service,这里分别称它们为serivceA,ServiceB。 它们的关系简单点来说是这样的: serviceA需要引用serviceB,在serviceB中定义了一个接口列表,serverA必须在serviceB初始化时设置进列表。 在纯bean的情况下,也就是这两个类不需要设置其他bean的情况下,循环引用是正常的,可以通过的。例如下面配置所表示:
<bean id="serviceA" class="A" autowire="byName" lazy-init="true"> <property name="serviceB"><ref local="serviceB"/></property> </bean> <bean id="serviceB" class="B" autowire="byName" lazy-init="true"> <property name="serviceA"><ref bean="serviceA"/></property> </bean> 但是作为一个业务接口,它应该是不需要关心事务,回滚这些无关的东西, 但现实又有这样的需求,所以我们必须保证透明的实现这个功能,于是引 入了AOP方式解决该问题,利用的是Spring自带的org.springframework.t ransaction.interceptor.TransactionProxyFactoryBean. 重新声明文件如下: <bean id="baseTxProxy" lazy-init="true" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> <property name="proxyTargetClass"><value>true</value></property> <property name="transactionAttributes"> <props> <prop key="*">PROPAGATION_REQUIRED</prop> </props> </property> </bean> <bean id="serviceA" parent="baseTxProxy"> <property name="target"><ref local="serviceAImpl"/></property> </bean> <bean id="serviceAImpl" class="serviceA" autowire="byName" lazy-init="true"> <property name="serviceB"> <ref bean="serviceB"/> </property> </bean> <bean id="serviceB" parent="baseTxProxy" lazy-init="true"> <property name="target"><ref local="serviceBImpl"/></property> </bean> <bean id="serviceBImpl" class="D" lazy-init="true"> <property name="serviceA"> <ref bean="serviceA"/> </property> </bean> 于是问题就出现了,Spring报了FactoryBeanCircularReferenceException,无法继续完成设置工作。 查看TransactionProxyFactoryBean源码,其实现了FactoryBean和InitializingBean接口,应该是 做了代理之后,两个代理Bean需要等待所有Bean设置完成后才会标识状态为初始化完毕,于是造成了 冲突。
由于两个业务服务互相调用的路径是不相交的,所以采用了一种变通的方法,在声明serviceA时, 直接定义serviceB: <bean id="serviceAImpl" class="serviceA" autowire="byName" lazy-init="true"> <property name="serviceB"> <bean class="B" autowire="byName"/> </property> </bean> 相当于serviceB和serviceA中使用的serviceB不是同一个实例。 但是如果确实调用重合时怎么办? 解决方法是这样的: <bean id="serviceAImpl" class="serviceA" autowire="byName" lazy-init="true"> <property name="serviceB"> <ref bean="serviceBImpl"/> </property> </bean> 非常简单,serviceAImpl调用时,可能已经在事务环境中了,不需再使用serviceB代理的事务支持, 于是直接引用serviceB实例。这个方法是我写这篇文章时想到的,-_-!!!,看来知识果真还是好好 整理呀。
环境Spring1.1.3,Hibernate 2.1.8, Websphere5.01
hbm文件采用通配符获取:
classpath:/**/*.hbm.xml
问题症状:
应用启动报错说不能重复定义某类,去掉该类后仍然报下一个类重复定义。
仔细查看Log输出发现,所有的hbm文件均找到了两份 -_-!!!
项目组认为应该是websphere不太厚道,在classpath中使用了多处目录(web-inf & classes),并以这些目录为根进行递归搜索匹配文件,可是如果这些目录有包含关系,WebSphere就没有处理重复查找的文件了。
于是在以上配置中改为:
classpath:/classes/**/*.hbm.xml
问题虽然解决了,可是tomcat中却又无效了。 :(
什么时候,企业应用才能一次拷贝,到处运行啊~~
问题1:JDBC Sql Server varchar的取出最大长度限制
环境: JDBC驱动inet tds驱动(版本不明),SQLServer2K
问题症状:对于数据库声明为varchar的长度大于256的字段,可以正常保存,但是无法取出多于256字符以后的内容
问题2:使用Hibernate映射时0长度字符串保存后,取出多加了一个空格
环境:inet tds驱动Hibernate2.1.8,SQL Server2K
问题症状:保存0长度字符串后,取出增加了多余的空格。
以上两个问题都是因为没有使用最新的通讯协议引起的,修改URL声明方式如下:
jdbc:inetdae7:127.0.0.1:1433?database=xxx
问题解决,收工。
ps:发现协议inetdae时,数据库字段为Null时,Hibernate取出声明为基本类型(例如boolean)的对象属性并不会报错,实际上在其他数据库如Oracle和新协议上是会报错的。为了避免此类问题出现,最好还是严格遵守:Hibernate声明对象的基本类型属性,一定不能在数据库端置为空值。
ps2:在解决以上问题中发现,Oracle居然对传人0长度字符串,会转为空值,不知道是为了节省空间还是别的什么理由。-_-!!!
(全文完)
2006年2月6日
在Java环境中,可以使用 java.awt.Toolkit.getScreenResolution()可以得到屏幕每英寸的象素数,但是好像没有什么方法能知道某一台打印机的分辨率,更别提去控制打印粒度了。于是可耻的使用着丑陋的缺省打印精度几年后,终于找到了解决方法,不知道该高兴还是悲伤,其原理说出来也是非常的简单: 提高打印精度,其实就是把本来是A3纸的内容往A4纸里画,也就是说,打印区域(这里对应着Java里的Graphics对象)需要缩小,然后由于缺省情况下打印是照72DPI来打的,不做改变的话,打印内容也会跟着变小。这样就不是我们想要的效果了,所以还得把打印内容成比例放大。一个缩小,一个放大,于是画完后,在指定大小的纸张内,便容纳了比以往更多象素的内容,这下世界总算完美了。
以上做法形象的说应该是这样:把需要产生的图形对象先放大,画在一张“纸上”,然后整体缩小,这样精度就提高了。
tips 1:在一般企业报表表格打印中,使用144DPI得到的表格线的宽度看起来最舒服。 tips 2:现在号称600DPI的打印机其实是576DPI,如果想使用这个分辨率的精度,需要用好一点的纸张,因为已经到极限了,纸张稍差点,打印墨粉就沾不上,导致线体残缺。
附源码(修改分辨率就改动变量iResMul就好):
import java.awt.*; import java.awt.print.*;
public class MyPrintableObject implements Printable { public int iResMul = 1; // 1 = 72 dpi; 4 = 288 dpi
public int print(Graphics g, PageFormat pf, int iPage) throws PrinterException { final int FONTSIZE = 12; final double PNT_MM = 25.4 / 72.; if (0 != iPage) return NO_SUCH_PAGE; try { int iPosX = 1; int iPosY = 1; int iAddY = FONTSIZE * 3 / 2 * iResMul; int iWdth = (int) Math.round(pf.getImageableWidth() * iResMul) - 3; int iHght = (int) Math.round(pf.getImageableHeight() * iResMul) - 3; int iCrcl = Math.min(iWdth, iHght) - 4 * iResMul; Graphics2D g2 = (Graphics2D) g; PrinterJob prjob = ((PrinterGraphics) g2).getPrinterJob(); g2.translate(pf.getImageableX(), pf.getImageableY()); g2.scale(1.0 / iResMul, 1.0 / iResMul); g2.setFont(new Font("SansSerif", Font.PLAIN, FONTSIZE * iResMul)); g2.setColor(Color.black); g2.drawRect(iPosX, iPosY, iWdth, iHght); g2.drawLine(iPosX, iHght / 2 + iWdth / 50, iPosX + iWdth, iHght / 2 - iWdth / 50); g2.drawLine(iPosX, iHght / 2 - iWdth / 50, iPosX + iWdth, iHght / 2 + iWdth / 50); g2.drawOval(iPosX + 2 * iResMul, iHght - iCrcl - 2 * iResMul, iCrcl, iCrcl); iPosX += iAddY; iPosY += iAddY / 2; g2.drawString("PrinterJob-UserName: " + prjob.getUserName(), iPosX, iPosY += iAddY); g2.drawString("Betriebssystem: " + System.getProperty("os.name") + " " + System.getProperty("os.version"), iPosX, iPosY += iAddY); g2 .drawString("Java-Version: JDK " + System.getProperty("java.version"), iPosX, iPosY += iAddY); g2.drawString("Width/Height: " + dbldgt(pf.getWidth()) + " / " + dbldgt(pf.getHeight()) + " points = " + dbldgt(pf.getWidth() * PNT_MM) + " / " + dbldgt(pf.getHeight() * PNT_MM) + " mm", iPosX, iPosY += iAddY); g2.drawString("Imageable Width/Height: " + dbldgt(pf.getImageableWidth()) + " / " + dbldgt(pf.getImageableHeight()) + " points = " + dbldgt(pf.getImageableWidth() * PNT_MM) + " / " + dbldgt(pf.getImageableHeight() * PNT_MM) + " mm", iPosX, iPosY += iAddY); g2.drawString("Imageable X/Y: " + dbldgt(pf.getImageableX()) + " / " + dbldgt(pf.getImageableY()) + " points = " + dbldgt(pf.getImageableX() * PNT_MM) + " / " + dbldgt(pf.getImageableY() * PNT_MM) + " mm", iPosX, iPosY += iAddY); g2.drawString("versuchte Druckaufl sung: " + 72 * iResMul + " dpi", iPosX, iPosY += iAddY); } catch (Exception ex) { throw new PrinterException(ex.getMessage()); } return PAGE_EXISTS; }
private static double dbldgt(double d) { return Math.round(d * 10.) / 10.; // show one digit after point }
public static void main(String[] args) { PrinterJob pj = PrinterJob.getPrinterJob(); pj.setPrintable(new MyPrintableObject()); if (pj.printDialog()) { try { pj.print(); } catch (PrinterException e) { System.out.println(e); } } } }
(全文完)
2005年12月6日
在上一篇文章里,我们使用了基于事件传递的机制来对企业应用的子系统进行解耦,但是由于需要强制地继承或者实现一个广播事件的接口EventBrocast,实际上,就职责分离和功能单一的角度来看,前篇文章中的例子中,这个机制对OrderService侵入太大了,我们必须寻找更为有效的方法,不需要程序实现某个接口或继承某个超类来完成这个工作,这一切必须对具体程序完全透明,这个责任谁能承担呢,毫无疑问,历史的重担就落在了AOP身上 ;) 。下面我们来看看具体的实现: OrderService已经实现,除了订单的处理,没有任何的职责,为了完成事件的广播,必须要有一个途径能够拦截到OrderService的所有方法调用,然后分析调用的语义(参数),并根据这些内容给广播出去。而恰好,AOP组织统一的接口MethodInterceptor可以完成这个功能。于是上篇文章的程序可以这样修改:
// 订单服务只负责做好自己的事
public class OrderService { public Order saveOrder(Order order){ 。。。。处理订单 。。。保存 } }
而为了拦截任何的方法调用,则实现了拦截器EventBrocaster:
public class EventBrocaster extends LifeEventBrocast implements MethodInterceptor { private List eventListeners; public void setEventListener(List list){ this.eventListeners=list; } public List geteEventListeners(){ return eventListeners; } public Object invoke(MethodInvocation invoke) { obj = invoke.proceed();// 执行被拦截的方法完成业务操作 Object[] params = invoke.getArguments(); Object param = params.length > 1 ? params : params[0]; Event le = new Event(param, eventType); brocast(le);// 广播 } }
事件侦听器:
public OrderEventListener implements EventListener{ private FinancialService financialService; public void setFinancialService(FinancialService fs){ this.financialService=fs; } public void performed(Event e){ Order order =(Order) e.getObject(); financialService.createRequestOfMoney(order.getAmount()); } }
然后,在Spring配置里将这些组件全部连接起来:
1.OrderService实现: <bean id="orderServiceImpl" class="OrderService" autowire="byName"> </bean>
2. 声明OrderService代理:
<bean id="orderService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target"> <ref local="orderServiceImpl"/> </property> <property name="interceptorNames"> <!--拦截器列表--> <list> <value>eventBrocaster</value> </list> </property> <property name="singleton"> <value>true</value> </property> </bean> 3.事件广播拦截器 <bean id="eventBrocaster" class="com.wolfsquare.core.service.EventBrocaster" singleton="true"> <property name="lifecycleListeners"> <list> <ref bean="orderEventListener"/> </list> </property> </bean> 4.具体的财务子系统的侦听器实现与财务系统的通讯: <bean id="orderEventListener" class="OrderEventListener" autowire="byName"> <propety name="financialService"><ref bean="financialService"/></property> </bean>
这样,我们与具体实现无关的事件广播就做到了,聪明的朋友看到这里,肯定想到了拦截器方式不仅仅适用与事件广播,还可以实现事务的统一管理,事实上Spring的事务管理就是这样完成的,还可以实现权限的控制例如Acegi,简直有点象万能的胶水,呵呵。
从两篇文章的逐步探讨下,同一个机器,同一个虚拟机之内的数据通讯都可以实现了,那么异构系统和多虚拟机间的通讯又如何处理呢,于是ESB(企业服务总线)的概念就慢慢浮现出来了,不过这个不在本文探讨的范畴了,也许在不久的将来,我会补上这一篇。
(全文完)
|