欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览
- 本篇是《quarkus依赖注入》系列的第八篇,目标是掌握quarkus实现的一个CDI特性:装饰器(Decorator)
- 提到装饰器,熟悉设计模式的读者应该会想到装饰器模式,个人觉得下面这幅图很好的解释了装饰器模式,左下角的红框是关键点:自己的send方法中,先调用父类的send(也就是被装饰类的send),然后才是自己的业务逻辑
<img src="https://typora-pictures-1253575040.cos.ap-guangzhou.myqcloud.com/54.png" alt="54" style="zoom:50%;" />
- quarkus也支持装饰器模式,通过注解Decorator和Delegate实现,今天咱们就通过实战掌握如何在quarks框架下通过装饰器扩展应用
- quarkus是按照CDI的标准来支持装饰器模式的,下图来自官方文档
- 接下来进入实战环节
实战功能说明
- 网上讲述装饰器模式的文章中,有个咖啡价格的例子非常经典,如下图所示:
- 一杯意式浓缩咖啡(Espresso)价格3美元
- 拿铁(Latte)由意式浓缩+牛奶组成,价格是意式浓缩和牛奶之和,即5美元
- 焦糖玛奇朵(CaramelMacchiato)由拿铁+焦糖组成,价格比拿铁多了焦糖的1美元,即6美元
- 每种咖啡都是一种对象,价格由<font color="blue">getPrice</font>方法返回
<img src="https://typora-pictures-1253575040.cos.ap-guangzhou.myqcloud.com/%E6%B5%81%E7%A8%8B%E5%9B%BE%20-%202022-04-09T165959.292.jpg" alt="流程图 - 2022-04-09T165959.292" style="zoom:50%;" />
- 在上述场景中,当咖啡的内容不断丰富,咖啡价格也要做相应调整,装饰器的作用是让代码优雅的应对变化,对内代码整洁低耦合,对外保持统一接口<font color="blue">getPrice</font>
- <font color="red">装饰器模式</font>本身并不是本篇的重点,咱们还是聚焦quarkus下的装饰器功能:在咖啡价格的基础上,通过装饰器计算出拿铁的价格
- 接下来开始编码
编码实战
- 首先定义接口Coffee.java,不论是意式浓缩、拿铁、还是其他种类,对外都称之为Coffee,都有getPrice方法
package com.bolingcavalry.decorator;
public interface Coffee {
/**
* 咖啡名称
* @return
*/
String name();
/**
* 当前咖啡的价格
* @return
*/
int getPrice();
}
- 然后是最基础的意式浓缩咖啡,非常简单的一个bean,定价3美元,这里有个细节要注意:name方法中写死了字符串<font color="blue">Espresso</font>,而没用getClass().getSimpleName(),这是因为在quarkus容器中,Espresso的bean并非Espresso类型,而是动态生成的代理类,所以getClass返回的类不是Espresso
package com.bolingcavalry.decorator.impl;
import com.bolingcavalry.decorator.Coffee;
import javax.enterprise.context.ApplicationScoped;
/**
* 意式浓缩咖啡,价格3美元
*/
@ApplicationScoped
public class Espresso implements Coffee {
@Override
public String name() {
return "Espresso";
}
@Override
public int getPrice() {
return 3;
}
}
- 接下来就是重点了,拿铁,由意式浓缩+牛奶组成,代码如下,有几处要注意的地方稍后会提到
package com.bolingcavalry.decorator.impl;
import com.bolingcavalry.decorator.Coffee;
import io.quarkus.arc.Priority;
import io.quarkus.logging.Log;
import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.inject.Inject;
@Decorator
@Priority(11)
public class Latte implements Coffee {
/**
* 牛奶价格:2美元
*/
private static final int MILK_PRICE = 2;
@Delegate
@Inject
Coffee delegate;
@Override
public String name() {
return "Latte";
}
@Override
public int getPrice() {
// 将Latte的代理类打印出来,看quarkus注入的是否正确
Log.info("Latte's delegate type : " + this.delegate.name());
return delegate.getPrice() + MILK_PRICE;
}
}
- 上述代码有以下几处要注意
- 先明确目的:我们设计Latte这个bean,本意是通过装饰器模式来装饰Espresso,因此才会用到quarkus的装饰器功能
- 使用quarkus的装饰器功能时,有两件事必须要做:装饰类要用注解<font color="blue">Decorator</font>修饰,被装饰类要用注解<font color="blue">Delegate</font>修饰
- 因此,Latte被注解<font color="blue">Decorator</font>修饰,Latte的成员变量delegate是被装饰类,要用注解<font color="blue">Delegate</font>修饰,
- Latte的成员变量delegate并未指明是Espresso,quarkus会选择Espresso的bean注入到这里
- 在getPrice方法中打印出delegate.name方法的返回值,验证delegate的身份,以确认quarkus注入的是否正确
- 注解<font color="blue">Priority</font>很重要,留在接下来的CaramelMacchiato类(焦糖玛奇朵)写完后再说清楚
- 接下来是CaramelMacchiato类(焦糖玛奇朵),有几处要注意的地方稍后会说明
package com.bolingcavalry.decorator.impl;
import com.bolingcavalry.decorator.Coffee;
import io.quarkus.arc.Priority;
import io.quarkus.logging.Log;
import javax.decorator.Decorator;
import javax.decorator.Delegate;
import javax.inject.Inject;
/**
* 焦糖玛奇朵:拿铁+焦糖
*/
@Decorator
@Priority(10)
public class CaramelMacchiato implements Coffee {
/**
* 焦糖价格:1美元
*/
private static final int CARAMEL_PRICE = 1;
@Delegate
@Inject
Coffee delegate;
@Override
public String name() {
return "CaramelMacchiato";
}
@Override
public int getPrice() {
// 将CaramelMacchiato的代理类打印出来,看quarkus注入的是否正确
Log.infov("CaramelMacchiato's delegate type : " + this.delegate.name());
return delegate.getPrice() + CARAMEL_PRICE;
}
}
- CaramelMacchiato代码的逻辑和Latte的差不多,都用了注解Decorator和Delegate,目的是为了做Latte的装饰器
- 要<font color="red">重点关注</font>的是成员变量<font color="blue">delegate</font>,其类型、名称、注解,都和Latte的delegate一模一样:
@Delegate
@Inject
Coffee delegate;
重要知识点
- 看到这里,相信您也发现了问题所在:CaramelMacchiato和Latte都有成员变量delegate,其注解和类型声明都一模一样,那么,如何才能保证Latte的delegate注入的是Espresso,而CaramelMacchiato的delegate注入的是Latte呢?
- 此刻就是注解<font color="blue">Priority</font>在发挥作用了,CaramelMacchiato和Latte都有注解<font color="blue">Priority</font>修饰,属性值却不同,<font color="red">属性值越大越接近原始类Espresso</font>,如下图,所以,Latte装饰的就是Espresso,CaramelMacchiato装饰的是Latte
<img src="https://typora-pictures-1253575040.cos.ap-guangzhou.myqcloud.com/%E6%B5%81%E7%A8%8B%E5%9B%BE%20-%202022-04-09T203421.135.jpg" alt="流程图 - 2022-04-09T203421.135" style="zoom:50%;" />
单元测试类
- 最后是单元测试类,成员变量的类型是Coffee,也就是说quarkus容器会自动注入装饰过的CaramelMacchiato类型的bean,而testDecoratorPrice方法中断言<font color="red">coffee.getPrice()</font>的值等于6,如果注入caffee的bean不是CaramelMacchiato类型,断言就会失败
package com.bolingcavalry;
import com.bolingcavalry.decorator.Coffee;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
@QuarkusTest
public class DecoratorTest {
@Inject
Coffee coffee;
@Test
public void testDecoratorPrice() {
Assertions.assertEquals(6, coffee.getPrice());
}
}
验证
- 执行单元测试,如下图,单元测试通过表示coffee注入的是CaramelMacchiato类型的bean,再看右侧的日志,CaramelMacchiato的成员变量delegate是Latte类型,Latte的成员变量delegate是Espresso类型,都按照咱们的预期准确注入了
- 紧接着再做个尝试:将Latte的注解Priority的属性值改小,小于CaramelMacchiato的10,如下图红框,如此一来,CaramelMacchiato的优先级更大,因此更靠近Espresso,由它去装饰Espresso,Latte离Espresso更远,所以它装饰的是CaramelMacchiato
<img src="https://typora-pictures-1253575040.cos.ap-guangzhou.myqcloud.com/image-20220409205649353.png" alt="image-20220409205649353" style="zoom:50%;" />
- 再次运行单元测试,如下图,首先测试依旧能通过,这个好理解,无论装饰逻辑怎么变,最终的bean的getPrice返回值,都是意式浓缩+牛奶+焦糖的价格之和,然后在看右侧日志信息,果然,CaramelMacchiato注入的成员变量是Espresso,Latte注入的成员变量是CaramelMacchiato
- 至此,装饰器的编码实战已完成,相信您可以在应用中用熟练使用装饰器来扩展bean能力,并且保持与原有bean之间的代码低耦合
与拦截器的不同
- 如果您看过《拦截器》一文,应该会发现,同样的功能用拦截器也能实现,那为何还要多出个装饰器呢?
- 其实网上也有类似的讨论,首先是Stack Overflow上分析,一个高赞的观点是:通常情况下,一个装饰器被用于一个特定类上,而拦截器用于拦截多个类
- 这篇2012年的关于CDI的文章《Interceptors and Decorators tutorial》中的对比更好理解:
- 个人理解:
- 拦截器适合做一些通用的事情,例如日志、异常处理等,可以为多个bean服务
- 装饰器适合做特定的事情,例如本篇的演示代码中,<font color="blue">计算价格</font>是被装饰类的特性,其他bean没有这个功能,所以装饰器也只能用在,作为核心功能的增强或者完善
欢迎关注思否:程序员欣宸
学习路上,你不孤单,欣宸原创一路相伴...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。