[转载]实用的不间断滚动图效果,具备良好兼容

mikel阅读(869)

[转载][分享] 实用的不间断滚动图效果,具备良好兼容; – 风帆 – 博客园.

网上有不少图片滚动的效果代码,但大多兼容性不好,或者结构混 乱,xhtml,css,js 均没有得到良好的优化,下面介绍一个方法;基本上实现结构跟行为相分离,两者的联系只需要一个
id就可以,使用起 来很方便,而且同一个js代码可以实现在本页面多个滚动图效果,之间互不干扰,

1.xhtml

<div class="demo" id="demo1"> <table cellpadding="0" cellspacing="0"> <tr> <td><ul> <li><img src="img.jpg"><br> 滚动图1</li> <li><img src="img.jpg"><br> 滚动图2</li> <li><img src="img.jpg"><br> 滚动图3</li> <li><img src="img.jpg"><br> 滚动图4</li> </ul></td> </tr> </table> </div>
<!--第二个滚动图开始--> <div class="demo" id="demo2"> <table cellpadding="0" cellspacing="0"> <tr> <td><ul> <li><img src="img.jpg"><br> 滚动图1</li> <li><img src="img.jpg"><br> 滚动图2</li> <li><img src="img.jpg"><br> 滚动图3</li> <li><img src="img.jpg"><br> 滚动图4</li> </ul></td> </tr> </table> </div>

2.css

ul,li,img,td{font-size:12px;list-style-type:none;text-align:center;margin:0;padding:0;} .demo{width:230px;margin-bottom:8px;height:172px;overflow:hidden;} .demo ul{width:408px;clear:both;} .demo li{width:102px;float:left;text-align:center;} .demo img{margin-bottom:8px;}

3.js

function startmarquee(lh,speed,delay,index){ var o=document.getElementById("demo"+index); var o_td=o.getElementsByTagName("td")[0]; var str=o_td.innerHTML; var newtd=document.createElement("td"); newtd.innerHTML=str; o_td.parentNode.appendChild(newtd); var t; var p=false; o.onmouseover=function(){p=true;} o.onmouseout=function() {p=false;} function start(){ t=setInterval(Marquee,speed); if(!p){o.scrollLeft += 3;} } function Marquee(){ if(o_td.offsetWidth-o.scrollLeft<=0) o.scrollLeft-=o_td.offsetWidth; else{ if(o.scrollLeft%lh!=0){ o.scrollLeft+= 3 }else{clearInterval(t); setTimeout(start,delay);} } } setTimeout(start,delay); } startmarquee(102,1,1500,1);//图片间停式滚动 startmarquee(102,30,1,2);//图片不间断滚动

测试代码:js需要在html文件加载完成后执行,才能正常运行,所以记 得将js放到div之后;图片滚动跟文字滚动实现原理上大同小异,关于此篇更多js代码的解释,请参看我的另一篇博客

浅析文字滚动效果

[转载]CLR类加载简述与OSGi.NET插件平台类加载机制简述

mikel阅读(1071)

[转载]CLR类加载简述与OSGi.NET插件平台类加载机制简述 – 道法自然 – 博客园.

一个插件平台除了需要考虑插件的结构、插件位置、插件类型空间、插件依赖、插件通讯、插件多版本支持、插件国际化等插件所需的基本要 素之外,还需要考虑一个开发人员如何开发、调试和部署插件。本文简要描述了插件类型空间相关的知识——CLR Loader、CLR Loader VS Java ClassLoader和插件的类型空间及类加载机制实现。
1 CLR Loader
关 于CLR加载器的详细描述可以查看《Essential .NET, Volume 1: The Common Language Runtime》,也可以下载我翻译的关于CLR部分的文档“CLR加载器/Files/baihmpgy/CRL加载器.rar”。在这里我将描述一下书中没有说明的关于加载器的一 些信息,这些信息包括:(1)何时触发类加载;(2)LoadFile的妙用及缺陷;(3)AppDomain.AssemblyResolve和 AssemblyLoad事件。
在我目前认知情况下,我知道类加载发生在:A)CLR执行线程进入 一个方法之前会触发JIT对方法进行编译并加载这个方法所需要的类型;B)调用Type.GetType等系统方法也会触发类型加载。为了让读者更好了解 到类加载时机,我编写了一个Sample。该Sample的目录结构如下:
ClassLoaderTesting.exe
ClassLibrary1.dll
ClassLibrary2.dll
lib<directory>
—CL3<directory>
——–ClassLibrary.dll
你 可以点击此处/Files/baihmpgy/ClassLoaderTesting.rar下载Sample源码,在 Program.cs的Main方法你可以看到类加载实验,AssemblyLoad和AssemblyResolve事件发生的时机。
LoadFile 和LoadFrome的区别在于LoadFile每次加载一个程序集的时候,不管目前AppDomain是否已经加载了该程序集,它都会加载一次。此 外,LoadFile不会加载目标程序集引用的程序集。它为我们提供了加载多版本程序集支持且可以做到按需加载一个程序集依赖的所有程序集。当然,目前而 言,我所知的LoadFile的一个最大缺点,就是每次加载一个程序集后,它会Lock住程序集文件。这个缺点在支持插件调试时非常突出。
AssemblyResolve 事件在CLR类加载器无法加载到所需类型时触发,它赋予了我们自定义类型加载的扩展,相反AssemblyLoad事件则发生在程序集被加载时。你可以通 过调试ClassLoaderTesting这个项目来检查类型何时加载以及这两个事件何事触发。
2 CLR Loader Vs Java ClassLoader
这 个问题比较古老了,已经有人比较过CLR Loader和Java ClassLoader。不过,要想公平公正的进行比较还真不容易,这就像公正比较Java和C#,J2EE和.NET一样。前一段时间,就有很多人在批 Java。在比较CLR Loader和Java ClassLoader之前,先说几句废话。我先声明一下,我是一个C#程序员,也接触过Java。对Java类加载机制、Flex类加载机制有一定的实 践。我体会到一些.NET平台相对于Java平台所不具备的东西,而这些差距除了.NET只支持Windows和Linux之外,还有就是.NET缺少的 开放规范和缺少灵活的类加载机制。众所周知,Java有一堆的规范,比如JDBC、JMS、JMX、JNDI、JTX等等,当然,也有基于这些规范的各种 Providers,基于Java平台我们可以有很多挑选的余地,并且可以从这些高端的开放规范中学习到很多的知识,.NET平台就很少有开放规范了,如 果你非得说C#语言规范的话,那也算,:)。那些标榜“C#比Java绝对好”结论相关的文章我一般都懒得回复,因为这没有任何意义,我只想根据实际需要 选择更好的,而不是片面的排斥。要知道,现在用Java的程序员可是C#的几倍,而且Java已经被认为是云计算的标准开发语言(插一句,我不幸的是用 C#构建了一个SaaS引擎,深受客户的质疑),存在即合理。
言归正传,我将在 http://blog.joycode.com/junfeng/archive/2004/04/10/18901.joy这篇文章之上谈一下二者。 在Java,类是通过ClassLoader和类型全程来标识的,这意味着一个ClassLoader不能加载重复的类型,但是两个 ClassLoader可以加载同一个名称的类型。不过这时候,被加载的这两个类型创建的实例就是隶属于不同类型了。据我说知,Java类加载触发有两种 情况:A)JVM执行过程中;B)Class.forName。JVM在执行字节码的过程中,碰到一个未知类型后,会从当前线程获取对应的 ClassLoader,然后使用ClassLoader加载类型。ClassLoader加载类型采用一种层级方式,它首先先调用父类加载器进行加载, 如果加载不到才使用当前线程的ClassLoader进行加载。我们可以通过重写ClassLoader对JVM类型加载进行扩展。著名的Eclipse 的OSGi内核——Equniox就是通过重写ClassLoader自定义了一个插件类型空间的类型加载器。Class.forName则是显式调用当 前线程类加载器加载类型的方法了。很多Java组件框架都是通过扩展ClassLoader来实现模块化技术的,比如JBoss和NetBeans。
CLR 加载器概念,我们接触的机会很少,相反,我们对Assembly.Load/LoadFrom/LoadFile、Type.GetType倒是经常碰到 了,你可以通过《Essential .NET, Volume 1: The Common Language Runtime》该书深入理解CLR加载器。和Java ClassLoader相比,它的缺点主要有:(1)扩展性差;(2)加载的类型卸载的唯一方式是随着AppDomain一起被卸载。扩展性差表现在,实 现CLR自定义类型加载只能通过AppDomain.AssemblyResolve来实现(还有一种方法,就是可以通过.config文件来配置类型加 载,但这种方法是静态的,我没有把它当成一种方法),Assembly.Load/LoadFrom/LoadFile这些方法并不能实现对CLR加载器 进行扩展,它们只是一些补充,原因很简单,在执行“Class3 cls = new Class3();”时,Class3只能是由CLR来加载的。第二个缺点,很明显,一个类型一旦被加载了,它是无法卸载的,因为在C#,我们在不使用低 性能和跨进程调用的AppDomain下是无法实现一个支持动态更新的插件平台。
我的结 论:Java ClassLoader简单优雅、功能强大、非常灵活,但对多版本不提供支持;CLR Loader非常简单、支持多版本、但不太灵活且扩展性稍差。
3 OSGi.NET BundleLoader
OSGi.NET 是OSGi规范移植到.NET的实现。OSGi是一个基于Java的动态模块化系统的规范。它提供了模块化与插件化、面向服务、安全与隔离和模块扩展支持 的功能。由于OSGi具有规范的模块化与插件化定义,且已经经过了Eclipse IDE考验,我们便制定了基于C#的OSGi.NET开放服务规范并基于C#开发实现。
在设计 OSGi.NET过程中,模块层花费了大量的设计和开发的时间。我从易用性、类型空间独立性、类加载性能等方面考了,设计了一个几乎和OSGi模块类加载 器一样优雅的插件类型加载机制。它类似OSGi实现了一个插件的独立类型空间,在OSGi.NET中,一个插件能加载的类型由插件与子插件本地程序集、插 件与子插件依赖程序集、插件动态依赖程序集组成,通俗的讲,一个插件只能从本地和依赖的程序集加载所需的类型。这种方式使得多版本程序集支持也变得可能。 OSGi.NET插件类型加载由BundleLoader即插件类加载器来实现,其实现原理和OSGi类似。
不 过,为了实现每一个插件独立的类型空间,我们需要对CLR加载器进行扩展,其方式如下:
1 AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(DomainAssemblyResolve);
2
3 Assembly DomainAssemblyResolve(object sender, ResolveEventArgs args)
4 {
5 string resolvingAsmName = args.Name;
6 AssemblyName name = new AssemblyName(resolvingAsmName); //获取程序集全名。
7 Assembly asm = SearchAssemblyFromAllBundles(name); //从所有Bundle的程序集中查找匹配的程序集并加载。
8
9 return asm;
10 }
到 目前为止我对OSGi.NET的插件化支持还算满意,但限于个人对Java ClassLoader、CLR Loader和OSGi等精深知识理解还不够深入,如有错误,欢迎指正。

Creative  Commons License

本文基于Creative Commons Attribution 2.5 China Mainland License发布,欢迎转载,演绎或用于商业目的,但是必 须保留本文的署名道法自然(包含链接)。如您有任何疑问或者授权方面的协商,请给我留言。

[转载]深入解析Hashtable、Dictionary、SortedDictionary、SortedList

mikel阅读(1215)

[转载]深入解析Hashtable、Dictionary、SortedDictionary、SortedList – 木子博客 – 博客园.

在《在线用户 实体缓存解决方案》方案中使用Dictionary来存储,评论里同事说SortedDictionary采用二分法查找比Dictionary快,于是我们都做了测试,最后发现 Dictionary是比SortedDictionary快的,前者 用的是Hash算法,而后者是RB-Tree算法

于是想深入地分析如题的4个字典的原理。

我们先看Hashtable

MSDN的解释:表示键/值对的集合,这些键/值对根据键的哈希代码进行组 织。

Hash算法是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不 同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。

Hashtable 对象由包含集合元素的存储桶组 成。存储桶是 Hashtable 中各元素的虚拟子组,与大多数集合中进行的搜索和检索相比,存储桶 可令搜索和检索更为便捷。每一存储桶都与一个哈希代码关联,该哈希代码是使用哈希函数生成的并基于该元素的键。

Hashtable 类默认的装填因子是 1.0,但实际上它默认的装填因子是 0.72。所有从构造函数输入的装填因子,Hashtable 类内部都会将其乘以0.72。这是一个要求苛刻的数字, 某些时刻将装填因子增减 0.01, 可能你的 Hashtable 存取效率就提高或降低了 50%,其原因是装填因子决定散列表容量,而散列表容量又影响 Key 的冲突几率,进而影响性能。0.72 是 Microsoft经过大量实验得出的一个比较平衡的值。

我们看Hashtable的一些源码:

Hashtable .ctor

public Hashtable() : this(0, (float) 1f)
{
}
public Hashtable(int capacity, float loadFactor)
{
if (capacity < 0)
{
throw new ArgumentOutOfRangeException(capacity, Environment.GetResourceString(ArgumentOutOfRange_NeedNonNegNum));
}
if ((loadFactor < 0.1f) || (loadFactor > 1f))
{
throw new ArgumentOutOfRangeException(loadFactor, Environment.GetResourceString(ArgumentOutOfRange_HashtableLoadFactor, new object[] { 0.1, 1.0 }));
}
this.loadFactor = 0.72f * loadFactor;
double num = ((float) capacity) / this.loadFactor;
if (num > 2147483647.0)
{
throw new ArgumentException(Environment.GetResourceString(Arg_HTCapacityOverflow));
}
int num2 = (num > 11.0) ? HashHelpers.GetPrime((int) num) : 11;
this.buckets = new bucket[num2];
this.loadsize = (int) (this.loadFactor * num2);
this.isWriterInProgress = false;
}

Hashtable 扩容是个耗时非常惊人的内部操作,它之所以写入效率仅为读取效率的 1/10 数量级,频繁的扩容是一个因素。当进行扩容时,散列表内部要重新 new 一个更大的数组,然后把原来数组的内容拷贝到新数组,并进行重新散列。如何 new这个更大的数组也有讲究。散列表的初始容量一般来讲是个素数。当扩容时,新数组的大小会设置成原数组双倍大小的相近的一个素数。

Hashtable expand

private void expand()
{
int prime = HashHelpers.GetPrime(this.buckets.Length * 2);
this.rehash(prime);
}
private void rehash(int newsize)
{
this.occupancy = 0;
Hashtable.bucket[] newBuckets
= new Hashtable.bucket[newsize];
for (int i = 0; i < this.buckets.Length; i++)
{
Hashtable.bucket bucket
= this.buckets[i];
if ((bucket.key != null) && (bucket.key != this.buckets))
{
this.putEntry(newBuckets, bucket.key, bucket.val, bucket.hash_coll & 0x7fffffff);
}
}
Thread.BeginCriticalRegion();
this.isWriterInProgress = true;
this.buckets = newBuckets;
this.loadsize = (int) (this.loadFactor * newsize);
this.UpdateVersion();
this.isWriterInProgress = false;
Thread.EndCriticalRegion();
}

HashTable 数据结构存在问题:空间利用率偏低、受填充因子影响大、扩容时所有的数据需要重新进行散列计算。虽然Hash具有O(1)的数据检索效率,但它空间开销却 通常很大,是以空间换取时间。所以Hashtable适用于读取操作频繁,写入操作很少的操作类型。

Dictionary<K, V> 也是用的Hash算法,通过数组实现多条链式结构。不过它是采用分离链接散列法。采用分离链接散列法不受到装填因子的影响,扩容时原有数据不需要重新进行 散列计算。

采用分离链接法的 Dictionary<TKey, TValue> 会在内部维护一个链表数组。对于这个链表数组 L0,L1,…,LM-1, 散列函数将告诉我们应当把元素 X 插入到链表的什么位置。然后在 find 操作时告诉我们哪一个表中包含了 X。 这种方法的思想在于:尽管搜索一个链表是线性操作,但如果表足够小,搜索非常快(事实也的确如此,同时这也是查找,插入,删除等操作并非总是 O(1) 的原因)。特别是,它不受装填因子的限制。
这种情况下,常见的装填因子是 1.0。更低的装填因子并不能明显的提高性能,但却需要更多的额外空间。

Dictionary .ctor

public Dictionary() : this(0, null)
{
}
public Dictionary(int capacity, IEqualityComparer<TKey> comparer)
{
if (capacity < 0)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
}
if (capacity > 0)
{
this.Initialize(capacity);
}
if (comparer == null)
{
comparer = EqualityComparer<TKey>.Default;
}
this.comparer = comparer;
}
private void Resize()
{
int prime = HashHelpers.GetPrime(this.count * 2);
int[] numArray = new int[prime];
for (int i = 0; i < numArray.Length; i++)
{
numArray[i] = 1;
}
Entry<TKey, TValue>[] destinationArray = new Entry<TKey, TValue>[prime];
Array.Copy(this.entries, 0, destinationArray, 0, this.count);
for (int j = 0; j < this.count; j++)
{
int index = destinationArray[j].hashCode % prime;
destinationArray[j].next = numArray[index];
numArray[index] = j;
}
this.buckets = numArray;
this.entries = destinationArray;
}

Dictionary 的插入算法:1、计算key的hash值,并且找到buckets中目标桶的链首索引,2、从链上依次查找是否key已经保存,3、如果没有的话,判断是 否存在freeList,4、如果存在freeList,从freeList上摘下结点保存数据,否则追加在count位置上。

Dictionary Add

private void Insert(TKey key, TValue value, bool add)
{
int freeList;
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (this.buckets == null)
{
this.Initialize(0);
}
int num = this.comparer.GetHashCode(key) & 0x7fffffff;
int index = num % this.buckets.Length;
for (int i = this.buckets[index]; i >= 0; i = this.entries[i].next)
{
if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
{
if (add)
{
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
}
this.entries[i].value = value;
this.version++;
return;
}
}
if (this.freeCount > 0)
{
freeList
= this.freeList;
this.freeList = this.entries[freeList].next;
this.freeCount;
}
else
{
if (this.count == this.entries.Length)
{
this.Resize();
index
= num % this.buckets.Length;
}
freeList
= this.count;
this.count++;
}
this.entries[freeList].hashCode = num;
this.entries[freeList].next = this.buckets[index];
this.entries[freeList].key = key;
this.entries[freeList].value = value;
this.buckets[index] = freeList;
this.version++;
}

buckets数组 保存所有数据链的链首,Buckets[i]表示在桶i中数据链的链首元素。entries结构体数组用于保存实际的数据,通过next值作为链式结构的 向后索引。删除的数据空间会被串入到freeList链表的首部,当再次插入数据时,会首先查找freeList链表,以提高查找entries中空闲数 据项位置的效率。在枚举器中,枚举顺序为entries数组的下标递增顺序。

Dictionary Remove

public bool Remove(TKey key)
{
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (this.buckets != null)
{
int num = this.comparer.GetHashCode(key) & 0x7fffffff;
int index = num % this.buckets.Length;
int num3 = 1;
for (int i = this.buckets[index]; i >= 0; i = this.entries[i].next)
{
if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
{
if (num3 < 0)
{
this.buckets[index] = this.entries[i].next;
}
else
{
this.entries[num3].next = this.entries[i].next;
}
this.entries[i].hashCode = 1;
this.entries[i].next = this.freeList;
this.entries[i].key = default(TKey);
this.entries[i].value = default(TValue);
this.freeList = i;
this.freeCount++;
this.version++;
return true;
}
num3 = i;
}
}
return false;
}

SortedDictionary,MSDN 是这样描述的:

SortedDictionary<(Of <(TKey, TValue>)>) 泛型类是检索运算复杂度为 O(log n) 的二叉搜索树,其中 n 是字典中的元素数。就这一点而言,它与 SortedList<(Of <(TKey, TValue>)>)  泛型类相似。这两个类具有相似的对象模型,并且都具有 O(log n) 的检索运算复杂度。这两个类的区别在于内存的使用以及插入和移除元素的速度:

  1. SortedList<(Of <(TKey, TValue>)>)  使用的内存比 SortedDictionary<(Of <(TKey, TValue>)>) 少。
  2. SortedDictionary<(Of <(TKey, TValue>)>) 可对未排序的数据执行更快的插入和移除操作:它的时间复杂度为 O(log n),而 SortedList<(Of <(TKey, TValue>)>) 为 O(n)。
  3. 如果使 用排序数据一次性填充列表,则 SortedList<(Of <(TKey, TValue>)>) 比 SortedDictionary<(Of <(TKey, TValue>)>) 快。

SortedDictionary<K, V>是按照K有序排列的(K, V)数据结构,以红黑树作为内部数据结构对K进行排列保存– TreeSet<T>,红黑树是一棵二叉搜索树,每个结点具有黑色或者红色的属性。它比普通的二叉搜索树拥有更好的平衡性。2-3-4树是红 黑树在“理论”上的数据结构。

2-3-4树插入算法:类似于二叉搜索树的插入(插入数据插入到树的叶子结点) ,如果插入位置是2-结点或者3-结点,那么直接插入到当前结点,如果插入位置是4-结点,需要将当前的4-结点进行拆分,然后再执行后继的插入操作。

SortedDictionary Add

public void Add(T item)
{
if (this.root == null)
{
this.root = new Node<T>(item, false);
this.count = 1;
}
else
{
Node
<T> root = this.root;
Node
<T> node = null;
Node
<T> grandParent = null;
Node
<T> greatGrandParent = null;
int num = 0;
while (root != null)
{
num
= this.comparer.Compare(item, root.Item);
if (num == 0)
{
this.root.IsRed = false;
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
}
if (TreeSet<T>.Is4Node(root))
{
TreeSet
<T>.Split4Node(root);
if (TreeSet<T>.IsRed(node))
{
this.InsertionBalance(root, ref node, grandParent, greatGrandParent);
}
}
greatGrandParent
= grandParent;
grandParent
= node;
node
= root;
root
= (num < 0) ? root.Left : root.Right;
}
Node
<T> current = new Node<T>(item);
if (num > 0)
{
node.Right
= current;
}
else
{
node.Left
= current;
}
if (node.IsRed)
{
this.InsertionBalance(current, ref node, grandParent, greatGrandParent);
}
this.root.IsRed = false;
this.count++;
this.version++;
}
}

我们来测试一下 Hashtable、Dictionary和SortedDictionary的插入和查找性能。

性能测试代码

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace DictionaryTest
{
class Program
{
private static int totalCount = 10000;

static void Main(string[] args)
{
HashtableTest();
DictionaryTest();
SortedDictionaryTest();
Console.ReadKey();
}

private static void HashtableTest()
{
Hashtable hastable
= new Hashtable();
Stopwatch watch
= new Stopwatch();
watch.Start();
for (int i = 1; i < totalCount; i++)
{
hastable.Add(i,
0);
}
watch.Stop();
Console.WriteLine(
string.Format(Hashtable添加{0}个元素耗时:{1}ms,totalCount, watch.ElapsedMilliseconds));
Console.WriteLine(
Hashtable不做查找测试);
hastable.Clear();
}

private static void DictionaryTest()
{
Dictionary
<int, int> dictionary = new Dictionary<int, int>();
Stopwatch watch
= new Stopwatch();
watch.Start();
for (int i = 1; i < totalCount; i++)
{
dictionary.Add(i,
0);
}
watch.Stop();
Console.WriteLine(
string.Format(Dictionary添加{0}个元素耗时:{1}ms,totalCount, watch.ElapsedMilliseconds));
watch.Reset();
watch.Start();
dictionary.Select(o
=> o.Key % 1000 == 0).ToList().ForEach(o => { });
watch.Stop();
Console.WriteLine(
string.Format(Dictionary查找能被1000整除的元素耗时:{0}ms, watch.ElapsedMilliseconds));
dictionary.Clear();
}

private static void SortedDictionaryTest()
{
SortedDictionary
<int, int> dictionary = new SortedDictionary<int, int>();
Stopwatch watch
= new Stopwatch();
watch.Start();
for (int i = 1; i < totalCount; i++)
{
dictionary.Add(i,
0);
}
watch.Stop();
Console.WriteLine(
string.Format(SortedDictionary添加{0}个元素耗时:{1}ms,totalCount, watch.ElapsedMilliseconds));
watch.Reset();
watch.Start();
dictionary.Select(o
=> o.Key % 1000 == 0).ToList().ForEach(o => { });
watch.Stop();
Console.WriteLine(
string.Format(SortedDictionary查找能被1000整除的元素耗时:{0}ms, watch.ElapsedMilliseconds));
dictionary.Clear();
}
}
}

最 终结果如图:

[转载]在线用户实体缓存解决方案

mikel阅读(1445)

[转载]在线用户实体缓存解决方案 – 木子博客 – 博客园.

随着网站访问量的增加,在线用户实体信息的存储方式变得重要起来。存储在线用户的信息一般有这三种方案:

1、用户的实体信息保存在Session里,简单方便,随着Session的过期用户信息自动过期。

2、用户信息保存在数据库中,用一个表存储在线的用户信息。

3、用户信息保存在内存。

当前项目用的是第一种方法,把用户的实体信息保存在Session中,虽然使用方便,但总感觉很别扭。Discuz!NT使用的是第二种方法,把在线用户 标识保存在一个表中,从cookie跟读取用户的ID,并从用户信息表查询该用户的信息,组装到实体中。如果有大量的用户在线同时操作时,这也不是一个很 好的解决办法。

这里选择第三种解决方案,把用户信息保存到内存。

我们使用Dictionary来存储用户信息,由于Dictionary不是线程安全的,因此需要注意只能单线程更新字典。

先定义一个保存用户信息的实体:

internal class UserEntity<T>
{
    /// <summary>
    /// 用户信息
    /// </summary>
    internal T UserInfo { get; set; }

    /// <summary>
    /// 添加到列表的时间戳
    /// </summary>
    internal DateTime Timestamp { get; set; }
}

使用泛型包装用户实体,并增加一个时间戳,表示该用户信息添加到内存的时间,过期是根据这个时间来判断的。

再增加一个在线用户信息管理类:

/// <summary>
/// 在线用户缓存管理
/// </summary>
/// <typeparam name="T"></typeparam>
public class UserCacheManager<T>
{
    #region 静态属性
    /// <summary>
    /// 静态用户缓存表
    /// </summary>
    private static Dictionary<long, UserEntity<T>> _UserList = new Dictionary<long, UserEntity<T>>();
    /// <summary>
    /// 过期时间
    /// </summary>
    private static int _ExpiredMinutes = 30;
    /// <summary>
    /// 定时器
    /// </summary>
    private static Timer _Timer = null;
    #endregion

    #region 静态构造函数
    /// <summary>
    /// 静态构造函数
    /// 初始化计时器
    /// </summary>
    static UserCacheManager()
    {
        _Timer = new Timer(new TimerCallback(TimerClear), null, 60000, _ExpiredMinutes * 60000);
    }
    #endregion

    #region 私有方法
    /// <summary>
    /// 清除在线用户
    /// </summary>
    /// <param name="sender"></param>
    private static void TimerClear(object sender)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncClear));
    }

    /// <summary>
    /// 异步清除过期的在线用户
    /// </summary>
    /// <param name="sender"></param>
    private static void AsyncClear(object sender)
    {
        //当前时间
        DateTime timestamp = DateTime.Now.AddMinutes(0 - _ExpiredMinutes);
        //过期的用户列表
        var expiredUserList =
            (from userEntity in _UserList where userEntity.Value.Timestamp <= timestamp select userEntity);
        if (expiredUserList != null && expiredUserList.Count() > 0)
        {
            List<long> expiredUserIdentities = expiredUserList.Select(o => o.Key).ToList();
            lock (_UserList)
            {
                foreach (long userId in expiredUserIdentities)
                    _UserList.Remove(userId);
            }
        }
    }
    #endregion

    #region 公共方法
    /// <summary>
    /// 增加在线用户
    /// </summary>
    /// <param name="userIdentity">用户身份标识</param>
    /// <param name="userInfo">用户实体</param>
    public static void Add(long userIdentity, T userInfo)
    {
        lock (_UserList)
        {
            #region 创建用户实体
            UserEntity<T> userEntity = new UserEntity<T>
            {
                Timestamp = DateTime.Now,
                UserInfo = userInfo
            };
            #endregion

            if (_UserList.Keys.Contains(userIdentity))
            {
                _UserList[userIdentity] = userEntity;
            }
            else
            {
                _UserList.Add(userIdentity, userEntity);
            }
        }
    }

    /// <summary>
    /// 获取用户信息
    /// </summary>
    /// <param name="userIdentity"></param>
    /// <returns></returns>
    public static T Get(long userIdentity)
    {
        lock (_UserList)
        {
            if (_UserList.Keys.Contains(userIdentity))
            {
                _UserList[userIdentity].Timestamp = DateTime.Now;
                return _UserList[userIdentity].UserInfo;
            }
            else
            {
                return default(T);
            }
        }
    }

    /// <summary>
    /// 移除用户缓存信息
    /// </summary>
    /// <param name="userIdentity"></param>
    public static void Remove(long userIdentity)
    {
        if (_UserList.Keys.Contains(userIdentity))
        {
            lock (_UserList)
            {
                _UserList[userIdentity].Timestamp = DateTime.Now.AddDays(-1);
            }
        }
    }
    #endregion
}

Dictionary<long, UserEntity<T>> _UserList用来保存在线的用户列表。

在静态构造函数中声明了一个定时器,定时器负责清理过期的用户信息。并把清理用户信息的方法装入线程池执行。

MSDN:只要不修改Dictionary,Dictionary就可以同时支持多个阅读器。即便如此,从头到尾对一个集合进行枚举本质上并不是一个线程 安全的过程。当出现枚举与写访问互相争用这种极少发生的情况时,必须在整个枚举过程中锁定集合。若允许多个线程对集合执行读写操作,您必须实现自己的同 步。

所以在更新Dictionary中,都锁定了字典,防止多线程冲突。

源代码在经过富文本编辑器后显示有点问题,感兴趣的朋友可以  从这里下载源码

http://blog.moozi.net/archives/onlineusercachemanager/

[转载]C# 委托知识总结

mikel阅读(942)

[转载]C# 委托知识总结 – 情缘 – 博客园.

1.什么是委托,为什么要使用委托

我正在埋头苦写程序,突然想喝水,但是又不想自己去掉杯水而打断自己的思路,于是 我就想让女朋友去给我倒水。她去给我倒水,首先我得让她知道我想让她干什么,通知她之后我可以继续写自己的程序,而倒水的工作就交给了她。这样的过程就相 当于一个委托。

在 程序过程中,当程序正在处理某个事件的时候,我需要另外的程序代码去辅助处理一些事情,于是委托另一个程序模块去处理,而委托就可以达到这种目的,我可以 利用委托通知另外的程序模块,该去调用哪个函数方法。委托其实就起到了这样一个作用,将函数签名传递到了另一个函数中。或许这样讲还是有些模糊,看看后面 的具体实例。

2.委托的定义

delegate int Add(int num1,int num2);

delegate void
ConvertNum(string result);


上面是定义两个委托的例子,其实很简单。声明一个委托使用delegate关键字,上面分别是定义的带返回值的委托和不带返回值的委托,

两个委托都有传递参数,当然也可以不传递参数。
其实委托也是一个类,委托派生为System.MulticastDelegate,而System.MulticastDelegate

又继承System.Delegate,如果你
知道这个也就明白委托其实是一个特殊的类。

委托的简单实用例子

1 public delegate string TeaDelegate(string spText);
2
3 public class DelegateSource
4 {
5 public void TestDelegate()
6 {
7 Operator op = new Operator();
8 TeaDelegate tea = new TeaDelegate(op.GetTea);
9 Console.WriteLine(去给我倒杯水);
10 Console.WriteLine();
11 string result=tea(去给我倒杯水);
12 Thread.Sleep(5000);
13 Console.WriteLine(result);
14 Console.WriteLine();
15 }
16 }
17
18 public class Operator
19 {
20 /// <summary>
21 /// 确定是否还有水
22 /// </summary>
23 private bool flag = true;
24
25 public string GetTea(string spText)
26 {
27 if (spText == 去给我倒杯水)
28 {
29 if (flag)
30 {
31 return 老公,茶来了;
32 }
33 else
34 {
35 return 老公,没有水了;
36 }
37 }
38 return 等待…….;
39 }
40 }

输出结果


上面使用最普通的一种方式来定义了一个委托的使用,这个例子虽然很简单,但是能够很形象的描述委托的使用。

3. 委托的三种形式


(1).推断

推断委托例子

1 public delegate string TeaDelegate(string spText);
2
3 public class DelegateSource
4 {
5 public void TestDelegate()
6 {
7 Operator op = new Operator();
8 TeaDelegate tea = op.GetTea;
9 Console.WriteLine(去给我倒杯水);
10 Console.WriteLine();
11 string result=tea(去给我倒杯水);
12 Thread.Sleep(5000);
13 Console.WriteLine(result);
14 Console.WriteLine();
15 }
16 }
17
18 public class Operator
19 {
20 /// <summary>
21 /// 确定是否还有水
22 /// </summary>
23 private bool flag = true;
24
25 public string GetTea(string spText)
26 {
27 if (spText == 去给我倒杯水)
28 {
29 if (flag)
30 {
31 return 老公,茶来了;
32 }
33 else
34 {
35 return 老公,没有水了;
36 }
37 }
38 return 等待…….;
39 }
40 }

在委托定义的例子中我们看到委托的使用方法是在委 托实例化的时候指定的[new DelegateName(FunctionName)],这里可能表述不是太但是代码应该看得白了。 而委托的推断,并没有new 委托这个步骤,而是直接将Function 指定给委托。

(2).匿名函数

匿名委托例子

1 public delegate string TeaDelegate(string spText);
2
3 public class DelegateSource
4 {
5 public void TestDelegate()
6 {
7 Operator op = new Operator();
8 bool flag = true;
9 TeaDelegate tea = delegate(string spText)
10 {
11 if (spText == 去给我倒杯水)
12 {
13 if (flag)
14 {
15 return 老公,茶来了;
16 }
17 else
18 {
19 return 老公,没有水了;
20 }
21 }
22 return 等待…….;
23 };
24
25 Console.WriteLine(去给我倒杯水);
26 Console.WriteLine();
27 string result=tea(去给我倒杯水);
28 Thread.Sleep(5000);
29 Console.WriteLine(result);
30 Console.WriteLine();
31 }
32 }


至于匿名委托,给人的感觉更为直接了,都不用显示的指定方法名,因为根本没有方法,而是指定的匿名方法。匿名方法在.NET 中提高了

代码的可读性和优雅性。对于更多操作较少的方法
直接写为匿名函数,这样会大大提高代码的可读性。这里有两个值得注意的地方: 第一,不能使用

跳转语句跳转到该匿名方法外,第二
不能使用ref,out修饰的参数

(3).多播委托

多播委托例子

1 public delegate string TeaDelegate(string spText);
2
3 public class DelegateSource
4 {
5 public void TestDelegate()
6 {
7 Operator op = new Operator();
8
9 TeaDelegate tea1 = op.GetTea;
10 TeaDelegate tea2 = op.Speak;
11 TeaDelegate tea = tea1 + tea2;
12
13 Console.WriteLine(去给我倒杯水);
14 Console.WriteLine();
15 string result=tea(去给我倒杯水);
16 Thread.Sleep(5000);
17 Console.WriteLine(result);
18 Console.WriteLine();
19 }
20 }
21
22 public class Operator
23 {
24 /// <summary>
25 /// 确定是否还有水
26 /// </summary>
27 private bool flag = true;
28
29 public string GetTea(string spText)
30 {
31 if (spText == 去给我倒杯水)
32 {
33 if (flag)
34 {
35 return 老公,茶来了;
36 }
37 else
38 {
39 return 老公,没有水了;
40 }
41 }
42 return 等待…….;
43 }
44
45
46 public string Speak(string spText)
47 {
48 Console.WriteLine(\n去把我的设计图稿拿来);
49 return null;
50 }
51 }


还是上面的那个实例,我不尽想让女朋友去给我掉杯水,还让她帮我将程序设计图稿拿过来。这个时候做的就不是一件事了,而是多件。

程序中也有很多这种情况,于是我们需要多播委
托,在一个委托上指定多个执行方法,这是在程序中可以行的。上面提到了,委托直接继承于

System.MulticastDelegate,
正是因为这个类可以实现多播委托。
如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void;否则,就只能得 到委托调用的最后一个方法的结果。所以在上面的这段代码中是得不到结果的

4.事件

使用C#编程,无论是 WinForm,WebForm 给人很难忘得就是它的控件,而他们的控件库使用方式都是使用使用事件驱动模式,而事件驱动模式却少不了委托。话不多说,看代码能够更清好的理解事件和委托 之间的联系.

事件的使用

1 public delegate void MyDelegate(string name);
2
3 public class EventSource
4 {
5 public event MyDelegate Event_Delegate;
6
7 public void SetCustomer(string name)
8 {
9 Console.WriteLine(事件发 生…..\n);
10 Console.WriteLine(hi! +name);
11 }
12
13 public void TestEvent()
14 {
15 EventSource source = new EventSource();
16 Console.WriteLine(订阅事件…..\n);
17 source.Event_Delegate += new MyDelegate(source.SetCustomer);
18 Console.WriteLine(触发事 件…..\n);
19 source.Event_Delegate(hechen);
20 Console.WriteLine(………………);
21 }
22 }


上面的代码中我们定义了一个委托,然后定义了一个类EventSource,这个类中声明了一个事件。定义一个事件使用event 关键字,定义一

个event必须指定这个event传递消息的委托,在触发事件之前必需订阅事件,我们使用+=
new 语法来订阅一个事件,也就相当于实例化一个事件。

当我们触发事件的时候,就会调用相应的方法去处理。

5. 泛型委托


委托是类型安全的引用,泛型委托就和我们常用的泛型类一样,这个类在使用的时候才能确定类型.通过泛型委托,我们可以在委托传递参数

之后知道它的类型.在.NET中有一个很典型的
泛型委托:

public delegate voie EventHandler<TEventArgs>(object
sender,TEventArgs e) where TEventArgs:EventArgs.

这是一个非常有特色的泛型委托,可能我们用的比较
少,但是作用是不能忽视的。
我们看看三个非常具有代表性的泛型委托.现在.NET4.0已经出来了,但是泛型委托.NET2.0就出来了,Linq 大家用的那叫一个甜,

为啥
函数式编程风格,匿名方法,Lamda表达式表达式使用是如此的魅力。但是大家仔细观察过没有,Linq 中的方法有几个经常出现的参数:

Action<T>,Predicate<T>,Func<T,
Result>

Func<T, E>:封装一个具有一个参数并返回 E 参数指定的类型值的方法,T 是这个委托封装方法的参数类型,E是方法的返回值类型。当然Func<T, Result> 只是其中的一种情况,这个委托还有其他的几种情况:Func<T> 这个是方法没有参数,返回值类型是T;Func<T1,T2,Result> 这个方法有两个参数,类型分别为T1,T2,返回值是Result,还有 Func<T1,T2,T3,Result>,Func<T1,T2,T3,T4,Result> 这几中情况,具体情况就不介绍了.我们还可以通过扩展类型,扩展为更多的参数.

Func 委托的使用

1 public void TestFunc()
2 {
3 TEventSource eventSource=new TEventSource();
4 Func<string, string> func = eventSource.GetTea;
5 string result = func();
6 Console.WriteLine(result);
7 }
8
9 public string GetTea(string context)
10 {
11 if (context == )
12 {
13 return 茶来了;
14 }
15 else
16 {
17 return 设计稿子来了;
18 }
19 }


Action<T>:封装一个方法,该方法只采用一个参数并且不返回值,包括
Action<T>,Action<T1,T2>,Action<T1,T2,T3>,Action<T1,T2,T3,T4>
几种情况,也可以
通过扩展方法去扩展参数的个数

Action 委托使用例子

1 public void TestAction()
2 {
3 TEventSource eventSource = new TEventSource();
4 Action<string> action = eventSource.Speak;
5 action(Action<T> 泛型委托);
6 }
7
8 public void Speak(string context)
9 {
10 Console.WriteLine(context);
11 }


Predicate<T>:表示定义一组条件并确定指定对象是否符合这些条件的方法。该委托返回的是一个bool类型的值,如果比较满足条件

返回true,否则返回false.其实上面的
Func 委托可以包含这个委托.不过这个委托和上面的两个不一样,它只有一种类型

Predicate 委托使用例子

1 public void TestPredicate()
2 {
3 TEventSource eventSource = new TEventSource();
4 Predicate<int> predicate = eventSource.IsRigth;
5 Console.WriteLine(predicate(0));
6 }
7
8 public bool IsRigth(int value)
9 {
10 if (value == 0)
11 {
12 return true;
13 }
14 else
15 {
16 return false;
17 }
18 }

6. 异步委托

投 票技术: 委托其实相当于一个线程,使用投票技术是使用异步委托的一种实现方式.Delegate类提供了方法BeginInvoke(),可以传送委托类型定义的 输入参数,其返回类型为IAsyncResult。IAsyncResult的IsCompleted属性可以判断委托任务是否完成

异步委托投票技术

1 public delegate int DelegateVote(int data, int ms);
2 /// <summary>
3 /// 使用投票操作完成委托任务
4 /// </summary>
5 public class VoteDelegate
6 {
7 /// <summary>
8 /// 休眠特定时间执行操作
9 /// </summary>
10 /// <param name=”data”></param>
11 /// <param name=”ms”></param>
12 /// <returns></returns>
13 public static int TakeWork(int data, int ms)
14 {
15 Console.WriteLine(开始调用TakeWork方法);
16 Thread.Sleep(ms);
17 Console.WriteLine(结束调用 TakeWork方法);
18 return data + 10;
19 }
20
21 public void TestDelegate()
22 {
23 DelegateVote voteDel = TakeWork;
24 IAsyncResult result = voteDel.BeginInvoke(1,5000,null,null);
25 while (result.IsCompleted == false)
26 {
27 Console.WriteLine(等待……);
28 Thread.Sleep(500);
29 }
30 int value = voteDel.EndInvoke(result);
31 Console.WriteLine(委托调用结果: +value);
32 }
33 }

等待句柄:等待句柄是使用 AsyncWaitHandle属性访问,返回一个WaitHandle类型的对象,它可以等待委托线程完成其任务。在这个参数中可以设置最大的等待时 间。

异步委托等待句柄

1 public delegate string WaitDelegate(string content);
2
3 public class WaitHandlerDelegate
4 {
5 public void TestWaitHander()
6 {
7 WaitDelegate del = GetTea;
8 IAsyncResult ar = del.BeginInvoke(hechen, null, null);
9 while (true)
10 {
11 Console.Write(.);
12 if (ar.AsyncWaitHandle.WaitOne(50, false))
13 {
14 break;
15 }
16 }
17 string result=del.EndInvoke(ar);
18 Console.WriteLine(result);
19
20 }
21
22 public static string GetTea(string content)
23 {
24 return 茶来了 +content;
25 }
26 }

异步回调:这个方式和投票技术 有点类似,不过在投票方式中BeginInvoke()方法第三个参数指定了一个方法签名,而这个方法参数接收IAsyncResult 类型的参数。

异步委托回调函数

1 public delegate string AsyDelegate(string content);
2
3 public class AsyncresultDelegate
4 {
5 public void TestAsync()
6 {
7 AsyDelegate del = GetTea;
8 del.BeginInvoke(hechen, delegate(IAsyncResult ar) {
9 Thread.Sleep(5000);
10 string result = del.EndInvoke(ar);
11 Console.WriteLine(result);
12 }, null);
13 for (int i = 0; i < 100; i++)
14 {
15 Console.WriteLine(等待…..);
16 Thread.Sleep(1000);
17 }
18 }
19
20 public static string GetTea(string content)
21 {
22 return 茶来了 + content;
23 }
24 }

[转载]Asp.Net 网站性能优化 缓字决 (上) 缓冲写数据

mikel阅读(1100)

[转载]Asp.Net 网站性能优化 缓字决 (上) 缓冲写数据 – 欢迎光临赵玉开的技术博客 – 博客园.

通常情况下ASP.NET 网站的底层数据存储都是关系数据库,关系数据库资源比较昂贵,而且也很容易造成瓶颈。缓字决文章就是为大家介绍如何有效使用缓存,异步写缓冲数据库的压 力,从而保证网站的性能。

大家已经看到很多关于ASP.NET缓存的文章了。所以我写的时候要改变一下思路,从缓冲写数据开始说起。缓冲写数据的意思是在数据需要更新时不马 上把数据存到数据库,而是先缓冲一下,然后在适当的时机再写入到数据库中。

缓冲写数据可以避免在网站并发访问多的时候,数据库瞬间承受过大压力,而造成死锁或响应不及时的情况。

那么什么时候适合缓冲写呢?是不是所有情况都适用呢?缓冲写会导致数据在内存中或者web server硬盘或者第三方存储中驻留一段时间,在这段时间内如果从数据库中查询最新数据的话,会有遗漏。大多数事物都有两面性,我们需要学会趋利避害; 换句话说在保证缓冲写不会导致用户感觉数据缺少的情况下,或者在使用适当措施不让用户感觉数据缺失的情况下就可以使用缓冲写。我有两个具体的实例来介绍如 何使用缓冲写:

1. Pv(页面浏览量)统计,大多数网站都有这个功能,有些网站还专门做这个服务
网站每有一个页面被浏览时,就会需要给对应页面的 Pv+1;这种情况下,如果直接更新到数据库中,访问量稍微大一些就会造成数据库压力过大的问题。

所以我们需要对Pv计数做缓冲,在单web server的情况下我们可以在内存中维护一个hashtable,然后用一个异步的线程去定时扫描这个hashtable,当点击数达到“一定数字”时 更新到数据库。听上去很简单,不过也需要一个小技巧,上一句话中说的“一定数字”四个字不能是随便的一个数字,如果它是4,试想一下会出现什么情况,我们 的所有Pv数都会是4的倍数,用户会怀疑我们是不是在Pv上造假了;我们没有造假却留下了造假的迹象!这个“一定数字”必须是一个素数,我们可以取7,也 可以用13,如果我们的访问量很大也可以取23或31;这样就不会出现“造假的迹象”了。

2. 发送站内短消息
站内短消息也是一个比较通用的模块,他可以说是一种离线消息,并非im即时消息,所以我们可以利用这个业务特性,   来对发消息做下缓冲。当有短消息发送时我们可以先将这个短消息放到硬盘文件中,然后很快的响应用户,在ui上告诉用户,你的消息已经发出去了,然后我 们可以用另一个线程(或者做一个windows服务)去监视缓冲短消息的目录顺序的将短消息存储到数据库中。

以上两个场景都是比较经典的利用缓冲写的例子,在现实中需要我们去具体分析业务和某个业务是否会造成数据库压力来决定是否缓冲写,如果业务本身对数 据库的压力就很小,那当然就没必要考虑了,反之如果业务压力较大我们就需要做一些工作避免缓冲写的问题并利用缓冲写。


请尊重作者的劳动,转载请保留链接 玉开的技术博客

[转载]Asp.Net 网站优化 数据库优化措施 使用主从库(下) - 欢迎光临赵玉开的技术博客 - 博客园

mikel阅读(1036)

[转载]Asp.Net 网站优化 数据库优化措施 使用主从库(下) – 欢迎光临赵玉开的技术博客 – 博客园.

上 一篇中我们配置好了主从库,现在我们尝试在程序中使用主从库。

主从库之间是一种发布订阅的关系,发布者和订阅者之间并非实时同步的,通常会有几分钟的延时,更有甚者会有几个小时的延时。所以我们需要通过合理的 使用来避开有延时这个问题。

我们希望主库尽可能的少参与查询,来提高写的及时性;同时要让从库在不影响读出数据的准确及时的前提下尽可能的分担主库的压力。

主从两个库需要在配置文件中配置两个连接字符串,CONN_Master和CONN_Slave。我们需要设定一些规则决定当前的查询应该从主库查 还是需要从从库查。这个规则没有定式,只能根据业务需要来确定。下面我举几个例子来说明:

1. 以豆瓣读书书的详细页为假定场景,你可以点击这里看下页面的结构(我不是豆瓣的技术,在这里只是拿这个页面举例)
我们来分析呈现这个 页面需要的数据和这些数据的实效性要求
1) 书的详细信息 时效性要求:要求及时
2) 豆瓣成员的常用标签 实效性:不需要很及时
3) 喜欢读这本书的人也喜欢读的书 属于分析数据,不需要很及时
4) 最新书评 要求及时
5) 读这本书的几个用户 及时性不高
6) 喜欢这本书的人常去的小组 属于分析数据不需要很及时
从上面的分析可以看出只有1),4)两项数据需要从主库读,而2),3),5),6)为非 及时数据从从库读取即可。当然我们可以对这些实效性不高的数据做缓存处理。

2. 以论坛帖子列表页面为假定场景,玩论坛的人都喜欢顶贴,把自己的帖子顶到第一页让更多的人关注,而对于50页之后的帖子则反读的人很少;我们可以根据这个 业务逻辑特征来决定在用户访问前50页帖子列表数据时从主库读,而当用户访问超过50页之后的数据时则从从库进行查询。

3. 以订单为例,通常超过三个月的订单就不会再有变化了,假定我们把订单号设计为日期格式时,根据订单号去查询订单时就可以根据订单号来决定该访问主库还是从 库。

举了几个适用的场景,我们以第三个场景为例,写一段简单的示意代码看下

01 //orderNo 的格式为 20100528120105000001 即yyyyMMddHHmmss + 序号
02 public OrderInfo GetOrder(string orderNo) {
03 string connString = ConnStringGetter.GetForOrder(orderNo);
04 using (SQLConnection conn = new SQLConnection(connString))
05 {
06 ...
07 }
08 }
09
10 public class ConnStringGetter
11 {
12 public static string GetForOrder(string orderNo) {
13 int year = int.Parse(orderNo.Substring(0,4));
14 int money = int.Parse(orderNo.Substring(4,2));
15 int date = int.Parse(orderNo.Substring(6,2));
16 DateTime orderTime = new DateTime(year, money, date);
17
18 TimeSpan ts = DateTime.Now - orderTime;
19 //根据订单的时间决定使用主库还是从库
20 if (ts.TotalDays > 30) return ConfigurationManager.ConnectionStrings["CONN_Slave"].ConnectionString;
21 return ConfigurationManager.ConnectionStrings["CONN_Master"].ConnectionString;
22 }
23 }

正确的使用主从库,可以很好的提升系统的性能。使用主库还是从库的选择权决定在业务逻辑的手里。

相关随笔:Asp.Net 网站优化 数据库优化措施 使用主从库(上)


请尊重作者的劳动,转载请保留链接 玉开的技术博客

[转载]Asp.Net 网站优化 数据库优化措施 使用主从库(上)

mikel阅读(1006)

[转载]Asp.Net 网站优化 数据库优化措施 使用主从库(上) – 欢迎光临赵玉开的技术博客 – 博客园.

网站规模到了一定程度之后,该分的也分了,该优化的也做了优化,但是还是不能满足业务上对性能的要求;这时候我们可以考虑使用主从库。

主从库是两台服务器上的两个数据库,主库以最快的速度做增删改操作+最新数据的查询操作;从库负责查询较旧数据,做一些对实效性要求较小的分析,报 表生成的工作。这样做将数据库的压力分担到两台服务器上从而保证整个系统响应的及时性。

SQL Server提供了复制机制来帮我们实现主从库的机制。我们看下如何在SQL server 2005中实践:

实践前需要新创建一个Test的数据库,这个库中建一个测试表。

1. 打开SQL server企业管理器,在对象资源管理器里面选择复制本地发布,右键选择新建发布

2. 打开新建发布向导,点下一步,选择发布数据的数据库

3. 我们选择Test数据库,并点击下一步,选择发布类型

这 里我们选择的是事务性发布,事务性发布保证数据在做更新之后尽可能快的分发到订阅服务器上。有关其他几种发布类型的使用场景请参考msdn
4. 点击下一步,选择要发布的对象,这里我们只对表进行发布

5. 点击下一步进入筛选数据设置,这里我们要复制表的所有数据所以不做设置

6. 点击下一步,指定何时运行快照,我们选择初始话数据,并选择默认的运行快照频率

7. 继续下一步,设置快照代理的运行账户,我们选择sql server agent账户

8. 点击下一步选择创建发布,再次点击下一步设置发布的名称

9. 点击完成,完成发布的设置,并创建发布,现在在本地发布出新添加了我们创建的发布

现在成功创建了发布,我们还需要创建订阅:在本地订阅文件夹上右击新建订阅,通过向导可以很容易的创建订阅,创建订阅时可以选择以发布者推送或者订 阅者主动的方式创建。具体步骤如下:
1. 通过右键菜单打开新建订阅,点击下一步,选择我们刚刚创建的发布作为订阅源


2. 选择是以推送还是以主动请求的方式同步数据,我们选择主动订阅

3. 设置执行分发代理的账户

4. 设置代理请求同步的频率

5. 设定是否立即做数据的初始化操作

6. 完成创建订阅

创建完成之后,我们可以通过在主库表中插入n条数据,然后在从库中查询的方式验证复制是否成功。

在Sql server2005中的复制创建起来很简单,我们需要根据业务需要设定复制的类型和同步的频率,下一篇我们谈谈如何有效的在程序中使用主从库。


请尊重作者的劳动,转载请保留链接 玉开的技术博客

[转载]ASP.NET 4新增功能(一) ASP.NET核心服务

mikel阅读(897)

[转载]ASP.NET 4新增功能(一) ASP.NET核心服务 – longgel – 博客园.

1.可扩展输出缓存

自从ASP.NET 1.0发布开始,一般都是通过使用输出缓存将页、控件和http响应保存在内存中。对于后续的Web请求,ASP.NET可以从内存中检索存在的缓存输出 并不是从头开始重新生成输出,从而更快地提供响应。但该方法有一个限制,就是缓存的数据必须存储在内存中。在负载较大的服务器上,输出缓存的内存需求可能 会和Web应用程序其它它部分的内存需求产生冲突。

ASP.NET 4 为输出缓存增加了扩展性,使您能够配置一个或多个自定义输出缓存提供程序。输出缓存提供程序可使用任何存储机制保存 HTML 内容。这些存储选项包括本地或远程磁盘、云存储和分布式缓存引擎。

借助ASP.NET 4 中可以定制输出缓存提供程序的功能,我们就可以为网站设计更为主动而且更加智勇双全能的输出缓存策略。例如,我们可以创建这样一个输出缓存提供程序,该程 序将站点流量“排名前10”的页面缓存在内存里,而将其它的页面缓存在磁盘里。或者,也可以对所呈现页面的各种变化因素组合进行缓存,但应该使用分布式缓 存以减少前端Web服务器的内存消耗。

我们可以创建继承自OutPutCacheProvider类型的类来自定义输出缓存提供程序。随后,可以通过在web.config中进行配置该提供程 序,设置outputCache节点的providers子节点,如下面的示例所示:

<caching>
<outputCache defaultProvider=”AspNetInternalProvider”>
<providers>
<add name=”DiskCache”
type
=”Test.OutputCacheEx.DiskOutputCacheProvider, DiskCacheProvider”/>
</providers>
</outputCache>
</caching>

在ASP.NET 4 的默认输出缓存策略中。所有的HTTP响应、所呈现的页面和控件缓存均使用上例所示的默认输出缓存提供程序(其中defaultProvider属性值为 AspNetInternalProvider)。通过为defaultProvider指定不同的提供程序。就可以更改web应用程序的默认输出缓存提 供程序。

另外,还可以针对每个用户控件和各个请求选择不同的输出缓存提供程序。要为不同的Web用户控件选择不同的输出缓存提供程序,最简便的方法 是设置页面或控件指令中新增加的providerName属性,如下面的示例所示:

<%@ OutputCache Duration="60" VaryByParam="None" 
    providerName="DiskCache" %>

若要为某个HTTP请求指定不同的输出缓存提供程序,可以覆盖Global.asax文件中新增加的 GetOutputCacheProviderName方法,以编程的方式指定要用于特定请求的提供程序。

2.预加载Web应用程序

某些Web应用程序在第一次请求提供服务之前,需要加载大量的数据或执行开销很大的初始化处理。在ASP.NET早期版本中,对于此类情况,必须采用自定 义方法“唤醒”ASP.NET应用程序,然后在Global.asax文件中的Application_Load方法中运行初始化代码。

为应对这种情况,当ASP.NET 4在Windows Server 2008 R2上的IIS7.5中运行时,ASP.NET 4提供一种新的应用程序预加载管理器。预加载功能提供了一种可控的方法,用于启动应用程序池,初始化ASP.NET应用程序,然后接受HTTP请求。通过 这种方法,您可以在处理第一项HTTP请求之前执行开销很大的应用程序初始化的工作。例如,可以使用预加载管理器初始化某个应用程序,然后向负载平衡器发 生信号,告知应用程序已初始化并做好接受HTTP请求的准备。

若要使用应用程序预加载管理器,就需要配置applicationHost.config文件,设置IIS 7.5中的应用程序池为自动启动,配置如下:

<applicationPools>
<add name=”MyApplicationPool” startMode=”AlwaysRunning” />
</applicationPools>

由 于一个应用程序池可以包含多个应用程序,因此我们就需要通过使用applicationHost.config文件中的以下配置分别指定要自动启动的各个 应用程序:

<sites>
<site name=”MySite” id=”1″>
<application path=”/”
serviceAutoStartEnabled
=”true”
serviceAutoStartProvider
=”PrewarmMyCache” >
<!– Additional content –>
</application>
</site>
</sites>

<!– Additional content –>

<serviceAutoStartProviders>
<add name=”PrewarmMyCache”
type
=”MyNamespace.CustomInitialization, MyLibrary” />
</serviceAutoStartProviders>

3.永久重定向页面

在应用程序的生命周期内,Web应用程序中有可能修改url的显示规则。

在ASP.NET 中,开发人员处理对旧的URL的请求的传统方式是使用Redirect方法将请求转发至新的URL。然而,Redirect方法会发出HTTP 302临时重定向。这会产生额外的HTTP往返。也不是对搜索引擎的友好。

ASP.NET 4 增加了一个RedirectPermanent帮助方法,使用该方法可以方便地发生http 301(永久跳转)的响应,如下面的示例所示:

RedirectPermanent("/newpath/foroldcontent.aspx");
4.会话状态压缩

默认情况下,ASP.NET 提供两用于存储整个Web应用程序中的会话状态的选项。第一个选项是一个调用进程外会话状态服务器的会话状态提供程序。第二个选项是一个在 Microsoft SQL Server数据库中存储数据的会话状态提供程序。

由于这两个选项均在 Web 应用程序的工作进程之外存储状态信息,因此在将会话状态发送至远程存储器之前,必须对其进行序列化。如果会话状态中保存了大量数据,序列化数据的大小可能 变得很大。

ASP.NET 4 针对这两种类型的进程外会话状态提供程序引入了一个新的压缩选项。使用此选项,在 Web 服务器上有多余 CPU 周期的应用程序可以大大缩减序列化会话状态数据的大小。

可以使用配置文件中 sessionState 元素的新增加的 compressionEnabled 属性设置此选项。当 compressionEnabled 配置选项设置为 true 时,ASP.NET 使用 .NET Framework GZipStream类对序列化会话状态进行压缩和解压缩。下面的示例演示如何设置该特性。

<sessionState
mode=”SQLServer
SQLConnectionString
=”data source=dbserver;Initial Catalog=aspnetstate”
allowCustomSqlDatabase
=”true”
compressionEnabled
=”true”
/>

[转载]Java:设计 REST 风格的 MVC 框架

mikel阅读(930)

[转载]设计 REST 风格的 MVC 框架.

传统的 JavaEE MVC 框架如 Struts 等都是基于 Action 设计的后缀式映射,然而,流行的 Web 趋势是 REST 风格的架构。尽管使用 Filter 或者 Apache mod_rewrite 能够通过 URL 重写实现 REST 风格的 URL,为什么不直接设计一个全新的 REST 风格的 MVC 框架呢? 本文将讲述如何从头设计一个基于 REST 风格的 Java MVC 框架,配合 Annotation,最大限度地简化 Web 应用的开发,您甚至编写一行代码就可以实现“Hello, world”。

Java 开发者对 MVC 框架一定不陌生,从 Struts 到 WebWork,Java MVC 框架层出不穷。我们已经习惯了处理 *.do 或 *.action 风格的 URL,为每一个 URL 编写一个控制器,并继承一个 Action 或者 Controller 接口。然而,流行的 Web 趋势是使用更加简单,对用户和搜索引擎更加友好的 REST 风格的 URL。例如,来自豆瓣的一本书的链接是 http://www.douban.com/subject/2129650/, 而非 http://www.douban.com/subject.do?id=2129650

有经验的 Java Web 开发人员会使用 URL 重写的方式来实现类似的 URL,例如,为前端 Apache 服务器配置 mod_rewrite 模块,并依次为每个需要实现 URL 重写的地址编写负责转换的正则表达式,或者,通过一个自定义的 RewriteFilter,使用 Java Web 服务器提供的 Filter 和请求转发(Forward)功能实现 URL 重写,不过,仍需要为每个地址编写正则表达式。

既然 URL 重写如此繁琐,为何不直接设计一个原生支持 REST 风格的 MVC 框架呢?

要设计并实现这样一个 MVC 框架并不困难,下面,我们从零开始,仔细研究如何实现 REST 风格的 URL 映射,并与常见的 IoC 容器如 Spring 框架集成。这个全新的 MVC 框架暂命名为 WebWind。

术语

MVC:Model-View-Controller,是一种常见的 UI 架构模式,通过分离 Model(模型)、View(视图)和 Controller(控制器),可以更容易实现易于扩展的 UI。在 Web 应用程序中,Model 指后台返回的数据;View 指需要渲染的页面,通常是 JSP 或者其他模板页面,渲染后的结果通常是 HTML;Controller 指 Web 开发人员编写的处理不同 URL 的控制器(在 Struts 中被称之为 Action),而 MVC 框架本身还有一个前置控制器,用于接收所有的 URL 请求,并根据 URL 地址分发到 Web 开发人员编写的 Controller 中。

IoC:Invertion-of-Control,控制反转,是目前流行的管理所有组件生命周期和复杂依赖关系的容器,例如 Spring 容器。

Template:模板,通过渲染,模板中的变量将被 Model 的实际数据所替换,然后,生成的内容即是用户在浏览器中看到的 HTML。模板也能实现判断、循环等简单逻辑。本质上,JSP 页面也是一种模板。此外,还有许多第三方模板引擎,如 Velocity,FreeMarker 等。


回页首

设计目标

和传统的 Struts 等 MVC 框架完全不同,为了支持 REST 风格的 URL,我们并不把一个 URL 映射到一个 Controller 类(或者 Struts 的 Action),而是直接把一个 URL 映射到一个方法,这样,Web 开发人员就可以将多个功能类似的方法放到一个 Controller 中,并且,Controller 没有强制要求必须实现某个接口。一个 Controller 通常拥有多个方法,每个方法负责处理一个 URL。例如,一个管理 Blog 的 Controller 定义起来就像清单 1 所示。
清单 1. 管理 Blog 的 Controller 定义

				
public class Blog { 
    @Mapping("/create/$1") 
    Public void create(int userId) { ... } 

    @Mapping("/display/$1/$2") 
    Public void display(int userId, int postId) { ... } 

    @Mapping("/edit/$1/$2") 
    Public void edit(int userId, int postId) { ... } 

    @Mapping("/delete/$1/$2") 
    Public String delete(int userId, int postId) { ... } 
} 

@Mapping() 注解指示了这是一个处理 URL 映射的方法,URL 中的参数 $1、$2 ……则将作为方法参数传入。对于一个“/blog/1234/5678”的 URL,对应的方法将自动获得参数 userId=1234 和 postId=5678。同时,也无需任何与 URL 映射相关的 XML 配置文件。

使用 $1、$2 ……来定义 URL 中的可变参数要比正则表达式更简单,我们需要在 MVC 框架内部将其转化为正则表达式,以便匹配 URL。

此外,对于方法返回值,也未作强制要求。


回页首

集成 IoC

当接收到来自浏览器的请求,并匹配到合适的 URL 时,应该转发给某个 Controller 实例的某个标记有 @Mapping 的方法,这需要持有所有 Controller 的实例。不过,让一个 MVC 框架去管理这些组件并不是一个好的设计,这些组件可以很容易地被 IoC 容器管理,MVC 框架需要做的仅仅是向 IoC 容器请求并获取这些组件的实例。

为了解耦一种特定的 IoC 容器,我们通过 ContainerFactory 来获取所有 Controller 组件的实例,如清单 2 所示。
清单 2. 定义 ContainerFactory

				
public interface ContainerFactory { 

    void init(Config config); 

    List<Object> findAllBeans(); 

    void destroy(); 
} 

其中,关键方法 findAllBeans() 返回 IoC 容器管理的所有 Bean,然后,扫描每一个 Bean 的所有 public 方法,并引用那些标记有 @Mapping 的方法实例。

我们设计目标是支持 Spring 和 Guice 这两种容器,对于 Spring 容器,可以通过 ApplicationContext 获得所有的 Bean 引用,代码见清单 3。
清单 3. 定义 SpringContainerFactory

				
public class SpringContainerFactory implements ContainerFactory { 
    private ApplicationContext appContext; 

    public List<Object> findAllBeans() { 
        String[] beanNames = appContext.getBeanDefinitionNames(); 
        List<Object> beans = new ArrayList<Object>(beanNames.length); 
        for (int i=0; i<beanNames.length; i++) { 
            beans.add(appContext.getBean(beanNames[i])); 
        } 
        return beans; 
    } 
    ... 
} 

对于 Guice 容器,通过 Injector 实例可以返回所有绑定对象的实例,代码见清单 4。
清单 4. 定义 GuiceContainerFactory

				
public class GuiceContainerFactory implements ContainerFactory { 
    private Injector injector; 

    public List<Object> findAllBeans() { 
        Map<Key<?>, Binding<?>> map = injector.getBindings(); 
        Set<Key<?>> keys = map.keySet(); 
        List<Object> list = new ArrayList<Object>(keys.size()); 
        for (Key<?> key : keys) { 
            Object bean = injector.getInstance(key); 
            list.add(bean); 
        } 
        return list; 
    } 
    ... 
} 

类似的,通过扩展 ContainerFactory,就可以支持更多的 IoC 容器,如 PicoContainer。

出于效率的考虑,我们缓存所有来自 IoC 的 Controller 实例,无论其在 IoC 中配置为 Singleton 还是 Prototype 类型。当然,也可以修改代码,每次都从 IoC 容器中重新请求实例。


回页首

设计请求转发

和 Struts 等常见 MVC 框架一样,我们也需要实现一个前置控制器,通常命名为 DispatcherServlet,用于接收所有的请求,并作出合适的转发。在 Servlet 规范中,有以下几种常见的 URL 匹配模式:

  • /abc:精确匹配,通常用于映射自定义的 Servlet;
  • *.do:后缀模式匹配,常见的 MVC 框架都采用这种模式;
  • /app/*:前缀模式匹配,这要求 URL 必须以固定前缀开头;
  • /:匹配默认的 Servlet,当一个 URL 没有匹配到任何 Servlet 时,就匹配默认的 Servlet。一个 Web 应用程序如果没有映射默认的 Servlet,Web 服务器会自动为 Web 应用程序添加一个默认的 Servlet。

REST 风格的 URL 一般不含后缀,我们只能将 DispatcherServlet 映射到“/”,使之变为一个默认的 Servlet,这样,就可以对任意的 URL 进行处理。

由于无法像 Struts 等传统的 MVC 框架根据后缀直接将一个 URL 映射到一个 Controller,我们必须依次匹配每个有能力处理 HTTP 请求的 @Mapping 方法。完整的 HTTP 请求处理流程如图 1 所示。
图 1. 请求处理流程
图 1. 请求处理流程

当扫描到标记有 @Mapping 注解的方法时,需要首先检查 URL 与方法参数是否匹配,UrlMatcher 用于将 @Mapping 中包含 $1、$2 ……的字符串变为正则表达式,进行预编译,并检查参数个数是否符合方法参数,代码见清单 5。
清单 5. 定义 UrlMatcher

				
final class UrlMatcher { 
    final String url; 
    int[] orders; 
    Pattern pattern; 

    public UrlMatcher(String url) { 
        ... 
    } 
} 

将 @Mapping 中包含 $1、$2 ……的字符串变为正则表达式的转换规则是,依次将每个 $n 替换为 ([^\\/]*),其余部分作精确匹配。例如,“/blog/$1/$2”变化后的正则表达式为:

 ^\\/blog\\/([^\\/]*)\\/([^\\/]*)$ 

请注意,Java 字符串需要两个连续的“\\”表示正则表达式中的转义字符“\”。将“/”排除在变量匹配之外可以避免很多歧义。

调用一个实例方法则由 Action 类表示,它持有类实例、方法引用和方法参数类型,代码见清单 6。
清单 6. 定义 Action

				
class Action { 
    public final Object instance; 
    public final Method method; 
    public final Class<?>[] arguments; 

    public Action(Object instance, Method method) { 
        this.instance = instance; 
        this.method = method; 
        this.arguments = method.getParameterTypes(); 
    } 
} 

负责请求转发的 Dispatcher 通过关联 UrlMatcher 与 Action,就可以匹配到合适的 URL,并转发给相应的 Action,代码见清单 7。
清单 7. 定义 Dispatcher

				
class Dispatcher  { 
    private UrlMatcher[] urlMatchers; 
    private Map<UrlMatcher, Action> urlMap = new HashMap<UrlMatcher, Action>(); 
    .... 
} 

当 Dispatcher 接收到一个 URL 请求时,遍历所有的 UrlMatcher,找到第一个匹配 URL 的 UrlMatcher,并从 URL 中提取方法参数,代码见清单 8。
清单 8. 匹配并从 URL 中提取参数

				
final class UrlMatcher { 
    ... 

    /** 
     * 根据正则表达式匹配 URL,若匹配成功,返回从 URL 中提取的参数,
     * 若匹配失败,返回 null 
     */ 
    public String[] getMatchedParameters(String url) { 
        Matcher m = pattern.matcher(url); 
        if (!m.matches()) 
            return null; 
        if (orders.length==0) 
            return EMPTY_STRINGS; 
        String[] params = new String[orders.length]; 
        for (int i=0; i<orders.length; i++) { 
            params[orders[i]] = m.group(i+1); 
        } 
        return params; 
    } 
} 

根据 URL 找到匹配的 Action 后,就可以构造一个 Execution 对象,并根据方法签名将 URL 中的 String 转换为合适的方法参数类型,准备好全部参数,代码见清单 9。
清单 9. 构造 Exectuion

				
class Execution { 
    public final HttpServletRequest request; 
    public final HttpServletResponse response; 
    private final Action action; 
    private final Object[] args; 
    ... 

    public Object execute() throws Exception { 
        try { 
            return action.method.invoke(action.instance, args); 
        } 
        catch (InvocationTargetException e) { 
            Throwable t = e.getCause(); 
            if (t!=null && t instanceof Exception) 
                throw (Exception) t; 
            throw e; 
        } 
    } 
} 

调用 execute() 方法就可以执行目标方法,并返回一个结果。请注意,当通过反射调用方法失败时,我们通过查找 InvocationTargetException 的根异常并将其抛出,这样,客户端就能捕获正确的原始异常。

为了最大限度地增加灵活性,我们并不强制要求 URL 的处理方法返回某一种类型。我们设计支持以下返回值:

  • String:当返回一个 String 时,自动将其作为 HTML 写入 HttpServletResponse;
  • void:当返回 void 时,不做任何操作;
  • Renderer:当返回 Renderer 对象时,将调用 Renderer 对象的 render 方法渲染 HTML 页面。

最后需要考虑的是,由于我们将 DispatcherServlet 映射为“/”,即默认的 Servlet,则所有的未匹配成功的 URL 都将由 DispatcherServlet 处理,包括所有静态文件,因此,当未匹配到任何 Controller 的 @Mapping 方法后,DispatcherServlet 将试图按 URL 查找对应的静态文件,我们用 StaticFileHandler 封装,主要代码见清单 10。
清单 10. 处理静态文件

				
class StaticFileHandler { 
    ... 
    public void handle(HttpServletRequest request, HttpServletResponse response) 
    throws ServletException, IOException { 
        String url = request.getRequestURI(); 
        String path = request.getServletPath(); 
        url = url.substring(path.length()); 
        if (url.toUpperCase().startsWith("/WEB-INF/")) { 
            response.sendError(HttpServletResponse.SC_NOT_FOUND); 
            return; 
        } 
        int n = url.indexOf('?'); 
        if (n!=(-1)) 
            url = url.substring(0, n); 
        n = url.indexOf('#'); 
        if (n!=(-1)) 
            url = url.substring(0, n); 
        File f = new File(servletContext.getRealPath(url)); 
        if (! f.isFile()) { 
            response.sendError(HttpServletResponse.SC_NOT_FOUND); 
            return; 
        } 
        long ifModifiedSince = request.getDateHeader("If-Modified-Since"); 
        long lastModified = f.lastModified(); 
        if (ifModifiedSince!=(-1) && ifModifiedSince>=lastModified) { 
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); 
            return; 
        } 
        response.setDateHeader("Last-Modified", lastModified); 
        response.setContentLength((int)f.length()); 
        response.setContentType(getMimeType(f)); 
        sendFile(f, response.getOutputStream()); 
    } 
} 

处理静态文件时要过滤 /WEB-INF/ 目录,否则将造成安全漏洞。


回页首

集成模板引擎

作为示例,返回一个“<h1>Hello, world!</h1>”作为 HTML 页面非常容易。然而,实际应用的页面通常是极其复杂的,需要一个模板引擎来渲染出 HTML。可以把 JSP 看作是一种模板,只要不在 JSP 页面中编写复杂的 Java 代码。我们的设计目标是实现对 JSP 和 Velocity 这两种模板的支持。

和集成 IoC 框架类似,我们需要解耦 MVC 与模板系统,因此,TemplateFactory 用于初始化模板引擎,并返回 Template 模板对象。TemplateFactory 定义见清单 11。
清单 11. 定义 TemplateFactory

				
public abstract class TemplateFactory { 
    private static TemplateFactory instance; 
    public static TemplateFactory getTemplateFactory() { 
        return instance; 
    } 

    public abstract Template loadTemplate(String path) throws Exception; 
} 

Template 接口则实现真正的渲染任务。定义见清单 12。
清单 12. 定义 Template

				
public interface Template { 
    void render(HttpServletRequest request, HttpServletResponse response, 
        Map<String, Object> model) throws Exception; 
} 

以 JSP 为例,实现 JspTemplateFactory 非常容易。代码见清单 13。
清单 13. 定义 JspTemplateFactory

				
public class JspTemplateFactory extends TemplateFactory { 
    private Log log = LogFactory.getLog(getClass()); 

    public Template loadTemplate(String path) throws Exception { 
        if (log.isDebugEnabled()) 
            log.debug("Load JSP template '" + path + "'."); 
        return new JspTemplate(path); 
    } 

    public void init(Config config) { 
        log.info("JspTemplateFactory init ok."); 
    } 
} 

JspTemplate 用于渲染页面,只需要传入 JSP 的路径,将 Model 绑定到 HttpServletRequest,就可以调用 Servlet 规范的 forward 方法将请求转发给指定的 JSP 页面并渲染。代码见清单 14。
清单 14. 定义 JspTemplate

				
public class JspTemplate implements Template { 
    private String path; 

    public JspTemplate(String path) { 
        this.path = path; 
    } 

    public void render(HttpServletRequest request, HttpServletResponse response, 
            Map<String, Object> model) throws Exception { 
        Set<String> keys = model.keySet(); 
        for (String key : keys) { 
            request.setAttribute(key, model.get(key)); 
        } 
        request.getRequestDispatcher(path).forward(request, response); 
    } 
} 

另一种比 JSP 更加简单且灵活的模板引擎是 Velocity,它使用更简洁的语法来渲染页面,对页面设计人员更加友好,并且完全阻止了开发人员试图在页面中编写 Java 代码的可能性。使用 Velocity 编写的页面示例如清单 15 所示。
清单 15. Velocity 模板页面

				
<html> 
    <head><title>${title}</title></head> 
    <body><h1>Hello, ${name}!</body> 
</html> 

通过 VelocityTemplateFactory 和 VelocityTemplate 就可以实现对 Velocity 的集成。不过,从 Web 开发人员看来,并不需要知道具体使用的模板,客户端仅需要提供模板路径和一个由 Map<String, Object> 组成的 Model,然后返回一个 TemplateRenderer 对象。代码如清单 16 所示。
清单 16. 定义 TemplateRenderer

				
public class TemplateRenderer extends Renderer { 
    private String path; 
    private Map<String, Object> model; 

    public TemplateRenderer(String path, Map<String, Object> model) { 
        this.path = path; 
        this.model = model; 
    } 

    @Override 
    public void render(ServletContext context, HttpServletRequest request, 
            HttpServletResponse response) throws Exception { 
        TemplateFactory.getTemplateFactory() 
                .loadTemplate(path) 
                .render(request, response, model); 
    } 
} 

TemplateRenderer 通过简单地调用 render 方法就实现了页面渲染。为了指定 Jsp 或 Velocity,需要在 web.xml 中配置 DispatcherServlet 的初始参数。配置示例请参考清单 17。
清单 17. 配置 Velocity 作为模板引擎

				
<servlet> 
    <servlet-name>dispatcher</servlet-name> 
    <servlet-class>org.expressme.webwind.DispatcherServlet</servlet-class> 
    <init-param> 
        <param-name>template</param-name> 
        <param-value>Velocity</param-value> 
    </init-param> 
</servlet> 

如果没有该缺省参数,那就使用默认的 Jsp。

类似的,通过扩展 TemplateFactory 和 Template,就可以添加更多的模板支持,例如 FreeMarker。


回页首

设计拦截器

拦截器和 Servlet 规范中的 Filter 非常类似,不过 Filter 的作用范围是整个 HttpServletRequest 的处理过程,而拦截器仅作用于 Controller,不涉及到 View 的渲染,在大多数情况下,使用拦截器比 Filter 速度要快,尤其是绑定数据库事务时,拦截器能缩短数据库事务开启的时间。

拦截器接口 Interceptor 定义如清单 18 所示。
清单 18. 定义 Interceptor

				
public interface Interceptor { 
    void intercept(Execution execution, InterceptorChain chain) throws Exception; 
} 

和 Filter 类似,InterceptorChain 代表拦截器链。InterceptorChain 定义如清单 19 所示。
清单 19. 定义 InterceptorChain

				
public interface InterceptorChain { 
    void doInterceptor(Execution execution) throws Exception; 
} 

实现 InterceptorChain 要比实现 FilterChain 简单,因为 Filter 需要处理 Request、Forward、Include 和 Error 这 4 种请求转发的情况,而 Interceptor 仅拦截 Request。当 MVC 框架处理一个请求时,先初始化一个拦截器链,然后,依次调用链上的每个拦截器。请参考清单 20 所示的代码。
清单 20. 实现 InterceptorChain 接口

				
class InterceptorChainImpl implements InterceptorChain { 
    private final Interceptor[] interceptors; 
    private int index = 0; 
    private Object result = null; 

    InterceptorChainImpl(Interceptor[] interceptors) { 
        this.interceptors = interceptors; 
    } 

    Object getResult() { 
        return result; 
    } 

    public void doInterceptor(Execution execution) throws Exception { 
        if(index==interceptors.length) 
            result = execution.execute(); 
        else { 
            // must update index first, otherwise will cause stack overflow: 
            index++; 
            interceptors[index-1].intercept(execution, this); 
        } 
    } 
} 

成员变量 index 表示当前链上的第 N 个拦截器,当最后一个拦截器被调用后,InterceptorChain 才真正调用 Execution 对象的 execute() 方法,并保存其返回结果,整个请求处理过程结束,进入渲染阶段。清单 21 演示了如何调用拦截器链的代码。
清单 21. 调用拦截器链

				
class Dispatcher  { 
    ... 
    private Interceptor[] interceptors; 
    void handleExecution(Execution execution, HttpServletRequest request, 
        HttpServletResponse response) throws ServletException, IOException { 
        InterceptorChainImpl chains = new InterceptorChainImpl(interceptors); 
        chains.doInterceptor(execution); 
        handleResult(request, response, chains.getResult()); 
    } 
} 

当 Controller 方法被调用完毕后,handleResult() 方法用于处理执行结果。


回页首

渲染

由于我们没有强制 HTTP 处理方法的返回类型,因此,handleResult() 方法针对不同的返回值将做不同的处理。代码如清单 22 所示。
清单 22. 处理返回值

				
class Dispatcher  { 
    ... 
    void handleResult(HttpServletRequest request, HttpServletResponse response, 
            Object result) throws Exception { 
        if (result==null) 
            return; 
        if (result instanceof Renderer) { 
            Renderer r = (Renderer) result; 
            r.render(this.servletContext, request, response); 
            return; 
        } 
        if (result instanceof String) { 
            String s = (String) result; 
            if (s.startsWith("redirect:")) { 
                response.sendRedirect(s.substring(9)); 
                return; 
            } 
            new TextRenderer(s).render(servletContext, request, response); 
            return; 
        } 
        throw new ServletException("Cannot handle result with type '"
                + result.getClass().getName() + "'."); 
    } 
} 

如果返回 null,则认为 HTTP 请求已处理完成,不做任何处理;如果返回 Renderer,则调用 Renderer 对象的 render() 方法渲染视图;如果返回 String,则根据前缀是否有“redirect:”判断是重定向还是作为 HTML 返回给浏览器。这样,客户端可以不必访问 HttpServletResponse 对象就可以非常方便地实现重定向。代码如清单 23 所示。
清单 23. 重定向

				
@Mapping("/register") 
String register() { 
    ... 
    if (success) 
        return "redirect:/reg/success"; 
    return "redirect:/reg/failed"; 
} 

扩展 Renderer 还可以处理更多的格式,例如,向浏览器返回 JavaScript 代码等。


回页首

扩展

使用 Filter 转发

对于请求转发,除了使用 DispatcherServlet 外,还可以使用 Filter 来拦截所有请求,并直接在 Filter 内实现请求转发和处理。使用 Filter 的一个好处是如果 URL 没有被任何 Controller 的映射方法匹配到,则可以简单地调用 FilterChain.doFilter() 将 HTTP 请求传递给下一个 Filter,这样,我们就不必自己处理静态文件,而由 Web 服务器提供的默认 Servlet 处理,效率更高。和 DispatcherServlet 类似,我们编写一个 DispatcherFilter 作为前置处理器,负责转发请求,代码见清单 24。
清单 24. 定义 DispatcherFilter

				
public class DispatcherFilter implements Filter { 
    ... 
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) 
    throws IOException, ServletException { 
        HttpServletRequest httpReq = (HttpServletRequest) req; 
        HttpServletResponse httpResp = (HttpServletResponse) resp; 
        String method = httpReq.getMethod(); 
        if ("GET".equals(method) || "POST".equals(method)) { 
            if (!dispatcher.service(httpReq, httpResp)) 
                chain.doFilter(req, resp); 
            return; 
        } 
        httpResp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); 
    } 
} 

如果用 DispatcherFilter 代替 DispatcherServlet,则我们需要过滤“/*”,在 web.xml 中添加声明如清单 25 所示。
清单 25. 声明 DispatcherFilter

				
<filter> 
    <filter-name>dispatcher</servlet-name> 
    <filter-class>org.expressme.webwind.DispatcherFilter</servlet-class> 
</filter> 
<filter-mapping> 
    <filter-name>dispatcher</servlet-name> 
    <url-pattern>/*</url-pattern> 
</filter-mapping> 

访问 Request 和 Response 对象

如何在 @Mapping 方法中访问 Servlet 对象?如 HttpServletRequest,HttpServletResponse,HttpSession 和 ServletContext。ThreadLocal 是一个最简单有效的解决方案。我们编写一个 ActionContext,通过 ThreadLocal 来封装对 Request 等对象的访问。代码见清单 26。
清单 26. 定义 ActionContext

				
public final class ActionContext { 
    private static final ThreadLocal<ActionContext> actionContextThreadLocal 
            = new ThreadLocal<ActionContext>(); 

    private ServletContext context; 
    private HttpServletRequest request; 
    private HttpServletResponse response; 

    public ServletContext getServletContext() { 
        return context; 
    } 

    public HttpServletRequest getHttpServletRequest() { 
        return request; 
    } 

    public HttpServletResponse getHttpServletResponse() { 
        return response; 
    } 

    public HttpSession getHttpSession() { 
        return request.getSession(); 
    } 

    public static ActionContext getActionContext() { 
        return actionContextThreadLocal.get(); 
    } 

    static void setActionContext(ServletContext context, 
            HttpServletRequest request, HttpServletResponse response) { 
        ActionContext ctx = new ActionContext(); 
        ctx.context = context; 
        ctx.request = request; 
        ctx.response = response; 
        actionContextThreadLocal.set(ctx); 
    } 

    static void removeActionContext() { 
        actionContextThreadLocal.remove(); 
    } 
} 

在 Dispatcher 的 handleExecution() 方法中,初始化 ActionContext,并在 finally 中移除所有已绑定变量,代码见清单 27。
清单 27. 初始化 ActionContext

				
class Dispatcher { 
    ... 
    void handleExecution(Execution execution, HttpServletRequest request, 
    HttpServletResponse response) throws ServletException, IOException { 
        ActionContext.setActionContext(servletContext, request, response); 
        try { 
            InterceptorChainImpl chains = new InterceptorChainImpl(interceptors); 
            chains.doInterceptor(execution); 
            handleResult(request, response, chains.getResult()); 
        } 
        catch (Exception e) { 
            handleException(request, response, e); 
        } 
        finally { 
            ActionContext.removeActionContext(); 
        } 
    } 
} 

这样,在 @Mapping 方法内部,可以随时获得需要的 Request、Response、 Session 和 ServletContext 对象。

处理文件上传

Servlet API 本身并没有提供对文件上传的支持,要处理文件上传,我们需要使用 Commons FileUpload 之类的第三方扩展包。考虑到 Commons FileUpload 是使用最广泛的文件上传包,我们希望能集成 Commons FileUpload,但是,不要暴露 Commons FileUpload 的任何 API 给 MVC 的客户端,客户端应该可以直接从一个普通的 HttpServletRequest 对象中获取上传文件。

要让 MVC 客户端直接使用 HttpServletRequest,我们可以用自定义的 MultipartHttpServletRequest 替换原始的 HttpServletRequest,这样,客户端代码可以通过 instanceof 判断是否是一个 Multipart 格式的 Request,如果是,就强制转型为 MultipartHttpServletRequest,然后,获取上传的文件流。

核心思想是从 HttpServletRequestWrapper 派生 MultipartHttpServletRequest,这样,MultipartHttpServletRequest 具有 HttpServletRequest 接口。MultipartHttpServletRequest 的定义如清单 28 所示。
清单 28. 定义 MultipartHttpServletRequest

				
public class MultipartHttpServletRequest extends HttpServletRequestWrapper { 
    final HttpServletRequest target; 
    final Map<String, List<FileItemStream>> fileItems; 
    final Map<String, List<String>> formItems; 

    public MultipartHttpServletRequest(HttpServletRequest request, long maxFileSize) 
    throws IOException { 
        super(request); 
        this.target = request; 
        this.fileItems = new HashMap<String, List<FileItemStream>>(); 
        this.formItems = new HashMap<String, List<String>>(); 
        ServletFileUpload upload = new ServletFileUpload(); 
        upload.setFileSizeMax(maxFileSize); 
        try { 

...解析Multipart ...

        } 
        catch (FileUploadException e) { 
            throw new IOException(e); 
        } 
    } 

    public InputStream getFileInputStream(String fieldName) throws IOException { 
        List<FileItemStream> list = fileItems.get(fieldName); 
        if (list==null) 
            throw new IOException("No file item with name '" + fieldName + "'."); 
        return list.get(0).openStream(); 
    }; 
} 

对于正常的 Field 参数,保存在成员变量 Map<String, List<String>> formItems 中,通过覆写 getParameter()、getParameters() 等方法,就可以让客户端把 MultipartHttpServletRequest 也当作一个普通的 Request 来操作,代码见清单 29。
清单 29. 覆写 getParameter

				
public class MultipartHttpServletRequest extends HttpServletRequestWrapper { 
    ... 
    @Override 
    public String getParameter(String name) { 
        List<String> list = formItems.get(name); 
        if (list==null) 
            return null; 
        return list.get(0); 
    } 

    @Override 
    @SuppressWarnings("unchecked") 
    public Map getParameterMap() { 
        Map<String, String[]> map = new HashMap<String, String[]>(); 
        Set<String> keys = formItems.keySet(); 
        for (String key : keys) { 
            List<String> list = formItems.get(key); 
            map.put(key, list.toArray(new String[list.size()])); 
        } 
        return Collections.unmodifiableMap(map); 
    } 

    @Override 
    @SuppressWarnings("unchecked") 
    public Enumeration getParameterNames() { 
        return Collections.enumeration(formItems.keySet()); 
    } 

    @Override 
    public String[] getParameterValues(String name) { 
        List<String> list = formItems.get(name); 
        if (list==null) 
            return null; 
        return list.toArray(new String[list.size()]); 
    } 
} 

为了简化配置,在 Web 应用程序启动的时候,自动检测当前 ClassPath 下是否有 Commons FileUpload,如果存在,文件上传功能就自动开启,如果不存在,文件上传功能就不可用,这样,客户端只需要简单地把 Commons FileUpload 的 jar 包放入 /WEB-INF/lib/,不需任何配置就可以直接使用。核心代码见清单 30。
清单 30. 检测 Commons FileUpload

				
class Dispatcher { 
    private boolean multipartSupport = false; 
    ... 
    void initAll(Config config) throws Exception { 
        try { 
            Class.forName("org.apache.commons.fileupload.servlet.ServletFileUpload"); 
            this.multipartSupport = true; 
        } 
        catch (ClassNotFoundException e) { 
            log.info("CommonsFileUpload not found."); 
        } 
        ... 
    } 

    void handleExecution(Execution execution, HttpServletRequest request, 
            HttpServletResponse response) throws ServletException, IOException { 
        if (this.multipartSupport) { 
            if (MultipartHttpServletRequest.isMultipartRequest(request)) { 
                request = new MultipartHttpServletRequest(request, maxFileSize); 
            } 
        } 
        ... 
    } 
    ... 
} 

回页首

小结

要从头设计并实现一个 MVC 框架其实并不困难,设计 WebWind 的目标是改善 Web 应用程序的 URL 结构,并通过自动提取和映射 URL 中的参数,简化控制器的编写。WebWind 适合那些从头构造的新的互联网应用,以便天生支持 REST 风格的 URL。但是,它不适合改造已有的企业应用程序,企业应用的页面不需要搜索引擎的索引,其用户对 URL 地址的友好程度通常也并不关心。

参考资料

学 习