竹磬网-邵珠庆の日记 生命只有一次,你可以用它来做些更多伟大的事情–Make the world a little better and easier


107月/180

【项目管理】敏捷项目管理流程-Scrum框架最全总结

发布在 邵珠庆

Scrum中的角色
Scrum Master——项目负责人、项目经理
保护团队不受外界干扰,是团队的领导和推进者,负责提升 Scrum 团队的工作效率,控制 Scrum 中的“检视和适应”周期过程。与 Product Owner 一起将投资产出最大化,他确保所有的利益相关者都可以理解敏捷和尊重敏捷的理念。

Team——开发人员、测试人员、美工设计、DBA等全职能性团队
团队负责交付产品并对其质量负责,团队与所有提出产品需求的人一起工作,包括客户和最终用户,并共同创建 Product Backlog 。团队按照大家的共识来创建功能设计、测试 Backlog 条目交付产品。

Product Owner——产品负责人、产品经理、运营人员
从业务角度驱动项目,传播产品的明确愿景,并定义其主要特性。Product Owner 的主要职责是确保团队只开发对于组织最重要的 Backlog 条目,在 Sprint 中帮助团队完成自己的工作,不干扰团队成员,并迅速提供团队需要的所有信息。

User——最终用户、运营人员、系统使用人员
很多人都可能成为最终用户,比如市场部人员、真正的最终用户、最好的领域专家,也可能是因其专业知识而被雇佣的资讯顾问。最终用户会根据自己的业务知识定义产品,并告知团队自己的期望,提出请求。

Manager——管理层、投资人
管理层要为 Scrum 团队搭建良好的环境,以确保团队能够出色工作,必要的时候,他们也会与 Scrum Master 一起重新组织结构和指导原则。

Customer——客户、系统使用人员、运营人员

客户是为 Scrum 团队提出产品需求的人,她会与组织签订合同,以开发产品。一般来说,这些人是组织中的高级管理人员,负责从外部软件开发公司购买软件开发能力。在为内部产品的公司中,负责批准项目预算的人就是客户。

Scrum中的产出物
Product Backlog——Backlog 待开发项,积压的任务。
产品 Backlog 包括了所有需要交付的内容,其内容根据业务需求的价值顺序排列,每个 Backlog 的优先级是可以调整的,需求是可以增减的,因此产品 Backlog 将根据不断增长来持续驱动维护。

Sprint Backlog——Sprint 本意为“冲刺”,指迭代周期,长度通常是一至六周。
在 Sprint 开始前,定义本次 Sprint 要讨论的“Sprint Backlog”,从中产生本次 Sprint 要完成的 “已定 Product Backlog”。
已定 Product Backlog是 Sprint 计划会议的产物,它定义了团队所接受的工作量,在整个 Sprint 过程中它将保持不变。

User Story、Task——用户故事、任务
用 User Story 来描述 Sprint Backlog 里的项目,User Story 是从用户的角度对系统的某个功能模块所作的简短描述。一个 User Story 描述了项目中的一个小功能,以及这个功能完成之后将会产生什么效果,或者说能为客户创造什么价值。一个 User Story 的大小和复杂度应该以能在一个 Sprint 中完成为宜。如果 User Story 太大,可能会导致对它的开发横跨几个 Sprint,此时就应该将这个 User Story 分解。为了能够及时,高效地完成每个 Story,Scrum 团队会把每个 Story 分解成若干个 Task。每个Task 的时间最好不要超过8小时,保证在1个工作日内完成,如果 Task 的时间超过了8个小时,就说明Task的划分有问题,需要特别注意。

障碍 Backlog——问题列表,积压的待处理事务。
列举了所有团队内部和团队相关的和阻碍项目的进度的问题,Scrum Master 需要确保所有的障碍 Backlog 中的问题都已分配并可以得到解决。

通用会议规则
基本要求
•每次会议都要准时开始、准时结束。
•每次会议都采取开放形式,所有人都可以参加。

会前准备
•提前邀请所有必须参会的人,让他们有时间准备。
•发送带有会议目标和意图的会议纲要。
•预订会议所需的全部资源:房间、投影仪、挂图、主持设备,以及此会议需要的其他东西。
•会前24小时发送提醒。
•准备带有会议规则的挂图。

会议推进
•展开讨论时,会议的推进人必须在场。他不能参与到具体讨论中,但是他需要注意讨论进程,如果讨论参与者失去重点,他还要将讨论带回正规。
•推进人展示会议的目标和意图。
•有必要时,推进人可以商定由某个撰写会议记录。
•推进人可以记录团队的意见,或是教授团队如何自己记录文档;而且推进人可能会在挂图上进行记录,将对话可视化。
•推进人会对会议进行收尾,并进行非常简短的回顾。

会议输出
•使用手写或挂图说明来记录文档,给白板和挂图上的内容拍照。
•必须传达会议记录和大家对会议结果的明确共同认知。

让团队坐在一起!
•大家都懒的动,尽量让“产品负责人”和“全功能团队”都坐在一起!
•互相听到:所有人都可以彼此交谈,不必大声喊,不必离开座位。
•互相看到:所有人都可以看到彼此,都能看到任务板——不用非得近到可以看清楚内容,但至少可以看到个大概。
•隔离:如果你们整个团队突然站起来,自发形成一个激烈的设计讨论,团队外的任何人都不会被打扰到,反之亦然。

团队建设
•Scrum 团队最佳人数控制在“5~9”人。
•全职能性团队:开发组(后台开发、前端开发、测试人员——3~8人)、Scrum Master(项目经理)、产品负责人
•兼职团队成员:美工、DBA、运维

每日立会(Daily Standup Meeting)——建议下班前开始
会议目的
•团队在会议中作计划,协调其每日活动,还可以报告和讨论遇到的障碍。
•任务板能够帮助团队聚焦于每日活动之上,要在这个时候更新任务板和燃尽图。

构成部分
•任务板、即时贴、马克笔
•提示:ScrumMaster 不要站在团队前面或是任务板旁边,不要营造类似于师生教学的气氛。

基本要求
•成员:团队、Scrum Master
•无法出席的团队成员要由同伴代表。
•持续时间/举办地点:每天15分钟,同样时间,同样地点。
•提示:团队成员在聆听他人发言时,都应该想这个问题:“我该怎么帮他做得更快?”

会议输出
•团队彼此明确知道各自的工作,最新的工作进度图。
•得到最新的“障碍 Backlog”
•得到最新的“Sprint Backlog”

会议过程
•团队聚在故事板旁边,可以围成环形。
•从左边第一个开始,向团队伙伴说明他到现在完成的工作。
•然后该成员将任务板上的任务放到正确的列中。
•如果可以的话,该成员可以选取新的任务,交将其放入“进行中工作”列。
•如果该成员遇到问题或障碍,就要将其报告给 Scrum Master。
•每个团队成员重复步骤2到步骤5。

每个人三个问题:
•上次会议时的任务哪些已经完成?:把任务从“正在处理”状态转为“已完成”状态。——今天完成了什么?
•下次会议之前,你计划完成什么任务?:如果任务状态为“待处理”,转为“正在处理”状态。如果任务不在 Sprint Backlog 上,则添加这个任务。如果任务不能在一天成,把这任务细分成多个任务。如果任务可以在一天内完成,把任务状态设为“正在处理”。如果任务状态已经是“正在 处理”,询问是否存在阻碍任务完成得问题。——明天做什么?
•有什么问题阻碍了你的开发?:如果有阻碍你的开发进度的问题,把该障碍加入到障碍 Backlog中。——今天遇到了什么问题?

注意事项
•不要迟到
•不要超出限制时间
•不要讨论技术问题
•不要转变会议话题
•不要在没有准备的情况下参加
•Scrum Master 不要替团队成员移动任务卡片,不要替团队更新燃尽图。
•Scrum Master 不要提出问题,团队成员不要向 Scrum Master 或管理层人员报告。
•如果不能出席会议,需要通知团队,并找一名代表参加。

任务板
•任务板集合了选择好的 Product Backlog 和 Sprint Backlog,并以可视化方式展示。
•任务板只能由团队维护,使用不同颜色的“即时贴”来区分开发人员,或者在“即时贴”写上接受任务的姓名。
•尽量使用大白板,也可以使用软件。

任务板有4列:
•选择好的 Product Backlog:按照优先级,将团队在当前 Sprint 中要着手的 Product Backlog 条目或是故事放在该列中。

•待完成的任务:要完成一个故事,你得完成一些任务。在 Sprint 规划会议中,或是在进行当前 Sprint 中,收集所有特定 Backlog 条目需要完成的新任务,并将它们放入该列。
•进行中的工作:当团队成员开始某个任务后,他会将该任务对应的卡片放到“进行中的工作”列中。从上个每日 Scrum 例会开始,没有完成的任务都会放在该列中,并在上面做标记(通常是个红点)。如果某个任务在“待完成任务”列中所处时间超过一天,就尽量将该任务分为更小 的部分,然后把新任务放到那一列,移除其所属大任务卡片。如果一个新任务因为某个障碍无法完成,就会得到一个红点标记,Scrum Master 就会记下一个障碍。
•完成:当一个任务卡完成后,完成此任务的成员将其放入“完成”列,并开始选取下一张任务卡
燃尽图
•跟踪进度要由团队来完成,燃尽图的横轴表示整个Sprint 的总时间,纵轴表示 Sprint 中所有的任务,其单位可以是小时,人天等。一般来说,燃尽图有”Sprint燃尽图”和”Release燃尽图”之分。
•团队每天更新燃尽图。
•如果燃尽图一直是上升状态,或当 Sprint 进行一段时间之后,Sprint 燃尽图上的Y值仍然与 Sprint 刚开始时相差无几,就说明这个 Sprint 中的 Story 过多,要拿掉一些 Story 以保证这个 Sprint 能顺利完成。 如果Sprint 燃尽图下降得很快,例如 Sprint 刚过半时Y值已经接近0了,则说明这个 Sprint 分配的任务太少,还要多加一些任务进来。在 Sprint 计划会议上,如果团队对即将要做的任务理解和认识不充分,就很可能导致这两种情况的出现。(锻炼团队人员的自我估算时间)
•燃尽图要便于团队更新,没必要让它看起来很炫,也不要过于复杂,难以维护。

Release 燃尽图:记录整个Scurm项目的进度,它的横轴表示这个项目的所有Sprint, 纵轴表示各个Sprint开始前,尚未完成的工作,它的单位可以是个(Story 的数量),人天等。

Sprint规划会议——第一部分(上午)
会议目的
•该会议的工作以分析为主,目的是要详细理解最终用户到底要什么,产品开发团队可以从该会议中详细了解最终用户的真实需要。在会议的结束,团队将会决定他们能够交付哪些东西。
•产品负责人在会前准备:条目化的需求(用户故事),优先级排序,最近1~2个迭代最希望看到的功能。会前准备至关重要,可帮助产品负责人理清头绪,不至于在迭代期内频繁提出变更、增加或删除故事。

基本要求
•迭代计划会在每个迭代第一天召开,目的是选择和估算本次迭代的工作项。
•只有团队成员才能决定团队在当前 Sprint 中能够领取多少个 Backlog 条目的工作。

构成部分:
•经过估算和排序的 Product Backlog。
•挂图、马克笔、剪刀、胶水、即时贴、白板、铅笔和蜡笔。
•假期计划表、重要人员的详细联系信息。
•参会成员:团队成员、Scrum Master、产品负责人

持续时间:在 Sprint 中,每周该会议占用时间为 60 分钟,在早上召开该会议,这样还有可能在同一天召开 Sprint 规划会议的第二部分。

会议输出
•选择好的 Product Backlog 条目。
•各个 Backlog 条目的需求。
•各个 Backlog 条目的用户验收测试。

会议过程
•从第一个 Product Backlog 条目(故事)开始。
•讨论该 Product Backlog 条目,以深入理解。
•分析、明确用户验收测试。
•找到非功能性需求(性能、稳定性...)
•找到验收条件。
•弄清楚需要“完成”到何种水平。
•获得 Backlog 条目各个方面的清晰了解。
•绘制出所需交付物的相关图表,包括流程图、UML图、手绘草图、屏幕 UI 设计等。
•回到步骤1,选取下一个 Backlog 条目。

流程检查:询问团队能否快速回答下列问题,只需要简要回答即可:“我们能在这个 Sprint 中完成第一个 Backlog 条目吗?”如果能得到肯定的回答,那么继续询问下一个 Backlog 条目,一直到已经分析完的最后一个 Backlog 条目。——接下来,休息一下。在休息后,对下一个 Backlog 条目展开上述流程。

结束流程:
•在 Sprint 规划会议第一部分结束前留出 20 分钟。
•再次提问——这次要更加严肃、正式:“你们能否完成第一个 Backlog 条目,...第二个,...?”
•如果团队认为他们不能再接受更多的 Backlog 条目,那就停下来。
•现在是非常重要的一步:送走 Product Owner,除了团队和 Scrum Master 之外的所有人,都得离开。
•当其他人都离开后,再询问团队:“说真的——你们相信自己可以完成这个列表?”
•希望团队现在能短暂讨论一下,看看他们到底认为自己能完成多少工作。
•将结果与 Product Owner 和最终用户沟通。

注意事项:不要改变 Backlog 条目大小,不要估算任务。

Sprint规划会议——第二部分(下午)
会议目的
•该会议的工作以设计为主,产品开发团队可以为他们要实现的解决方案完成设计工作,在会议结束后,团队知道如何构建他们在当前 Sprint 中要开发的功能。

基本要求
•只有产品开发团队才能制定解决方案,架构师或其他团队之外的人只是受邀帮助团队。

构成部分:
•能够帮助团队在该 Sprint 中构建解决方案的人,比如厂商或是来自其他团队的人员。
•选择好的 Product Backlog 条目。
•挂图......

注意事项:不要估算任务,不要分配任务。

会议输出
•应用设计、架构设计图、相关图表
•确保团队知道应该如何完成任务!

会议过程
•从第一个 Backlog 条目开始。
•查看挂图,确定对于客户的需求理解正确。
•围绕该 Backlog 条目进行设计,并基于下列类似问题: •我们需要编写什么样的接口?
•我们需要创建什么样的架构?
•我们需要更新哪些表?
•我们需要更新或是编写哪些组件?
•......

当团队明确知道自己应该如何开发该功能后,就可以转向下一个 Backlog 条目了。在会议的最后 10 分钟,团队成员使用即时贴写出初步的任务。这能帮助团队成员知道接下来的工作从哪里开展,将这些任务放在任务板上。

持续时间:在 Sprint 规划会议第一部分完成后,召开该会议。可以将午餐作为两次会议的一个更长久的休息。但是要在同一天完成 Sprint 规划第一部分,在 Sprint 中,每周该会议占用时间为 60 分钟。

估算会议——根据项目情况合并到Sprint第二部分会议
会议目的
•要做好战略规划,你需要知道 Backlog 中各项的大小,这是版本规划的必要输入;如果想知道团队在一个 Sprint 中能够完成多少工作,这个数据也是必须的。
•团队成员可以从会议中知道项目接下来的阶段会发生哪些事情。

基本要求
•只有团队才能作估算,Product Owner(产品负责人)需要在场,以帮助判定某些用户故事能否拆分为更小的故事。

构成部分:
•Product Owner 根据业务价值排定 Product Backlog 各项顺序。
•需要参加的人员:Team、Product Owner、User、Scrum Master

注意事项:
•不要估算工作量大小——只有团队能这么做。
•Product Owner 不参与估算。

会议过程
•Prodcut Owner 展示她希望得到估算的 Product Backlog 条目。
•团队使用规划扑克来估算 Backlog 条目。
•如果某个 Backlog 条目过大,需要放到下一个或是后续的 Sprint 中,团队就会将该大 Backlog 条目划分为较小的几个 Backlog 条目,并对新的 Backlog 条目使用规划扑克进行估算。
•重新估算 Backlog 中当前没有完成、但是可能会在接下来三个 Sprint 中要完成的条目。

持续时间:该会议时间限制为不超过90分钟。如果 Sprint 持续时间长于一周,那么每个 Sprint 举行两次估算会议比较合适。

会议输出
•经过估算的 Product Backlog。
•更小的 Backlog 条目。

扑克牌估算
具体步骤:
•每个人各自估算后独立出暗牌,听口令一起开牌。
•数值最大者与最小者PK,其他人旁听也可参考。
•讨论结束后重新出牌和开牌。
•重复上述过程,直到结果比较接近。

常见问题

1、为什么任务要分给组而不是个人?
答:因为怕出错了牌又说不出所以然,这样即使日后他不做这个功能,也对这个功能很了解。

2、为什么不让最后领任务的人自己估算?
答:因为他很可能因为不知道某代码可用、不知道某软件不行....而选择了错误的实现方法。

3、为什么不让师傅估算大家采纳,他不是最厉害吗?
答:师傅的想法常常是徒弟们理解不了的,比如为什么不留在女儿国而偏偏去西天取经之类的,共同估算就是让大家在思考中对照自己的实现方法和师傅差异的过程。

Sprint评审会议(Review Meeting)
会议目的
•Scrum 团队在会议中向最终用户展示工作成果,团队成员希望得到反馈,并以之创建或变更 Backlog 条目。

基本要求
•Sprint 复审会议允许所有的参与者尝试由团队展示的新功能。

构成部分
•有可能发布的产品增量,由团队展示。

会议输出
•来自最终用户的反馈。
•障碍 Backlog 的输入。
•团队 Backlog 的输入。
•来自团队的反馈为 Product Backlog 产生输入。

持续时间:90分钟,在 Sprint 结束时进行。

会议过程
•Product Owner 欢迎大家来参加 Sprint 复审会议。
•Product Owner 提醒大家关于本次 Sprint 的目的:Sprint 目标、Scrum 团队在本次 Sprint 中选定要开发的故事。
•产品开发团队展示新功能,并让最终用户尝试新功能。
•Scrum Master 推进会议进程。
•最终用户的反馈将会由 Product Owner 和/或 Scrum Master 记录在案。

注意事项:
•不要展示不可能发布的产品增量。
•Scrum Master 不要负责展示结果。
•团队不要针对 Product Owner 展示。

Sprint反思会议(Retrospective Meeting)
会议目的
•该会议的对应隐喻:医疗诊断!其目的不是为了找到治愈方案,而是要发现哪些方面需要改进。

构成部分
•参与人员:团队成员、Scrum Master

基本要求
•从过去中学习,指导将来。
•改进团队的生产力。

注意事项
•不要让管理层人员参与会议。
•不要在团队之外讨论找到的东西。

会议输出
•障碍 Backlog 的输入。
•团队 Backlog 的输入。

持续时间:90分钟,在 Sprint 评审会议结束后几分钟开始。

会议过程
•准备一个写着“过去哪些做的不错?”的挂图。
•准备一个写着“哪些应该改进?”的挂图。
•绘制一条带有开始和结束日期的时间线。
•给每个团队成员发放一叠即时贴。
•开始回顾。
•做一个安全练习。
•收集事实:发放即时贴,用之构成一条时间线。每个团队成员(包括 Scrum Master)在每张即时贴上写上一个重要的事件。
•“过去哪些做的不错?”:采取收集事实同样的过程,不过这次要把即时贴放在准备好的挂图上。
•做一个分隔,以区分“过去哪些做的不错”和接下来要产出的东西。
•“哪些应该改进?”:像“过去哪些做的不错”那样进行。
•现在将即时贴分组:
•我们能做什么》团队 Backlog 的输入。
•哪些不在我们掌控之内?》障碍 Backlog 的输入。
•根据团队成员的意见对两个列表排序。
•将这两个列表作为下个 Sprint 的 Sprint 规划会议第一部分和 Sprint 规划会议第二部分的输入,并决定到时候要如何处理这些发现的信息。

272月/170

PHP命名空间namespace/类别名 use/框架自动载入 机理

发布在 邵珠庆

摘要: PHP 命名空间 namespace / 类别名 use / 框架自动载入 机理的

相比 PHP5.2 版本 PHP5.3 新增了三大主要新特性

命名空间 

延迟静态绑定 

lambda匿名函数

命名空间的出现也使PHP可以更加合理的组织项目结构,同时通过命名空间和自动载入机制一大批 PHP 的 MVC 框架也随之出现,明了的项目结构的同时也按需载入,进一步减轻内存压力,加快执行效率。

因为命名空间是对目录结构友好的

namespace Home\Controller; 
class IndexController { }

而 PHP5.2 之前是按造类的下划线去做类似 命名空间 的定义的

class Home_Controller_IndexController  { }

一、 命名空间 及 USE 的本质

php 的 use 关键字并不是立刻导入所use的类,它只是声明某类的完整类名(命名空间::类标示符),而后你在上下文中使用此类时系统才会根据 use 声明获取此类的完整类名 然后利用自动加载机制进行载入

 
namespace Home\Controller; 
use Home\Model\User; 
use Home\Model\Order as OrderList; 
class IndexController {     
public function index() {         
//只有当你调用此类时,系统才会根据 use 声明获取此类的完整类名 然后利用自动加载机制进行载入         
$user = new User();         
$order = new OrderList();     
} 
}
就像如下的代码 自动载入函数是在 use 两个类之后方才实现的 因为 use 并不会立即使用此类 只有在你调用此类时系统才会在找不到此类的情况下通过 autoload 函数动态延迟加载,若仍加载不到,则报错
 

1、某命名空间下的类 的完整名称为 namespace\className,当在某命名空间上下文中访问其它命名空间下的类时,我们可以使用 use 做别名化,或者使用此类的完整名称,但要以 '\' 根命名空间开头,否则解释器会认为你是在当前命名空间上下文中调用,即 foo\bar 方式会以 currentNamespace\foo\bar的方式去加载

命名空间与linux文件系统很相似,'\' 代表根,不以根开始的皆认为以当前命名空间为基点

2、use 只是给你使用的类定义短别名,use foo\bar 后则new bar() 即new  \foo\bar(),还有个小技巧,当我们同时引用不同命名空间下的类名相同的类时可以使用 as 为其定义一个新别名

use foo\bar\sameName as classA; 
use bar\foo\sameName as classB; 
new classA(); // new \foo\bar\sameName; 
new classB(); // new \bar\foo\sameName;

3、当我们通过 入口文件 加载参数配置 实例化一个应用主体 加载路由组件解析请求 分派控制器调用方法时,期间会调用其他的类,比如 

use yii\web\Controller;

系统便会去通过自动载入函数做最一次载入尝试,若仍加载不到此类则报错

下面我们看下 Yii2 从入口文件开始一个应用实体后注册自动载入函数的流程

index.php

入口文件载入配置和系统框架时会使用require调用,因为现在还没有注册自动加载函数

载入 Yii bootstrap 文件时便通过 spl_autoload_register 注册了自动载入函数 

Yii.php

Yii2的自动载入函数

继承至 BaseYii 它要做的就是根据你命名空间类型的类名去映射为此类所在的文件路径

比如 yii\web\Controller类会根据 yii 而映射到  YII2_PATH . '/web/Controller.php' 文件中,而这个文件则是命名空间为 yii\web 的 Controller 类,将此文件载入即可访问 yii\web\Controller 类

而我们自己编写的控制器或者模型则访问时为 'app\controllers\IndexController' 'app\models\EntryForm'

则 autoload 函数会根据 app 为 映射关键字将其定位到 controllers 或 models 文件夹下从而读取对应的文件即可载入相应的类,这也是为什么 类名 与 文件名 相互对应的原因所在,若不存在对应,则你只能通过固定的 require 某个文件去加载你写在其中的类了 

扩展自己的类库

我们可以通过Yii2的自动载入机制灵活的归类我们自己写的工具类等,比如我想创建一个自己的组件库

你可以定义一个  yii\tools 命名空间的类文件 MyTools.php,比如

 

放入 vendor\yiisoft\yii2\tools 文件夹下,

通过

<?php

namespace app\controllers;

// yii一级命名空间 则 映射到 YII_PATH 下
// 根据 tools\MyTools 定位到 YII_PATH 下的 tools文件夹下的 MyTools.php
use yii\tools\MyTools;
use yii\web\Controller;

class MyController extends Controller {
}
?>

当然你也可以在你的项目目录下新建一个 tools 文件夹 把 MyTools.php 放进去,将里面的命名空间改为 app\tools 即可,系统会根据 app 映射到项目根目录 通过 tools\MyTools 把 tools文件夹下的 MyTools.php文件载入 即载入了 MyTools 类

三、剖析TP的自动载入

thinkphp的自动加载规则也一样,只不过 tp autoload函数并没有像 Yii2 basic 版预先定义一个项目根目录的映射规则,  Yii2则是以 app 顶级命名空间为默认的应用命名空间,yii顶级命名空间作为框架命名空间,所以你只要把自己的类归属到项目根目录(app下)或 YII_PATH(框架路径) 下,然后放对文件路径即可,

tp的话有的你自己想tp可以在 APP_PATH 下放多个  module ,像其预先定义的 Home ,或者你可以 BIND_MODULE来帮定义一个自己的模块,这样在通过入口文件载入的应用实体做路由时便能判断你请求的是哪个模块下的控制器和方法

tp有几个系统占用的顶级命名空间

Think Org Behavior Com Vendor

而你自己的则会以 APP_PATH 为根目录进行加载,比如 Home\Controller\IndexController.class.php,当你访问 Index 时路由解析出来的类为 Home\Controller\IndexController,自动载入函数则根据 Home 非系统命名空间而定位到你的APP_PATH下进行加载,所以TP也可以自己定义的  AUTOLOAD_NAMESAPCE做自定义扩展

'AUTOLOAD_NAMESAPCE' => [     'Tools' => APP_PATH . 'Vendor\Tools' ]

这样便把 Tools 顶级命名空间注册到了自动载入函数中,当我们

use Tools\Extension\MyTools 时

传入 autoload 的 $class 即为 Tools\Extension\MyTools,得到的 $name 其实为一级命名空间名 这里为 Tools,Tools 不符合第一条件,在 else 中读取自定义的  AUTOLOAD_NAMESAPCE,发现我们有设置 键名为 Tools 的成员

便使用 dirname(键值)得到 APP_PATH . 'Vendor',我是觉得这里 dirname 写的有些鸡肋....所以便成功的映射定位出 Tools一级命名空间所在的文件目录为 APP_PATH . 'Vendor' 下,在与完整的类名 Tools\Extension\MyTools 拼接上 EXT即可定位到类文件,加载即可。

239月/130

PHP CodeIgniter框架源码解析

发布在 邵珠庆

1.index.php :入口文件
|-->define('ENVIRONMENT')  |主要用于设置errors日志输出级别
|-->$system_path |设置系统路径
|-->设置BASEPATH、FCPATH、SYSDIR、APPPATH等    |设置路径信息变量,为加载相应文件信息准备
|-->require_once BASEPATH.core/CodeIgniter.php | 最后加载CodeIgniter.php作为总控制器

2.CodeIgniter.php加载过程,主要用于加载core核心目录下相应文件

|-->require(BASEPATH.'core/Common.php');  |加载core目录下的Common文件,见2.1解析
|-->require(APPPATH.'config/'.ENVIRONMENT.'/constants.php'); |加载constants目录,与开发环境无关时直接使用config目录下的constants目录
|-->get_config(array('subclass_prefix' => $assign_to_config['subclass_prefix']));   |设置子文件,扩展类的前缀
|-->$BM =& load_class('Benchmark', 'core');  |加载benchmark类,mark记录当前的时间
|-->$EXT =& load_class('Hooks', 'core');     |加载core目录下的Hooks钩子类
|-->$EXT->_call_hook('pre_system');  |调用_call_hook(pre_system),根据pre_system内部调用_run_hook执行钩子,在系统开始正式工作前作预处理
|-->$CFG =& load_class('Config', 'core');    |继续执行core下的Config配置文件,
|-->$CFG->_assign_to_config($assign_to_config); 
|-->|$this->set_item($key, $val);      |解析指定给config的配置文件,实质为对config[]赋值
|-->$UNI =& load_class('Utf8', 'core');      |加载了UTF-8编码类,CI_Utf8
|-->$URI =& load_class('URI', 'core');       |加载core目录的URI类,CI_URI
|-->$RTR =& load_class('Router', 'core');    |设置route路由及覆盖信息,见2.2解析
|-->_set_routing()
|-->_set_overrides()
|-->$OUT =& load_class('Output', 'core');    |实例化输出类,加载core目录下的output文件
|-->$OUT->_display_cache($CFG, $URI)         |判断是否存在页面缓存,是则输出文件
|-->$SEC =& load_class('Security', 'core');  |加载core目录下的安全处理文件
|-->$IN =& load_class('Input', 'core');      |实例化输入类,加载core目录下的input文件
|-->$LANG =& load_class('Lang', 'core');     |加载语言类
|-->require BASEPATH.'core/Controller.php';  |加载基本控制器类,见2.3解析
|-->require APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php';  |尝试加载扩展的自定义子类控制器
|-->include(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class().'.php');  |加载自定义控制器下的控制器类
|-->$BM->mark('loading_time:_base_classes_end'); |设定一个benchmark测试点
|-->$class  = $RTR->fetch_class();     |分别获取uri地址的控制器类名和方法名
|-->$method = $RTR->fetch_method();
|-->if ( ! class_exists($class)              |判断方法及类是否合理
OR strncmp($method, '_', 1) == 0
OR in_array(strtolower($method), array_map('strtolower', get_class_methods('CI_Controller')))
)
|-->$EXT->_call_hook('pre_controller');      |处理器执行前进行预处理,并做benchmark设置
|-->$CI = new $class();                      |获取执行的控制器实例,实例化构造器
|-->$EXT->_call_hook('post_controller_constructor');  |实例化控制器类后的钩子处理
|-->if (method_exists($CI, '_remap'))
|-->$CI->_remap($method, array_slice($URI->rsegments, 2))  |如果控制器存在_remap()方法,则执行, 判断条件$CI为控制器类 
|-->else |判断方法在类当中的存在似,如果不存在,则设置
|-->call_user_func_array(array(&$CI, $method), array_slice($URI->rsegments, 2)); |最终传递参数供调用控制类方法
|-->$BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_end'); |benchmark标记时间结束点
|-->$EXT->_call_hook('post_controller');     |控制器生存周期,在控制器执行完成后执行后续操作
|-->$OUT->_display();  |输出页面进行展示
|-->$EXT->_call_hook('post_system');         |请求生存周期完成后的终结操作
|-->$CI->db->close();                        |自动关闭数据库资源

2.1 Core/Common.php加载
|-->function is_php($version)                |用于比较版本号的函数
|-->function is_really_writable($file)       |用于判断是否可以写文件,在不同的系统中可靠程度不同,
      W中通过判断is_readonly,U中如果safe_mode为开则不确定性
|-->function load_class($class, $directory = 'libraries', $prefix = 'CI_')   |用于加载目录下的PHP文件的class类
|-->foreach (array(APPPATH, BASEPATH) as $path)    |分别在application和system目录下轮循
|-->file_exists($path.$directory.'/'.$class.'.php' |找到对应的PHP文件
|-->require($path.$directory.'/'.$class.'.php');   |require加载对应的PHP文件内的类,加了前缀,此处可扩展
|-->break;    |如正确加载则退出,否则继续尝试加载文件
|-->file_exists(APPPATH.$directory.'/'.config_item('subclass_prefix').$class.'.php')  |自扩展的class类,如My_Test
|-->if ($name === FALSE)   |如果$name不存在,则exit()退出 ,(在自定义类加载时,此处可作为扩展点,增加边际条件)
|-->is_loaded($class);     |确类已经加载
|-->$_classes[$class] = new $name();  |加载至静态的classes数祖中,用于缓存,调用时首先从classes中获取
|-->function is_loaded($class = '')                      
|-->设置$_is_loaded数祖,参数$class不为空,判断是否存在gf $_is_loaded,否则设置
|-->function &get_config($replace = array())|用于获取Config的实例化文件
|-->static $_config;       |定义config类型
|-->$file_path = APPPATH.'config/config.php';      |确定application目录路径下定义的config.php的路径
|-->require($file_path);   |加载application/config/config.php类
|-->count($replace) > 0    |对于config.php中定义的变量,如果有replace,则逐个替代
|-->foreach ($replace as $key => $val)
|-->$config[$key] = $val;
|-->return $_config[0] =& $config;   |最后返回定义的config的结果集
|-->function config_item($item)    |配置选项,从config的数祖对象中返还特殊的配置项
|-->$config =& get_config();
|-->$_config_item[$item] = $config[$item];
|-->function show_error            |用于错误信息输出
|-->$_error =& load_class('Exceptions', 'core');    |加载Exceptions类
|-->echo $_error->show_error($heading, $message, 'error_general', $status_code);  |直接输出错误
|-->function show_404              |用于输出404页面,输出的错误信息页面可配置
|-->function log_message           |用于写日志信息
|-->$_log =& load_class('Log');
|-->$_log->write_log($level, $message, $php_error);

|-->function set_status_header     |用于输出状态的heade信息
|-->function _exception_handler
|-->function remove_invisible_characters
|-->function html_escape           |过滤HTML变量
|-->return htmlspecialchars($var, ENT_QUOTES, config_item('charset'));

2.2Router路由信息设置
|-->_set_routing() 
|-->$segments = array()    |根据目录,控制器,函数的触发器设定segment[]的uri段值,分别fetch()方法去取对象值
|-->include(APPPATH.'config/routes.php');       |加载config下的routes文件
|-->$this->routes          |设置routes数祖值,从config的route中获取
|-->$this->default_controller       |设置routes的控制器值,从config的route中获取
|-->return $this->_validate_request($segments); |验证uri的segment合法性
|-->$this->uri->_remove_url_suffix();$this->uri->_reindex_segments();  |进一步清理解析uri,使segment从1开始x
|-->_set_overrides()  |根据$routing的值,重新设定directory、controller、function参数
|-->$this->set_directory($routing['directory']);
|-->$this->set_class($routing['controller']);
|-->$this->set_method($routing['function']);

2.3 core/Controller.php加载
|-->__construct()                                         |构造函数
|-->self::$instance =& $this;  
|-->foreach (is_loaded() as $var => $class)       |根据is_loaded()的信息加载相应的类
|-->$this->$var =& load_class($class);   
|-->$this->load =& load_class('Loader', 'core');  |加载core/Loader的php文件
|-->$this->load->initialize();                    |主要用于autoload加载信息,如libraries、database等等
|-->function &get_instance                                |返回当前实例
|-->return self::$instance

扩展点:PHP自动加载机制在CodeIgniter中的应用
1.PHP自动加载机制:PHP5后,提供了类的自动加载机制,即类在加载时才被使用,即Lazy loading,共有二种方式
1.1: __autoload()通过扩展可实现,实质为设定规则加载相应路径的PHP文件(require、include方式)
1.2: 将autoload_func指向php文件,这个一般用c语言扩展实现 
  详见:http://blog.csdn.net/flyingpig4/article/details/7286438

2.在CodeIgniter中的应用
根据上述源码分析可知:CodeIgniter中所有的操作都是以Controller为起始,只需在Cotroller加载的过程中,
使__autoload()函数自动加载即可,目前的加载方式为在application/config/config.php中设置__autoload()
函数

272月/130

CI框架源码完全分析之核心文件Codeigniter.php

发布在 邵珠庆

$assign_to_config['subclass_prefix']));
}

/*
*php 程序运行默认是30s,这里用set_time_limt延长了,关于set_time_Limit() http://www.phpddt.com/php/set_time_limit.html
* 扩展阅读,关于safe_mode:http://www.phpddt.com/php/643.html ,你会完全明白的
*/
if (function_exists("set_time_limit") == TRUE AND @ini_get("safe_mode") == 0)
{
@set_time_limit(300);
}

/*
* 加载Benchmark,它很简单,就是计算任意两点之间程序的运行时间
*/
$BM =& load_class('Benchmark', 'core');
$BM->mark('total_execution_time_start');
$BM->mark('loading_time:_base_classes_start');

//加载钩子,后期会分析到,这玩意特好,扩展它能改变CI的运行流程
$EXT =& load_class('Hooks', 'core');

//这里就是一个钩子啦,其实就是该钩子程序在这里执行
$EXT->_call_hook('pre_system');

//加载配置文件,这里面都是一些加载或获取配置信息的函数
$CFG =& load_class('Config', 'core');

// 如果在index.php中也有配置$assign_to_config,则也把它加入到$CFG
if (isset($assign_to_config))
{
$CFG->_assign_to_config($assign_to_config);
}

//加载utf8组件、URI组件、Router组件
$UNI =& load_class('Utf8', 'core');
$URI =& load_class('URI', 'core');
$RTR =& load_class('Router', 'core');
$RTR->_set_routing();

//如果在index.php中定义了$routing,那么就会覆盖上面路由
if (isset($routing))
{
$RTR->_set_overrides($routing);
}

//加载output输出组件,不然你怎么用$this->Load->view()啊
$OUT =& load_class('Output', 'core');

//又见钩子,这里你可以自己写钩子程序替代Output类的缓存输出
if ($EXT->_call_hook('cache_override') === FALSE)
{
if ($OUT->_display_cache($CFG, $URI) == TRUE)
{
exit;
}
}

//安全组件啦,防xss攻击啊,csrf攻击啊
//关于xss攻击:http://www.phpddt.com/php/php-prevent-xss.html
//关于csrf:攻击:http://www.phpddt.com/reprint/csrf.html
$SEC =& load_class('Security', 'core');

//加载输入组件,就是你常用的$this->input->post();等
$IN =& load_class('Input', 'core');

//加载语言组件啦
$LANG =& load_class('Lang', 'core');

//引入CI的控制器父类
require BASEPATH.'core/Controller.php';

function &get_instance()
{
return CI_Controller::get_instance();
}

//当然你扩展了CI_Controller控制器的话,也要引入啦
if (file_exists(APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php'))
{
require APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php';
}

//加载你自己应用中的控制器Controller,如果没有当然error啦
if ( ! file_exists(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class().'.php'))
{
show_error('Unable to load your default controller. Please make sure the controller specified in your Routes.php file is valid.');
}
include(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class().'.php');

// 好的基础的类都加载完毕了,咱可以mark一下
$BM->mark('loading_time:_base_classes_end');

//路由获取了控制器名和方法名,比如说默认welcome/index
$class = $RTR->fetch_class();
$method = $RTR->fetch_method();
//这里CI规定一般非公共的方法以_开头,下面是判断,如果URI不可访问就show_404()
if ( ! class_exists($class)
OR strncmp($method, '_', 1) == 0
OR in_array(strtolower($method), array_map('strtolower', get_class_methods('CI_Controller')))
)
{
if ( ! empty($RTR->routes['404_override']))
{
$x = explode('/', $RTR->routes['404_override']);
$class = $x[0];
$method = (isset($x[1]) ? $x[1] : 'index');
if ( ! class_exists($class))
{
if ( ! file_exists(APPPATH.'controllers/'.$class.'.php'))
{
show_404("{$class}/{$method}");
}

include_once(APPPATH.'controllers/'.$class.'.php');
}
}
else
{
show_404("{$class}/{$method}");
}
}

//又是钩子,该钩子发生在控制器实例化之前的
$EXT->_call_hook('pre_controller');

//又mark一个点
$BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_start');
//终于实例化控制器了
$CI = new $class();

//钩子,不想多说了
$EXT->_call_hook('post_controller_constructor');

/*
* ------------------------------------------------------
* Call the requested method
* ------------------------------------------------------
*/
// Is there a "remap" function? If so, we call it instead
if (method_exists($CI, '_remap'))
{
$CI->_remap($method, array_slice($URI->rsegments, 2));
}
else
{
// is_callable() returns TRUE on some versions of PHP 5 for private and protected
// methods, so we'll use this workaround for consistent behavior
if ( ! in_array(strtolower($method), array_map('strtolower', get_class_methods($CI))))
{
// Check and see if we are using a 404 override and use it.
if ( ! empty($RTR->routes['404_override']))
{
$x = explode('/', $RTR->routes['404_override']);
$class = $x[0];
$method = (isset($x[1]) ? $x[1] : 'index');
if ( ! class_exists($class))
{
if ( ! file_exists(APPPATH.'controllers/'.$class.'.php'))
{
show_404("{$class}/{$method}");
}

include_once(APPPATH.'controllers/'.$class.'.php');
unset($CI);
$CI = new $class();
}
}
else
{
show_404("{$class}/{$method}");
}
}

// 终于调用方法了,$this->load->view()把内容放到缓存区
call_user_func_array(array(&$CI, $method), array_slice($URI->rsegments, 2));
}
$BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_end');
$EXT->_call_hook('post_controller');

//这里就是把缓存区的内容输出了
if ($EXT->_call_hook('display_override') === FALSE)
{

$OUT->_display();
}

$EXT->_call_hook('post_system');
//关闭数据库的链接
if (class_exists('CI_DB') AND isset($CI->db))
{
$CI->db->close();
}

 
59月/121

公司&年度&月份经济运营分析(框架)

发布在 邵珠庆

一、主要经济指标完成情况

(一)产量:当期,环比,同比,预算;累计,同比,预算

(二)销量:当期,环比,同比,预算;累计,同比,预算

(三)收入:当期,环比,同比,预算;累计,同比,预算

(四)利润:当期,环比,同比,预算;累计,同比,预算

(五)现金流:当期,环比,同比,预算;累计,同比,预算

二、影响利润变动的因素分析

(一)环比分析

完成,环比;

影响利润增加因素:

影响利润减少因素:

(二)同比分析

完成,同比;

影响利润增加因素:

影响利润减少因素:

(三)累计同比分析

完成,同比;

影响利润增加因素:

影响利润减少因素:

三、影响现金流变动的因素分析

(一)环比分析

(二)同比分析

(三)累计同比分析

四、经济运行中存在的主要问题

五、下月份指标安排及主要措施

(一)指标安排

(二)保证完成指标的主要措施

附表:

1、主要经济技术指标完成情况表(附会计报表)

2、统计类:生产、销售报表、存货变动统计表

3、财务类:往来分析表、资金收支报表

4、预算控制类:成本费用明细表、经营(预算)安排表(年度、季度、月度)

5、财产类:固定资产明细表、盘点表

175月/122

用户体验覆盖表现,框架,结构,范围,战略四个层面

发布在 邵珠庆

 一款产品,必须能够为用户提供价值,才可以称为实用产品。通常,我们会谈论某家餐厅的服务很好,或某电影院的环境很差等,其实,这些都是围绕着用户体验而展开的讨论。

  以购物类网站为例,用户体验主要表现在如下几个层面——

  一、表现层

  在表现层(surface),你看到的是一系列的网页,有图片和文字组成。一些图片是可以点击的,从而执行某种功能,例如把你带到购物车里去。一些图片就只是图片,比如一本书的封面或网站自己的标志。

  二、框架层

  在表现层之下是网站的框架层(skeleton):按钮、表格、照片和文本区域的位置。框架层用于优化设计布局,以达到这些元素的最大效果和效率——使你在需要的时候,能记得标识并找到购物车的按钮。

  三、结构层

  与框架层相比更抽象的是结构层(structure),框架则是结构的具体表达方式。框架层确定了我们的结账页面上交互元素的位置;而结构层则用来设计用户如何到达某个页面,并且在他们做完事情之后能去什么地方。

  框架层定义了导航条上各项的排列方式,允许用户可以浏览书籍的不同类别;结构层则确定哪些类别应该出现在那里。

  四、范围层

  结构层确定网站各种特性和功能的最合适的组合方式,而这些特性和功能就构成了网站的范围层(scope)。有些卖书的网站提供了一个功能,使用户可以保存之前的邮寄地址,这样他们可以再次使用它。

  这个功能——或任何一个功能——是否应该成为网站的功能之一,就属于范围层要解决的问题。

  五、战略层

  网站的范围基本上是由网站的战略层(strategy)所决定的。这些战略不仅仅包括了经营者想从网站得到什么,还包括了用户想从网站得到什么。

  就网上书店的例子而言,一些战略目标是显而易见的:用户想要买书,我们想要卖出它们。另一些目标可能并不是那么容易说清楚的。

136月/100

PHP开源框架

发布在 邵珠庆

国外的开源技术也影响和推动了国内开源程序的发展,上文我介绍的《国外优秀开源PHP建站程序一览》中,很多国外开源程序并不太符合中国人的使用习惯,而 国内有一些厂家或个人也做了一些不错的产品,不少程序是提供源代码下载的,虽然有些在许可协议上和开源许可证有些出入,但其在使用上还是挺符合中国人的使 用习惯,今天我就介绍一些国内的PHP“开源”建站程序。

论坛: Discuz

Discuz非常流行,也是中国地区最多用户使用的论坛程序,论坛搭建非常简单易用,使用风格符合中国人的口味。另一个流行的论坛程序是 PHPWind。

SNS: Ucenter Home

UCenter Home是采用PHP+MySQL构建的社会化网络软件(Social Network Software,简称SNS)。 通过 UCenter Home,建站者可以轻松构建一个以好友关系为核心的交流网络,用户可以使用迷你博客记录;方便快捷地发布日志、上传图片;与其好友们一起分享信息、讨论 话题;了解好友最新动态。

E-Commerce: ECSHOP

ECSHOP是一款开源免费的网上商店系统,用户可以根据自己的商务特征对ECSHOP进行定制,增加自己商城的特色功能。另一个流行的网上商 城系统是shopex。

点评: Modoer

Modoer一款PHP点评系统,可针对多种行业进行点评,可以自由调控点评项目,类型,采用Web 2.0的建站方式,网站会员能让快速上手。

Digg: PBDigg

PBDigg是基于PHP+MYSQL的开源Digg社区资讯系统,融合了社会性标签、主题评论、Rss订阅等多种WEB2.0元素,是一个高 效、快速的网站解决方案。

Wiki: HDWiki

HDWiki是专为中文用户设计和开发的开源、高效的中文百科建站解决方案,免费、易用、功能强大,和UCenter可无缝整合。

RSS: IXNA

IXNA是国内开源PHP新闻聚合程序,支持RSS多核心切换,默认支持lastrss、simplepie、magpierss,支持 RDF、RSS、ATOM,支持智能识别。

CMS: 关于CMS的PHP产品很多,这里主要介绍下面三个。

SupeSite

SupeSite 是一套独立的内容管理系统(CMS),并且拥有对Discuz!论坛信息和UCenter Home个人空间信息聚合的功能,是一个不错的社区门户解决方案。

DedeCMS

DedeCMS是一个比较老的PHP CMS系统,很多早期的用户都是使用这个建立网站的。

KingCMS

KingCMS是一套简单易学,操作简单的开源内容管理系统(CMS),KingCMS分为PHP+MySQL和 ASP+MSSQL/ACCESS两种语言版本的系统。

257月/090

java 媒体框架基础(JMF)

发布在 邵珠庆

Java媒体架构基础
内容表格
1. 关于此指南
2. 一个简单的音频播放器
3. JMF用户接口组件
4. JMF概念
5. 传播和接收媒体
6. 总结以及资源
第一节. 关于此指南

此指南包含的内容?

Java媒体架构(JMF)是一个令人激动的通用的API,它允许Java开发者用许多不同的方法处理媒体。本指南主要通过使用工作的例子提供一个JMF的一些主要的特征的概述。阅读完本指南后,你将会明白JMF体系结构中的主要播放功能。你同样能正确的使用JMF,使用现存的例子和可为更多特殊功能扩展的源代码。

本指南包含着以下主题:
· 下载和安装JMF
· 主要的JMF类以及它们在JMF体系结构中的应用
· 播放本地的媒体文件
· 为媒体的存取和操作制作以和图形用户界面(GUI)
· 通过网络传播媒体
· 通过网络接收媒体

几乎所有的媒体类型的操作和处理都可以通过JMF来实现。全面的讨论JMF所提供的所有特征已经超过了本指南的范围,我们将使用三个简单的媒体应用程序来学习此框架的构建模块。通过这个方法,本指南将为你未来学习和实施更多特殊的应用提供准备。
我应该使用此指南吗?

本指南会带你学习使用JMF工作的基础。为完成这些,我们会创建三个的独立工作的例程序。每个例子都会建立前一个例子的基础上,显示JMF功能性的不同方面。
在本指南中的例子假定你曾经使用过并且已经熟悉了Java程序语言。除了Java核心和JMF的类之外,我们会使用一些Java AWT和Swing类(用于创建GUI),也会有一些Java网络类(用于在网络中传输媒体)。对GUI和网络类一些熟悉有助于你更快的明白观点和这里的例子,但并非是阅读本指南必须的。

我们将学习的例程序如下
· 一个简单的音频播放器(JMF的HelloWorld应用):这个字符界面的播放器通过在命令行中简单的输入媒体文件的名字就可以播放大多数的音频类型。此音频播放器的演示大体上显示了JMF的特有的类。
· 一个图形界面的媒体播放器:我们将使用JMF内置的接口组件来建立图形界面,所以在此练习中必须有一些图形界面的编程经验。这个媒体阅览器演示使用了一些Java AWT和Swing类来为用户显示图形组件。
· 一个媒体广播应用:此应用程序允许一个本地媒体文件通过网络传播。此程序能灵活的使媒体只传输到指定的网络节点,或者传输到一个子网络中的所有节点。此演示使用了一些Java的网络APIs来在网络中传输媒体。
作为第三个练习的一部分,我们将修改图形界面的播放器,让其能接收并且播放媒体。
跳至23页观看Resources,文章,指南,和其他参考书目的列表,这会帮助你学习到更到关于此指南包括的主题。

安装需求
要运行此指南中的例程序,你需要如下的工具和组件:
· Java 2 平台,标准版,编译和运行演示程序
· Java媒体框架,版本2.1.1a或者更高
· 一块已经安装并且配置号的适当的声卡
· 一台或者多台测试机器
· 演示的源代码文件在mediaplayer.jar中
最后的一个演示应用显示了JMF在网络中的应用。如果需要,此演示能运行在一个独立的机器上,使用此机器即是传输方也是接收方。可是要观察到在网络中使用JMF的所有功能,你仍然需要至少两台联网的机器。
在23页中的Resources可下载Java 2平台,完整的源代码文件,以及其他一些完成本指南所需要的工具。

下载安装文件
将JMF安装到你的计算机中的第一步是在JMF的主页中下载安装文件,它同样包括了JMF源代码和API文档的链接。23页的Resources中有下载JMF的链接。
目前,JMF有Windows, Solaris, Linux等版本,以及可运行在任何装有虚拟机的计算机上一个纯Java版本。为了增加性能,你需要下载一个与你操作系统所适应的版本。任何在一个操作系统JMF版本下书写和编译的代码都可以方便的移植到另外的操作系统上。例如,如果你下载了一个Solaris版本的JMF并且编译了一个类,这些类就可以在Linux上使用,不会有任何问题。
作为选择,你可以选择下载纯Java版本,或者跨平台版本的JMF。这些版本没有使用操作系统特有的库文件。如果没有合适的JMF版本适合的操作系统,那么跨平台版本就是一个不错的选择。

安装JMF
下载完JMF安装程序后,双击安装程序的图标。
大部分安装程序都会有个选项,安装本地库到系统目录中;例如,Windows版本安装程序会有一个选项“Move DLLs to Windows/System directory.”。最好将此选项选中,因为它能确保这些操作系统的库文件能正确的安装
在安装的过程中,你还需要选择项目来更新系统的CLASSPATH和PATH变量。如果这些选项被关闭,那么在你编译和运行本指南的例程序的时候就需要在classpath中引入JMF的jar文件。

关于作者
Eric Olson在Retek Inc工作的软件工程师。它在Java平台上有四年的工作经验,并且在不同的基于Java的技术上富有经验,包括JMF, Jini, Jiro, JSP, servlets, and EJBs。Eric毕业于St. Paul, MN的St. Thomas大学,获得计算机科学的学位。他在IBM的SanFrancisco项目组工作,负责WebSphere商业组件。他同时再为Imation Corp.工作,负责存储应用。现在,他正在开发零售行业的基于web的软件解决方案。再业余的时间,Eric和Paul
Monday在Stereo Beacon上合作—一个分布式的点对点的基于JMF的媒体播放器。联系
Eric zpalffy@yahoo.com.
第二节. 一个简单的音频播放器

在本节中,我们将进行创建一个简单的音频播放器的第一个练习。本例将介绍Manager类和Player接口,中两个都是建立大多数基于JMF应用的重要部分。
本例的功能目标是在字符界面下播放本地的音频文件。我们将学习此源代码,并了解每一行所做的任务。完成本节后,你将会有一个基于JMF的可播放包括MP3, WAV, AU等多种音频文件的演示程序。
在本练习后的源代码分类种可查询文件SimpleAudioPlayer.java。

引入必要的类
SimpleAudioPlayer类中包括了一些调用,在其前几行中需要引入所有必要的类:
import javax.media.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.MalformedURLException;

The javax.media包是由JMF定义的多个包之一。javax.media是一个核心包,包括了定义Manager类和Player接口等。本节中,我们主要学习Manager类和Player接口,其余的javax.media类放在后面的章节中。
除了引入javax.media声明外,以上的代码片断引入了一些创建媒体播放器的输入的声明。

Player接口
在下面的代码片断中,创建一个公共类SimpleAudioPlayer并举例定义一个Player变量:
public class SimpleAudioPlayer {
private Player audioPlayer = null;

术语Player听起来由点熟悉,因为它是建立在我们公用的音频或者视频播放器的基础上的。事实上,这个接口的例子就像是当作它们的真实的副本。Players揭示了一个实体上的媒体播放器(如立体音箱系统或者VCR)涉及到功能上的方法。例如,一个JMF媒体播放器可以开始和结束一个媒体流。在本节种,我们将使用Player的开始和结束功能。

在一个文件上创建一个Player
使用JMF获得一个特定媒体文件的Player实例非常简单。Manager类在JMF中如同一个工厂制作许多的特殊接口类型,包括Player接口。因此,Manager类的责任就是创建Player实例,如下例:
public SimpleAudioPlayer(URL url) throws IOException,
NoPlayerException,
CannotRealizeException {
audioPlayer = Manager.createRealizedPlayer(url);
}
public SimpleAudioPlayer(File file) throws IOException,
NoPlayerException,
CannotRealizeException {
this(file.toURL());
}
如果你看完本节的代码,你可以注意到Manager类包含了创建一个Player实例的其他方法。我们会研究其中的一些,如在后面的章节中的DataSource或者MediaLocator的实例化。

Player的状态
JMF定义了大量的一个Player实例可能存在的不同状态。如下:
· Prefetched
· Prefetching
· Realized
· Realizing
· Started
· Unrealized

使用这些状态
因为使用媒体常常是资源非常密集的,由JMF对象揭示的许多方法都是不闭塞的,允许一系列事件监听的状态改变的异步通知。例如,一个Player在它可以启动之前,必须经过Prefetched和Realized状态。由于这些状态的改变都需要一些时间来完成,JMF媒体应用可以分配一个线程来初始化创建Player实例,然后再继续其他的操作。当Player准备就绪的时候,它会通知应用程序其状态已经改变。

在一个如同我们的这样简单的程序中,多功能性的类型并不是很重要。处于这个原因,Manager类也提供了一些创建Realized player的有用方法。调用一个createRealizedPlayer()方法来阻塞调用线程,直到player达到Realized状态。为了调用一个无阻塞的创建player的方法,我们在Manager类中使用了一个createPlayer()方法。下面的一行代码中创建了一个我们需要在例程序中使用的
Realized player:
audioPlayer = Manager.createRealizedPlayer(url);
启动和停止Player
设定一个Player实例的启动或是停止就如同调用Player的一个简单的认证方法,如下所示:
public void play() {
audioPlayer.start();
}
public void stop() {
audioPlayer.stop();
audioPlayer.close();
}

调用SimpleAudioPlayer类中的play()方法来实现调用Player实例的start()方法。调用此方法后,你能听到本地的喇叭的声音文件。同样的,stop()方法使player停止并且关闭掉Player对象。

对于读取和或者播放本地媒体文件来说,关闭Player实例释放所有资源是一个有用的方法。因为这是一个简单的例子,关闭Player是终止一个会话可接受的方法。但是在实际的应用中,你需要小心的确认在除掉Player之前必须要关闭掉。一但你已经关闭掉player,在再次播放一个媒体之前你必须要创建一个新的Player实例(等待它的状态改变)。

建立一个SimpleAudioPlayer

最后,这个媒体播放应用程序要包含一个可以从命令提示行中输入命令而调用的main()方法。在此main()方法中,我们将调用创建SimpleAudioPlayer的方法:
File audioFile = new File(args[0]);
SimpleAudioPlayer player = new SimpleAudioPlayer(audioFile);
在播放音频文件之前的唯一的一些事情就是调用已经创建的音频player的方法play(),如下所示:

player.play();
要停止和清除掉音频player,在main()方法中也应该有如下调用:
player.stop();
编译和运行SimpleAudioPlayer
通过在命令提示行输入javac SimpleAudioPlayer.java来编译例程序。所创建的文件SimpleAudioPlayer.class在当前工作目录中。
然后在命令提示行中键入如下命令来运行例程序:
java SimpleAudioPlayer audioFile

将audioFile替换成你本地机器上的音频文件。所有的相对文件名都试相对于当前的工作目录。你会看到一些当前正在播放文件的标志信息。要终止播放,按下回车键。
如果编译失败,确认JMF的jar文件已经正确的包含在CLASSPATH环境变量中。
第三节. JMF用户界面组件
播放视频
在前一节中,我们学习了建立一个通过字符界面播放音频文件的应用程序。JMF中一个最重要的特点就是你不需要为了配置媒体播放器而去了解媒体文件的格式;一切都内置了。举一个例子,再我们前面的例子中,需要使用MP3格式的时候,我们不需要让应用程序为一个MP3文件建立一个特殊的Player。
如同你将会再本节所见到的,对于视频文件的操作同样有效。JMF有所有媒体文件类型接口的详细资料。
处理视频媒体与音频最大的不同就是,我们必须建立一个能播放视频的显示屏幕。幸运的是,JMF能处理许多的这些资料。如同再上例一样我们会建立一个Player对象,并且使用很多的可视组件来直接从JMF对象中创建我们的可视的媒体浏览器。
本节中,我们将学习两个例程序。In this section, we'll walk through the second example application. 请再后面的练习的源代码分布中查阅MediaPlayerFrame.java。

关于例子
在本节中,我们将创建一个能显示和运行本地音频和视频媒体的应用程序。作为练习的一部分,我们将研究JMF内置的一些GUI组件。熟悉AWT和Swing将有助于你理解本例,但这并不是必须的。除非需要直接涉及到JMF的GUI组件,或者我们是不会详细介绍源代码的。你可以在源代码的注释中找到这里未涉及的详细说明。
本例中我们使用的许多概念,类和方法都和第一个例子的类似。建立Player的基本操作大都一样。最大的不同就是我们需要对Player对象专研更深一点,特别当需要从Player获取媒体信息的时候。

如何开始
视频播放器例子被设计得如同音频播放例子一样通过命令行来运行,但是本例需要建立在GUI基础上。如同在上节一样,我们先通过媒体文件名调用应用。然后,应用程序显示一个带有可操作媒体组件的窗体。
在MediaPlayerFrame开始的一行中我们定义了类并扩展自,javax.swing.Jframe类。这就是使媒体播放器如同一个在桌面上的单独窗体的方法。任何客户机程序创建了本媒体播放对象后都可以通过调用Jframe类中定义的show()方法来显示。
下面是一个MediaPlayerFrame正在播放MPEG电影的屏幕截图:

获取GUI组件
Player界面有一些方法来获取已选择可视组件的涉及。在MediaPlayerFrame中,我们使用如下组件:
· player.getVisualComponent()是一个播放所有视频媒体的可视组件。
· player.getControlPanelComponent() 是一个操作时间轴的可视组件(包括开始,停止,回放),也包含了一些媒体流的有用信息。
· player.getGainControl().getControlComponent() 是操作音量(增加)的可视组件。getGainControl()方法返回一个GainControl实例,可用于改变节目的增加等级。

使用可视化组件
上面的界面方法都返回一个java.awt.Component类的实例。没个实例都视可加载到我们窗体上的可视组件。这些组件都与Player有直接的联系,所以在这些组件上的所有可视元素的处理都会产生Player播放媒体后相应的变化。
在我们将这些组件加入到我们的窗体的之前,必须要保证它们不为空。因为并不是所有的媒体播放器包括每一种可视组件,我们只需添加相关播放器类型的组件。比如,一般来说一个音频播放器没有可视组件,所以getVisualComponent()就要返回空。你不会想在音频播放器窗体上添加可视组件的。

获得媒体的特殊控制
一个Player实例也可以通过getControl()和getControls()方法来暴露其控制,getControls()返回一个控制对象集,而getControl()返回一个控制。不同的播放器类型可选择为特殊的操作来暴露控制集去指定的媒体类型,或者用于获取该媒体的传输机制。如果你在写一个只支持某些媒体类型的播放器,你需要依靠某些在Player实例中可用Control对象。
由于我们的播放器是非常抽象的,被设计于播放多种不同媒体类型,我们简单的为用户暴露所有的Control对象。如果找到任何扩展的控制集,我们就可使用getControlComponent()方法来增加相应的可视控件到标签面板上。通过这个办法,用户就可以观察播放器上的所有组件。以下代码片断将所有的控制对象暴露给用户:
Control[] controls = player.getControls();
for (int i = 0; i < controls.length; i++) {
if (controls[i].getControlComponent() != null) {
tabPane.add(controls[i].getControlComponent());
}
}

为了使一个真实的应用程序能用Control实例做一些有用的事(除了能显示可视组件之外),应用程序需要知道该Control的特殊类型,并分配它。此后,应用程序就可使用这些control来控制媒体节目了。例如,如果你知道你经常使用的媒体暴露javax.media.control.QualityControl类型的Control,你能使用QualityControl界面,之后在QualityControl界面上通过调用各种方法来改变性质设定。

使用一个MediaLocator
在我们新的基于GUI的媒体播放器和我们的第一个简单播放器之间最大的不同就是,我们使用一个MediaLocator对象而不是URL来创建Player实例,如下所示:
public void setMediaLocator(MediaLocator locator) throws IOException,
NoPlayerException, CannotRealizeException {
setPlayer(Manager.createRealizedPlayer(locator));
}

我们将在稍后的章节中讨论这个变化的原因。目前,在网络上资源站点上,关于MediaLocator对象和URL的描述被认为是非常相似的。事实上,你可以从一个URL创建一个MediaLocator,也可以从MediaLocator获取到URL。我们的新媒体播放器一个URL中创建一个MediaLocator,并使用该MediaLocator通过文件创建了一个Player。

编译和运行MediaPlayerFrame
通过在命令提示行输入javac MediaPlayerFrame.java来编译例程序。在工作目录下将创建一个名为MediaPlayerFrame.class的文件。
在命令提示行中键入如下来运行例程序:
java MediaPlayerFrame mediaFile

你需要用你本机上的一个媒体文件来替换掉mediaFile(音频或者视频文件都可以)。所有的相对文件名都是相对于当前工作目录。你会看见一个显示控制媒体文件的GUI控制集的窗口。欲了解JMF支持的音频和视频文件列表,在23页的资源。
如果初始编译时失败,请确认JMF的jar文件已经包含在当前的CLASSPATH环境变量中。

MediaPlayerFrame在行动
在本节前你看见的一个视频播放器正在播放MPEG视频文件的屏幕截图。下面的屏幕截图显示了一个音频播放器正在播放一个MP3文件:
要更多的学习本练习中的例子,查看完成的MediaPlayerFrame源代码。

第四节. JMF概念
JMF体系结构
你曾见过了使用JMF播放本地媒体文件是多么的容易,现在我们将后退一步,来看看一幅是如何通过JMF创建了如此成熟的基于媒体的应用程序的大的画面,是如何通过JMF创建了如此成熟的基于媒体的应用程序。全面的了解JMF体系结构是没有意义的,本节将给你一个大体的概念,关于高级的JMF组件是如何组合起来创建想得到的东西。
JMF的组件结构非常的灵活,它的组件一般可以分成三个部分:
· Input描述某种被用于在进程休息的时候作为一个输入的媒体。
· process执行某些输入上的活动。一个过程有一个明确的输入和输出。大量的过程可用, 能被用于一个输入或者一批输入。这些过程能被联系起来,一个过程的输出被用于另外一个过程的输入。在这种风格中,大量的过程可能被应用于一个输入。(这段期间是可选择的——我们开始的两个例子没有包含真正的数据过程,只有一个来自文件的输入和一个通过Player的输出。)
· Output 描述了媒体的某些目的地。

从这些描述中,你可以想象到JMF组件体系结构听起来就好像在一个典型的立体声系统或者VCR之后。很容易设想到,使用JMF就如同打开电视或者在立体声音箱系统下调节声音的风格。例如,录制喜爱的电视节目的简单的动作能在这些组件的基础中:
· Input 是电视广播流,在同一个频道运输音频和视频。
· Process 是一个记录设备(就是,一个VCR或者许多的数字设备)转换模拟或者数字音频视频广播流成适合复制到磁带或其他媒体上的格式。
· Output 是记录已格式化轨迹(音频和视频)到某些类型的媒体上。

JMF资料处理模式
以下图片说明了JMF数据处理模块并对每个类型给出了例子:
使用此模式,很容易明白我们前面的两个例子,从文件中输入音频和视频并输出到本地计算机上。在后面的章节中,我们也会谈论一些通过传播和接收音频媒体的JMF网络功能。

处理模型例子
将JMF的输入,处理和输出模式联系起来,我们能开始想象许多基于媒体的操作都可能通过JMF完成。一个例子,转换一种媒体类型为其他类型并将其输出存储到一个新的文件。举一个例子,我们想要在不损坏原始文件的前提下转化一个WAV格式的音频文件为MP3格式。以下的过程模式插图,就是我们将开始执行转换的步骤:
本例的输入是一个WAV文件。它被一个媒体格式转换工具加工,并输出到一个新的文件。现在,让我们看看JMF API中的这个模式的每一步。我们使用输入,处理和输出模式作为概念上的路标。

JMF输入
再JMF中,一般由一个MediaLocator对象来描述一个输入。如先前规定的,
MediaLocator的外观和行为都非常象一个URL,这样它可以唯一确定网络上的一个资源。事实上,使用一个URL来创建一个MediaLocator是完全可能的;我们在前面的两个例子中就是这样做的。
为了我们的媒体转换例子,我们需要建立一个MediaLocator来描述最初的WAV文件。如同我们将在后面的章节中见到的,一个MediaLocator也可以用于描述一个跨越网络中媒体流。在这个案例中,MediaLocator会描述传播的URL――很像一个被URL指定的在Web上的资源,用于取代指定一个本地文件系统的文件来建立MediaLocator。

一个MediaLocator和一个URL之间的不同
要成功的建立一个URL对象,需要适当的java.net.URLStreamHandler安装于系统中。这个流处理的用途是能够处理被URL描述的流类型。一个MediaLocator对象并没有这个需要。例如,我们的下个应用程序将使用实时传输协议(RTP)在网络上传输音频。由于多数的系统都未为RTP协议安装一个URLStreamHandler,所以创建一个URL对象会失败。在这个应用中,只有MediaLocator对象会成功。
要理解更多关于URL对象以及创建和注册一个URLStreamHandler的信息,查阅JDK帮助文档(查看23页资源)。

JMF处理机
当我们使用JMF的时候,应用程序的处理机组件被Processor接口实例描述。你需要已有些熟悉Processor,它扩展至Player接口。由于Processor继承直Player接口,它同样也从Player继承所有可用属性。另外,Processor增加了两个属性:Configuring和Configured。这些扩展的属性(和与之关联的方法)用于Processor从输入流收集信息时的通信。
在我们的最后的例程序中,我们将建立一个Processor用于将MP3编码格式的音频转换成适合在网络上传播的格式。在稍后的板块中我们会讨论创建一个简单的Processor的步骤。

JMF输出
有少许的方法用于描述JMF中处理模式的输出状态。最简单的(并且我们将在最后一个例子中使用的)是javax.media.DataSink接口。一个DataSink读取媒体内容并且将其传送到一些目的地。本节中最开始的音频格式转换过程中,MP3(输出)文件将被DataSink描述。在我们最后一个例子中,我们将使用一个DataSink在实际上完成网络中传播音频媒体的工作。一个DataSink是在Manager类中,由指定一个DataSource(输入到DataSink)和一个MediaLocator(输出到DataSink)完成的。
一个DataSource实例描述可用于Players,Processors和DataSinks的输入数据。一个处理机的输出也被描述成一个DataSource对象。
这就是为什么处理器能彼此联系起来,在同一媒体数据中完成多种操作。这也是来自Processor的输出能作为输入被Player或者DataSink使用的原因(它可将媒体传递到输出目的地)。
一个DataSink的最后目的文件由一个MediaLocator对象说明。如同前面一样,MediaLocator描述一个网络资源;这就是媒体流将被传递的地方。

第五节.传播接收媒体
JMF和实时传输协议(RTP)
许多的友善网络的特征直接建立在JMF中,这些使为客户端程序通过网络传输和接收媒体非常容易。当在一个网络上的一个用户想要接收任何种类的媒体流的时候,它不需要在观看媒体前等待全部的广播下载到机器上;用户可以实时的观看广播。在流媒体中些提出了这个概念。通过流媒体,一个网络客户端能接收到其他机器上广播的音频,甚至获取正在发生的实况视频广播。
在IETF RFC 1889中定义了实时传输协议(RTP)。发展在快速和可靠的状态下通过网络传输时间极其敏感的数据,RTP在JMF中用于提供给用户向其他网络节点中传输媒体流的方法。
在本节中,我们将学习我们的最后一个例程序。这里,你将学习到如何传输一个存储在一台机器上的MP3文件到另外的在同一个网络的机器上去。实际的MP3源文件并不从主计算机上移除,它也不使复制到其他机器上去;事实上它将会转换成能使用RTP传输的文件格式并通过网络发送。一旦被一个客户端接收到,源文件(现在是RTP信息包的形式)可以再次传输,这一次是在接收机器上可播放的一种格式。
在MediaTransmitter.java文件中源代码查看学习以下练习。

设置处理模式
我们可以在前面的章节中定义的处理模式的基础下来讨论我们的最终的例子。在传输机器上,处理模式看起来像这样:
事实上,MediaTransmitter对象源代码包括了以下三行:
private MediaLocator mediaLocator = null;
private DataSink dataSink = null;
private Processor mediaProcessor = null;
这三个实例变量可以直接映射到前面的处理模式图表,如下:
· mediaProcessor变量是我们的处理器;它将负责转换音频文件从MP3文件模式到一个适合通过RTP协议传输的格式。
· dataSink变量是我们的输出块。
· 当我们建立DataSink时我们需要指定一个MediaLocator,它是DataSink的目的文件。

当我们通过运行DataSink我们的处理过的媒体,它将传输到我们在MediaLocator中指定的地点。

RTP MediaLocator
在前面的两个练习中,我们通过从文件中获得的一个URL建立了MediaLocator实例。 在本练习中,我们必须建立一个MediaLocator来描述网络上媒体传播输出流;换句话说,我们必须创建一个能我们的音频传播的目的地的MediaLocator。一个RTP MediaLocator符合如下规则,看起来就像一个典型的URL:
rtp://address:port/content-type
让我们看看上面URL规范的每一段:
· address 是将传输的媒体的地址。以单播的模式传输(一个专用IP地址),地址将会是有意接收的机器的IP地址。以广播的模式传播(到子网中的所有机器),地址将会是以255作为最后的一块的子网地址。举个例子,如果我再子网中可指定地址为192.168.1和想要传播到子网中的所有节点,我可以指定192.168.1.255作为地址;这样允许子网中的每个节点监听广播媒体。
· port 必须是被传输者和接收者都允许的一个端口。
· content-type 是媒体流类型。在我们的案子中,这个将会是音频。
下面的一个简单的RTP传播MediaLocator例子会让所有在指定网络中的机器接收到媒体流:
rtp://192.168.1.255:49150/audio
创建一个处理机
在setDataSource()方法中我们首先要做的就是创建一个Processor实例。
下面的Processor的职责是转换MP3音频媒体为一个RTP来表示:
public void setDataSource(DataSource ds) throws IOException,
NoProcessorException, CannotRealizeException, NoDataSinkException {
mediaProcessor = Manager.createRealizedProcessor(
new ProcessorModel(ds, FORMATS, CONTENT_DESCRIPTOR));
在Manager类中,我们能创建一个Processor对象,通过两种方法中的一种:
createProcessor()或者createRealizedProcessor()。你很可能会注意到这两个方法样式的显示和前面例子中创建一个Player的方法很相似。在目前的例子中,我们将创建一个已实现的Processor。我们这样做是因为我们使用的应用非常简单,在Processo处于Realized状态时我们不需要关心任何真实的工作。

创建一个ProcessorModel
创建一个已实现的Processor,我们需要创建一个为Processor描述输入和输出媒体类型的ProcessorModel实例。为了创建ProcessorModel,我们需要下面的一些:
· 一个DataSource,将被处理的媒体(输入文件)。
· 一个javax.media.Format数组,描述输入媒体的格式。
· 一个javax.media.protocol.ContentDescriptor实例,为我们的处理机描述输出格式。传送者的DataSource是通过一个参数传递到此方法。

定义输入和输出格式
因为我们的MediaTransmitter类会被时常用于将输入媒体格式(MP3)转换成一种输出格式(音频RTP),中学对象被定义成静态。我们创建一个新的javax.media.format.AudioFormat实例用于描述媒体输入类型(在java帮助文档中查看可用格式)。这就是我们的处理机可以获取MP3音频文件的原因。
我们也创建一个javax.media.protocol.ContentDescriptor实例来描述想要处理机输出的。在我们的案子中,这是一个RTP媒体流。

这就是为什么我们的处理机可以只制造RTP流。
下面的代码片断显示了我们如何设置格式和内容描述符变量,用于创建ProcessorModel对象:
private static final Format[] FORMATS = new Format[] {
new AudioFormat(AudioFormat.MPEG_RTP)};
private static final ContentDescriptor CONTENT_DESCRIPTOR =
new ContentDescriptor(ContentDescriptor.RAW_RTP);
连接输入,处理机和输出
现在我们有一个处于Realized状态的Processor,我们需要设置DataSink以能实际上传播RTP媒体。创建DataSink是简单的大概使用另外一个调用给Manager对象,如下所示:

dataSink = Manager.createDataSink(mediaProcessor.getDataOutput(),
mediaLocator);
createDataSink()方法获取新Processor的输出(作为一个DataSource参数)和MediaLocator对象,我们和MediaTransmitter对象同时建立的。通过这样,你能开始我们的不同的组件是如何在处理模式中联系起来的:我们从一个Processor中获取输出并使用他们作为输入到其他组件。在这个特殊的应用中,Processor输出用于传输媒体的DataSink的一个输入。

创建一个DataSource实例
在这点上,我们全部都是做和设置我们的媒体播放器的广播传输。
我们需要创建DataSource对象,我们用于创建处理机(就是,在我们的MediaTransmitter中,参数传递到setDataSource()方法)。下面是创建一个DataSource实例的代码:
File mediaFile = new File(args[1]);
DataSource source = Manager.createDataSource(new MediaLocator(
mediaFile.toURL()));
这段代码是在MediaTransmitter对象中的vmain()方法。这里我们通过从命令行输入的第二个参数创建一个File对象。我们通过文件创建一个MediaLocator,而后通过位置创建一个DataSource。这个新近创建的DataSource是一个涉及到传送者的输入文件。我们能使用这个DataSource初始化传输者。

开始和停止MediaTransmitter
我们通过调用其中的startTransmitting()方法来开始MediaTransmitter,如下所示:
public void startTransmitting() throws IOException {
mediaProcessor.start();
dataSink.open();
dataSink.start();
}
这个方法首先开启处理机,然后打开并启动DataSink。在这个调用后,接收机器就可在媒体传送者上监听。
停止传输者是非常简单的。以下代码将DataSink和Processor都停止和关闭掉:
public void stopTransmitting() throws IOException {
dataSink.stop();
dataSink.close();
mediaProcessor.stop();
mediaProcessor.close();
}
编译和运行MediaTransmitter
通过在命令行中输入javac MediaTransmitter.java来编译例程序,可在你的工作目录中生成一个同名的.class文件。
要运行例程序,在命令提示行中输入以下代码:
java MediaTransmitter rtpMediaLocator audioFile
此例将创建一个myAudio.mp3文件的媒体广播。不要忘记将rtpMediaLocator替换成一个媒体传输的RTP URL,如同先前讨论的。
你同样也需要将audioFile替换成你本机的音频文件名。
所有的相对文件名都是相对于当前工作目录的。你会看见一些信息标志正在播放的文件。按下Enter键来停止播放。

为传送者的一个例命令行交互如下:
java MediaTransmitter rtp://192.168.1.255:49150/audio myAudio.mp3
如果初始编辑失败,确定JMF的jar文件包含CLASSPATH环境变量中。要近一步探索本程序和练习,请查阅MediaTransmitter源代码。

接收传输的媒体
现在你可能会问,“如果没有人可以看或者收听的话,这个传播媒体有什么好的?”
幸运的是,设定一个接收传播媒体的客户端只需要对我们在第二个例程序的MediaPlayerFrame源代码做很小的改动。
MediaPlayerFrame类需要一个很小的调节来接收和播放音频文件。在main()方法中,你需要注释掉如下的一行:
mpf.setMediaLocator(new MediaLocator(new File(args[0]).toURL()));

并且输入如下的一行:
mpf.setMediaLocator(new MediaLocator(args[0]));

这个简单的改动允许我们通过String来创建一个MediaLocator对象,而不是通过创建一个File来创建MediaLocator。
其他代码都一样。

指定RTP URL
在12页的说明编译和运行MediaPlayerFrame介绍了如何编译和运行MediaPlayerFrame例程序。这唯一的不同就是你需要为传输者指定RTP URL。为接收者的例命令行交互如下:
java MediaPlayerFrame rtp://192.168.1.255:49150/audio
运行网络媒体传送者的注意事项
如果你在网络上只有权使用一台机器,你仍然可以运行传输程序。当你启动传送程序的时候,你可以即使用RTP URL传输地址,也可指定你工作的机器的机器地址。为了能够调节传输,在开始前接收者必须使用精确的同样的RTP URL。
如果你运行本例真实的网络版本,每台你使用的机器都需要安装JMF,不论是传输还是接收媒体流。这是必须的,因为不论是传送程序还是接收程序都大量的使用了JMF的API。
在任一个案子中,确认在指定的RTP URL中使用了相同的地址和端口;否则媒体传输是不会工作的。

第六节. 约束和资源
摘要
我希望本指南能给你提供如何使用JMF的API的有用的浏览。
我们建立了三个小的应用程序来播放本地的音频和视频,也通过网络传播和接收媒体。这些应用程序的源代码中包含了很多的javadoc样式的注释。这就有助于你理解你剩余的问题。
许多JMF的主题在本指南中并没有涉及。实际上,我们更关注JMF的基本概念和应用;在此基础上,我们能轻易地扩展学习的其他范围。要更深入JMF的应用程序,你可能想要学习下面的面板中所提到的主题。更近一步的阅读本指南中的主题,查阅23页的资源。

高级主题
大量的值得做的练习在本指南的范围之上。在简单的说明之下自己更进一步的学习,你可以扩展我们的应用程序代码,也可以反展你的JMF相关知识。使用以下的练习开始:
· 媒体捕获:JMF包含了丰富的API来捕获媒体数据。如果你对使用JMF捕获媒体感兴趣,你可以使用javax.media.CaptureDeviceManager类和javax.media.protocol.CaptureDevice接口的API来学习。对于一个高级的练习,考虑使用CaptureDeviceManager和CaptureDevice接口来增加媒体捕获功能到媒体播放应用程序的GUI版本上。
· 会话管理:由于本指南是一个JMF的说明,我们使输出表现非常的简单,仅仅实现了javax.media.DataSink输出。
另外的输出表示是使用javax.media.rtp.SessionManager。这个管理类允许客户端创建并监视他们的RTP流和连接。通过SessionManager并随后创建流,它可能非常的接近监视RTP会话。作为一个高级的练习,转换我们的地三个演示程序来使用SessionManager,然后监听流出的RTP流已经哪些客户端在收听。
· 使用JMF的多点传送:我们的广播演示应用程序说明了如何传送一个网络的媒体到另外一个网络的一或多台机器上去。它也可能使用JMF中的多点传输协议来提供给更复杂,多用户的网络。
JMF用户指南提供了一个使用JMF的多播协议的更深入的探讨。更进一步追踪本主题查看23页资源。
· 传输视频: 我们的最后一个演示应用程序着眼于如何传输一个MP3音频文件,但是JMF也能够通过网络传递视频。关注API文档中的Format和ContentDescriptor类获得如何使用的更好的方法。
· 导入/导出RTP媒体流: JMF同样允许将RTP流保存为文件以便将来使用。举一个实例,一个远程电信会议可以保存下来以后再看。
由于流已经保存再RTP格式中,已经不需要再次转换,这样可导致传输程序的性能改进。通过一个文件而不是URL来设置DataSink对象中输入/输出MediaLocator。你会再JMF用户指南中发现更深层次的主题探讨。

资源
JMF
· 下载mediaplayer.jar,本指南中使用的完整的例源代码。
· JMF主页 (http://www.javasoft.com/jmf)是最好的探讨JMF更多信息的资源。
· 你可以找到JMF说明书(http://java.sun.com/products/java-media/jmf/2.1.1/specdownload.html),再Java开发者联盟上包括API文档和JMF用户指南。你必须有权使用所有的这些资源,如果你想做任何更深入的JMF编程的话。
· 官方的JMF支持文件格式 页面
(http://java.sun.com/products/java-media/jmf/2.1.1/formats.html) 列出了所有可为JMF辨识并播放的文件格式。此文件格式页面也包括了学习更多关于捕获设备和RTP格式的参考。
· MPEG-4 Video for JMF (http://www.alphaworks.ibm.com/tech/mpeg-4), 来自IBM
alphaWorks, 是一个JMF的视频编解码器。
RTP
· IETF RTP RFC (http://www.ietf.org/rfc/rfc1889.txt) 非常详细的描述了RTP协议。
· 查看 JMF API Guide
(http://java.sun.com/products/java-media/jmf/2.1.1/specdownload.html) ,有许多有关于RTP协议和描述以及它是如何在JMF上应用的。
· 哥伦比亚大学有一个比较有用的RTP FAQ(http://www.cs.columbia.edu/~hgs/rtp/faq.html).
Java技术
· Java 2 Platform, Standard Edition (http://java.sun.com/j2se/) 可从sun公司获得。
· sun的指南关于JFC/Swing (http://java.sun.com/docs/books/tutorial/uiswing/index.html)
和 AWT (http://java.sun.com/docs/books/tutorial/information/download.html#OLDui) 是非常好的能学习到很多关于Java程序语言中GUI编程的好地方。
· 另外一个sun指南学习network programming 基础
(http://java.sun.com/docs/books/tutorial/networking/index.html)。
多点传输协议
· Explicit Multicast (XCAST)
(http://oss.software.ibm.com/developerworks/opensource/xcast/) 是IP多点传输的一种形式,为非常多的多点传输组设计提供可升级的支持,这些组有些少量的参与者代表。XCAST 代码得到了IBM Common Public License的认可。
· Todd Montgomery 的 MTP page (http://www.nard.net/~tmont/rm-links.html),
在这里你能找到一个广泛的涉及到多点传输协议的列表。
附加资源
· 你可以在
developerWorks Java technology zone (http://www-106.ibm.com/developerworks/java/)中找到许多的关于Java各方面的内容。
· 查看 developerWorks tutorials page
(http://www-105.ibm.com/developerworks/education.nsf/dw/java-onlinecourse-bytitle?OpenDocument&Count=for a complete listing of free tutorials.

311月/080

SSH框架的优点

发布在 邵珠庆

spring:Aop管理事务控制,IoC管理各个组件的耦合,DaoTemplate作为常规持久层的快速开发模板!
struts:控制层Action,页面标签和Model数据,调用业务层
hibernate:负责数据库和对象的映射,负责DAO层(Data Access Object:数据访问)

典型的J2EE三层结构,分为表现层、中间层(业务逻辑层)和数据服务层。三层体系将业务规则、数据访问及合法性校验等工作放在中间层处理。客户端不直接与数据库交互,而是通过组件与中间层建立连接,再由中间层与数据库交互。

表现层是传统的JSP技术,自1999年问世以来,经过多年的发展,其广泛的应用和稳定的表现,为其作为表现层技术打下了坚实的基础。

中间层采用的是流行的Spring+Hibernate,为了将控制层与业务逻辑层分离,又细分为以下几种。

Web层,就是MVC模式里面的“C”(controller),负责控制业务逻辑层与表现层的交互,调用业务逻辑层,并将业务数据返回给表现层作组织表现,该系统的MVC框架采用Struts。

Service层(就是业务逻辑层),负责实现业务逻辑。业务逻辑层以DAO层为基础,通过对DAO组件的正面模式包装,完成系统所要求的业务逻辑。

DAO层,负责与持久化对象交互。该层封装了数据的增、删、查、改的操作。

PO,持久化对象。通过实体关系映射工具将关系型数据库的数据映射成对象,很方便地实现以面向对象方式操作数据库,该系统采用Hibernate作为ORM框架。

Spring的作用贯穿了整个中间层,将Web层、Service层、DAO层及PO无缝整合,其数据服务层用来存放数据。

一个良好的框架可以让开发人员减轻重新建立解决复杂问题方案的负担和精力;它可以被扩展以进行内部的定制化;并且有强大的用户社区来支持它。框架通常能很好的解决一个问题。然而,你的应用是分层的,可能每一个层都需要各自的框架。仅仅解决UI问题并不意味着你能够很好的将业务逻辑和持久性逻辑和UI 组件很好的耦合。

不可否认,对于简单的应用,采用ASP或者PHP的开发效率比采用J2EE框架的开发效率要高。甚至有人会觉得:这种分层的结构,比一般采用JSP + Servlet的系统开发效率还要低。

笔者从一下几个角度来阐述这个问题。

— 开发效率:软件工程是个特殊的行业,不同于传统的工业,例如电器、建筑及汽车等行业。这些行业的产品一旦开发出来,交付用户使用后将很少需要后续的维护。但软件行业不同,软件产品的后期运行维护是个巨大的工程,单纯从前期开发时间上考虑其开发效率是不理智的,也是不公平的。众所周知,对于传统的ASP和 PHP等脚本站点技术,将整个站点的业务逻辑和表现逻辑都混杂在ASP或PHP页面里,从而导致页面的可读性相当差,可维护性非常低。即使需要简单改变页面的按钮,也不得不打开页面文件,冒着破坏系统的风险。但采用严格分层J2EE架构,则可完全避免这个问题。对表现层的修改即使发生错误,也绝对不会将错误扩展到业务逻辑层,更不会影响持久层。因此,采用J2EE分层架构,即使前期的开发效率稍微低一点,但也是值得的。

— 需求的变更:以笔者多年的开发经验来看,很少有软件产品的需求从一开始就完全是固定的。客户对软件需求,是随着软件开发过程的深入,不断明晰起来的。因此,常常遇到软件开发到一定程度时,由于客户对软件需求发生了变化,使得软件的实现不得不随之改变。当软件实现需要改变时,是否可以尽可能多地保留软件的部分,尽可能少地改变软件的实现,从而满足客户需求的变更?答案是——采用优秀的解耦架构。这种架构就是J2EE的分层架构,在优秀的分层架构里,控制层依赖于业务逻辑层,但绝不与任何具体的业务逻辑组件耦合,只与接口耦合;同样,业务逻辑层依赖于DAO层,也不会与任何具体的DAO组件耦合,而是面向接口编程。采用这种方式的软件实现,即使软件的部分发生改变,其他部分也尽可能不要改变。

注意:即使在传统的硬件行业,也有大量的接口规范。例如PCI接口、显卡或者网卡,只要其遵守PCI的规范,就可以插入主板,与主板通信。至于这块卡内部的实现,不是主板所关心的,这也正是面向接口编程的好处。假如需要提高电脑的性能,需要更新显卡,只要更换另一块PCI接口的显卡,而不是将整台电脑抛弃。如果一台电脑不是采用各种接口组合在一起,而是做成整块,那将意味着即使只需要更新网卡,也要放弃整台电脑。同样,对于软件中的一个个组件,当一个组件需要重构时,尽量不会影响到其他组件。实际上,这是最理想的情况,即使采用目前最优秀的架构,也会有或多或少的影响,这也是软件工程需要努力提高的地方。

技术的更新,系统重构:软件行业的技术更新很快,虽然软件行业的发展不快,但小范围的技术更新特别快。一旦由于客观环境的变化,不得不更换技术时,如何保证系统的改变最小呢?答案还是选择优秀的架构。

在传统的Model 1的程序结构中,只要有一点小的需求发生改变,将意味着放弃整个页面。或者改写。虽然前期的开发速度快,除非可以保证以后永远不会改变应用的结构,否则不要采用Model 1的结构。

采用Hibernate作为持久层技术的最大的好处在于:可以完全以面向对象的方式进行系统分析、系统设计。

DAO模式需要为每个DAO组件编写DAO接口,同时至少提供一个实现类,根据不同需要,可能有多个实现类。用Spring容器代替DAO工厂

通常情况下,引入接口就不可避免需要引入工厂来负责DAO组件的生成。Spring实现了两种基本模式:单态模式和工厂模式。而使用Spring可以完全避免使用工厂模式,因为Spring就是个功能非常强大的工厂。因此,完全可以让Spring充当DAO工厂。

由Spring充当DAO工厂时,无须程序员自己实现工厂模式,只需要将DAO组件配置在Spring容器中,由 ApplicationContext负责管理DAO组件的创建即可。借助于Spring提供的依赖注入,其他组件甚至不用访问工厂,一样可以直接使用 DAO实例。

优点:
Struts跟Tomcat、Turbine等诸多Apache项目一样,是开源软件,这是它的一大优点。使开发者能更深入的了解其内部实现机制。
除此之外,Struts的优点主要集中体现在两个方面:Taglib和页面导航。Taglib是Struts的标记库,灵活动用,能大大提高开发效率。另外,就目前国内的JSP开发者而言,除了使用JSP自带的常用标记外,很少开发自己的标记,或许Struts是一个很好的起点。
关于页面导航,我认为那将是今后的一个发展方向,事实上,这样做,使系统的脉络更加清晰。通过一个配置文件,即可把握整个系统各部分之间的联系,这对于后期的维护有着莫大的好处。尤其是当另一批开发者接手这个项目时,这种优势体现得更加明显。
缺点:
Taglib是Struts的一大优势,但对于初学者而言,却需要一个持续学习的过程,甚至还会打乱你网页编写的习惯,但是,当你习惯了它时,你会觉得它真的很棒。
Struts将MVC的Controller一分为三,在获得结构更加清晰的同时,也增加了系统的复杂度。
Struts从产生到现在还不到半年,但已逐步越来越多运用于商业软件。虽然它现在还有不少缺点,但它是一种非常优秀的J2EE MVC实现方式,如果你的系统准备采用J2EE MVC架构,那么,不妨考虑一下Struts。