狂奔 lion

自强不息

2009年3月4日

浅谈Java中的同步的方法和原理

Java的内存模型中Thread会附有自己的堆栈,寄存器,必要时需要和主存即heap之间同步。
可以使用Synchornized关键字和Concurrent包中的Lock可以保证线程互斥和可见性。

互斥性体现在类锁或者对象锁上,每个对象自身都包含一个监视器,该监视器是一个每次只能被一个线程所获取进入的临界区,可以通过wait和notify来退出和准入临界区。可以看出这是一个生产者-消费者的模型。而Concurrent包中的Lock为了能够获得更好的性能和更好的扩展性,以及不依赖于关键字的可读代码,自己实现了这样一个生产消费队列,也就是AbstractQueuedSynchronizer,被称为AQS的机制。每个Lock都内置了一个AbstractQueuedSynchronizer。需要说明的是AbstractQueuedSynchronizer内部实现采用了CAS机制,通过getState, setState, compareAndSetState访问控制一个32bit int的形式进行互斥。

那么可见性是如何保证的呢?

对于关键字的同步机制,其实可见性就是线程和主存之间的同步时机问题。共有4个时间点需要注意:
1 获取或释放类锁/对象锁的时候。Thread保证reload/flush全部变更
2 volatile就是flush on write或者reload on read
3 当线程首次访问共享变量时,可以得到最新的结果。
题外:所以在构造方法中公布this时很危险的。简单的说,就是构造时不逃脱任何变量,不开启新的线程,只做封装。关于安全构造,请参考
http://www.ibm.com/developerworks/cn/java/j-jtp0618/#resources
4 线程结束时,所有变更会写回主存

关于Concurrent Lock如何实现可见性的问题,Doug Lea大侠,只在他的论文中提到,按照JSR133,Unsafe在getState, setState, compareAndSetState时保证了线程的变量的可见性,不需要额外的volatile支持,至于具体这些native做了哪些magic就不得而知了,总之,最后的contract就是保证lock区间的共享变量可见性。开发团队被逼急了就这样回答:
There seems to be a real reluctance to explain the dirty details. I think the question was definitely understood on the concurrent interest thread, and the answer is that synchronized and concurrent locking are intended to be interchangable in terms of memory semantics when implemented correctly. The answer to matfud's question seems to be "trust us.”

不过这个地方的确是开发团队给我们用户迷惑的地方,在同样应用了CAS机制的Atomic类中,都内嵌了volatile变量,但是再lock块中,他告诉我们可以保证可见性。

感兴趣的同学可以下面的两个thread和Doug Lea的thesis:
http://altair.cs.oswego.edu/pipermail/concurrency-interest/2005-June/001587.html
http://forums.sun.com/thread.jspa?threadID=631014&start=15&tstart=0
http://gee.cs.oswego.edu/dl/papers/aqs.pdf

posted @ 2010-07-09 19:49 杨一 阅读(1842) | 评论 (0)编辑 收藏

commons-net FTPClient API存取设计

文件系统无非就是文件的存取和组织结构。
访问一个文件系统的API也应该是写,读,定位方法(Pathname?/URI?)

FTPClient针对文件的保存和获取各提供了两个方法,分别是:

public boolean storeFile(String remote, InputStream local)
public OutputStream storeFileStream(String remote)

public boolean retrieveFile(String remote, OutputStream local)
public InputStream retrieveFileStream(String remote)

 

两个方法貌似相同,实际不同,返回流的那个因为不能马上处理流,所以需要用户手工调用completePendingCommand,而另一个传递流进去的则不需要。可能有同学已经遇到过这个问题了,读写第一个文件时总是正确的,当相同API读写第二个文件时,block住了。这是因为FTPClient要求在进行流操作之后执行completePendingCommand,以确保流处理完毕,因为流处理不是即时的,所以也没有办法不手工调用completePendingCommand。问题是开发者把不返回流的方法末尾加上了completePendingCommand,如果不看代码可能根本不知道。
文档上说:

     * There are a few FTPClient methods that do not complete the
     
* entire sequence of FTP commands to complete a transaction.  These
     
* commands require some action by the programmer after the reception
     
* of a positive intermediate command.  After the programmer's code
     * completes its actions, it must call this method to receive
     
* the completion reply from the server and verify the success of the
     
* entire transaction.


但是这样仍然还是让人有点困惑,为什么都是存储/读取的方法,有时候要调用completePendingCommand,有时候不调用?更严重的问题是completePendingCommand调用了getReply,如果一个命令通过socket stream传了过去但是没有getReply,即没有completePendingCommand,那么下次发命令时,将会受到本次返回码的干扰,得到无效的响应。而如果在completePendingCommand之后又进行了一次无辜的completePendingCommand,那么因为FTP Server上没有Reply了,就会block。所以completePendingCommand并不是可以随意添加的。

现在出现了两个问题:
1 completePendingCommand很容易多出来或遗漏
2 显式调用completePendingCommand暴露了底层实现,给用户带来不便,用户只想要InputStream或者OutputStream

为了解决这个问题,可以对InputStream进行扩展,建立一个ReplyOnCloseInputStream,如下:

private static ReplyOnCloseInputStream extends InputStream{
  
//
  public ReplyOnCloseInputStream(InputStream is, FTPClient c){
    
//
  }

  
//
  @override
  
public void close(){
    
if(c.completePendingCommand){
      is.close();
    }
else{
      
//throw Exception
    }

  }

}
 
//
return new ReplyOnCloseInputStream(is, client);


这样封装之后,FTPClient的用户只需要正常在处理完流之后关闭即可,而不必暴露实现细节。保存文件也可以用相同的方法封装OutputStream。

posted @ 2010-07-07 23:08 杨一 阅读(3431) | 评论 (1)编辑 收藏

关于ThreadLocal的内存泄露

ThreadLocal是一种confinement,confinement和local及immutable都是线程安全的(如果JVM可信的话)。因为对每个线程和value之间存在hash表,而线程数量未知,从表象来看ThreadLocal会存在内存泄露,读了代码,发现实际上也可能会内存泄露。

事实上每个Thread实例都具备一个ThreadLocal的map,以ThreadLocal Instance为key,以绑定的Object为Value。而这个map不是普通的map,它是在ThreadLocal中定义的,它和普通map的最大区别就是它的Entry是针对ThreadLocal弱引用的,即当外部ThreadLocal引用为空时,map就可以把ThreadLocal交给GC回收,从而得到一个null的key。

这个threadlocal内部的map在Thread实例内部维护了ThreadLocal Instance和bind value之间的关系,这个map有threshold,当超过threshold时,map会首先检查内部的ThreadLocal(前文说过,map是弱引用可以释放)是否为null,如果存在null,那么释放引用给gc,这样保留了位置给新的线程。如果不存在slate threadlocal,那么double threshold。除此之外,还有两个机会释放掉已经废弃的threadlocal占用的内存,一是当hash算法得到的table index刚好是一个null key的threadlocal时,直接用新的threadlocal替换掉已经废弃的。另外每次在map中新建一个entry时(即没有和用过的或未清理的entry命中时),会调用cleanSomeSlots来遍历清理空间。此外,当Thread本身销毁时,这个map也一定被销毁了(map在Thread之内),这样内部所有绑定到该线程的ThreadLocal的Object Value因为没有引用继续保持,所以被销毁。

从上可以看出Java已经充分考虑了时间和空间的权衡,但是因为置为null的threadlocal对应的Object Value无法及时回收。map只有到达threshold时或添加entry时才做检查,不似gc是定时检查,不过我们可以手工轮询检查,显式调用map的remove方法,及时的清理废弃的threadlocal内存。需要说明的是,只要不往不用的threadlocal中放入大量数据,问题不大,毕竟还有回收的机制。

综上,废弃threadlocal占用的内存会在3中情况下清理:
1 thread结束,那么与之相关的threadlocal value会被清理
2 GC后,thread.threadlocals(map) threshold超过最大值时,会清理
3 GC后,thread.threadlocals(map) 添加新的Entry时,hash算法没有命中既有Entry时,会清理

那么何时会“内存泄露”?当Thread长时间不结束,存在大量废弃的ThreadLocal,而又不再添加新的ThreadLocal(或新添加的ThreadLocal恰好和一个废弃ThreadLocal在map中命中)时。

posted @ 2010-07-02 18:27 杨一 阅读(2263) | 评论 (2)编辑 收藏

关于软件文档,我的看法

文档应该包括两大部分,一部分是清晰的代码结构和注释,比如Concurrent API就是这样,还有一部分是文字文档,包括三个小部分:一是开发文档,应该讲架构和功能;二是索引文档,详细介绍功能和参数,三是用户文档,包括安装和使用说明

文档最困难的莫过于版本的一致性,当软件升级后,一些obsolete的内容和新的feature很难同步。要是架构发生了变化,那就更困难了。一般document team都不是太精于技术,所以也会产生一些问题。

只能说任何事物永远都有改进的空间,但是同样也永远没有达到完美的程度

posted @ 2010-06-29 18:26 杨一 阅读(314) | 评论 (0)编辑 收藏

NIO学习之Web服务器示例

     摘要: 1 根据cpu core数量确定selector数量 2 用一个selector服务accept,其他selector按照core-1分配线程数运行 3 accept selector作为生产者把获得的请求放入队列 4 某个selector作为消费者从blocking queue中取出请求socket channel,并向自己注册 5 当获得read信号时,selector建立工作...  阅读全文

posted @ 2010-06-25 19:19 杨一 阅读(1952) | 评论 (0)编辑 收藏

多线程的知识

多线程的优点:
1 多核利用
2 为单个任务建模方便
3 异步处理不同事件,不必盲等
4 现代的UI也需要它
风险:
1 同步变量易错误
2 因资源限制导致线程活跃性问题
3 因2导致的性能问题
用途:
框架,UI,Backend
线程安全的本质是什么:
并非是线程和锁,这些只是基础结构,本质是如何控制共享变量访问的状态
什么是线程安全:
就是线程之间的执行还没有发生错误,就是没有发生意外
一个线程安全的类本身封装了对类内部方法和变量的异步请求,调用方无需考虑线程安全问题
无状态的变量总是线程安全的
原子性:
完整执行的单元,如不加锁控制,则会发生竞态条件,如不加锁的懒汉单例模式,或者复合操作。
锁,内在锁,重入:
利用synchronized关键字控制访问单元,同一线程可以重入锁内部,避免了面向对象产生的问题。同一变量的所有出现场合应该使用同一个锁来控制。synchronized(lock)。
即使所有方法都用synchronized控制也不能保证线程安全,它可能在调用时编程复合操作。
活跃性和性能问题:
过大的粒度会导致这个问题,用锁进行异步控制,导致了线程的顺序执行。
简单和性能是一对矛盾,需要适当的取舍。不能在没有考虑成熟的情况下,为了性能去牺牲简洁性。
要尽量避免耗时操作,IO和网络操作中使用锁

posted @ 2010-06-25 19:17 杨一 阅读(365) | 评论 (0)编辑 收藏

Ext Store Filter的实现和问题

Store包含两个数据缓存 - snapshot和data,grid,combo等控件的显示全部基于data,而snapshot是数据的完整缓存,当首次应用过滤器时,snapshot从data中备份数据,当应用过滤器时,filter从snapshot获取一份完整的数据,并在其中进行过滤,过滤后的结果形成了data并传递给展示,及data总是过滤后的数据,而snapshot总是完整的数据,不过看名字让人误以为它们的作用正好相反。
相应地,当进行store的增删改时,要同时维护两个缓存。
问题
Store包含两个增加Record的方法,即insert和add,其中的insert没有更新snapshot所以当重新应用filter时,即data被重新定义时,在data中使用insert新增的记录是无效的。
解决方法
用add不要用insert,如果用insert,记得把数据写进snapshot: store.snapshot.addAll(records)

posted @ 2010-06-25 19:16 杨一 阅读(1275) | 评论 (0)编辑 收藏

Ext中Combo组件的联动封装

     摘要: 在Extjs中构造N级联动下拉的麻烦不少,需定制下拉数据并设定响应事件。通过对Combo集合的封装,无需自己配置Combo,只需设定数据和关联层级,即可自动构造出一组支持正向和逆向过滤的联动下拉并获取其中某一个的实例。 如: 数据: Ext.test = {};       Ext.test.lcbdata&nb...  阅读全文

posted @ 2010-06-25 19:14 杨一 阅读(1297) | 评论 (0)编辑 收藏

前端框架动态组件和代码生成之间的选择

目前主流的SSH开发架构中,为减轻开发者工作,便于管理开发过程,往往用到一些公共代码和组件,或者采用了基于模版的代码生成机制,对于后台的DAO,Service等因为架构决定,代码生成必不可少,但是在前端页面的实现上,却可以有两种不同的思路,一种是把配置信息直接封装成更高级别的组建,一种是进行代码生成。请大家讨论一下这两种方案的优劣,这里先抛砖引玉了。

相同点:
配置信息:XML OR 数据库

控件化:
优点:
1 易于添加公共功能
2 修改配置数据直接生效
3 代码结构清晰,对开发者友好
缺点:
1 重组内存中对象结构,性能没有代码生成好(但渲染时间相同)
2 仅能控制组件自身封装的配置,不支持个性化修改,如果配置文件不支持的参数,则控件不支持
3 必须保证每个控件一个配置

代码生成:
优点:
1 性能较好
2 易于定制内容
3 可以只配置一个模版,然后做出多个简单的修改
缺点:
1 不能针对多个页面同时添加公共功能
2 业务修改需要重新生成代码
3 开发者需要修改自动生成的代码,并需要了解一些底层的实现结构

=====================20091029
代码生成并不能提高工作效率,尤其是针对复杂的富客户端开发
开发组件可提提供一种有效的选项,但是在运行效率和内存处理上需要细心处理

posted @ 2010-06-25 19:11 杨一 阅读(440) | 评论 (0)编辑 收藏

Javascript工作流引擎代码及实例

 

最近在学习jBPMJavascript,所以按照一些相关概念自己写了下面的200行代码的“工作流引擎”,工作流管理系统包含了流程定义,引擎,及应用系统三个主要部分,下面的代码实现了流程的分支合并,目前只支持一种环节上的迁移。拷贝到html,双击就可以跑起来。

 

var workflowDef = {
         start:{
                   fn:
"begin"//对应处理方法可以在内部定义,也可以在外部定义
                   next:[
"task1","task2"]
         },
         end:
"end",
         tasks:[{
                   id:
"task1",
                   fn:
function(){
                            alert(
"执行任务一");
                   },
                   before:
function(){
                            alert(
"执行任务一前");
                   },
                   after:
function(){
                            alert(
"执行任务一后");
                   },
                   next:[
"task4","task5"]
         },{
                   id:
"task2",
                   fn:
function(){
                            alert(
"执行任务二");
                   },
                   before:
function(){
                            alert(
"执行任务二前");
                   },
                   after:
function(){
                            alert(
"执行任务二后");
                   },
                   next:[
"task3"]
         },{
                   id:
"task3",
                   fn:
function(){
                            alert(
"执行任务三");
                   },
                   before:
function(){
                            alert(
"执行任务三前");
                   },
                   after:
function(){
                            alert(
"执行任务三后");
                   },
                   
//定义合并的数量
                   merge: 
3,
                   next:
"EOWF"
         },{
                   id:
"task4",
                   fn:
function(){
                            alert(
"执行任务四");
                   },
                   before:
function(){
                            alert(
"执行任务四前");
                   },
                   after:
function(){
                            alert(
"执行任务四后");
                   },
                   next:[
"task3"]
         },{
                   id:
"task5",
                   fn:
function(){
                            alert(
"执行任务五");
                   },
                   before:
function(){
                            alert(
"执行任务五前");
                   },
                   after:
function(){
                            alert(
"执行任务五后");
                   },
                   next:[
"task3"]
         }]
}

 

 

//////////定义引擎////////////

Yi 
= {};
Yi.Utils 
= {};
Yi.Utils.execute 
= function(o){
         
if(typeof o != 'function')
                   eval(o)();
         
else
                   o();
}
//工作流类
Yi.Workflow 
= function(workflowDef){
         
this.def = workflowDef;
         
this.tasks = this.def.tasks;
}
//public按照环节id查找查找
Yi.Workflow.prototype.findTask 
= function(taskId){
         
for(var i=0;i<this.tasks.length;i++){
                   
if(this.tasks[i].id == taskId)
                            
return this.tasks[i];
         }
}
//public启动工作流
Yi.Workflow.prototype.start 
= function(){
         
this.currentTasks = [];
         Yi.Utils.execute(
this.def.start.fn);
         
for(var i=0;i<this.def.start.next.length;i++){
                   
this.currentTasks[i] = this.findTask(this.def.start.next[i]);
                   Yi.Utils.execute(
this.currentTasks[i].before);
         }
}
//private
Yi.Workflow.prototype.findCurrentTaskById 
= function(taskId){
         
for(var i=0;i<this.currentTasks.length;i++){
                   
if(this.currentTasks[i].id == taskId)
                            
return this.currentTasks[i];
         }
         
return null;
}
//private
Yi.Workflow.prototype.removeFromCurrentTasks 
= function(task){
         
var temp = [];
         
for(var i=0;i<this.currentTasks.length;i++){
                   
if(!(this.currentTasks[i] == task))
                            temp.push(
this.currentTasks[i]); 
         }
         
this.currentTasks = temp;
         temp 
= null;
}
//public触发当前环节
Yi.Workflow.prototype.signal 
= function(taskId){
         
//只处理当前活动环节
         
var task = this.findCurrentTaskById(taskId);
         
if(task == null){
                   alert(
"工作流未流转到此环节!");
                   
return;
         }
         
//对于合并的处理
         
if(task.merge != undefined){
                   
if(task.merge != 0){
                            alert(
"工作流流转条件不充分!");
                            
return;
                   }
else{
                            Yi.Utils.execute(task.before);
                   }        
         }
         
//触发当前环节
         Yi.Utils.execute(task.fn);
         
//触发后动作
         Yi.Utils.execute(task.after);
         
//下一步如果工作流结束
         
if(task.next === "EOWF"){
                   Yi.Utils.execute(
this.def.end);
                   
delete this.currentTasks;
                   
return;
         }
         
//遍历下一步环节
         
this.removeFromCurrentTasks(task);
         
for(var i=0;i<task.next.length;i++){
                   
var tempTask = this.findTask(task.next[i]);
                   
if(!tempTask.inCurrentTasks)
                            
this.currentTasks.push(tempTask);
                   
if(tempTask.merge != undefined){
                            tempTask.merge
--;
                            tempTask.inCurrentTasks 
= true;
                   }
                   
else
                            Yi.Utils.execute(tempTask.before);
         }
}
//public获取当前的活动环节
Yi.Workflow.prototype.getCurrentTasks 
= function(){
         
return this.currentTasks;
}
//public获取流程定义
Yi.Workflow.prototype.getDef 
= function(){
         
return this.def;
}

 

////////应用系统///////////////
var wf = new Yi.Workflow(workflowDef);
alert(
"启动工作流");
wf.start();
alert(
"尝试手工执行任务3,返回工作流没有流转到这里");
wf.signal(
"task3");
alert(
"分支开始");
alert(
"手工执行任务1");
wf.signal(
"task1");
alert(
"手工执行任务2");
wf.signal(
"task2");
alert(
"手工执行任务4");
wf.signal(
"task4");
alert(
"手工执行任务5");
wf.signal(
"task5");
alert(
"手工执行任务3");
wf.signal(
"task3");
function begin(){
         alert(
"流程开始,该函数在外部定义");
}
function end(){
         alert(
"流程结束");
}

posted @ 2009-03-06 17:39 杨一 阅读(1978) | 评论 (1)编辑 收藏

Spring Security 2 中动态角色权限的实现

 

安全框架的主体包括两部分即验权和授权。Spring Security2可以很好的实现这两个过程。Spring Security2对其前身acegi最大的改进是提供了自定义的配置标签,通过Security的命名空间定义了httpauthentication-provider等标签,这样做的好处是极大地简化了框架的配置,并很好地隐藏了框架实现的细节,在配置的表述上也更清晰,总体上提高了框架的易用性。

然而,该框架默认的权限配置方式在xml中,又因为新版本隐藏了实现细节,在动态权限的扩展上,能力变小了。在验权过程中,遇到的问题不多。但在授权时,如果是acegi,人们可以通过继承AbstractFilterInvocationDefinitionSource类实现在授权(即资源角色和用户角色的匹配)前,针对资源的角色的获取。而新版本因为用新标签进行了整合,这个过程被默认的类实现隐藏掉了,包括过滤器,资源获取和角色定义等过程都由框架来实现,于是很多人在使用Spring Security2时也想通过改动DefaultFilterInvocationDefinitionSource对资源的获取来实现数据库或文件中的动态的角色。不过这样的改动侵入性比较高,而且还保留了acegi的痕迹,也违背了开闭的原则。

其实,我们完全可以通过Spring Security2 accessManager提供的自定义投票机制来解决这个问题,这样既不影响现有的基于URL的配置,还可以加入自己的动态的权限配置。

         其实现策略如下:

1 定义类DynamicRoleVoter实现AccessDecisionVoter,注入实现接口DynamicRoleProvider(用来定义获取角色的方法)的提供动态角色的类

2 在两个supports方法中返回true

3 vote方法中,有三个参数(Authentication authentication, Object object,

ConfigAttributeDefinition config) 通过第一个获取用户的权限集合,第二个可以获取到资源对象,进而通过DynamicRoleProvider获取到角色集合进行匹配。

4 在配置文件中加入DynamicRoleVoter,如下:

<beans:bean id="accessDecisionManager" class="org.springframework.security.vote.AffirmativeBased">
<beans:property name="decisionVoters">
<beans:list>
<beans:bean class="org.springframework.security.vote.RoleVoter" />
<beans:bean class="org.springframework.security.vote.AuthenticatedVoter" />
<beans:bean class="DynamicRoleVoter">
    
<beans:property name="dynamicRoleProvider">
        
<beans:ref local="dynamicRoleProvider"/>
</beans:property>
</beans:bean>
</beans:list>
</beans:property>
</beans:bean>
<beans:bean id=” dynamicRoleProvider” class=”…”>
    
……
</beans:bean
>

posted @ 2009-03-04 12:55 杨一 阅读(5051) | 评论 (0)编辑 收藏

实现Ext表单对checkBoxGroup的统一管理

1 对于类型是checkboxgroup的数据,数据库中保存数据的格式是value1,value2...valueN,其中1~N的数据有可能不存在,如果选中则存在,最后拼接成一个串。
在Ext中,通过Record对象向FormPanel中的内置对象BasicForm加载数据时,采用的是setValues方法,而setValues第一步要通过Record中定义的name使用findField方法找到表单元素,遗憾的是,继承了Field的checkboxgroup组件并不能正确的通过getName返回自身引用,所以,需要对getName方法进行重写,此外,为了适应我们采用的数据格式,对于该组件的setValue(被setValues调用)和getValue(获取到已加工的数据,此事后话)也要进行重写。故而对于形如:
{     
   xtype: 'checkboxgroup',   
   name: 'biztype',     
   width: 
220,   
   columns: 
3,   
   fieldLabel: '业务类别',   
   items: [     
       {boxLabel: '类别1', inputValue: '
01'},     
       {boxLabel: '类别2', inputValue: '
02'},     
       {boxLabel: '类别3', inputValue: '
03'},     
       {boxLabel: '类别4', inputValue: '
04'}     
       ]     
 }  

的checkboxgroup定义,需重写类如下:
 
Ext.override(Ext.form.CheckboxGroup,{    
    
//在inputValue中找到定义的内容后,设置到items里的各个checkbox中    
    setValue : function(value){   
        
this.items.each(function(f){   
            
if(value.indexOf(f.inputValue) != -1){   
                f.setValue(
true);   
            }
else{   
                f.setValue(
false);   
            }   
        });   
    },   
    
//以value1,value2的形式拼接group内的值   
    getValue : function(){   
        
var re = "";   
        
this.items.each(function(f){   
            
if(f.getValue() == true){   
                re 
+= f.inputValue + ",";   
            }   
        });   
        
return re.substr(0,re.length - 1);   
    },   
    
//在Field类中定义的getName方法不符合CheckBoxGroup中默认的定义,因此需要重写该方法使其可以被BasicForm找到   
    getName : function(){   
        
return this.name;   
    }   
});

2 通过内置对象basicForm的getValues方法可以获取到一个form的完整json数据,但遗憾的事,这里取到的是dom的raw数据,类似emptyText的数据也会被返回,而Field的getValue方法不存在这个问题,所以如果想要返回一个非raw的json集合,可以给formpanel添加如下方法:
getJsonValue:function(){   
    
var param = '{';   
    
this.getForm().items.each(function(f){   
        
var tmp = '"' + f.getName() + '":"' + f.getValue() + '",';   
        param 
+=  tmp;   
    });   
    param 
= param.substr(0,param.length - 1+ '}';   
    
return param;   
}  

这个方法同样适用于上面定义的checkboxgroup,如此就可以把前后台的数据通过json统一起来了

posted @ 2009-03-04 12:50 杨一 阅读(2229) | 评论 (0)编辑 收藏

<2009年3月>
22232425262728
1234567
891011121314
15161718192021
22232425262728
2930311234

导航

公告

本人在blogjava上发表的文章及随笔除特别声明外均为原创或翻译,作品受知识产权法保护并被授权遵从 知识分享协议:署名-非商业性使用-相同方式共享 欢迎转载,请在转载时注明作者姓名(杨一)及出处(www.blogjava.net/yangyi)
/////////////////////////////////////////
我的访问者

常用链接

留言簿(5)

随笔分类(55)

随笔档案(55)

相册

Java

其他技术

生活

最新随笔

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜

自强不息


用心 - 珍惜时间,勇于创造