2008年10月19日
不止一次我们的项目在靠开发人员硬扛着,bug来不及修改,文档还在猛补,项目经理又在催着出版本,每修改一个bug都要在代码的泥沼中摸爬滚打半天,却又在制造着另外的bug,为了文档而制造着根本没有参考价值的文档。每发布一个版本都要瞻前顾后并且总要无休止的加班,似乎开发人员永远有干不完的活。
我们不禁问自己,为什么会有那么多失败的软件,软件开发我们到底还需要什么?
工具?我们不乏伟大的工具,IDE我们有eclipse、intelliJ、VC++等等,建模我们有Rose、together、Visio等等,配置管理我们Subversion、CVS、ClearCase等等,bug管理我们有ClearQuest、bugzilla等等,文档我们word、wps等,还有集成、测试甚至生成代码等等我们都有伟大的工具,工具我们不缺。流程?瀑布模型、迭代模型、UP、XP我们不缺流程管理的理论。知识?软件开发算是一项知识型的工作,我们的开发人员一般都是本科硕士毕业,况且搜索引擎如此发达的今天,知识根本不是问题,况且我们的软件开发需要多么高深的知识吗?
优秀的理论支持、有能力的人员、先进的工具,这些我们都具备,我们所缺的只是一种软件开发的理念,缺少软件开发的情商。我们一开始学习编程知识接触到的就是C语言和数据结构,慢慢的一些结构化的思想就扎根于大脑,其实在商业软件中数据结构、算法很少涉及,我们所需要的仅仅是一种设计、开发的理念。比如用面向对象本来是一种简单的思想,目的是为了降低软件的复杂性而出现的,可是让熟悉了结构化编程的人去搞反而觉得很难。一些好的实践经验我们也经常提到,比如模块化、松散耦合、面向接口编程、类应只关注本职工作等等开发设计理念以及规范命名、详尽使用的注释、清晰的结构等代码规范以及每日构建、有效沟通、配置管理、bug管理等一些管理理念,这些做起来都非常容易,关键是懒惰是人的本性,不知不觉中我们就会犯着大家都在重复的错误。如果在项目开工之初就充分贯彻这些优秀的理念,在项目进行中无论时间多紧都持之以恒,并且项目进行中不断的反思代码中的坏味道,一经发现立即重构,相信我们的开发过程会进入一个良性的循环中去,我们的开发人员将会体会到什么是快乐开发。
徐辛波,西安,从事软件开发设计工作,熟悉Java语言,爱好开发工作,特别是java相关的编程,业余关注开源项目,诚心结识志同道合之士组建开源团队,共同学习、进步、协作、为中国开源事业贡献微薄之力。
徐辛波 sinpo.xu@gmail.com
JDOM因其简洁易用易懂的API而被广泛的使用。JDOM常用的核心类及它们间的关系如下图所示:
Document代表了文档对象,抽象类Content表示文档中的内容元素,各种内容组成了文档对象。常用的内容元素有xml元素Element、xml注释Comment、文本Text。下面以如下片段来说明各类的含义。
<?xml version="1.0" encoding="UTF-8"?>
<customers>
<customer>
<name>徐辛波</name>
<occupation>developer</occupation>
<!-- comment:following is contact info -->
<contact>
<email>sinpo.xu@hotmail.com</email>
<mobile>15029357227</mobile>
<fix-phone>02985457683</fix-phone>
</contact>
</customer>
</customers>
上述文档用Document来抽象;customers为文档的根元素(root element ),Element即一个封闭起来的元素,element元素可以有子元素,如<mobile>15029357227</mobile>是一个元素,而<contact>...</contact>也是一个元素,甚至<customers>...</customers>也是一个大元素;<!-- ... -->代表了xml中注释,注释在JDOM中用Comment类来抽象;Text代表了xml中的文本值,如元素属性的值、元素的值、注释的内容等,父元素的Text为子元素和值组成的串,使用Text类可以方便的表示一些特殊字符,如:
Element element = new Element("name");
Text text = new Text("AAA.<、BBB/>.<CCC>");
element.addContent(text);
值得一提的是Element的方法addContent(Content content),因参数是抽象父类Content,所以可以添加Text、Element和Comment等,如果添加的是Text则自动作为element的文本值,如果是Element则作为element的子元素,如果是Comment则作为element的注释,使用十分方便。元素的值如<name>徐辛波</name>中的“徐辛波”也是一个和元素平行的Content对象(Text对象),当使用Element的getDescendants()方法时将返回一个该元素所有后代的迭代器,这些后代包括Element、Comment、Text等,如元素<contact>的后代包括email、mobile、fix-phone三个元素以及这三个元素的Text共6个后代,如果计算后代时有父子嵌套则应注意,父元素作为一个后代,其嵌套的子元素作为另一个后代。
刚才提到核心类都包含在org.jdom包下,jdom还包含了org.jdom.input和org.jdom.output两个包分别来处理xml内容的输入输出。当要读取xml资源时我们通常使用input包下的SAXBuilder类从输入流构建dom对象,当资源加载后常用的做法是在内存中缓存,这样后续的查找修改等操作就非常快。文档加载后内存的中各个元素是记录有各自的位置和关系的,即保持有上下文环境的。如果想要删除一段内容(Element Comment Text),只用调用该内容的detach方法即可,这样元素即和文档脱离关系了,再对文档进行遍历或者持久化到磁盘上时游离的元素就不可见了。Jdom的输出类包括XMLOutputter、DOMOutputter、SAXOutputter。最常用的是XMLOutputter,通过它可以将dom对象输出到指定的输出流,并且可以指定所输出xml文件的格式,比如缩进的样式等。DOMOutputter输出org.w3c.dom.Document对象,用于JDOM对象同w3c dom对象转换,SAXOutputter可以注册回调函数来处理相应的sax事件。
一下示例代码实现一个常用的读取配置文件并且允许更改后同步到磁盘的操作:
package sinpo.usagedemo;
import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.util.List;
import org.jdom.Document; import org.jdom.Element; import org.jdom.input.SAXBuilder; import org.jdom.output.Format; import org.jdom.output.XMLOutputter;
/** * 读取配置文件,并且修改后及时同步到磁盘 * @author 徐辛波(sinpo.xu@hotmail.com) * Oct 23, 2008 */ public class Configuration {
private Element root = null;
private Document dom = null;
private static final String resourceName = "/config.xml";
private static Configuration _INSTANCE = null;
public static synchronized Configuration getInstance() { if (_INSTANCE == null) { _INSTANCE = new Configuration(); }
return _INSTANCE; }
private Configuration() { load(); }
public String getConfig(String configName) { String configValue = null; Element found = findRecursively(configName, root); if (found != null) { configValue = found.getText(); } return configValue; }
public void updateConfig(String configName, String newValue) throws IOException { Element found = findRecursively(configName, root); if (found != null) { found.setText(newValue); } else { Element configNode = new Element(configName); configNode.addContent(newValue); // also: configNode.setText(newValue); root.addContent(configNode); } sync(); }
public void deleteConfig(String configName) throws IOException { Element found = findRecursively(configName, root); if (found != null) { found.detach(); } sync(); } private void load() { SAXBuilder builder = new SAXBuilder(); InputStream source = getClass().getResourceAsStream(resourceName); try { dom = builder.build(source); root = dom.getRootElement(); } catch (Exception e) { e.printStackTrace(); } }
// 递归查找. 在指定的父节点下查找叶子元素 private Element findRecursively(String name, Element parent) { Element found = null; List<Element> children = parent.getChildren(); if (children != null) { for (int i = 0; i < children.size(); i++) { Element element = children.get(i); String tmpName = element.getName(); if ((name.equals(tmpName)) && (!hasChild(element))) { return element; } }
for (int i = 0; i < children.size(); i++) { Element element = children.get(i); if (hasChild(element)) { found = findRecursively(name, element); if (found != null) { return found; } } } }
return found; }
private boolean hasChild(Element element) { boolean hasChild = false; List children = element.getChildren(); if ((children != null) && (children.size() > 0)) { hasChild = true; }
return hasChild; }
private void sync() throws IOException { Format format = Format.getPrettyFormat(); XMLOutputter outputter = new XMLOutputter(format); File file = null; URL url = getClass().getResource(resourceName); if (url == null) { file = new File(resourceName); } else { file = new File(url.getPath());
OutputStream out = null; try { out = new FileOutputStream(file); outputter.output(dom, out); out.close(); out = null; } catch (Exception e) { e.printStackTrace(); if (out != null) { out.close(); } } } } } |
关于线程间的交互和共享数据通常有轮询和通知机制。一下举例说明:Thread1和Thread2共享一块数据ShareData,Thread1使用数据,Thread2更新数据。当Thread1使用数据时发现数据没有更新就可以先休眠(sleep())一段时间然后再去判断是否更新,如此反复直到数据可用,这就是所述的轮询机制。可以看出轮询机制需要不断的轮询数据状态,很耗费资源;当采用通知机制时过程是这样的,Thread1发现数据不可用就在ShareData上等待(ShareData.wait()),当Thread2更新数据后就通知所有在ShareData上等待的线程(ShareData.notifyAll()),这样Thread1受到通知继续运行。
关于等待和休眠还有另一个区别就是当线程等待时,该线程锁定的资源是释放掉的,这时其它线程是可以锁定这些资源的,当线程被唤醒或者等待时限到时线程重新获取资源才能继续运行;而当线程休眠时线程锁定的资源是不被释放的。
还有一点就是要在对象lock上等待时是必须先要获取lock的对象锁才能进行的,即必须要类似下面的逻辑
synchronized(lock){ lock.wait()}
以下为一个简单的示例:
package
sinpo.usagedemo;
/**
* 该例子说明线程休眠与等待以及注意事项。
*
*
@author
徐辛波(sinpo.xu@hotmail.com)
* Oct 22, 2008
*/
public class
PendingThreadDemo
{
public
Console console =
new
Console
()
;
private
void
writeToConsole1
() {
synchronized
(
console
){
try
{
Thread.sleep
(
1
*
1000
)
;
//NOTE:sleep时并未释放console别的线程是不能锁定console的
//TODO do things
}
catch
(
InterruptedException e
) {
e.printStackTrace
()
;
}
}
}
private
void
writeToConsole2
() {
synchronized
(
console
){
try
{
console.wait
(
1
*
1000
)
;
//NOTE:wait时别的线程是可以锁定console的
//TODO do things
}
catch
(
InterruptedException e
) {
e.printStackTrace
()
;
}
}
}
}
//控制台类
class
Console
{
//TODO implements me
}
|
麻烦有谁知道的告知一下。。。
今天终于找到一个不错的工具,能将用eclipse等编辑的java代码做成一样风格的html格式,试用了挺好用的,谢谢
Java2Html团队。
这几天需要实现一个底层基于UDP的协议,该协议底层使用UDP传输但是具有拥塞控制、超时重发、数据确认等功能又比TCP简单 (RUDP,Reliable UDP)。在实现协议底层的UDP服务时准备使用Java的NIO,在网上查资料都是以TCP为例讲的,于是自己研究了一下基于UDP的NIO。
NIO的思路是基于多路选择的,即由原来的每个连接都由一个线程来等待消息,改为每个连接都在选择器上注册,由选择器来等待。当然NIO引入了很多新的概念,如Channel,Buffer、Charset、Selector等,使得编程更简洁、更面向对象化。
下面贴出用NIO API改造成UDP示例代码,注意其中使用Charset来编码解码的过程(当然Charset还支持很多其他编码不仅局限于默认编码)以及Buffer的使用。
package
sinpo.usagedemo;
import
java.net.DatagramSocket;
import
java.net.InetSocketAddress;
import
java.net.SocketAddress;
import
java.nio.ByteBuffer;
import
java.nio.CharBuffer;
import
java.nio.channels.DatagramChannel;
import
java.nio.channels.SelectionKey;
import
java.nio.channels.Selector;
import
java.nio.charset.Charset;
import
java.util.Iterator;
import
java.util.Set;
/**
*
@author
徐辛波(sinpo.xu@hotmail.com) Oct 19, 2008
*/
public class
UDPServer
extends
Thread
{
public
void
run
() {
Selector selector =
null
;
try
{
DatagramChannel channel = DatagramChannel.open
()
;
DatagramSocket socket = channel.socket
()
;
channel.configureBlocking
(
false
)
;
socket.bind
(
new
InetSocketAddress
(
5057
))
;
selector = Selector.open
()
;
channel.register
(
selector, SelectionKey.OP_READ
)
;
}
catch
(
Exception e
) {
e.printStackTrace
()
;
}
ByteBuffer byteBuffer = ByteBuffer.allocate
(
65536
)
;
while
(
true
) {
try
{
int
eventsCount = selector.select
()
;
if
(
eventsCount >
0
) {
Set selectedKeys = selector.selectedKeys
()
;
Iterator iterator = selectedKeys.iterator
()
;
while
(
iterator.hasNext
()) {
SelectionKey sk =
(
SelectionKey
)
iterator.next
()
;
iterator.remove
()
;
if
(
sk.isReadable
()) {
DatagramChannel datagramChannel =
(
DatagramChannel
)
sk
.channel
()
;
SocketAddress sa = datagramChannel
.receive
(
byteBuffer
)
;
byteBuffer.flip
()
;
// 测试:通过将收到的ByteBuffer首先通过缺省的编码解码成CharBuffer 再输出
CharBuffer charBuffer = Charset.defaultCharset
()
.decode
(
byteBuffer
)
;
System.out.println
(
"receive message:"
+ charBuffer.toString
())
;
byteBuffer.clear
()
;
String echo =
"This is the reply message from 服务器。"
;
ByteBuffer buffer = Charset.defaultCharset
()
.encode
(
echo
)
;
datagramChannel.write
(
buffer
)
;
}
}
}
}
catch
(
Exception e
) {
e.printStackTrace
()
;
}
}
}
public static
void
main
(
String
[]
args
) {
new
UDPServer
()
.start
()
;
}
}
|
Client
package
sinpo.usagedemo;
import
java.net.InetSocketAddress;
import
java.net.SocketAddress;
import
java.nio.ByteBuffer;
import
java.nio.channels.DatagramChannel;
import
java.nio.channels.SelectionKey;
import
java.nio.channels.Selector;
import
java.nio.charset.Charset;
import
java.util.Iterator;
import
java.util.Set;
/**
*
@author
徐辛波(sinpo.xu@hotmail.com)
* Oct 19, 2008
*/
public class
UDPClient
extends
Thread
{
public
void
run
() {
DatagramChannel channel =
null
;
Selector selector =
null
;
try
{
channel = DatagramChannel.open
()
;
channel.configureBlocking
(
false
)
;
SocketAddress sa =
new
InetSocketAddress
(
"localhost"
,
5057
)
;
channel.connect
(
sa
)
;
}
catch
(
Exception e
) {
e.printStackTrace
()
;
}
try
{
selector = Selector.open
()
;
channel.register
(
selector, SelectionKey.OP_READ
)
;
channel.write
(
Charset.defaultCharset
()
.encode
(
"Tell me your time"
))
;
}
catch
(
Exception e
) {
e.printStackTrace
()
;
}
ByteBuffer byteBuffer = ByteBuffer.allocate
(
100
)
;
while
(
true
) {
try
{
int
eventsCount = selector.select
()
;
if
(
eventsCount >
0
) {
Set selectedKeys = selector.selectedKeys
()
;
Iterator iterator = selectedKeys.iterator
()
;
while
(
iterator.hasNext
()) {
SelectionKey sk =
(
SelectionKey
)
iterator.next
()
;
iterator.remove
()
;
if
(
sk.isReadable
()) {
DatagramChannel datagramChannel =
(
DatagramChannel
)
sk
.channel
()
;
datagramChannel.read
(
byteBuffer
)
;
byteBuffer.flip
()
;
//TODO 将报文转化为RUDP消息并调用RUDP协议处理器来处理
System.out.println
(
Charset.defaultCharset
()
.decode
(
byteBuffer
)
.toString
())
;
byteBuffer.clear
()
;
datagramChannel.write
(
Charset.defaultCharset
()
.encode
(
"Tell me your time"
))
;
}
}
}
}
catch
(
Exception e
) {
e.printStackTrace
()
;
}
}
}
}
|
关于读取资源文件(如文本文件、图像、二进制文件等),一般不推荐直接给出操作系统的路径,而是给出相对于当前类的相对路径,这样就可以使用类的装载器来装载资源文件。常用的方法有:
Class类的getResourceAsStream(String resourcePath);
ClassLoader类的getResourceAsStream(String resourcePath)
Class类的该方法最终还是委派给ClassLoader的getResourceAsStream方法,但是使用中发现Class#getResourceAsStream()使用的是绝对路径(以/开头),而ClassLoader#getResourceAsStream()使用的相对路径。
propterty文件经常放在类路径的根路径下(最顶层包的上层目录,如classes),这样加载property文件时就可以先用Class#getResourceAsStream方法获取输入源,再从该输入源load各entry。
code piece:
package sinpo.usagedemo;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;
import junit.framework.TestCase;
/**
* @author 徐辛波(sinpo.xu@hotmail.com)
* Oct 19, 2008
*/
public class LoadResource extends TestCase {
public void test() throws Exception {
//usage 1: use absolute path (mostly used)
InputStream in1 = this.getClass().getResourceAsStream("/sinpo/test2.properties");
//usage 2: use relative path
InputStream in2 = this.getClass().getClassLoader().getResourceAsStream("sinpo/test2.properties");
//usage 3: use system class path
InputStream in3 = ClassLoader.getSystemResourceAsStream("system.properties");
//将读取的资源作为Properties的输入源
Properties props = new Properties();
props.load(in1);
String propValue = props.getProperty("propKey");
System.out.println(propValue);
//将读取的资源作为文本输出
InputStreamReader reader = new InputStreamReader(in1);
BufferedReader bReader = new BufferedReader(reader);
String content = bReader.readLine();
//输出第一行内容
System.out.println(content);
//TODO close them
}
} |