请先关注 [低调大师] 公众号 优秀的自媒体个人博客,低调大师,许军

低调大师

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

文章详情

每日一博 | 谈谈 Act 的依赖注入和模板输出

2019-12-07 19热度

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 的评论, 很认真, 信息量很多, 所以我也很认真地使用一篇来回答你的评论.

收藏 (0)

相关文章

    文章评论

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