#
在用strust2.1.6做小项目,结果居然发现在post数据的时候,居然有乱码。
自认为对编码也算了解,立马check应用的content type,struts2配置的struts.locale,struts.i18n.encoding,没错,都是统一使用了UTF-8。
那是为什么呢?没办法,只能debug应用,结果发现:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
prepare.createActionContext(request, response);
prepare.assignDispatcherToThread();
prepare.setEncodingAndLocale(request, response);
request = prepare.wrapRequest(request);
ActionMapping mapping = prepare.findActionMapping(request, response);
if (mapping == null) {
boolean handled = execute.executeStaticResourceRequest(request, response);
if (!handled) {
chain.doFilter(request, response);
}
} else {
execute.executeAction(request, response, mapping);
}
} finally {
prepare.cleanupRequest(request);
}
}
看到没?
1)
prepare.createActionContext(request, response);
2) prepare.setEncodingAndLocale(request, response);
setEncodingAndLocale居然在createActionContext之后,在没有设置正确的encoding之前,解析request中的parameters,能成吗?
无奈之下,只能暂时用CharacterEncodingFilter这个filter设置request的character,猥琐地临时解决问题。
今天打算向Struts提交bug的时候,发现该bug在2.1.7版本中被修复,详见:https://issues.apache.org/struts/browse/WW-3075%3Bjsessionid=3EAC5B44A949CA77B4471AA0D45754E9?page=com.atlassian.jira.plugin.ext.subversion%3Asubversion-commits-tabpanel
哎,在使用2.1.7之前,先用CharacterEncodingFilter吧 :)
在SOA架构中,JMS协议中的“点对点”消息方式,已经能够很好的支持1对1系统之间的通讯;
但是,类似事件消息的通知(应用产生一个事件消息,其他多个系统做相应处理),理论上JMS协议中的“订阅”方式,能够支持此类场景,不过协议说:在消息产生通知订阅者的时候,如果某个订阅者系统不在线,则消息丢失--此订阅者接受不到消息。
并且,始终抱着对客户端简单,友好的态度,我希望client(应用)本身只要发出事件消息,并不需要去关注消息通知哪些订阅者,而这一切,应该由“事件消息通知系统”,代为完成。
经过昨天晚上空闲时间的思考,大概设计了“事件消息通知系统”的概念模型。(此概念模型,基于IP,路由,DNS思考得来)
详见下图:
概念解释:
1)Application:业务应用,一旦有事件消息产生,不再关注需要发送到哪些目的地,只需要统一发送到Event Message Center;
2)Event Message Center:一切事件消息的暂存地;
3)Event Message Router:事件消息路由器,根据Event Message Configuration Center(事件消息配置中心,即事件消息路由配置中心),将不同的消息,路由分发到不同的订阅者目的地;
4)Event Message Configuration Center:事件消息配置中心,即事件消息路由配置中心;
5)Event Message Registrar:事件消息登记中心,通过GUI界面,将Event Message Configuration Center中的路由信息展现给用户,并且允许用户进行事件消息路由信息的配置;
6)Event Message Subscription:事件消息订阅中心,同一类事件消息的暂存地;
7)Event Message Subscription Distributer:事件消息订阅分发者,根据Event Message Subscription Configuration Center(事件消息订阅配置中心)的配置信息,讲事件消息分发到不同的订阅者目的地;
8)Event Message Subscription Configuration Center:事件消息订阅配置中心,即事件消息订阅分发路由信息配置;
9)Event Message Subscription Registrar:事件消息订阅登记中心,通过GUI界面,将 Event Message Subscription Configuration Center中的分发路由信息展现给用户,并且允许用户进行事件消息订阅路由信息的配置;
10)Event Message Destination:事件消息目的地;
11)Event Message Consumer:不同订阅者的消费端
写下此随笔,仅仅把把自己对“事件消息系统”的感观认识记录。
由于思考和整理时间很短,此概念模型存在很多缺陷之处,还望大家多多指点。
最近是进入公司来,遇到的第三次疯狂加班时期,记录如下:
04/09 -- 凌晨1点;
04/10 -- 通宵;
04/11 -- 晚上23点;
04/12 -- 凌晨12点;
04/13 -- 晚上22点;
04/14 -- 晚上23点;
04/15 -- 凌晨3点;
04/16 -- 晚上22点;
04/17 -- 凌晨3点;
04/18 -- 通宵到第二天12点;
没有参与项目前期设计工作,导致一些接口设计存在问题,项目上线后,疯狂暴露问题.
结果俺成了救火队员:
分析错误(救火之前还不清楚接口逻辑,通过分析错误日志,慢慢了解一些逻辑和实现)
做数据订正;
重新设计新接口实现;
晚上还要留下来监控任务执行情况--光靠SA监控已经不够了;
上一次连续通宵加班,是由于和美国Verisign合作,做域名项目,由于有时差,只能晚上干活,到也也心甘情愿;
而这次连续加班,完全是被不切实际的"Dead Line"项目整死的.
忍不住抱怨下 :(
摘要: 最近在做小需求的时候,需要用到目录树,特地写了一个基于java的实现。
由于需求原因,目前只实现了读部分的功能--如何将平面节点build成树。动态新增,删除等功能尚未实现。
目录结构概念:
Node:目录节点,具备节点属性信息
NodeStore:平面目录节点持久化接口,提供方法如下:
public List<T> findByType(String t...
阅读全文
Velocity在渲染页面的时候,提供了不同的EventHanlder,供开发者callback。
本文简要说明下,在Velocity1.6.1版本下,不同EventHanlder的作用:
EventHandler(接口):
Base interface for all event handlers
仅仅是一个事件侦听标志
IncludeEventHandler(接口):
Event handler for include type directives (e.g.
#include()
,
#parse()
)
Allows the developer to modify the path of the resource returned.
在使用#include(),#parse()语法的时候,允许开发修改include或者parse文件的路径(一般用于资源找不到的情况)
IncludeNotFound(IncludeEventHandler实现类):
Simple event handler that checks to see if an included page is available.
If not, it includes a designated replacement page instead.By default, the name of the replacement page is "notfound.vm", however this
page name can be changed by setting the Velocity property
eventhandler.include.notfound
, for example:
eventhandler.include.notfound = error.vm
当使用#include(),#parse()语法的时候,如果提供的资源文件找不到,则默认使用notfound.vm模板代替。
开发者可以通过设置eventhandler.include.notfound
属性,修改替代模板的路径。
IncludeRelativePath(IncludeEventHandler实现类):
Event handler that looks for included files relative to the path of the
current template. The handler assumes that paths are separated by a forward
slash "/" or backwards slash "\".
使用相对路径方式,寻找#include或者#parse()中指定的资源文件
InvalidReferenceEventHandler(接口):
Event handler called when an invalid reference is encountered. Allows
the application to report errors or substitute return values
当渲染页面的时候,一旦遇到非法的reference,就会触发此事件。开发者可以侦听此事件,用于错误的报告,或者修改返回的内容。
ReportInvalidReferences(InvalidReferenceEventHandler实现类):
Use this event handler to flag invalid references.
使用这个实现类用于标志非法的references。修改eventhandler.invalidreference.exception属性,可以在捕捉到第一个非法references的时候,停止模板的渲染。
MethodExceptionEventHandler(接口):
Event handler called when a method throws an exception. This gives the
application a chance to deal with it and either
return something nice, or throw.
Please return what you want rendered into the output stream.
渲染模板,一旦发现调用的方法抛出异常的时候,就会触发此事件。允许开发者处理这个异常,输出友好信息或者抛出异常。必须返回一个值用于模板的渲染。
PrintExceptions(MethodExceptionEventHandler实现类):
Simple event handler that renders method exceptions in the page
rather than throwing the exception. Useful for debugging.
By default this event handler renders the exception name only.
To include both the exception name and the message, set the property
eventhandler.methodexception.message
to true
. To render
the stack trace, set the property eventhandler.methodexception.stacktrace
to true
.
模板渲染时,遇到方法异常,输出异常名,而不是抛出这个异常。对于调式,非常有帮助。
通过eventhandler.methodexception.message
和eventhandler.methodexception.stacktrace
属性的设置,可以输出异常message和stacktrace.
NullSetEventHandler(接口):
Event handler called when the RHS of #set is null. Lets an app approve / veto
writing a log message based on the specific reference.
当使用#set()语法,设置一个null值的时候,会触发此事件。--目前Velocity官方没有提供默认实现。
ReferenceInsertionEventHandler(接口):
Reference 'Stream insertion' event handler. Called with object
that will be inserted into stream via value.toString().
Please return an Object that will toString() nicely
当渲染变量(reference)的时候,就会触发此事件。允许开发者返回更加友好的值--一般用于内容的escape,比如HtmlEscape等。
EscapeHtmlReference(ReferenceInsertionEventHandler实现类):
html escape
EscapeJavaScriptReference(ReferenceInsertionEventHandler实现类):
javascript escape
EscapeSqlReference(ReferenceInsertionEventHandler实现类):
sql escape
EscapeXmlReference(ReferenceInsertionEventHandler实现类):
xml escape
以上是Velocity组件中提供的EventHandler介绍。下面写一个简单的例子来说明EventHandler的使用。
模拟需求,假如输出的内容带有html标签,而输出的内容需要过滤这些标签。如果我们手工对输出变量通过StringEscapeUtils.escapeHtml()来实现,则太过繁琐。所以,我们就可以使用Velocity中的EscapeHtmlReference。demo代码如下:
VelocityEngine ve = new VelocityEngine();
EventCartridge eventCartridge = new EventCartridge();
eventCartridge.addEventHandler(new EscapeHtmlReference());
Context context = new VelocityContext();
context.put("name", "<table></table>");
eventCartridge.attachToContext(context);
StringWriter writer = new StringWriter();
ve.mergeTemplate(VM_LOCATION, "utf-8", context, writer);
System.out.println("================================");
System.out.println(writer.toString());
System.out.println("================================");
模板文件中,仅仅为 $name
则输出内容如下:
================================
<table></table>
================================
很多应用中,数据库表结构都会存在一些状态字段。在关系性数据库中,一般会用VARCHAR类型。使用ibatis的应用,传统做法,往往会使用String的属性,与之对应。
例如一张member表,结构设计如下:
其中status为状态字段。
ibatis中,使用class MemberPO 与之mapping,设计往往如下:
public class MemberPO implements Serializable {
private Integer id;
private String loginId;
private String password;
private String name;
private String profile;
private Date gmtCreated;
private Date gmtModified;
private String status;
//getter/setters
}
缺点:
1)不直观,没人会知道status具体有哪些值。在缺乏文档,并且历史悠久的系统中,只能使用“select distinct(status) from member”,才能得到想要的数据。如果是在千万级数据中,代价太大了;
2)类型不安全,如果有人不小心拼写错误,将会导致错误状态。假设上面列子中,status只允许ENABLED/DISABLED,如果一不小心,memberPO.setStatus("ENABLEDD"),那么将会造成脏数据。
既然jdk5之后,引入了enum,是否可以让ibatis支持enum类型呢?事实上,最新的ibatis版本,已经支持enum类型(本文使用的是2.3.4.726版本--mvn repsitory上最新的版本)。
以上代码可以修改成:
1)Status类:
public enum Status {
/** enabled */
ENABLED,
/** disabled */
DISABLED;
}
2)MemberPO类:
public class MemberPO implements Serializable {
private Integer id;
private String loginId;
private String password;
private String name;
private String profile;
private Date gmtCreated;
private Date gmtModified;
private Status status;
//getter/setters
}
除此之外,其他均无需改动。
为什么呢?ibatis如何知道VARCHAR/Enum的mapping呢?
看过ibatis源码的同学,知道,ibatis是通过jdbcType/javaType得到对应的TypeHandler做mapping处理的。ibatis有基本类型的TypeHandler,比如StringTypeHandler,IntegerTypeHandler等等。在最新版本中,为了支持enum,增加了一个EnumTypeHandler。
并且在TypeHandlerFactory中,加了对enum类型的判断,请看:
public TypeHandler getTypeHandler(Class type, String jdbcType) {
Map jdbcHandlerMap = (Map) typeHandlerMap.get(type);
TypeHandler handler = null;
if (jdbcHandlerMap != null) {
handler = (TypeHandler) jdbcHandlerMap.get(jdbcType);
if (handler == null) {
handler = (TypeHandler) jdbcHandlerMap.get(null);
}
}
if (handler == null && type != null && Enum.class.isAssignableFrom(type)) {
handler = new EnumTypeHandler(type);
}
return handler;
}
ibatis使用了取巧的方法,当取不到基本类型的handler时候,判断javaType是否是Enum类型--
Enum.class.isAssignableFrom(type),如果是,则使用 EnumTypeHandler进行mapping处理。
为什么说它取巧,原因是早期ibatis设计过程中,自定义的接口无法得到具体的java class type。故早期的ibatis中,要实现对enmu的支持,非常苦难。而新版本中,为了达到这个功能,作者直接修改了TypeHandlerFactory的实现,打了一个补丁,如下:
if (handler == null && type != null && Enum.class.isAssignableFrom(type)) {
handler = new EnumTypeHandler(type);
}
这个设计有悖于和早前的设计思想。早期,TypeHandler都是通过public void register(Class type, String jdbcType, TypeHandler handler)方式事先注册到factory中的,而这次,是在运行期,通过new方法动态得到
EnumTypeHandler。
当然,新版本ibatis能支持enum,已经是一件开心的事情了。
Status枚举类除了描述状态,就足够了吗?回想起很多应用,我是做web开发的,在view层(velocity,jsp,等),见多了类似这样的代码:
#if($member.getStatus()==Status.ENABLED)开通#elseif($member.getStatus()==Status.DISABLED)关闭#end
<select>
<option value="ENABLED" #if($member.getStatus()==Status.ENABLED) selected="selected"#end >开通</option>
<option value="DISABLED" #if($member.getStatus()==Status.DISABLED) selected="selected"#end >关闭</option>
</select>
web层需要多少个页面,就需要维护多少份这样的代码;以后每添加/删除一种状态,多个地方都需要修改,还要担心逻辑不一致。
而事实上,关于状态的信息描述,按照职责分,就应该由枚举类来维护:
1)制定一个接口,
EnumDescription.java
public interface EnumDescription {
public String getDescription();
}
2)写一个ResourceBundleUtil.java,通过Properties文件得到描述信息:
public class ResourceBundleUtil {
private ResourceBundle resourceBundle;
public ResourceBundleUtil(String resource) {
this.resourceBundle = ResourceBundle.getBundle(resource);
}
public ResourceBundleUtil(String resource, Locale locale) {
this.resourceBundle = ResourceBundle.getBundle(resource, locale);
}
public String getProperty(String key) {
return resourceBundle.getString(key);
}
}
3)Status等枚举类实现
EnumDescription:
public enum Status implements EnumDescription {
/** enabled */
ENABLED,
/** disabled */
DISABLED;
private static ResourceBundleUtil util = new ResourceBundleUtil(Status.class.getName());
public String getDescription() {
return util.getProperty(this.toString());
}
}
这样,有什么好处:
1)通过Properties文件,支持国际化。
2)描述信息统一由自己来维护,方便维护,并且显示层逻辑简化,如:
$member.getStatus().getDescription()
<select>
#foreach($status in $Status.values())
<option value="$status" #if($member.getStatus()==$status)selected="selected"#end >$status.getDescription()</option>
#end
</select>
##############################################################################
那么使用老版本ibatis的客户怎么办呢?就像我们公司使用ibatis 2.3.0,难道只能眼馋着?解决方案:
1)升级到最新版本。 :)
2)ibatis提供了TypeHandler/TypeHandlerCallback接口,针对每种枚举类型,写相应的TypeHandler/TypeHandlerCallback的接口实现即可--工作量大,重复的劳动力。
主要是早期ibatis TypeHandler无法得到javaType类型,无法从jdbc value转成对应的枚举。在我看来,TypeHandler是作mapping用的,它至少有权知道javaType。
3)实现伪枚举类型(允许继承)来实现状态类型安全,而抛弃jdk5的方式--不方便日后升级。
不知道大家是否还有更好的方案?
本文涉及演示代码如下:
演示代码
workspace file encoding:utf-8
build tool: maven
repository:spring/2.5.5;ibatis/2.3.4.726
如今web应用上,ajax技术是大行其道。
ajax框架层出不穷,prototype,dojo,jquery,mootools,dwr,buffalo,ext,yui,spry。。。
ajax框架的出现,在提升开发生产效率的同时,也让不少同学不明其内在原理,仅仅成为了某些框架的使用者。
(对于产品生产是好事,对于技术追求是坏事)
本文不涉及任何ajax框架的使用,本文仅通过一个模拟需求,在不使用任何ajax框架的前提下,以demo演示的方式,
向大家介绍ajax的原理以及应用场景。
ajax全称是:
Asynchronous JavaScript And XML。
其本意是,通过javascript技术(JavaScript),通过异步http请求方式(Asynchronous),得到XML文本内容(XML)之后,通过javascript技术局部刷新web页面内容。
从广义的概念看,只要符合“异步请求,局部刷新web页面”的技术,都可以成为ajax。
未必一定要使用javascript,一般情况下,大多数client端脚本代码都可以;返回内容也未必一定要是xml,目前json格式,更为流行。
如何异步请求内容呢?
以javascript代码作演示,如下:
function xmlhttpPost(url,func) {
var xmlHttpReq = false;
var self = this;
// Mozilla/Safari
if (window.XMLHttpRequest) {
self.xmlHttpReq = new XMLHttpRequest();
}
// IE
else if (window.ActiveXObject) {
self.xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
}
self.xmlHttpReq.open('POST', url, true);
self.xmlHttpReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
self.xmlHttpReq.onreadystatechange = function() {
if (self.xmlHttpReq.readyState == 4) {
func(self.xmlHttpReq.responseText);
}
}
self.xmlHttpReq.send(null);
}
参数一,url:表明异步请求的资源地址
参数二,func:表明请求结束后,采用什么函数对请求结果内容进行回调处理
其实,这一个js代码,就诠释了ajax的全部含义--异步请求资源,将得到的资源内容,使用指定的function进行处理。
所以,ajax很简单,大家千万别被如今层出不穷的ajax框架给吓怕了。要了解ajax的原理,就只要参看这段代码即可。
如今的一些框架,仅仅在此基础上,是封装了一些公用的函数,方便开发人员调用。(当然,说说简单,其实所谓的这些函数,大大方便了开发人员使用ajax技术。具体请参看ajax framework的官方介绍。)
特别说明:这个xmlhttpost方法改进了
simple-ajax。在原基础上,将回调方法作为参数传递。
解释了原理性的内容之后,接下来,以一个模拟的应用场景,demo说明ajax的使用,以及它的主要应用场景。
模拟场景:
目录选择,即当选择一个目录的时候,需要显示这个目录下的所有子目录。
首先,我们来虚拟一个目录结构,如下:
那么,要实现目录选择,有三个方式:
1)页面初始化的时候,服务端将所有的目录信息都put到页面中。
优点:选择操作简单,有了全部的目录信息,做选择操作,都可以使用js完成,无需和服务端进行交互
缺点:当目录信息很大的时候,比如有上万个节点,整个目录信息有1m左右大小,那么要渲染这个页面,估计得20秒左右(视网速)
并且,很可能用户仅仅只要选择有限的几个节点就可以,比如上万个节点中选择6-7个节点,那么浪费太大了;
2)页面初始化的时候,服务端将当前需要的节点信息put到页面上,一旦有选择操作,重新刷新页面。
优点:选择操作简单,对于节点信息,每次取需要的内容,不存在浪费现象
缺点:每次都要刷新整个页面,除节点信息外,其他不变的东西都需要重新从服务端取,增加无谓的消耗。
3)页面初始化的时候,服务端将当前需要的节点信息put到页面上,一旦有选择操作,只刷新节点相关的内容;
优点:每次只load需要的信息,局部刷新页面内容,不存在任何浪费现象
缺点:需要异步请求数据,每次请求都需要和服务器交互,选择操作稍显复杂(异步请求,局部刷新)
通过这三种方式做对比,发现ajax主要适用的场景如下:
1)整体内容量大(几百k,几m,甚至几十m),而页面只需要其中一小部分信息即可;
2)数据显示,只涉及一个页面中部分数据信息的变动;
特别说明:至于使用ajax性能如何,需要对1,3两个情况做性能测试,权衡使用。
针对第三种方案,
首先需要一个取节点资源的url,
演示代码中,为了演示方便,使用php语言,而非使用主要语言java;
tree_node.php
<?php
$id = $_GET['id'];
if("1" == $id) {
echo("{\"id\":1,\"parentId\":-1,\"name\":\"1-1\",\"children\":[{\"id\":2,\"name\":\"2-1\"},{\"id\":3,\"name\":\"2-2\"},{\"id\":4,\"name\":\"2-3\"}]}");
} else if("2" == $id) {
echo("{\"id\":2,\"parentId\":1,\"name\":\"2-1\",\"children\":[]}");
} else if("3" == $id) {
echo("{\"id\":3,\"parentId\":1,\"name\":\"2-2\",\"children\":[]}");
} else if("4" == $id) {
echo("{\"id\":4,\"parentId\":1,\"name\":\"2-3\",\"children\":[{\"id\":5,\"name\":\"3-1\"},{\"id\":6,\"name\":\"3-2\"}]}");
} else if("5" == $id) {
echo("{\"id\":5,\"parentId\":4,\"name\":\"3-1\",\"children\":[]}");
} else if("6" == $id) {
echo("{\"id\":6,\"parentId\":4,\"name\":\"3-2\",\"children\":[{\"id\":7,\"name\":\"4-1\"}]}");
} else if("7" == $id) {
echo("{\"id\":7,\"parentId\":6,\"name\":\"4-1\",\"children\":[]}");
} else {
echo("");
}
?>
该文件中,写死了目录结构(一般情况下,往往根据树对象,动态取得需要的节点)。
通过js,动态请求节点信息,部分刷新页面内容:
<script type="text/javascript">
//模拟需求js
var nodeSelect = function(text) {
var tree = toJsonObje(text);
var options = document.getElementById("tree").options;
options.length = 0;
options.add(new Option("请选择","-1"));
if(tree == null) {
return;
} else {
var children = tree.children;
for(i = 0; i < children.length; i++) {
var child = children[i];
options.add(new Option(child.name,child.id));
}
if(tree.parentId != "-1") {
options.add(new Option("上一级",tree.parentId));
}
}
document.getElementById("l").innerHTML = "当前位置:" + tree.name;
}
function nodeSelectAjax(id) {
var TREE_NODE_URL = "tree_node.php";
var url = TREE_NODE_URL + "?id=" + id;
xmlhttpPost(url,nodeSelect);
}
</script>
nodeSelectAjax,异步请求节点资源
nodeSelect,回调函数,根据请求信息,局部刷新页面
至于请求资源信息格式,任何方式都可以,只要client端能解析就行。
目前json格式,比较流行。
最后,附上java使用json库,生成json格式的方法:
JSONObject node = new JSONObject();
node.put("id", 1);
node.put("parentId", -1);
node.put("name", "1-1");
JSONArray children = new JSONArray();
JSONObject c1 = new JSONObject();
c1.put("id", 2);
c1.put("name", "2-1");
JSONObject c2 = new JSONObject();
c2.put("id", 3);
c2.put("name", "2-2");
children.put(c1);
children.put(c2);
node.put("children", children);
System.out.println(node.toString());
ajax demo
工程文件编码:utf-8
工程运行:http server with php supported
ubuntu firefox下测试通过
其他:
不知道是不是ie的bug,居然不支持 select.innerHTML = value的方式
只能通过select.options.add(new Option("content","value") 动态往select中添加选项。
万物之间都有联系,这句话一点都没错,一组数学公式恰好可以反应常见的几种部门合作情况。
假设:
1) 一部门按质量交付产品的权值为 1
2) 质量每提升一分,权值加0.1
3) 质量每降低一分,权值减0.1
4) 某公司产品线需要4个部门合作
结果:
情况A:每个部门,都按质量完成自己的工作,则
产品总体质量 = 1 * 1 * 1 * 1 = 1
符合公司产品质量要求
情况B:每个部门交付的产品,均只有要求的9成,则
产品总体质量 = 0.9 * 0.9 * 0.9 * 0.9 = 0.6561
产品质量仅仅为要求的6成,刚好达到及格线而已
每个部门完成9分,似乎并不是很差,但是整体产品,却只达到及格而已
情况C:每个部门交付的产品,均超质量1分,则
产品总体质量 = 1.1 * 1.1 * 1.1 * 1.1 = 1.4641
产品质量为要求的1.4641倍,优质产品.
每个部门多完成1.分,似乎额外工作量并不是很大,但是最终产品却能成为优质产品
情况D:两个部门交付的产品,只有要求的9成,另两个部门为了弥补产品的缺陷,努力做到超质量1分,则
产品总体质量 = 0.9 * 0.9 * 1.1 * 1.1 = 0.9801
和情况A同样的工作强度,结果还是没有达到产品质量要求
情况E:两个部门交付的产品,只有要求的8成,另两个部门为了弥补产品的缺陷,努力做到超质量2分,则
产品总体质量 = 0.8 * 0.8 * 1.2 * 1.2 = 0.9216
和合情D同样的工作强度,结果总体质量比情况D更差
总结:
一个公司,只要每个部门对工作懈怠一点,公司产品就会和产品要求差很多,部门越多,差距越大
一个公司,只要每个部门对工作要求更严格一点,公司产品质量却远远高于产品要求,部门越多,质量越高
一个公司,如果有部门A对工作不到位,与其让后续其他部门加强工作强度,弥补部门A的产品缺陷,还不如加强对部门A的教育和培训,让其交付符合质量要求的产品.
公司产品开发历经PD(产品设计规划部门),RA(需求分析),Developer(开发工程师),Test(测试部门)四个环节.
在自己经历的一些项目中,因为时间关系等原因,往往出现:
PD提交的需求逻辑流程有点问题;提交的demo不符合要求;RA UC中仅仅考虑主流程,遗忘一些分支流程;开发对原有代码不敢做重构,搭积木似的添加功能,埋地雷等等...几个环节下来,试问最终产品质量如何??
我仅仅是一名普通的程序员,原先在面对逻辑不完善,demo不符合等等情况,都试图通过自己最大的努力,来弥补这些缺陷,但是当明白上述公式之后,我越来越希望每个部门都能尽自己最大的努力,来交付高质量的产品.
一个成功的公司,不可能仪仗个人英雄,而是需要各个部门协同工作.
最近,在小组内部做了一次关于“单元测试”的分享。把自己两年来做单元测试遇到的问题和对单元测试的认识做了一次总结和讨论。
本文不会详细地讲述分享的内容,仅仅是ppt的大纲显示:
使用单元测试前提:
最小的成本,换来最大的收益
单元测试目的:
1)测试代码错误(?) -- 不是主要目的
2)便于重构时的测试
3)改善既有代码的设计
分享核心:
1)如何脱离“webx“--做隔离测试
2)dal/biz/web 层如何做单元测试
3)如何通过改善代码设计,更方便测试
dal层(数据库访问层)特点:
1)独立,逻辑单一,对表做操作
2)业务相对比较稳定
3)采用ibatis,写sql的方式
dal层测试方式:
1)压根儿就不需要测试
2)仅仅配置spring bean,通过日志打印的方式(无法达到自检)
3) 自检方式 -- AbstractTransactionalDataSourceSpringContextTests (高成本,不轻易使用)
需要权衡
biz层(业务层)测试方式--分BO和AO:
BO:(即所谓的Service/Manager)
AO:(一个UseCase对应的业务流)
隔离 + 设计 (主要通过代码演示--见附件)
单元测试的缺点:
专注于单一业务测试,衔接点容易出错
解决方案:
接口输入输出明确
集成测试
web层:
集成测试的入口
分享文档和演示代码 (ppt是在ubuntu下制作,可能效果并不是很好)
备注:
自己对单元测试了解也比较肤浅,欢迎一起探讨
使用ibatis,如果要更新表记录,一般常用的做法就是,查找出记录,然后修改部分字段,进行update操作.
以member表为例:
MemberDO member = memberDAO.findById(1);
member.setName("stone");
memberDAO.update(member);
这种是最常用的方法.不错,在很多应用场景下,这么干,完全没有问题.
但是(往往存在但是),如果member表中存在一个或者多个text(或者blob)字段.难道仅仅为了更新一个name字段,需要重新update那些本不需要更新的text/blob字段吗?
于是乎,人们又想出了一个办法,参数采用map,把需要更新的字段put到map中,
演示代码(省略ibatis的sqlmap文件):
Map<String,Object> map = new HashMap<String,Object>();
map.put("name","stone");
memberDAO.update(map);
没错,这种方法不错.需要更新哪些字段,只需要动态put到map中就可以.
但是,对于这种方法,需要调用更新的地方,需要手工维护数据库的字段名,如果在put的时候,一不小心拼错字段名,那么更新操作肯定和你预计的会有差别.
比如上面的代码:
Map<String,Object> map = new HashMap<String,Object>();
map.put("nama","stone");
memberDAO.update(map);
不小心把name拼成了nama,那么新的name字段就无法保存到数据库中.试想一下,任何需要更新字段的地方,都存在拼写错误的风险.
于是乎,人们又想到了参数类,比如就把MemberDO当成参数类:
MemberDO memberParam = new MemberDO();
memberParam.setName("stone");
memberDAO.update(memberParam);
sqlmap.xml如下:
update member
set gmt_modified = current_date
<dynamic>
<isNotNull property="loginId",prepend=",">
login_id = #loginId#
</isNotNull>
<isNotNull property="name",prepend=",">
name = #name#
</isNotNull>
</dynamic>
where id = #id#
这方法貌似不错,不会存在字段名拼写错误的风险.并且需要更新哪些字段,动态set一下就可以.
但是,如果要把某个字段设置为null,那怎么办?那没辙咯...(sqlmap中约定,只有不为null的时候,才更新).
那...那...那怎么办呢?
貌似只有Map才能满足需求嘛...因为sqlmap中有个
"isPropertyAvailable"和"isNull"属性支持.只要配合这两个属性,就能区分需要更新为null,还是不更新保持原字段内容.
sqlmap文件演示:
<isPropertyAvailable property="loginId" prepend=",">
<isNotNull property="loginId">
<![CDATA[
login_id = #loginId#
]]>
</isNotNull>
<isNull property="loginId">
<![CDATA[
login_id = null
]]>
</isNull>
</isPropertyAvailable>
只要map不put loginId,那么更新的时候,就不会更新这个字段,如果map.put("loginId",null),那么就会把loginId更新为null.
看来只有map能胜任.
不是说,使用map,维护字段内容很麻烦嘛.但是好像又只能使用它?
于是乎,又想到了一种思路(也是本文要介绍的一个方法)
通过方法拦截,在设置参数类的时候,把设置的属性值put到map中.(cglib是很胜任这样的场合的)
首先,需要一个BaseDO.java DataObject的基类,仅仅用于维护一份Map对象.
BaseDO.java:
public class BaseDO implements Serializable {
private static final long serialVersionUID = -315506079592557582L;
private Map<String, Object> setterMap;
public synchronized void initSetterMap() {
if (setterMap == null) {
setterMap = new HashMap<String, Object>();
}
}
public Map<String, Object> getSetterMap() {
return setterMap;
}
}
采用Cglib,写一个对set方法的拦截器:
SetterInterceptor.java 用于对截获set操作,把set的对象put到map中
public class SetterInterceptor implements MethodInterceptor {
private static final String SET_METHOD = "set";
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
// 拦截DataObject中所有的set方法,把set的属性放入到map中
if (method.getName().startsWith(SET_METHOD)) {
if (obj instanceof BaseDO) {
BaseDO baseDO = (BaseDO) obj;
baseDO.initSetterMap();
String attribute = StringUtils.substring(method.getName(),
SET_METHOD.length());
attribute = StringUtils.uncapitalize(attribute);
if (args != null && args.length == 1) {
baseDO.getSetterMap().put(attribute, args[0]);
}
}
}
return proxy.invokeSuper(obj, args);
}
}
写一个创建Setter的工厂类,用于创建带方法拦截的DataObject对象
public class SetterFactory {
private static final SetterInterceptor setterInterceptor = new SetterInterceptor();
@SuppressWarnings("unchecked")
public static <T extends BaseDO> T getSetterInstance(Class<T> clazz) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clazz);
enhancer.setCallback(setterInterceptor);
return (T) enhancer.create();
}
}
那么对于client调用,就非常简单了.
如:
public class Client {
private static final Log log = LogFactory.getLog(Client.class);
private static final String APP_CONFIG_FILE = "cn/zeroall/javalab/ibatis/app.xml";
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext(
APP_CONFIG_FILE);
MemberDAO memberDAO = (MemberDAO) ctx.getBean("memberDAO");
MemberDO setter = SetterFactory.getSetterInstance(MemberDO.class);
setter.setId(1);
setter.setLoginId("stone1");
setter.setName("stone1");
memberDAO.updateById(setter);
MemberDO member = memberDAO.findById(1);
log.info(member.getLoginId());
}
}
sqlmap文件如下:
<update id="update-by-id" parameterClass="java.util.Map">
<![CDATA[
update member
set gmt_modified = current_date
]]>
<dynamic>
<isPropertyAvailable property="loginId" prepend=",">
<isNotNull property="loginId">
<![CDATA[
login_id = #loginId#
]]>
</isNotNull>
<isNull property="loginId">
<![CDATA[
login_id = null
]]>
</isNull>
</isPropertyAvailable>
<isPropertyAvailable property="password" prepend=",">
<isNotNull property="password">
<![CDATA[
password = #password#
]]>
</isNotNull>
<isNull property="password">
<![CDATA[
password = null
]]>
</isNull>
</isPropertyAvailable>
<isPropertyAvailable property="name" prepend=",">
<isNotNull property="name">
<![CDATA[
name = #name#
]]>
</isNotNull>
<isNull property="name">
<![CDATA[
name = null
]]>
</isNull>
</isPropertyAvailable>
<isPropertyAvailable property="profile" prepend=",">
<isNotNull property="profile">
<![CDATA[
profile = #profile#
]]>
</isNotNull>
<isNull property="profile">
<![CDATA[
profile = null
]]>
</isNull>
</isPropertyAvailable>
</dynamic>
<![CDATA[
where id = #id#
]]>
</update>
一旦采用了Setter对象,那么对于表记录的更新操作,仅仅需要一个sql,就能解决.比较方便.
附件中,把整个演示代码附上,有兴趣的朋友,可以了解下:
采用maven构建,workspace编码采用utf-8.数据库采用pgsql
demo附件
备注:
member表创建sql如下:
-- Table: member
-- DROP TABLE member;
CREATE TABLE member
(
id serial NOT NULL,
login_id character varying(16),
"password" character varying(16),
"name" character varying(32),
profile text,
gmt_created timestamp without time zone,
gmt_modified timestamp without time zone,
CONSTRAINT member_pkey PRIMARY KEY (id)
)
WITH (OIDS=FALSE);
ALTER TABLE member OWNER TO javalab;
特别说明:
此方法原创作者为公司同事,本文只是盗用了他的创意.