您现在的位置是:首页 > 文章详情

谈谈 Act 的依赖注入 和 模板输出 - 回 drinkjava 同学的评论

日期:2019-11-30点击:437

1. 背景

依赖注入工具 jBeanBox 的作者 drinkjava 同学最近在 gitee 上对 Actframework 项目 提出了如下评论:

首先感谢 drinkjava 同学的意见, 看得出来是问题是认真思考之后提出来的. 本文将就评论中的两段意见分别作答. 在此之前先解释一下上面那段评论中的术语:

  • DI - Dependency Injection - 依赖注入, 反向控制的一个特例, 指在构造对象时对象的(某些)状态不由对象类自身逻辑初始化,由 DI 框架依照环境配置以及状态上的申明注入.
  • AOP - Aspect Oriented Programming - 面向切面编程, 允许使用特定技术将逻辑编织进程序结构中, 逻辑对应程序结构中特定的切点.

2. [意见一] 一个 MVC 工具为什么要引入 DI 依赖注入

你这个DI工具的出发点可能有问题,一个MVC工具为什么要引入DI依赖注入?

这个问题有两个地方值得商榷:

  1. 上面这个问题隐含的一个前提假设是 Act 是一个 MVC 工具. 实际上这个前提有一点问题, 我启动 Act 项目的动机是希望弄一个符合自己想法的 PlayFramework V1.x 的后继者. Play 本身除了是一个开发框架,也是一个运行时平台, Act 也是. 单单用 "MVC 工具" 来描述 Act 并不符合我自己的想法. 用 "MVC 工具" 来描述 Act 的依赖 osgl-mvc 更能够贴切一点.

  2. MVC 工具为什么不能引入 DI 依赖注入. 后面 drinkjava 同学也提到 "直接引入 Spring 或 Guice 的不好吗?", 说明 drinkjava 并不是认为 MVC 工具不能引入 DI 依赖注入, 而是认为 Act 引入的 DI 依赖注入 Genie 没有提供 AOP 功能, 而 AOP 功能在他看来是实现声明式事务必须的,所以才认为不适合.

2.1 Act 的依赖注入机制与应用

下面我们就 javadrink 同学上面的关切来谈谈 Act 的依赖注入机制与应用.

这里是来自 Wikipedia 对依赖注入的定义 "In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object". 简单地说就是对象的状态不由自己来创建, 而是交给另外的对象来注入. 举个例子:

public class UserService { @Inject private Dao<User> userDao; @GetAction("/users/{userId}") public User get(String userId) { return userDao.findById(userId); } } 

上面是一个简单的 UserService 端口. 其中需要使用对应与 User 实体类的 Dao. 在上面的代码中我们没有看到 userDao 是如何初始化的, 因为 userDao 是 Act 框架在实例化 UserService 的时候注入的. 这就是一个典型的 Act 应用依赖注入的方式. 当然 Act 对于依赖注入的使用还有其他的扩展. 我们稍后会提到.

2.1.1 为什么不用 Guice 或者 Spring

这个和Spring或Guice的功能重叠了。直接引入Spring或Guice的不好吗?

现在我来回答 drinkjava 的这个问题: "直接引入 Spring 或 Guice 的不好吗?". 实际上在开发 Genie 之前 Act 尝试过另外两种依赖注入:

在 Act 正式发布之前, 这上面两种注入都曾经在 act 0.x 版本中进入过实际项目 (当然是老码农所在公司的 - 开坑自己先踩是老码农做开源的基本原则).最终我选择了自己开发 Genie 来提供 Act 的依赖注入, 主要原因有一下几点:

  • Feather 的实现足够简单轻量; 但并不是 JSR 330 的完整实现, 比如不支持方法注入, 对 Scope 的支持不完善, 扩展性不够好
  • Guice 提供了完整的 JSR 330 实现; 但同时也引入了一些额外的特性, 比如 Servlet 集成等. Guice 的代码实现也相对比较晦涩.
  • Spring 的依赖注入至始至终都不是我的一个选项, 首先 Spring 的依赖注入不是 JSR 330 标准的实现, 另外 Spring 的依赖注入运行时效率太低 (参见依赖注入性能测试项目).

最终 Feather 简洁的代码实现激励了我启动 Genie 项目, 这个依赖注入库完整实现了 JSR 330, 同时提供了一些有趣且实用的扩展, 比如注入集合类型数据以及注入值数据 等, 这些扩展对实现 Act 里面的 @LoadResource, @LoadConfig 还有 @Configuration 等机制至关重要. 另外因为代码实现比较紧凑, 运行时效率也很不错, 在多项测试中都领先 Guice; 具体数据可以参考这个项目

2.1.2 依赖注入扩展 I - 请求处理方法参数的注入

接下来说说 Act 对传统依赖注入的第一个扩展扩展: 注入请求处理方法参数.

上面那个 UserService 的例子是经典的依赖注入使用方式, 除了将依赖注入字段, Act 还允许直接将服务注入请求处理方法, 例如:

@GetAction("/users/userId") public User get(String userId, Dao<User> userDao) { return userDao.findById(userId); } 

上面代码中, get 方法有两个参数, userIduserDao, 其中 userId 绑定到 URL 路径参数, 假如请求是 /users/abc123, 那 userId 的值就是 abc123; 而第二个参数 userDao 就是依赖注入了, 这个和前面注入到 userDao 字段是一样的. 但

又例如:

 @PostAction("login") public void login(String email, char[] password, ActionContext context) { User user = userDao.findByEmail(email); badRequestIf(null == user, MSG_BAD_EMAIL_PASSWORD); badRequestIfNot(user.verifyPassword(password), MSG_BAD_EMAIL_PASSWORD); context.login(user); } 

上面代码中的 ActionContext 也是依赖注入的对象, 而 email, password 则绑定自请求参数.

2.1.3 依赖注入的扩展 II - 资源和配置参数注入

得益于 Genie 的扩展机制, Act 中可以很轻易地注入加载资源和配置参数.

public static class Dao extends MorphiaDao<Contact> { @LoadResource("industry_type.mapping") private Map<String, String> industryTypeMapping; @Configuration("sql.url") private String jdbcUrl; ... } 

上面的代码来自实际项目, 其中使用了两种扩展注入:

  • @LoadResource("industry_type.mapping) 将资源文件 industry_type.mapping 的内容加载进 Map<String, String> industryTypeMapping 字段
  • @Configuration("sql.url") 将配置项 sql.url 的值加载进 String jdbcUrl 字段

资源文件 industry_type.mapping 的内容为:

AG1 - Agriculture=Agriculture AG2 - Agribusiness=Agribusiness AG3 - Hospitality=Tourism and Hospitality AG4 - Food Manufacturing/Processing=Food Manufacturing/ Processing ... 

可以看出依赖注入在这种场景的使用减少了 boilerplate 代码的使用, 让应用代码变得更加简洁易懂.

值得一提的是 @LoadResource 除了可以注入上面显示的 Map<String, String> 类型, 还能注入很多其他类型, 包括:

  • List<T>
  • File
  • InputStream
  • ...

更多关于 @LoadResource 的示例参见 resource loading 示例项目.

2.1.4 依赖注入机制总结

通过上面关于依赖注入机制的介绍, 可以看出依赖注入在 Act 应用中是基本的机制, 其适用范围远远超出了 drinkjava 同学在问题中表达的观点 "DI唯一比较经典的用法只是用来进行声明式事务才需要".

2.2 关于声明式事务和 AOP

按我的观点,DI唯一比较经典的用法只是用来进行声明式事务才需要,从这个角度出发,就必须要DI支持AOP切面功能,而你却没有加入这个功能,这就很尴尬了,当需要声明式事务的时候,不得不引入一个支持AOP的DI工具,例如Spring/Guice/jFinal,这就造成了使用ACT的项目随时都具备了2套DI工具,也就是说你自带的DI工具实际上是多余的,尤其在流行的Boot环境下,各种配置都是建立在Spring-Core这个IOC/AOP工具基础上,别人不大可能移到Genie内核上。

如果说依赖注入并不只是用来进行声明式事务, 将 drinkjava 同学上面的声明换个说法 "声明式事务是 AOP 少数几个经典用法之一" 我觉得倒是可以接受. drinkjava 同学后面讲到 "从这个角度出发,就必须要DI支持AOP切面功能,而你却没有加入这个功能", 这一点我不同意. DI 和 AOP 是完全不同的概念, 我从来不知道 DI 必须支持 AOP, DI 的 Java 标准 JSR 330 也完全没有提到这个特性. 而 Wikipedia 上 AOP 的页面 也根本没有谈到 Dependency Injection 的概念. 把 DIAOP 放在一起 Google 了一下, 发现这篇 .net 的文章详细分解了这两个概念, 比较其异同以及相关性, 有兴趣的同学可以点击进去看看.

Act 目前不支持通用的 AOP, 但 Act 提供的 SQL DB 插件, 包括 act-ebean, act-hibernate 以及 act-eclipselink 都支持声明式事务. 具体应用代码可以参考下面几个示例项目:

act-ebeanact-hibernate, act-eclipselink 对声明式事务的实现机制是不同的.

  • act-ebean 将声明式事务的实现交给 ebean 引擎. 而 Ebean 是采用了 java agent 对代码做增强来实现声明式事务
  • act-hibernate 和 act-eclipselink 对声明式事务的实现机制都在 act-jpa-common 插件中, 是通过 ASM 对代码做增强来实现的.

我并不认为通用的 AOP 对于应用开发来说是一个非常重要的特性, 也一直没有动手做 AOP 的支持. 我的理由如下:

  1. 我认为 AOP 的应用场合并不是非常普遍的, 可能的场景有:
    • 声明式事务 - ACT 有相应的实现机制 (act-db)
    • Auditing (审计) - ACT 有相应的实现机制 (act-aaa)
    • 性能监控 - ACT 有相应的实现机制 (act-core, ASM based MetricEnhancer)
    • 异常处理 - 我不赞同复杂而精巧的异常处理 - 更新的语言包括 .net C# 还有 groovy, kotlin 等都去掉了 CheckedException 这个概念. Web 应用程序的异常处理应该尽量轻量化.
  2. 从框架角度出发以上 AOP 应用场景都不应该交给应用来完全负责, 通过框架或者插件提供专门的支持才是更加合理的处理方式.
  3. 通用的 AOP 的对于应用开发来说太过晦涩, 且容易导致难以调试的功能性以及性能方面的问题.

基于上面的阐述, 我可以断言 drinkjava 同学评论中的说法是不成立的: "当需要声明式事务的时候,不得不引入一个支持AOP的DI工具,例如Spring/Guice/jFinal,这就造成了使用ACT的项目随时都具备了2套DI工具,也就是说你自带的DI工具实际上是多余的". 一句话, 在 Act 中使用声明式事务以及我上面提到的另外两种 AOP 应用场景都不需要 AOP.

另一方面, 即便需要支持通用 AOP 的支持也不一定使用 DI 工具, 即 Proxy-Based AOP. 如果我一定要在 Act 中加入通用 AOP 的支持, 我倾向于 LTW (Load Time Weaving), 即在进行类加载的时候使用 ASM 工具对类进行增强, 这样做起码在运行时效率上要高很多.

3. [意见二] 考虑一下支持多种模板输出,如包括PDF输出 ... 必要时抄源码

另外考虑一下支持多种模板输出,如包括PDF输出,这才是MVC的V层要做的事,可以参见SpringMVC和Jfinal,必要时可抄它们的源码

看到这个建议我感觉 drinkjava 同学可能还不太熟悉 Act 的模板输出机制. views 示例项目展示了 Act 中同时使用多种不同的模板引擎的特性, 包括:

  • beetl
  • freemarker
  • mustache
  • rythm
  • thymeleaf
  • velocity

response-type 示例项目中展示了 Excel 模板的输出 (采用 JXLS 引擎). 可以说 Act 的模板输出框架是足够满足 (同时) 使用多种模板的. 当然到目前我还没有开发 PDF 的模板插件, 这个可以作为今后的一个工作.

至于评论原文中谈到的必要时抄源码, 我这里可以小自豪一下, 老码农做开源没有抄袭源码的习惯, 从 greenscriptplay-morphia, 再到 rythm 系列, osgl 系列 以及 act 系列, 你要是找到了老码农抄袭别人的代码可以大声叫出来, 算我输. 我做开源的理念很简单: 做别人家没有的, 或者比别人好的; 没有理由去抄袭.

另外我也可以不客气地说 Act 的对输出的支持包括后台模板渲染应该很少有框架能匹敌. 不同意的话我们可以专门讨论一下这个话题.

4. [意见三] 集中精力在 MVC

这里回答 drinkjava 同学评论的最后一部分:

jFinal的问题是DAO、IOC、MVC混成一团,是优点,更是一个大缺点,希望你将主要精力集中在MVC,将它做成一个精致、干净的后台表现层,不要介入任何DI、AOP、DAO、事务的工作,这方面优秀的、流行的工具太多,没必要重造轮子。你现在这个HTTP内核也是自已做的,为了一点点效率或钻牛角尖的小特性,放弃通用性,也是冒风险的,在项目MVC架构选型的第一步就可能被Pass,如果能将HTTP内核和MVC分开,比如说ACT的MVC即可以和自已的内核合用,也可以和Jetty或内嵌Tomcat内核合用,这才是一个比较优秀的架构。

  1. "希望你将主要精力集中在MVC,将它做成一个精致、干净的后台表现层" - 在博客开头我也有讲, Act 并不是一个 MVC 工具, 而是 Web 应用框架以及运行时平台. 另外 "后台表现层" 是个什么鬼? 原谅我读书少, 理解不了这个术语 ^_^.
  2. "这方面优秀的、流行的工具太多,没必要重造轮子" - 这句话我不太赞同, MVC 本身优秀的,流行的工具也很多,SpringMVC 就是一张大伞, 按照这个说法, Act 干脆就不要做了. 我的策略是, 首先看市面上有没有满足自己要求的, 有就用 (比如以前的 Play 1.x, FastJSON, JXLS 等等), 没有就自己做 (比如 Rythm, Act, Genie 等等). 另外 drinkjava 同学自己也造了很多轮子, 包括 DI/AOP 工具 JBeanBox数据库访问工具 JSqlBox 等等. 这意思是说你的工具很优秀, 老码农的水准不够, 就不要玩了吗 ;-)
  3. "你现在这个HTTP内核也是自已做的,为了一点点效率或钻牛角尖的小特性,放弃通用性,也是冒风险的". - 这里 drinkjava 同学高看我了, Act 的 HTTP 核心实现是 JBoss 的 Undertow. 我有打算在未来的版本中迁移到 Netty 上去. 不过我想问问 drinkjava 同学, 你说的 "钻牛角尖的小特性"是指什么? "通用性" 又是指什么?
  4. "如果能将HTTP内核和MVC分开,比如说ACT的MVC即可以和自已的内核合用,也可以和Jetty或内嵌Tomcat内核合用,这才是一个比较优秀的架构。" - Jetty 和 Tomcat 是基于 Servlet 架构的 (不是 HTTP 网络层核心, 而是 API 层的架构), 我认为 Servlet 架构背负太多的历史报复, 对于现代 Web 框架来说并不是一个很好的选择. 至于 Act 是否是一个比较优秀的架构, 我并不是特别在意. 能在开发时提供友好的支持, 运行时提供足够的性能就好.

最后再次感谢 drinkjava 的评论, 很认真, 信息量很多, 所以我也很认真地使用一篇来回答你的评论.

原文链接:https://my.oschina.net/greenlaw110/blog/3135822
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章