文章分类 » 技术

领域驱动设计在马蜂窝优惠中心重构中的实践

前言

正如领域驱动设计之父 Eric Evans 所著一书的书名所述,领域驱动设计(Domain Driven Design)是一种软件核心复杂性应对之道。

在我们解决现实业务问题时,会面对非常复杂的业务逻辑。即使是同一个事物,在多个子业务单元下代表的意思也是不完全一样的。比如「商品」这个词,在商品详情页语境中,是指「商品基本信息」;在下单页语境中,是指「购买项」;而在物流页面语境中,又变成了「被运送的货物」。

DDD 的核心思想就是让正确的领域模型发挥作用。所谓「术业有专攻」,DDD 指导软件开发人员将不同的子业务单元划分为不同的子领域,在各个子领域内部分别对事物进行建模,来应对业务的复杂性。

Part 1

重构优惠中心的背景

我们在实际的开发过程中都遇到过这种情况,最初因为业务逻辑比较单一,为了快速实现功能, 以及对成本、风险等因素的综合考虑,我们会为业务统一创建一个大的模型,各个模块都使用这同一个模型。但随着业务的发展,各子领域的逻辑越来越复杂,对这个大模型的修改就会变成一种灾难,有时明明是要改一个 A 子领域的逻辑,却莫名其妙影响到了 B 或者 C 子领域的线上功能。

优惠中心就是一个例子。优惠中心主要负责马蜂窝各业务线商品的优惠活动管理,以及计算不同用户的优惠结果。「商品管理」和「优惠管理」作为两个不同的业务单元,在初期被设计为共用一个商品模型,由商品模块统一管理。

图1 :初期商品模型

出现的问题

随着业务的发展,优惠的形式不断推陈出新,业务形态逐渐多样,业务方的需求也越来越个性化,导致后期的优惠中心无论从功能上还是系统上都出现了一些具体的问题:

1. 功能上来说,不够灵活

优惠信息是作为商品信息的一个属性在商品管理模块配置的。比如为了引导用户使用 App 需要设置 A 类型优惠,就通过在商品信息的编辑页面增加一个 A 类型优惠配置项实现;如果某个商品的 A 类型优惠需要在 0:00 分生效,业务同学就必须在电脑前等到 0:00 更新商品信息来上线优惠活动。

另外,如果想要创建针对所有商品都适用的优惠,按照之前的模式,所有的商品都要设置一遍,这几乎是不可接受的。

2. 从系统层面看,不易扩展

优惠信息存储在商品信息中,优惠信息是通过商品管理模块的接口输出的。如果要新增一种优惠类型,商品信息相关的表就要增加字段,商品的表会越来越大;如果要迭代一个优惠的逻辑,就有可能影响到商品管理模块的功能。

3. 不利于迭代

由于优惠信息仅仅作为商品的一个属性,没有自己的生命周期,所以很难去统计某一次设置的优惠的投入产出比,从而指导后续的功能优化。

重构优惠中心的预期

  • 系统层面上,要把优惠相关的业务逻辑独立出来,单独设计和实现;
  • 应用层面上,优惠中心会有自己的独立后台,负责管理优惠活动;也会有独立的优惠计算接口,负责 C 端用户使用优惠时的计算。

Part 2

为什么选择 DDD

避免贫血模型

基于传统的 MVC 架构开发功能的时候,Model 层本质上是一个 DAO 层,业务逻辑通常会封装在 Service 层,然后 Controller 通过调用 Service 层来完成对外的功能。这种模式下,数据和行为分别被割裂到了 Model 和 Service 两层。我们把这种只承载数据,但没有业务行为的 Model 称为「贫血模型」。

我们在和业务方了解需求的过程中,使用到的对象都是现实业务的映射,是行为和属性的综合体。需求确定好之后,我们开发的过程中,人为把行为和数据拆分成了两部分,做了一次转换。随着需求的迭代,人员的更迭,开发看到的代码和业务方的需求越来越对应不上,导致很多代码谁也不知道对应的是什么业务逻辑,这种现象被称为由贫血模型带来的「失忆症」,最终导致的是一个维护成本极高的大泥潭系统。

领域驱动设计的核心就是基于业务逻辑去建模,避免贫血模型,减少设计和开发过程中对业务信息的丢失和转换。在业务逻辑迭代的过程中,系统通过调整对应的业务模型就可以完成迭代。

Part 3

落地过程

关键点:业务逻辑抽象

要做到基于业务逻辑建模,就要合理地抽象。因为业务表象千差万别,产品经理和软件设计人员需要和业务专家深入交流,并且从离散的信息中抽象出业务内在的逻辑。

比如旅游业务售卖的商品和标品不同,有些优惠是不考虑人群的,比如使用优惠券,所有类型的库存都可以享受;但如 N 人 N 折这类优惠,成人价可以享受,儿童价和单房差就不可以。基于这个特点,我们对优惠中心的商品模型做了抽象,抽象出来「是否可以参与件数计算」和 「是否可以参与价格计算」两个通用属性。这样既实现了基于业务逻辑建模,又不会陷入业务逻辑千差万别的表象中。

3.1 战术设计

第一步:统一语言,提炼关键词

准确的语言对于产品、运营、开发等各方对齐需求非常重要,我们需要将优惠逻辑当中的概念抽象为各方都能理解的词语,以达成共识。作为开发人员来说,对领域的理解一般来说是比较少的,为了抽象出合理的语言让产品和业务方都能理解,就需要充分理解业务背景和需求。在熟悉业务和需求的过程中,提炼出若干关键字,这些关键词就是最初产生的领域概念和通用语言。比如:

  • 优惠类型:表示一种优惠规则和对应的优惠方案。比如早鸟优惠,就是早多少钱买(优惠规则),减多少钱/打几折(优惠方案);
  • 优惠活动:拥有完整的生命周期,需要包含时间、平台、人员、商品等(限制维度)的某种优惠类型的使用过程信息;
  • 优惠发现:根据指定的商品、人员和平台,找出可以使用的优惠活动列表服务;
  • 优惠计算:根据指定的商品、人员、平台以及购买数量,计算出这一次购买行为可以享受的优惠金额及优惠明细;
  • 优惠排序:各种优惠类型在计算的时候是有先后顺序的,如果有打折的优惠存在,那顺序不同,计算的结果也会不同;
  • 优惠互斥:某些优惠之间存在互斥的关系,比如使用了金卡 96 折优惠,就不能使用马蜂窝优惠券。

第二步:抽象领域模型

根据单一职责的原则,一个领域概念对应一个领域对象。领域对象有实体值对象之分:

  • 实体:实体是有状态的和唯一标识的,包含属性和行为;
  • 值对象:值对象是无状态的,是只读的,包含属性和行为。

区分实体和值对象对系统设计有很大意义,实体是我们需要重点关注和设计的,而值对象则只使用它的「值」就可以了。这样可以简化系统的复杂度,将精力聚焦在核心领域对象。不难理解,优惠活动毋庸置疑是一个实体,优惠类型就是一个值对象。

但也存在某些业务行为是不能归于某个实体或值对象的,可以将它们归为领域服务:

  • 领域服务:领域服务本质上就是一些操作,不包含状态,通常用于协调多个实体。实体和值都属于领域对象,领域对象之间的交互逻辑不能放在领域对象内部,必须由服务来实现,从而有效地保护领域模型。

有一些领域逻辑,比如「优惠排序」和「优惠互斥」,他们涉及到多个优惠类型,也就是多个领域对象。如果也被设计为领域对象,就打破了单一职责的原则,所以我们把这部分跨多个领域对象的业务逻辑放到「领域服务」层。

第三步:抽象领域对象之间的关联关系

将相关联的领域对象进行显式分组,来表达整体的概念(也可以是单一的领域对象),也就是「聚合」

比如优惠活动是优惠类型、优惠范围等的聚合;优惠类型是优惠规则和优惠方案的聚合;优惠规则是限制维度的聚合;优惠方案是优惠手段的聚合:

图2 :关联关系示意

聚合的主要功能是把领域对象分组,外部的唯一访问点就是聚合根,这样可以避免处理领域对象间的一一对应关系,只需要处理聚合和聚合之间的关系就行了。

第四步:走查场景,调整领域模型

领域模型的调整是贯穿整个设计和开发过程的,随着业务的调整,领域模型也需要调整。比如优惠中心后期引入了会员卡的优惠类型,那么就需要把优惠券这个优惠类型的显示,调整为与会员卡互斥的优惠券和与会员卡不互斥的两种。

第五步:简化设计,降低系统复杂度

建模的本质是对现实事物的一种简化和抽象,指导我们忽略和问题域无关的事实,提取和问题域息息相关的信息。以优惠中心为例,最初的方案里我们设计了优惠类型管理的功能,根据不同的优惠规则和优惠方案自动组合成不同类型的优惠类型。但是可以预见,未来的优惠类型是有限的,并且每个优惠类型都有会自己的特殊配置,比如 N 人优惠里的 每 N 人/第 N 人;早鸟中的提前 N 天等。也就是说,根据优惠规则和优惠方案自动生成优惠类型基本是没有使用场景的,因此也就去掉了这个设计。

再如,对优惠的限制我们最初是设计在优惠活动维度,经过权衡,为了降低系统复杂度,最后实现在了优惠类型层面。以「蜂抢」优惠类型为例,它的规则是所有的蜂抢活动都是 1 个用户只能抢一次,没有必要把这个限制放在优惠活动维度,在优惠类型层面控制就可以了。

3.2 战略设计

战略设计处理的是不同限界上下文之间的拆分和集成逻辑。限界上下文比较抽象,结合我们在文章开始提到的不同语境中的「商品」例子来理解,同一个词如果不说明白所处的语境,是无法准确描述清楚其表达的含义的。「语境」其实就是「上下文」,对应不同「子领语」。同理,如果不在一个限定好的上下文中去设计领域模型,设计出的领域模型是不清晰的,它就会同时支持多个上下文。

这里需要说明一点,如果是从零搭建一个全新的电商系统,首先需要做的应该是战略设计。而优惠中心是建立在现有大的电商系统基础上,相当于作为其中一个子领域进行重构,所以我们才会先来做战术设计,再考虑在完整的电商系统下它与外部其他环境之间的关系,也就是战略设计。

优惠中心内部场景区分

优惠中心包括了服务于 B 端用户的优惠活动管理和服务于 C 端用户的优惠计算这两个不同的子业务单元:

图3 :优惠中心内部场景区分

  • 优惠活动处理的是优惠活动的增删改查,以及配套的统计等业务;优惠活动在这里是一个实体,有完整的生命周期,有上线、下线等状态,可以被创建和删除;
  • 优惠计算处理的是一个订单能享受哪些优惠,并减多少钱的问题;在这个场景里,优惠活动是一个值对象,只提供优惠计算需要的必要参数即可。

优惠中心与外部系统集成

在整个电商系统的环境下,优惠中心作为一个子域,处于自己的限界上下文当中。使用优惠中心服务的详情页、下单页都处于自己各自的限界上下文,所以调用优惠中心的时候就需要设计它们之间的上下文映射方式。

调用和被调用方使用的战略设计方法通常有以下几种:

  • 客户方-供应方:适用于同一个团队之间的协作,上游会有严格的自动化测试,来保证给到下游的数据是一定符合约定的;
  • 遵奉者:适用于不同团队协作,且上游不关心下游的标准,下游又完全「逆来顺受」地接受了上游给的数据的场景;
  • 防腐层:适用于上游不关心下游的标准,但是下游不甘心「逆来顺受」,就增加一层,来做转换处理,保持下游系统的独立性;
  • 开放主机服务:适用于中台(通用能力平台),对接方非常多,业务重复度高,并且已经有完善的测试机制和通用的模型。

结合我们的实际情况来看,调用优惠中心的可能会是不同团队的开发人员,而优惠中心又不想被不同的上游侵入内部设计中,所以「客户方-供应方」和「遵奉者」模型都不适合;另外优惠中心前期接入方会比较少,而且会不断迭代,使用「开放主机服务」也不太合适。综合考虑下,防腐层的设计比较适合优惠中心。

下图是优惠中心的业务架构示意,中间的应用服务层采用的就是防腐层的设计,反映优惠中心与外部系统集成时的上下文映射关系:

图4 :优惠中心业务架构

3.3 架构实现

优惠中心选择的是经典的分层架构。从上到下为用户接口层、应用服务层、领域层和仓储层。图中不同的颜色块分别对映外部服务、应用服务、领域服务、聚合根、实体、值对象和仓储。

图5 :优惠中心分层领域模型

  • 用户接口层:处理和终端用户的交互逻辑;
  • 应用服务层:负责封装和转换领域层的返回数据给用户接口层;
  • 领域层:优惠中心的核心逻辑都在这一层,包括领域对象和领域服务。
  • 仓储层:仓储层负责把内存中的领域对象落地到存储介质,也负责从存储介质拿到原始数据后构造领域对象给领域层使用;这一层对领域层隐藏了底层的存储细节。虽然仓储层处在领域层下方,但是我们实现过程中采用了依赖注入的方式,将仓储层的具体实现注入到领域层中。

Part 4

问题及近期规划

1. 价格层优惠

现在公司面没有一个统一的商品中心,并且各业务线对商品的定义差别很大。比如自由行的商品包括出行日期、价格类别(成人价、儿童价)和套餐类别等层级;而火车票的商品包含座次、席别、目的地和出发地等层级。

如果优惠中心抽象出一种通用的商品层级来适配各个业务线,那实际上就是优惠中心要对商品进行标准定义,但是这个标准与后续商品中心的标准定义很有可能是不一致的,如果不一致优惠中心就要做大的改版。所以最终的解决方案可能还要通过推进统一商品中心的建立来解决。

2. 性能问题

领域驱动设计带来的弊端就是类的增多。目前优惠中心的技术栈基于 PHP, PHP 是一种解释型语言,在DDD 模式下即使有了 OPCode 等缓存技术,执行阶段的耗时相对其他静态数据类型的语言还是较大。所以后面计划将优惠中心使用 Java 技术栈重构,来进行性能上的优化。

Part 5

小结

本文介绍了马蜂窝电商优惠中心基于 DDD 进行重构的一些实践经验。DDD 的思想也帮助我们在业务迭代的过程中将架构设计得更加合理。

当然,是否采用业务驱动设计的思想,需要取决于业务和团队的实际情况。在马蜂窝业务的快速发展下,我们在架构设计上还将做更多的探索,也将持续与大家交流。

方法内统计耗时的几种方法

1、自己写,计时开始结束使用System.currentTimeMillis()

long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(3L);
System.out.println(“method finish , use time ” +(System.currentTimeMillis() – start) + “ms”);

2、使用StopWatch类来实现

StopWatch类有不同的实现,spring有一个,guava也有一个,具体使用根据自己项目的依赖情况。真正使用上基本一致。

使用方式:

System.out.println(“SLAMonitorThread.main() start”);
StopWatch sw = new StopWatch();
sw.start();
TimeUnit.SECONDS.sleep(1L);
sw.split();
System.out.println(
“SLAMonitorThread.main() end. split:” + sw.getSplitTime() + “, ” + sw.toSplitString());
TimeUnit.SECONDS.sleep(1L);
sw.split();
System.out.println(
“SLAMonitorThread.main() end. split:” + sw.getSplitTime() + “, ” + sw.toSplitString());
TimeUnit.SECONDS.sleep(1L);
sw.split();
System.out.println(
“SLAMonitorThread.main() end. split:” + sw.getSplitTime() + “, ” + sw.toSplitString());
TimeUnit.SECONDS.sleep(1L);
System.out.println(“SLAMonitorThread.main() end. end:” + sw.getTime() + “, ” + sw.toString());
long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(1L);

两种方式的优缺点:
方式1是不用任何外部依赖就可以实现的,但是缺点也同样明显,每个位置要打印一次的时候,都需要自己计算,并且还要重新拿到开始时间。
方式2则正好相反,需要guava的依赖,但是好处是提供了比较常用的接口支持,在不同的位置统计,并且计算一些东西,比如某一段耗时占比之类的,都可以友好支持。

具体使用时根据自己情况即可。

链家小区数据爬取

这次爬取链家数据时爬取的链家北京区域的所有小区数据。
区域比较少,手动去把每个区的列表页面罗列了出来(所有分区全家起来288个)
没有去处理当前总共多少页,之类的数据,而是把这个数据导入到一张表里,表字段设计了页数和当前爬取的页码,这样方便重试,也不做无用功跑已经跑过的数据。
由于一开始只需要小区的名称,地址,当前均价等,这些信息都有了,所以也没有去爬详情。
列表页面的URL格式为:https://bj.lianjia.com/xiaoqu/guangqumen/
默认为第一页,首页之后的格式均为在当前URL后面添加 pg+pageNo+/ 。
String realUrl = baseUrl + “pg” + pageNo + “/”;
通过写好的可以使用代理的HttpClient发起请求,列表页还是很容易请求成功的。
然后使用Jsoup解析请求到的网页字符串(据大神说,用XPath更高端更牛皮,不过我用着Jsoup还很顺手,就暂时不换了 – 主要以前用jquery习惯,所以找起来也方便)
import org.jsoup.Jsoup;
Document doc = Jsoup.parse(html);
打开网页调试工具,找到翻页位置的元素,发现:翻页的所有链接都在li[class=house-lst-page-box]元素内的超链接标签<a>上,并且最后一个超链接标签就是最大页码的标签,标签上的属性 data-page 就是页码,也就是最大页数,正好提取出来
代码:
Elements pageList = doc.select(“li[class=house-lst-page-box]”).select(“a”);
Element a = Safes.first(Lists.reverse(pageList));
Integer pageCount = Integer.valueOf(a.attr(“data-page”));
至此最大页数和当前页都已经拿到了。接着是收集小区信息。
小区列表中每个小区的信息就在一堆class=xiaoquListItem的li元素里,包含了列表里需要的所有小区的信息,先把每个小区的块都拿到。(当时爬的时候,下面填充用的标签还用的是a标签,写文章时候已经改成div和a标签交叉的了,感觉链家也不是没有做反爬虫,只是做的比较简单,下面呈上原来的老代码)
ElementsxiaoquList=doc.select(“li[class=xiaoquListItem]”).select(“a”).select(“a[class=PageLink]”)
接下来看源码就比较清楚了,里面分了三大块,左侧是图片,中间是小区名称、最近的成交信息、地址信息、代理人信息和标签信息,放在class=info的div里,每一块信息是一个div,右侧是价格、在售信息等内容,放在class=xiaoquListItemRight的div里。
从里面分别取出来这些信息,并放到对应对象里
List<LianJiaXiaoquEntity> entityList = Lists.newArrayList();
Safes.of(xiaoquList).forEach(xiaoqu -> {
    LianJiaXiaoquEntity entity = new LianJiaXiaoquEntity();
    Element title = Safes.first(xiaoqu.select(“div[class=title]”)).selectFirst(“a”);
    entity.setName(title.text());
    entity.setUrl(title.attr(“href”));
    Optional.ofNullable(Safes.first(xiaoqu.select(“div[positionInfo]”))).ifPresent(positionInfo -> {
        positionInfo.text();// \r\n&nbsp;\r\n&nbsp;/板楼/塔板结合/r/n&nbsp;2002年建成
        entity.setNameDetail(StringUtils.join(positionInfo.select(“a”).stream().map(Element::text).collect(Collectors.toList()), “-“) + entity.getName());
    });
    Optional.ofNullable(Safes.first(xiaoqu.select(“div[class=xiaoquListItemRight]”))).ifPresent(price -> {
        entity.setAveaPrice(Safes.first(price.select(“span”)).text());
        entity.setPriceTime(Safes.first(price.select(“div[class=priceDesc]”)).text());
    });
    entityList.add(entity);
这样,就把列表里一些小区的基本信息保存下来了。
但是我需要的不只是这些,主要还需要小区的坐标,这个在列表里没有。找小区坐标的过程也是一波三折,接下来说。

在mybatis中清空/全部删除表数据

有个定时任务一直以来直接跑的增量,基于跑了一段时间的定时任务的情况来看,发现有些情况没法通过增量来处理,比如被扫描表里有数据删除掉了;这种情况在我的定时任务里就没法定位出来对相应数据做处理。而在跑了定时任务之后如果再扫描结果表把已经被删除的数据筛选出来感觉是个很糙很糟糕的做法。

于是决定,在跑定时任务之前先清空整个结果表中的数据。思考之后有以下四种方式。

1、删除表重建表(drop table , create table )
虽然这种方式速度也很快,但是在业务工程里去做DDL操作感觉不太合适,而且操作相对复杂的多,放弃这种方案。

2、使用delete from table语句删除所有数据。
从逻辑上来说这个完全没有问题。但是delete语句虽然我们在执行的时候是清空所有数据,不需要加where条件,但是实际上数据库做的操作还是一条一条删除数据。在这个过程中数据库需要对每一次操作记录事务日志。数据量比较小的时候,这个操作也很快,当数据量比较大的时候,这个操作将会耗费比较长的时间。而且delete操作并不释放空间。

3、使用 delete table 语句删除所有数据。
这种方式也是删除表中所有数据,速度也快,唯一的一点,就是不释放空间。如果没有更好的方式,我将选择这种方式来实现我的设计。显然,有更好的方式。

4、使用truncate语法清空表。
查询过truncate语法和truncate与delete语法之间的区别就会知道,不同之处就在于,truncate会直接删掉相应的数据文件,这样不仅清空数据,而且释放了空间。另外就是,速度非常快。truncate的实现方式也是通过系统直接删除文件,这样的方式基本没有更快而且更节省时间的了。

经过思考,决定选择第四种。那么在mybatis中如何去执行truncate语句呢?查询资料,篇博客说使用@Select注解 ,然后执行的脚本字符串写truncate就可以,于是按照这个方式做。做完之后单元测试的时候发现,进入方法之后既没有执行结果,也没有执行错误的异常,很是尴尬啊。。。

后来想想,按说这里是一个对表(数据)的操作,按说不应该使用Select注解啊,这是个只读的。于是尝试,将注解改为使用@Update,果然成功!

因此得出结论:在mybatis中执行truncate语句需要按照如下方式:

@Update("TRUNCATE TABLE tmp_truncate_table")
void truncate();

ps.我这里使用的是mybatis的全局注解的方式,如果是使用配置文件,应该只需要在mapper文件中添加Update标签并将语句写到里面即可。

利用JAVA注解实现pv统计功能

利用JAVA注解实现pv统计功能用到的部分资料

 

http://www.tuicool.com/articles/JN73Ejb

http://www.jb51.net/article/67050.htm

mybatis表名含有参数

表较多需要分表,或者日志等需要按照模块分表的时候,为了统一接口经常需要将最后不同的部分作为参数传入到sql中进行查询,如

表名 t_log_user,t_log_item等,在拼接如下sql

select * from t_log_*

的时候,需要将 user,item等字符串作为参数传进去。开始使用的mybatisgenerator生成的那种方式,里面传递参数都用的#{module}的方式,但是在参数存在于表名的时候,如果是字符串,mybatis会给加上单引号,即最后拼接成的sql为 select * from t_log_’user’,执行必然报错。

查找了一下网上资料,参考《http://blog.sina.com.cn/s/blog_4822be6d0101g0da.html》,需要在sql mapper xml文件中的语句块里添加 statementType=”STATEMENT” , 并且参数声明必须使用 ${module}($)符号才行。

示例:

<sql id=”selectListByModule” resultMap=”tLog” parameterMap=”tLog” statementType=”STATEMENT” >
select * from t_log_${module} where 1=1
</sql>

HTML表格table中单元格内容过多自动换行设置

让HTML表格table的相应单元格自动换行:
1、设置单元格宽度
2、给当天的表格设置样式style=”word-wrap: break-word;word-break:break-all;”

在spring中,代码里获取spring加载的properties文件中值的方法

平时用spring的时候,配置的值多数情况都是直接在配置文件中注入给管理的类了,比如数据库连接池,数据库,缓存等,不需要再在代码中获取值做相应的处理。今天遇到这种情况,需要在代码中判断某个值是否为空,如果不为空则解析其值并根据解析的值给HTTPclient加上代理,否则则不加。

首先是使用spring的placeholder加载所有的properties文件

然后是常见的第一种注入配置值的方法:

使用配置文件管理需要注入的实例,在初始化实例的时候,在配置文件中使用property参数注入相应的值,入
<bean id=”cacheManager” class=”cn.outofmemory.util.MemCacheManager” init-method=”init”>
<property name=”nodeList” value=”${memcache.nodelist}”/>
</bean>
这里的${memcache.nodelist}就是properties文件中key值为memcache.nodelist的配置值。
但是这样做会打乱原有的使用注解管理bean的布局,显得比较混乱,而且如果改动类还需要同时改动spring的配置文件。

第二种:
1、引入命名空间:
xmlns:util=”http://www.springframework.org/schema/util”
xsi:schemaLocation=”http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.0.xsd”
内容中写入
<util:properties id=”propertiesReader” location=”classpath:test.properties” />
2、在类中需要注入的属性实现 setter 和 getter 方法。
3、在 setter 方法前(或者该属性声明位置),添加 @Value 注解
@Value(“#{propertiesReader[propertiesName]}”)
propertiesName 为 properties 文件中的键。这样,在容器启动过程中, Spring 将自动注入值。

第三种:
实现一个 PropertyPlaceholderConfigurer 扩展的类,在这个类中重写protected void processProperties方法,在方法体中,将传入的
Properties props的值全都重新复制一份给当前方法的静态变量,这样就可以直接使用该静态变量获取系统加载的参数了。示例如下:

private static Map<String, String> ctxPropertiesMap;
@Override
protected void processProperties(
ConfigurableListableBeanFactory beanFactoryToProcess,
Properties props) throws BeansException {

super.processProperties(beanFactoryToProcess, props);

ctxPropertiesMap = new HashMap<String, String>();

for (Object key : props.keySet()) {
String keyStr = key.toString();
String value = props.getProperty(keyStr);
ctxPropertiesMap.put(keyStr, value);
}

}
public static String getContextProperty(String name) {
return ctxPropertiesMap.get(name);
}
第四种:
很简单,只要是使用PropertyPlaceholderConfigurer来管理加载了所有的配置文件,那么我们在某个spring管理的实例里的属性上,
在某个set方法上可以用@Value(“${db.driverclass}”)来完成注入,也可以在成员变量上注入。
例子:
例子代码如:
@Service
public class DatabaseInfo {
@Value(“${db.driverclass}”)①
private String driverClass;
//也可以在这里注入
@Value(“${db.driverclass}”)②
private void setDriverClass(String dc) {
this.driverClass = dc;
}
}
参考文献
《http://blog.csdn.net/achilles12345/article/details/38614387》
《http://jackyrong.iteye.com/blog/1330946》

linux系统下rz/sz 命令安装使用说明

对于经常使用Linux系统的人员来说,少不了将本地的文件上传到服务器或者从服务器上下载文件到本地,rz / sz命令很方便的帮我们实现了这个功能,但是很多Linux系统初始并没有这两个命令。今天,我们就简单的讲解一下如何安装和使用rz、sz命令。

1.软件安装

(1)编译安装

root 账号登陆后,依次执行以下命令:

1 cd /tmp
2 wget http://www.ohse.de/uwe/releases/lrzsz-0.12.20.tar.gz
3 tar zxvf lrzsz-0.12.20.tar.gz && cd lrzsz-0.12.20
4 ./configure && make && make install

上面安装过程默认把lsz和lrz安装到了/usr/local/bin/目录下,现在我们并不能直接使用,下面创建软链接,并命名为rz/sz:

1 cd /usr/bin
2 ln -s /usr/local/bin/lrz rz
3 ln -s /usr/local/bin/lsz sz

(2)yum安装

root 账号登陆后执行以下命令:

1 yum install -y lrzsz

2.使用说明

sz命令发送文件到本地:

1 # sz filename

rz命令本地上传文件到服务器:

1 # rz

执行该命令后,在弹出框中选择要上传的文件即可。
说明:打开SecureCRT软件 -> Options -> session options -> X/Y/Zmodem 下可以设置上传和下载的目录。

参看文档地址《http://www.lihuai.net/linux/commands/558.html》

LinkedHashMap : 记录插入顺序的Map

今日做一个曲线图,用到map存储数据,开始习惯性的使用了hashmap,结果发现在计算出来结果展现的时候,日期并没有按照预计的顺序排列。比如,统计出来的数据是20150626和20150627两天的,真正在页面上展现的时候,反而是20150626在后面。
经过仔细观察,发现数据对应并没有错,也就是说,是一开始用来存储以天为key值的节点数据的时候顺序是混乱的,在遍历的时候并没有按照存入的顺序进行遍历。

由于日期与数据的列表时分开存储的,计算完成之后再按照日期排序的方法不可取。印象里linkedhashmap和treemap是有顺序的,从网上找资料确认一下,发现,linkedhashmap是可以保留存入的元素的顺序的,遍历的时候是按照存入的顺序遍历,这个符合我的要求。treemap是有个顺序,不过是默认按照key值进行排序,而非存储时候的顺序。但由于我在获取数据列表的时候已经按照时间排序,不需要再次进行排序,因此没有选择用treemap。

LinkedHashMap的实现整理如下:

参考文档地址《http://www.cnblogs.com/children/archive/2012/10/02/2710624.html》

1. LinkedHashMap概述:

LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。

LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变
LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

 

根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。

默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。  可以重写removeEldestEntry方法返回true值指定插入元素时移除最老的元素。

 

2. LinkedHashMap的实现:

对于LinkedHashMap而言,它继承与HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。下面我们来分析LinkedHashMap的源代码:

类结构:

  1. public class LinkedHashMap<K, V> extends HashMap<K, V> implements Map<K, V>

 

1) 成员变量:

LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。看源代码:

 

  1. //true表示按照访问顺序迭代,false时表示按照插入顺序
  2.  private final boolean accessOrder;
  1. /**
  2.  * 双向链表的表头元素。
  3.  */
  4. private transient Entry<K,V> header;
  5. /**
  6.  * LinkedHashMap的Entry元素。
  7.  * 继承HashMap的Entry元素,又保存了其上一个元素before和下一个元素after的引用。
  8.  */
  9. private static class Entry<K,V> extends HashMap.Entry<K,V> {
  10.     Entry<K,V> before, after;
  11.     ……
  12. }

HashMap.Entry:

  1. static class Entry<K,V> implements Map.Entry<K,V> {
  2.         final K key;
  3.         V value;
  4.         Entry<K,V> next;
  5.         final int hash;
  6.         Entry(int h, K k, V v, Entry<K,V> n) {
  7.             value = v;
  8.             next = n;
  9.             key = k;
  10.             hash = h;
  11.         }
  12. }

 

2) 初始化:

通过源代码可以看出,在LinkedHashMap的构造方法中,实际调用了父类HashMap的相关构造方法来构造一个底层存放的table数组。如:

  1. public LinkedHashMap(int initialCapacity, float loadFactor) {
  2.     super(initialCapacity, loadFactor);
  3.     accessOrder = false;
  4. }

HashMap中的相关构造方法:

  1. public HashMap(int initialCapacity, float loadFactor) {
  2.     if (initialCapacity < 0)
  3.         throw new IllegalArgumentException(“Illegal initial capacity: “ +
  4.                                            initialCapacity);
  5.     if (initialCapacity > MAXIMUM_CAPACITY)
  6.         initialCapacity = MAXIMUM_CAPACITY;
  7.     if (loadFactor <= 0 || Float.isNaN(loadFactor))
  8.         throw new IllegalArgumentException(“Illegal load factor: “ +
  9.                                            loadFactor);
  10.     // Find a power of 2 >= initialCapacity
  11.     int capacity = 1;
  12.     while (capacity < initialCapacity)
  13.         capacity <<= 1;
  14.     this.loadFactor = loadFactor;
  15.     threshold = (int)(capacity * loadFactor);
  16.     table = new Entry[capacity];
  17.     init();
  18. }

我们已经知道LinkedHashMap的Entry元素继承HashMap的Entry,提供了双向链表的功能。在上述HashMap的构造器中,最后会调用init()方法,进行相关的初始化,这个方法在HashMap的实现中并无意义,只是提供给子类实现相关的初始化调用。
LinkedHashMap重写了init()方法,在调用父类的构造方法完成构造后,进一步实现了对其元素Entry的初始化操作。

  1. void init() {
  2.     header = new Entry<K,V>(-1nullnullnull);
  3.     header.before = header.after = header;
  4. }

3) 存储:

LinkedHashMap并未重写父类HashMap的put方法,而是重写了父类HashMap的put方法调用的子方法void recordAccess(HashMap m)   ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的双向链接列表的实现。

HashMap.put:

 

  1. public V put(K key, V value) {
  2.         if (key == null)
  3.             return putForNullKey(value);
  4.         int hash = hash(key.hashCode());
  5.         int i = indexFor(hash, table.length);
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  7.             Object k;
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  9.                 V oldValue = e.value;
  10.                 e.value = value;
  11.                 e.recordAccess(this);
  12.                 return oldValue;
  13.             }
  14.         }
  15.         modCount++;
  16.         addEntry(hash, key, value, i);
  17.         return null;
  18.     }

重写方法:

  1. void recordAccess(HashMap<K,V> m) {
  2.             LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
  3.             if (lm.accessOrder) {
  4.                 lm.modCount++;
  5.                 remove();
  6.                 addBefore(lm.header);
  7.             }
  8.         }

 

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2.     // 调用create方法,将新元素以双向链表的的形式加入到映射中。
  3.     createEntry(hash, key, value, bucketIndex);
  4.     // 删除最近最少使用元素的策略定义
  5.     Entry<K,V> eldest = header.after;
  6.     if (removeEldestEntry(eldest)) {
  7.         removeEntryForKey(eldest.key);
  8.     } else {
  9.         if (size >= threshold)
  10.             resize(2 * table.length);
  11.     }
  12. }
  1. void createEntry(int hash, K key, V value, int bucketIndex) {
  2.     HashMap.Entry<K,V> old = table[bucketIndex];
  3.     Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
  4.     table[bucketIndex] = e;
  5.     // 调用元素的addBrefore方法,将元素加入到哈希、双向链接列表。
  6.     e.addBefore(header);
  7.     size++;
  8. }
  1. private void addBefore(Entry<K,V> existingEntry) {
  2.     after  = existingEntry;
  3.     before = existingEntry.before;
  4.     before.after = this;
  5.     after.before = this;
  6. }

 

4) 读取:

LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。

HashMap.containsValue:

  1. public boolean containsValue(Object value) {
  2.     if (value == null)
  3.             return containsNullValue();
  4.     Entry[] tab = table;
  5.         for (int i = 0; i < tab.length ; i++)
  6.             for (Entry e = tab[i] ; e != null ; e = e.next)
  7.                 if (value.equals(e.value))
  8.                     return true;
  9.     return false;
  10.     }

 

  1.  /*查找Map中是否包含给定的value,还是考虑到,LinkedHashMap拥有的双链表,在这里Override是为了提高迭代的效率。
  2.  */
  3. public boolean containsValue(Object value) {
  4.         // Overridden to take advantage of faster iterator
  5.         if (value==null) {
  6.             for (Entry e = header.after; e != header; e = e.after)
  7.                 if (e.value==null)
  8.                     return true;
  9.         } else {
  10.             for (Entry e = header.after; e != header; e = e.after)
  11.                 if (value.equals(e.value))
  12.                     return true;
  13.         }
  14.         return false;
  15.     }

 

 

  1. /*该transfer()是HashMap中的实现:遍历整个表的各个桶位,然后对桶进行遍历得到每一个Entry,重新hash到newTable中,
  2.  //放在这里是为了和下面LinkedHashMap重写该法的比较,
  3.  void transfer(Entry[] newTable) {
  4.         Entry[] src = table;
  5.         int newCapacity = newTable.length;
  6.         for (int j = 0; j < src.length; j++) {
  7.             Entry<K,V> e = src[j];
  8.             if (e != null) {
  9.                 src[j] = null;
  10.                 do {
  11.                     Entry<K,V> next = e.next;
  12.                     int i = indexFor(e.hash, newCapacity);
  13.                     e.next = newTable[i];
  14.                     newTable[i] = e;
  15.                     e = next;
  16.                 } while (e != null);
  17.             }
  18.         }
  19.     }
  20.  */
  21.  /**
  22.  *transfer()方法是其父类HashMap调用resize()的时候调用的方法,它的作用是表扩容后,把旧表中的key重新hash到新的表中。
  23.  *这里从写了父类HashMap中的该方法,是因为考虑到,LinkedHashMap拥有的双链表,在这里Override是为了提高迭代的效率。
  24.  */
  25.  void transfer(HashMap.Entry[] newTable) {
  26.    int newCapacity = newTable.length;
  27.    for (Entry<K, V> e = header.after; e != header; e = e.after) {
  28.      int index = indexFor(e.hash, newCapacity);
  29.      e.next = newTable[index];
  30.      newTable[index] = e;
  31.    }
  32.  }

 

  1. public V get(Object key) {
  2.     // 调用父类HashMap的getEntry()方法,取得要查找的元素。
  3.     Entry<K,V> e = (Entry<K,V>)getEntry(key);
  4.     if (e == null)
  5.         return null;
  6.     // 记录访问顺序。
  7.     e.recordAccess(this);
  8.     return e.value;
  9. }
  1. void recordAccess(HashMap<K,V> m) {
  2.     LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
  3.     // 如果定义了LinkedHashMap的迭代顺序为访问顺序,
  4.     // 则删除以前位置上的元素,并将最新访问的元素添加到链表表头。
  5.     if (lm.accessOrder) {
  6.         lm.modCount++;
  7.         remove();
  8.         addBefore(lm.header);
  9.     }
  10. }

 

  1. /**
  2.          * Removes this entry from the linked list.
  3.          */
  4.         private void remove() {
  5.             before.after = after;
  6.             after.before = before;
  7.         }

 

 

  1. /**clear链表,设置header为初始状态*/
  2. public void clear() {
  3.  super.clear();
  4.  header.before = header.after = header;
  5. }

 

 

5) 排序模式:

LinkedHashMap定义了排序模式accessOrder,该属性为boolean型变量,对于访问顺序,为true;对于插入顺序,则为false。

  1. private final boolean accessOrder;

一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。看LinkedHashMap的构造方法,如:

  1. public LinkedHashMap(int initialCapacity, float loadFactor) {
  2.     super(initialCapacity, loadFactor);
  3.     accessOrder = false;
  4. }

这些构造方法都会默认指定排序模式为插入顺序。如果你想构造一个LinkedHashMap,并打算按从近期访问最少到近期访问最多的顺序(即访问顺序)来保存元素,那么请使用下面的构造方法构造LinkedHashMap:

  1. public LinkedHashMap(int initialCapacity,
  2.          float loadFactor,
  3.                      boolean accessOrder) {
  4.     super(initialCapacity, loadFactor);
  5.     this.accessOrder = accessOrder;
  6. }

该哈希映射的迭代顺序就是最后访问其条目的顺序,这种映射很适合构建LRU缓存。LinkedHashMap提供了removeEldestEntry(Map.Entry<K,V> eldest)方法。该方法可以提供在每次添加新条目时移除最旧条目的实现程序,默认返回false,这样,此映射的行为将类似于正常映射,即永远不能移除最旧的元素。

 

当有新元素加入Map的时候会调用Entry的addEntry方法,会调用removeEldestEntry方法,这里就是实现LRU元素过期机制的地方,默认的情况下removeEldestEntry方法只返回false表示元素永远不过期。

  1.   /**
  2.     * This override alters behavior of superclass put method. It causes newly
  3.     * allocated entry to get inserted at the end of the linked list and
  4.     * removes the eldest entry if appropriate.
  5.     */
  6.    void addEntry(int hash, K key, V value, int bucketIndex) {
  7.        createEntry(hash, key, value, bucketIndex);
  8.        // Remove eldest entry if instructed, else grow capacity if appropriate
  9.        Entry<K,V> eldest = header.after;
  10.        if (removeEldestEntry(eldest)) {
  11.            removeEntryForKey(eldest.key);
  12.        } else {
  13.            if (size >= threshold)
  14.                resize(2 * table.length);
  15.        }
  16.    }
  17.    /**
  18.     * This override differs from addEntry in that it doesn’t resize the
  19.     * table or remove the eldest entry.
  20.     */
  21.    void createEntry(int hash, K key, V value, int bucketIndex) {
  22.        HashMap.Entry<K,V> old = table[bucketIndex];
  23. Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
  24.        table[bucketIndex] = e;
  25.        e.addBefore(header);
  26.        size++;
  27.    }
  28.    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  29.        return false;
  30.    }

此方法通常不以任何方式修改映射,相反允许映射在其返回值的指引下进行自我修改。如果用此映射构建LRU缓存,则非常方便,它允许映射通过删除旧条目来减少内存损耗。

例如:重写此方法,维持此映射只保存100个条目的稳定状态,在每次添加新条目时删除最旧的条目。

  1. private static final int MAX_ENTRIES = 100;
  2. protected boolean removeEldestEntry(Map.Entry eldest) {
  3.     return size() > MAX_ENTRIES;
  4. }

来源:http://zhangshixi.iteye.com/blog/673789

参考:http://hi.baidu.com/yao1111yao/blog/item/3043e2f5657191f07709d7bb.html

部分修改。

 

 

使用LinkedHashMap构建LRU的Cache

http://tomyz0223.iteye.com/blog/1035686

基于LinkedHashMap实现LRU缓存调度算法原理及应用

http://woming66.iteye.com/blog/1284326

 

 

其实LinkedHashMap几乎和HashMap一样,不同的是它定义了一个Entry<K,V> header,这个header不是放在Table里,它是额外独立出来的。LinkedHashMap通过继承hashMap中的Entry<K,V>,并添加两个属性Entry<K,V>  before,after,和header结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。