一、引言

  在前面一专题介绍到,要让缓存生效还需要实现对AOP(面向切面编程)的支持。所以本专题将介绍了网上书店案例中AOP的实现。关于AOP的概念,大家可以参考文章:http://www.cnblogs.com/jin-yuan/p/3811077.html。这里我简单介绍下AOP:AOP可以理解为对方法进行截获,这样就可以在方法调用前或调用后插入需要的逻辑。例如可以在方法调用前,加入缓存查找逻辑等。这里缓存查找逻辑就在方法调用前被执行。通过对AOP的支持,每个方法就可以分为3部分了,方法调用前逻辑->具体需要调用的方法->方法调用后的逻辑。也就是在方法调用的时候“切了一刀”。

二、网上书店AOP的实现

  你可以从零开始去实现AOP,但是目前已经存在很多AOP框架了,所以在本案例中将直接通过Unity的AOP框架(Unity.Interception)来实现网上书店对AOP的支持。通常AOP的实现放在基础设施层进行实现,因为可能其他所有层都需要加入对AOP的支持。本案例中将对两个方面的AOP进行实现,一个是方法调用前缓存的记录或查找,另一个是方法调用后异常信息的记录。在实现具体代码之前,我们需要在基础设施层通过Nuget来引入Unity.Interception包。添加成功之后,我们需要定义两个类分别去实现AOP框架中IInterceptionBehavior接口。由于本案例中需要对缓存和异常日志功能进行AOP实现,自然就需要定义CachingBehavior和ExceptionLoggingBehavior两个类去实现IInterceptionBehavior接口。首先让我们看看CachingBehavior类的实现,具体实现代码如下所示:

 // 缓存AOP的实现
public class CachingBehavior : IInterceptionBehavior
{
private readonly ICacheProvider _cacheProvider; public CachingBehavior()
{
_cacheProvider = ServiceLocator.Instance.GetService<ICacheProvider>();
} // 生成缓存值的键值
private string GetValueKey(CacheAttribute cachingAttribute, IMethodInvocation input)
{
switch (cachingAttribute.Method)
{
// 如果是Remove,则不存在特定值键名,所有的以该方法名称相关的缓存都需要清除
case CachingMethod.Remove:
return null;
// 如果是Get或者Update,则需要产生一个针对特定参数值的键名
case CachingMethod.Get:
case CachingMethod.Update:
if (input.Arguments != null &&
input.Arguments.Count > )
{
var sb = new StringBuilder();
for (var i = ; i < input.Arguments.Count; i++)
{
sb.Append(input.Arguments[i]);
if (i != input.Arguments.Count - )
sb.Append("_");
} return sb.ToString();
}
else
return "NULL";
default:
throw new InvalidOperationException("无效的缓存方式。");
}
} #region IInterceptionBehavior Members
public IEnumerable<Type> GetRequiredInterfaces()
{
return Type.EmptyTypes;
} public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
// 获得被拦截的方法
var method = input.MethodBase;
var key = method.Name; // 获得拦截的方法名
// 如果拦截的方法定义了Cache属性,说明需要对该方法的结果需要进行缓存
if (!method.IsDefined(typeof (CacheAttribute), false))
return getNext().Invoke(input, getNext); var cachingAttribute = (CacheAttribute)method.GetCustomAttributes(typeof (CacheAttribute), false)[];
var valueKey = GetValueKey(cachingAttribute, input);
switch (cachingAttribute.Method)
{
case CachingMethod.Get:
try
{
// 如果缓存中存在该键值的缓存,则直接返回缓存中的结果退出
if (_cacheProvider.Exists(key, valueKey))
{
var value = _cacheProvider.Get(key, valueKey);
var arguments = new object[input.Arguments.Count];
input.Arguments.CopyTo(arguments, );
return new VirtualMethodReturn(input, value, arguments);
}
else // 否则先调用方法,再把返回结果进行缓存
{
var methodReturn = getNext().Invoke(input, getNext);
_cacheProvider.Add(key, valueKey, methodReturn.ReturnValue);
return methodReturn;
}
}
catch (Exception ex)
{
return new VirtualMethodReturn(input, ex);
}
case CachingMethod.Update:
try
{
var methodReturn = getNext().Invoke(input, getNext);
if (_cacheProvider.Exists(key))
{
if (cachingAttribute.IsForce)
{
_cacheProvider.Remove(key);
_cacheProvider.Add(key, valueKey, methodReturn.ReturnValue);
}
else
_cacheProvider.Update(key, valueKey, methodReturn);
}
else
_cacheProvider.Add(key, valueKey, methodReturn.ReturnValue); return methodReturn;
}
catch (Exception ex)
{
return new VirtualMethodReturn(input, ex);
}
case CachingMethod.Remove:
try
{
var removeKeys = cachingAttribute.CorrespondingMethodNames;
foreach (var removeKey in removeKeys)
{
if (_cacheProvider.Exists(removeKey))
_cacheProvider.Remove(removeKey);
}
 // 执行具体截获的方法
var methodReturn = getNext().Invoke(input, getNext);
return methodReturn;
}
catch (Exception ex)
{
return new VirtualMethodReturn(input, ex);
}
default: break;
} return getNext().Invoke(input, getNext);
} public bool WillExecute
{
get { return true; }
}
#endregion
}

  从上面代码可以看出,通过Unity.Interception框架来实现AOP变得非常简单了,我们只需要实现IInterceptionBehavior接口中的Invoke方法和WillExecute属性即可。并且从上面代码可以看出,AOP的支持最核心代码实现在于Invoke方法的实现。既然我们需要在方法调用前查找缓存,如果缓存不存在再调用方法从数据库中进行查找,如果存在则直接从缓存中进行读取数据即可。自然需要在 getNext().Invoke(input, getNext)代码执行前进缓存进行查找,然而上面CachingBehavior类正式这样实现的。

  介绍完缓存功能AOP的实现之后,下面具体看看异常日志的AOP实现。具体实现代码如下所示:

// 用于异常日志记录的拦截行为
public class ExceptionLoggingBehavior :IInterceptionBehavior
{
/// <summary>
/// 需要拦截的对象类型的接口
/// </summary>
/// <returns></returns>
public IEnumerable<Type> GetRequiredInterfaces()
{
return Type.EmptyTypes;
} /// <summary>
/// 通过该方法来拦截调用并执行所需要的拦截行为
/// </summary>
/// <param name="input">调用拦截目标时的输入信息</param>
/// <param name="getNext">通过行为链来获取下一个拦截行为的委托</param>
/// <returns>从拦截目标获得的返回信息</returns>
public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
// 执行目标方法
var methodReturn = getNext().Invoke(input, getNext);
// 方法执行后的处理
if (methodReturn.Exception != null)
{
Utils.Log(methodReturn.Exception);
} return methodReturn;
} // 表示当拦截行为被调用时,是否需要执行某些操作
public bool WillExecute
{
get { return true; }
}
}

  异常日志功能的AOP实现与缓存功能的AOP实现类似,只是一个需要在方法执行前注入,而一个是在方法执行后进行注入罢了,其实现原理都是在截获的方法前后进行。方法截获功能AOP框架已经帮我们实现了。

  到此,我们网上书店AOP的实现就已经完成了,但要正式生效还需要通过配置文件把AOP的实现注入到需要截获的方法当中去,这样执行这些方法才会执行注入的行为。对应的配置文件如下标红部分所示:

 <!--Unity的配置信息-->
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />
<container>
<extension type="Interception" /> <!--Cache Provider-->
<register type="OnlineStore.Infrastructure.Caching.ICacheProvider, OnlineStore.Infrastructure" mapTo="OnlineStore.Infrastructure.Caching.EntLibCacheProvider, OnlineStore.Infrastructure" /> <!--仓储接口的注册-->
<register type="OnlineStore.Domain.Repositories.IRepositoryContext, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.EntityFrameworkRepositoryContext, OnlineStore.Repositories">
<lifetime type="singleton" />
</register>
<register type="OnlineStore.Domain.Repositories.IProductRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ProductRepository, OnlineStore.Repositories" />
<register type="OnlineStore.Domain.Repositories.ICategoryRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.CategoryRepository, OnlineStore.Repositories" />
<register type="OnlineStore.Domain.Repositories.IProductCategorizationRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ProductCategorizationRepository, OnlineStore.Repositories" />
<register type="OnlineStore.Domain.Repositories.IUserRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.UserRepository, OnlineStore.Repositories" />
<register type="OnlineStore.Domain.Repositories.IShoppingCartRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ShoppingCartRepository, OnlineStore.Repositories" />
<register type="OnlineStore.Domain.Repositories.IShoppingCartItemRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.ShoppingCartItemRepository, OnlineStore.Repositories" />
<register type="OnlineStore.Domain.Repositories.IOrderRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.OrderRepository, OnlineStore.Repositories" />
<register type="OnlineStore.Domain.Repositories.IUserRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.UserRepository, OnlineStore.Repositories" />
<register type="OnlineStore.Domain.Repositories.IUserRoleRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.UserRoleRepository, OnlineStore.Repositories" />
<register type="OnlineStore.Domain.Repositories.IRoleRepository, OnlineStore.Domain" mapTo="OnlineStore.Repositories.EntityFramework.RoleRepository, OnlineStore.Repositories" /> <!--Domain Services-->
<register type="OnlineStore.Domain.Services.IDomainService, OnlineStore.Domain" mapTo="OnlineStore.Domain.Services.DomainService, OnlineStore.Domain" />
<!--应用服务的注册-->
<register type="OnlineStore.ServiceContracts.IProductService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.ProductServiceImp, OnlineStore.Application">
<!--注入AOP功能的实现-->
<interceptor type="InterfaceInterceptor" />
<interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.CachingBehavior, OnlineStore.Infrastructure" />
<interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, OnlineStore.Infrastructure" />
</register> <register type="OnlineStore.ServiceContracts.IOrderService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.OrderServiceImp, OnlineStore.Application">
<!--注入AOP功能的实现-->
<interceptor type="InterfaceInterceptor" />
<interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.CachingBehavior, OnlineStore.Infrastructure" />
<interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, OnlineStore.Infrastructure" />
</register>
<register type="OnlineStore.ServiceContracts.IUserService, OnlineStore.ServiceContracts" mapTo="OnlineStore.Application.ServiceImplementations.UserServiceImp, OnlineStore.Application">
<!--注入AOP功能的实现-->
<interceptor type="InterfaceInterceptor" />
<interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.CachingBehavior, OnlineStore.Infrastructure" />
<interceptionBehavior type="OnlineStore.Infrastructure.InterceptionBehaviors.ExceptionLoggingBehavior, OnlineStore.Infrastructure" />
</register>
<!--Domain Event Handlers-->
<register type="OnlineStore.Domain.Events.IDomainEventHandler`1[[OnlineStore.Domain.Events.OrderDispatchedEvent, OnlineStore.Domain]], OnlineStore.Domain" mapTo="OnlineStore.Domain.Events.EventHandlers.OrderDispatchedEventHandler, OnlineStore.Domain" name="OrderDispatchedEventHandler" />
<register type="OnlineStore.Domain.Events.IDomainEventHandler`1[[OnlineStore.Domain.Events.OrderConfirmedEvent, OnlineStore.Domain]], OnlineStore.Domain" mapTo="OnlineStore.Domain.Events.EventHandlers.OrderConfirmedEventHandler, OnlineStore.Domain" name="OrderConfirmedEventHandler" /> <!--Event Handlers-->
<register name="orderSendEmailHandler" type="OnlineStore.Events.IEventHandler`1[[OnlineStore.Domain.Events.OrderDispatchedEvent, OnlineStore.Domain]], OnlineStore.Events" mapTo="OnlineStore.Events.Handlers.SendEmailHandler, OnlineStore.Events.Handlers" /> <!--Event Aggregator-->
<register type="OnlineStore.Events.IEventAggregator, OnlineStore.Events" mapTo="OnlineStore.Events.EventAggregator, OnlineStore.Events">
<constructor>
<param name="handlers">
<array>
<dependency name="orderSendEmailHandler" type="OnlineStore.Events.IEventHandler`1[[OnlineStore.Domain.Events.OrderDispatchedEvent, OnlineStore.Domain]], OnlineStore.Events" />
</array>
</param>
</constructor>
</register> <!--Event Bus-->
<!--<register type="OnlineStore.Events.Bus.IEventBus, OnlineStore.Events" mapTo="OnlineStore.Events.Bus.EventBus, OnlineStore.Events">
<lifetime type="singleton" />
</register>--> <!--注入MsmqEventBus-->
<register type="OnlineStore.Events.Bus.IEventBus, OnlineStore.Events"
mapTo="OnlineStore.Events.Bus.MsmqEventBus, OnlineStore.Events">
<lifetime type="singleton" />
<constructor>
<param name="path" value=".\Private$\OnlineStoreQueue" />
</constructor>
</register>
</container>
</unity>
<!--END: Unity-->

  到此,网上书店案例中AOP的实现就完成了。通过上面的配置可以看出,客户端在调用应用服务方法前后会调用我们注入的行为,即缓存行为和异常日志行为。通过对AOP功能的支持,就不需要为每个需要进行缓存或需要异常日志行为的方法来重复写这些相同的逻辑了。从而避免了重复代码的重复实现,提高了代码的重用性和降低了模块之间的依赖性。

三、网上书店案例中站点地图的实现

  在大部分网站中都实现了站点地图的功能,在Asp.net中,我们可以通过SiteMap模块来实现站点地图的功能,在Asp.net MVC也可以通过MvcSiteMapProvider第三方开源框架来实现站点地图。所以针对网上书店案例,站点地图的支持也是必不可少的。下面让我们具体看看站点地图在本案例中是如何去实现的呢?

  在看实现代码之前,让我们先来理清下实现思路。

  本案例中站点地图的实现,并没有借助MvcSiteMapProvider第三方框架来实现。其实现原理首先获得用户的路由请求,然后根据用户请求根据站点地图的配置获得对应的配置节点,接着根据站点地图的节点信息生成类似">首页>"这样带标签的字符串;如果获得的节点是配置文件中某个父节点的子节点,此时会通过递归的方式找到其父节点,然后递归地生成对应带标签的字符串,从而完成站点地图的功能。分析完实现思路之后,下面让我们再对照下具体的实现代码来加深理解。具体的实现代码如下所示:

public class MvcSiteMap
{
private static readonly MvcSiteMap _instance = new MvcSiteMap(); private static readonly XDocument Doc = XDocument.Load(HttpContext.Current.Server.MapPath(@"~/SiteMap.xml")); private UrlHelper _url = null; private string _currentUrl; public static MvcSiteMap Instance
{
get { return _instance;}
} private MvcSiteMap()
{
} public MvcHtmlString Navigator()
{
// 获得当前请求的路由信息
_url = new UrlHelper(HttpContext.Current.Request.RequestContext);
var routeUrl = _url.RouteUrl(HttpContext.Current.Request.RequestContext.RouteData.Values);
if (routeUrl != null)
_currentUrl = routeUrl.ToLower(); // 从配置的站点Xml文件中找到当前请求的Url相同的节点
var c = FindNode(Doc.Root);
var temp = GetPath(c); return MvcHtmlString.Create(BuildPathString(temp));
} // 从SitMap配置文件中找到当前请求匹配的节点
private XElement FindNode(XElement node)
{
// 如果xml节点对应的url是否与当前请求的节点相同,如果相同则直接返回xml对应的节点
// 如果不同开始递归子节点
return IsUrlEqual(node) == true ? node : RecursiveNode(node);
} // 判断xml节点对应的url是否与当前请求的url一样
private bool IsUrlEqual(XElement c)
{
var a = GetNodeUrl(c).ToLower();
return a == _currentUrl;
} // 递归Xml节点
private XElement RecursiveNode(XElement node)
{
foreach (var c in node.Elements())
{
if (IsUrlEqual(c) == true)
{
return c;
}
else
{
var x = RecursiveNode(c);
if (x != null)
{
return x;
}
}
} return null;
} // 获得xml节点对应的请求url
private string GetNodeUrl(XElement c)
{
return _url.Action(c.Attribute("action").Value, c.Attribute("controller").Value,
new {area = c.Attribute("area").Value});
} // 根据对应请求url对应的Xml节点获得其在Xml中的路径,即获得其父节点有什么
// SiteMap.xml 中节点的父子节点一定要配置对
private Stack<XElement> GetPath(XElement c)
{
var temp = new Stack<XElement>();
while (c != null)
{
temp.Push(c);
c = c.Parent;
}
return temp;
} // 根据节点的路径来拼接带标签的字符串
private string BuildPathString(Stack<XElement> m)
{
var sb = new StringBuilder();
var tc = new TagBuilder("span");
tc.SetInnerText(">");
var sp = tc.ToString();
var count = m.Count;
for (var x = ; x <= count; x++)
{
var c = m.Pop();
TagBuilder tb;
if (x == count)
{
tb = new TagBuilder("span");
}
else
{
tb = new TagBuilder("a");
tb.MergeAttribute("href", GetNodeUrl(c));
} tb.SetInnerText(c.Attribute("title").Value);
sb.Append(tb);
sb.Append(sp);
} return sb.ToString();
}
}

  对应的站点地图配置信息如下所示:

<?xml version="1.0" encoding="utf-8" ?>
<node title="首页" area="" action="Index" controller="Home">
<node title="我的" area="UserCenter" action="Manage" controller="Account">
<node title="订单" area="" action="Orders" controller="Home" />
<node title="账户" area="" action="Manage" controller="Account" />
<node title="购物车" area="" action="ShoppingCart" controller="Home" />
</node>
<node title="关于" area="AboutCenter" action="About" controller="Home" >
<node title="Online Store 项目" area="" action="About" controller="Home" />
<node title="联系方式" area="" action="Contact" controller="Home" />
</node>
</node>

  实现完成之后,下面让我们具体看看本案例中站点地图的实现效果看看,具体运行效果如下图所示:

四、小结

  到这里,本专题的内容就结束了。本专题主要借助Unity.Interception框架在网上书店中引入了AOP功能,并且最后简单介绍了站点地图的实现。在下一专题将对CQRS模式做一个全面的介绍。

  本案例所有源码:https://github.com/lizhi5753186/OnlineStore_Second/

[.NET领域驱动设计实战系列]专题九:DDD案例:网上书店AOP和站点地图的实现的更多相关文章

  1. [.NET领域驱动设计实战系列]专题十一:.NET 领域驱动设计实战系列总结

    一.引用 其实在去年本人已经看过很多关于领域驱动设计的书籍了,包括Microsoft .NET企业级应用框架设计.领域驱动设计C# 2008实现.领域驱动设计:软件核心复杂性应对之道.实现领域驱动设计 ...

  2. [.NET领域驱动设计实战系列]专题一:前期准备之EF CodeFirst

    一.前言 从去年已经接触领域驱动设计(Domain-Driven Design)了,当时就想自己搭建一个DDD框架,所以当时看了很多DDD方面的书,例如领域驱动模式与实战,领域驱动设计:软件核心复杂性 ...

  3. [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店

    一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这 ...

  4. [.NET领域驱动设计实战系列]专题十:DDD扩展内容:全面剖析CQRS模式实现

    一.引言 前面介绍的所有专题都是基于经典的领域驱动实现的,然而,领域驱动除了经典的实现外,还可以基于CQRS模式来进行实现.本专题将全面剖析如何基于CQRS模式(Command Query Respo ...

  5. [.NET领域驱动设计实战系列]专题七:DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能

    一.引言 在当前的电子商务平台中,用户下完订单之后,然后店家会在后台看到客户下的订单,然后店家可以对客户的订单进行发货操作.此时客户会在自己的订单状态看到店家已经发货.从上面的业务逻辑可以看出,当用户 ...

  6. [.NET领域驱动设计实战系列]专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现

    一.引言 在上一专题中,商家发货和用户确认收货功能引入了消息队列来实现的,引入消息队列的好处可以保证消息的顺序处理,并且具有良好的可扩展性.但是上一专题消息队列是基于内存中队列对象来实现,这样实现有一 ...

  7. [.NET领域驱动设计实战系列]专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现

    一.前言 在前面2篇博文中,我分别介绍了规约模式和工作单元模式,有了前面2篇博文的铺垫之后,下面就具体看看如何把这两种模式引入到之前的网上书店案例里. 二.规约模式的引入 在第三专题我们已经详细介绍了 ...

  8. [.NET领域驱动设计实战系列]专题六:DDD实践案例:网上书店订单功能的实现

    一.引言 上一专题已经为网上书店实现了购物车的功能了,在这一专题中,将继续对网上书店案例进行完善,本专题将对网上书店订单功能的实现进行介绍,现在废话不多说了,让我们来一起看看订单功能是如何实现的吧. ...

  9. [.NET领域驱动设计实战系列]专题四:前期准备之工作单元模式(Unit Of Work)

    一.前言 在前一专题中介绍了规约模式的实现,然后在仓储实现中,经常会涉及工作单元模式的实现.然而,在我的网上书店案例中也将引入工作单元模式,所以本专题将详细介绍下该模式,为后面案例的实现做一个铺垫. ...

随机推荐

  1. 用Action的属性接受参数

    版本, @Override是Java5的元数据,自动加上去的一个标志,告诉你说下面这个方法是从父类/接口 继承过来的,需要你重写一次,这样就可以方便你阅读,也不怕会忘记@Override是伪代码,表示 ...

  2. Ajax提交与传统表单提交的区别说明

    Ajax提交是通过js来提交请求,请求与响应均由js引擎来处理,页面不会刷新,用户感觉不到实际上浏览器发出了请求.比如说我们希望网页总是显示最新的新闻,而又不想老是去点刷新按钮,我们就可以用Ajax机 ...

  3. mysql 行变列(多行变成一行/多行合并成一行/多行合并成多列/合并行)

    数据库结构如图: 而我想让同一个人的不同成绩变成此人在这一行不同列上显示出来,此时分为2中展现: 第一种展现如图----[多行变一列](合并后的数据在同一列上): sql如下: select name ...

  4. CSS里常见的块级元素和行内元素

    根据CSS规范的规定,每一个网页元素都有一个display属性,用于确定该元素的类型,每一个元素都有默认的display属性值,比如div元素,它的默认display属性值为“block”,成为“块级 ...

  5. Servlet下载文件和http响应

    下载文件等: 1.得到公共的内容ServletContext sc = this.getServletContext(); 2.在链接名字后面加个? 一个参数?参数1=值 两个参数?参数1=值& ...

  6. 添加Properties取值和枚举取值

    <!--配置文件--><property name="ppp"> <props> <prop key="11"> ...

  7. 不同包中继承关系访问protected内部类问题

    有两个包pack1和pack2,pack1中是父类,pack2中子类继承自pack1中的父类.这里主要探索一下子类访问父类中protected内部类的问题: 第一个类: package pack1; ...

  8. Rails中的content_tag与concat用法,可以连接任意html元素

    想输出如下html <% if user.present? %> <li> <a href="<%= user_info_url(user.id) %&g ...

  9. Tiled Map地图编辑器键盘快捷键

    Tiled是款不错的地图编辑器,不过快捷键真是隐蔽啊,不看github上得wiki根本不知道,用的过程中查英文文档总是觉得慢,所以翻译成了中文. 通用 右键点击图块(tile):复制图块到图章刷(拖动 ...

  10. 一些webGL的资源

    作为一个新手,把资源写在这里. 一个简介: http://www.html5china.com/HTML5features/WebGL/20111129_2985.html 类似NEHE OPENGL ...