目录
在Java中使用代码(11 +,Spring boot 2.2 +,Spring Boot AOP,AspectJ)
在C#中使用代码(7,.NET MVC Core 2.1 +,Autofac,Caste Core动态代理)
REST异步控制器方法的ActionFilterAttributes
在现代的OO语言和框架中,使用了许多模式,例如,面向切面,外观(facade)(facade ),IOC,CQRS。本文介绍了其中的一些概念和模式,可能的解决方案,并提供了Java和C#的示例实现。
介绍
本文提供了用于微服务的现代平台环境下的面向切面的最新视图。它描述了面向切面的利弊以及实现方面的利弊。它没有深入描述面向切面的概念。还有其他文章可以很好地完成这项工作。
如何建立这篇文章?
- Java和C#中的示例最终用法
- 现代微服务中的面向切面、为什么以及如何实现?
- 高级设计和基本原理(设计的利弊)
- Java实现细节
- C#实现细节
在Java中使用代码(11 +,Spring boot 2.2 +,Spring Boot AOP,AspectJ)
// Java code usage example
//
// The @around @CheckPoint (long x) aspect does a high level (distributed)
// logging of the checkpoint
// The @CheckPointParameter annotated method parameters are logged
// as properties with the log
@CheckPoint(10)
public void annotatedCheckPointMethod(@CheckPointParameter String user) {
// -> results in @Before checkpoint logging
....code ...
}// -> results in @After checkpoint logging
在C#中使用代码(7,.NET MVC Core 2.1 +,Autofac,Caste Core动态代理)
// C# code usage example
//
// The [CheckPoint (long x)] dynamic proxy interceptor
// aspect does a high level (distributed) logging of the checkpoint
// The [CheckPointParameter] annotated method parameters are logged
// as properties with the log
[CheckPoint(10)]
public void AnnotatedCheckPointMethod([CheckPointParameter] String user)
{// -> results in @Before checkpoint logging
....code ...
}// -> results in @After checkpoint logging
概念:基本原则
面向切面如何工作?
在这篇文章中的实现,我们使用CTW(Compile Time Weaving)和LTW(Load Time Weaving)。
LTW weaving 在运行时“weaves”一个中间对象,该对象实现相同的接口并执行方面工作,然后委托给实现代码。
CTW weaving 在编译时“weaves”,将切面代码放置在适当的位置。C#和Java都使用中间(字节或IL代码)进行编译。因此,通常情况下,首先是本机语言编译器生成字节码,然后方面编译器将方面编织到字节码中。
为什么要使用方面定向?
在现代的,面向微服务的应用程序环境中,通常将不会只有一个,两个或三个服务,而会有十几个甚至数百个微服务。通常,如果我们要对该软件进行模块化,则至少存在以下可能性:
- 使用分布式服务调用
- 使用二进制库
- 复制代码
- 使用面向切面
注意:这种分离的视图过于简化、不完整,是的,实现可以很好地执行分布式调用,或者可以用二进制库中的客户机facade模式隐藏分布式服务调用。所以,我知道,这种世界观是错误的,但任何对所有系统的看法分歧都有例外,是有争议的。我只是用它来说明如何使用切面。
1.使用分布式服务调用
我们在一个单独的服务中拆分了可重用部分,并从需要此功能的所有其他服务中调用该服务。在所有情况下,例如对于诸如日志记录之类的情况,这都不是最快、最具可伸缩性和鲁棒性的解决方案。通常,在不知不觉中,这便成为“上帝”服务模式,每个人都在微服务领域警告不要使用...
public void DistributedCheckPointMethod(String user)
{
SendCheckPoint("before", 10, user);
...code...
SendCheckPoint("after", 10, user);
}
public void SendCheckPoint(String place, long id, String user)
{
List<string> props = new List<string>(){user};
rest.CheckPoint(place, id, props);
}
2.使用二进制库
我们在一个单独的二进制库(jar,dll,nuget,maven等)中拆分了可重用的部分,并从需要此功能的所有其他服务中调用该API。
public void DistributedCheckPointMethod(String user)
{
libApi.SendCheckPoint("before", 10, user);
...code...
libApi.SendCheckPoint("after", 10, user);
}
3.复制粘贴代码
只需将代码复制粘贴到所有服务中即可。这种方法肯定有优点。这就是为什么它仍然被大量使用的原因:-((.。它简单、易于调整以适应小的异常情况,并且通常在性能上具有可伸缩性。过去在所有情况下,这都被认为是一个糟糕的设计。
注意:在现代的OO设计中,存在诸如数据传输对象之类的模式,可以将其复制粘贴并针对该用法的特定需求进行定制,如果明智地使用它们,就不会被认为是“不良”的设计。
public void DistributedCheckPointMethod(String user)
{
List<string> props = new List<string>(){user};
rest.CheckPoint(place, id, props);
...code...
rest.CheckPoint("after", 10, props);
}
4.使用面向切面
尽管这听起来像是一个全新的选择,但它通常位于前三个解决方案的顶部。其优点是,API占用量实际上是您可以拥有的几乎最小的API。
注意:对于类似Command的切面,我最喜欢的解决方案是将切面与二进制库和Facade Client/proxy方法以及带有队列的CQRS模式相结合,我将在本文后面进行解释。
“客户端”服务代码通常仅定义一个标签(注释),例如:
@CheckPoint(10)
...
@FeatureFlag("Feature1")
...
因此,API被定义为“一个带有可选参数的单词”。无论调用或实现是什么,都将隐藏在切面代码中。这样,实现和API可以很干净地分开,并且由于实现更改而使API发生更改的机会确实很小。别忘了,您的API调用最终可能会在每个服务的数百个地方,以及数十个或更多的服务中结束。而且,是的,如果为此使用一个切面,并且使用一个(简单但有缺陷的设计)调用分布式服务,则您仍然创建了“SPOF God”服务。但是,至少,您对此具有最少的API,并且在代码中调用的次数最少,因此,实现更改的影响通常要小得多。
当然,如果您想为类中的所有方法完成某件事,则可以定义一个类级别的切面:
[Log("SomeClass")]
public class SomeClass
{
....methods go here, and all methods do logging...
}
高级设计
应用程序和切面库的打包。Aspect和Client是(二进制)库中的程序包
正如我们在这里看到的,应用了四种外观(facade)(facade )模式:
- Facade CheckPoint Aspect,从应用程序SomeClassUsingCheckPoints隐藏CheckPoint Aspect行为。
- 在CheckPoint Aspect内,Facade CheckPointClient,从Checkpoint Aspect隐藏消息的发送。
- 在客户端外观(facade)(facade )中,有一个检查点队列,该队列对客户端隐藏了一个CheckPoint服务。
- 队列中有一个检查点服务,该服务从队列中隐藏对消息执行的操作。
换句话说,应用关注点分离:
- 应用程序的关注点是在应用程序级别(annotatedCheckPointMethod)上做某事
- 该切面的关注点是拦截和处理检查点
- 客户的关注点是发送消息
- 队列的关注点是接收和发送消息
- 服务的重点是处理检查点消息。
- 因此,一切都有一个问题。
并且,请注意,该应用程序的CheckPoint API非常干净,甚至与该应用程序分离的二进制文件也是如此。最后,队列部分开出CQRS(CommandQueryResponsibilitySeparation)。我们需要一个检查点的事实是一个命令:创建/发送一个检查点(命令)。
现在,我们可以看到,在此示例中应用外观(facade)(facade),客户端,队列和CQRS组合模式,我们实现了关注点分离,松散耦合以及灵活性和可重复使用的许多可能性。面向切面为我们提供了非常干净的API拆分,并且完成了出色的外观(facade)工作。
一般实现细节
通常,对于Java和.NET,我将省略CheckPoint服务器/服务端。
由于本文主要是关于面向切面的,因此我将把实现重点放在“客户端”上。
对于队列消息传递实现,我选择了RabbitMQ。Java和C#,Windows和Linux以及“云”都相当普遍地接受和支持这种方法。
注意:在运行代码之前,必须首先在系统上安装RabbitMQ。请参阅https://www.rabbitmq.com/download.html。
Java实现细节
Java方面
在深入研究代码之前,我在这里针对Java方面进行了一些评论。然后,您可以预先决定选择其中一项,然后深入了解其细节。
备注1:我确实在Java中同时发现了“编译时编织”和“加载时编织”。
备注2:两种实现都使用Spring Boot。
备注3:加载时间编织使用“普通” Spring AOP。“按设计的功能”(您可以称其为错误,约束或其他任何一种)之一是,切面仅适用于接口调用,因为这样您便有了com.sun代理。它不适用于实现类。结果,您需要在接口上定义切面。而且,如果您随后在纵横比高的方法内部调用另一个实现方法,则该纵横比不会执行。
优点LTW
- 可与大多数Spring AOP版本一起使用
缺点LTW
- 性能不是那么好。我选择较便宜的@Before切面,而不是最昂贵的性能切面@Around;
- 内部切面不起作用。
备注4:编译时织法使用Spring AOP,aspectjrt和pre-aspect编译器。但是,我再次遇到了jar mvn版本地狱。我只能使其与Aspectj-Maven-plugin编译器的一些私有开发分支(1.12.6,com.nickwongdev)一起使用,并与Spring Boot(Hoxton.SR3)在2020年3月发布的最新最大发行版2.x train一起使用。 ):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Hoxton.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.2.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
<plugin>
<groupId>com.nickwongdev</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.12.6</version>
</plugin>
优点CTW
- 更好的性能
- “内部”实现切面确实可行
缺点CTW
- 需要更多设置
- Spring Boot和AspectJ-Maven-Plug编译器之间的版本约束
LTW Java代理
以下步骤将实现您的LTW切面。
首先,定义切面注释:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPoint {
long value() default 0;
}
接下来,定义@Before切面实现:
@Configurable
@Aspect
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class CheckPointAspect {
/**
* Fires checkpoint with properties.
*
* @throws java.lang.Throwable
* @see unitests for examples
*
*
* @param joinPoint place of method annotated
* @param checkPointAnnotation the checkpoint
* @return proceeding object return value of method being annotated
*/
@Before("@annotation(checkPointAnnotation)")
public void CheckPointAnnotation(
final JoinPoint joinPoint,
CheckPoint checkPointAnnotation) throws Throwable {
log.info("1. for checkPoint {} ", checkPointAnnotation.value());
...do check point send here...
}
}
接下来,使用定义一个(测试)接口@CheckPoint。确保在接口方法上定义属性:
public interface CheckPointTest {
@CheckPoint(id=10)
void doCheck();
}
然后,实现测试类:
@Component
public class CheckPointTestImpl implements CheckPointTest {
@Override
public void doCheck() {
log.info("2. CheckPointTestImpl.doCheck");
}
CTW Java Aspect编译器
以下步骤将实现您的CTW切面。
实际上,编码完全没有区别。因此,如上所述,所有编码与LTW完全相同。
唯一的区别是pom项目配置:org.aspectjrt依赖项和maven aspectj-maven-plugin编译器插件:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
<plugin>
<groupId>com.nickwongdev</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.12.6</version>
<configuration>
<source>11</source>
<target>11</target>
<proc>none</proc>
<complianceLevel>11</complianceLevel>
<showWeaveInfo>true</showWeaveInfo>
<sources>
<source>
<basedir>src/main/java</basedir>
<excludes>
<exclude>nl/bebr/xdat/checkpoint/api/*.*</exclude>
</excludes>
</source>
</sources>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
</plugin>
注1:当您执行LTW Spring AOP时,切面类是由Spring Boot的容器加载和管理的,因此自动为您完成了布线。CTW没有这样的事情。Aspect编译器以及您的切面和Spring Boot IOC在开箱即用时效果不佳。您必须手动连接各个切面。否则,您@Autowiring将失败。
....
/**
* This is plumbing code to connect the CTW aspect to the Spring Boot IOC container
* of your app context.
* Otherwise, the CheckPointAspect is factored by the aspectj compiler,
* and then, your autowiring fails, resulting in nulls for all your @Autowired props.
* So, you need to include this method in all your CTW aspects.
* I suppose the aspectjrt checks if this method is available,
* and then uses it to factor your objects via Spring Boot?
* @return CheckPointAspect
*/
public static CheckPointAspect aspectOf() {
return SpringApplicationContextHolder.getApplicationContext().
getBean(CheckPointAspect.class);
}
...
//Helper class for your CTW/Spring Boot container wiring stuff
@Component
public class SpringApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
}
注意:Lombok是我最喜欢的组件之一。它节省了很多样板代码,并且@Builder模式很完美。但是,Maven Aspectj编译器不能与Lombok配合使用。由于Aspectj在Lombok之前运行,因此您还没有构建器和getter/setter,并且Aspectj编译器不再理解您的代码。我只能通过排除DTO软件包来使其工作:
<sources>
<source>
<basedir>src/main/java</basedir>
<excludes>
<exclude>nl/bebr/xdat/checkpoint/api/*.*</exclude>
</excludes>
</source>
</sources>
Java Aspect总结
我们从以前的面向切面的Java实现中学到了什么?
Java CTW和LTW规范的工作原理
- 在Java 11中,使用Spring Boot
- 您确实需要使用实现,接口和IOC设置对程序进行清洁
Java LTW有效
- 相对简单,只有Spring AOP依赖
- 有局限性:性能,并非所有内部方面都起作用
Java CTW工作
- 有更好的表现
- 内部切面确实起作用
- 需要手动连接到IOC容器
- 干扰其他编译器,例如Lombok
- 具有繁琐的、非常不稳定的版本树依赖项(仅适用于Spring boot 2.5.x,带有分支编译器版本)
DotNET C#实现细节
DotNET切面
关于C#中的各个切面,有几点说明。
备注1:我没有找到.NET的编译时字节码方面编织实现。.NET中有IL编织功能,但是在某些方面却找不到Fody编织功能。因此,我将仅实现LTW代理解决方案。
备注2:.NET和Java在语法和方法签名中的同步/异步方面有很大的不同。这对.NET的拦截和切面都有影响。我在使用普通同步方法的标准类上使用AutoFac进行了拦截,但是在异步HTTP Rest控制器方法上却有所区别。因此,为了在REST控制器上进行拦截,我使用了遇到的另一种解决方案,ActionFilterAttributes。我用它来拦截REST Controller方法。对于其他类,我们使用AutoFac和Castle Core Dynamic代理组合的拦截机制。而且,是的,我们可以在任何地方使用ActionFilterAttributes。
C#动态代理
备注1:“ Maven、Nuget、DotNet、dll、jar、Spring,它们都是版本地狱……”您必须找出Castle、AutoFac、 AutoFac.Extra.DynamicProxy,...的哪个版本能够以一种有效的方式协同工作。
这在2020年5月为我做到了,但是如果您使用其他组件或想要更高的版本,则可能需要花很多时间,就像我做了一些令人沮丧的工作一样:
Autofac : 5.1.0
Autofac.Extras.DynamicProxy : 5.0.0
Castle.Core: 4.4.0
FakeItEasy: 6.0.0
这个c#中的DynamicProxy是如何工作的?
首先,定义一个接口。确保在接口方法上定义属性:
public interface AOPTest
{
//
// Castle DynamicProxy interceptor
//
[CheckPoint(Id = 10)]
void DoSomething();
//
// FilterAttribute
//
[CheckPointActionAtrribute(Id = 10)]
void DoAction();
}
然后,实现接口:
public class AOPTestImpl : AOPTest
{
// -> invocation point
// @Before is done in the interceptor below, 1. and 2.
public void DoSomething()
{
//
// invocation.Proceed() : Method block code goes below here
//
Debug.Print("3. DoSomething\n");
//
// 4. @After invocation is done in the interceptor below,
// after the invocation.Proceed(); call
//
}
public void DoAction()
{
}
}
然后,定义拦截器。
注意,在Java和.NET中,都有一些“调用点:已定义”。
public class CheckPointInterceptor : Castle.DynamicProxy.IInterceptor
{
//
// As we use PropertiesAutowired() with AutoFac,
// this object is set by the IOC container
//
public CheckPointClient checkPointClient { get; set; }
public void Intercept(Castle.DynamicProxy.IInvocation invocation)
{
Debug.Print($"1. @Before Method called {invocation.Method.Name}");
var methodAttributes = invocation.Method.GetCustomAttributes(false);
CheckPointAttribute theCheckPoint =(CheckPointAttribute)methodAttributes.Where
(a => a.GetType() == typeof(CheckPointAttribute)).SingleOrDefault();
if (theCheckPoint == null)
{
//@before intercepting code goes here
checkPointClient.CheckPoint(theCheckPoint.Id);
Debug.Print($"2. CheckPointAttribute on method found with cp id =
{theCheckPoint.Id}\n");
}
//
// This is the actual "implementation method code block".
// @Before code goes above this call
//
invocation.Proceed();
//
// Any @After method block code goes below here
//
Debug.Print($"4. @After method: {invocation.Method.Name}");
}
}
接下来,使用AutoFac注册对象:
var builder = new ContainerBuilder();
...
builder.RegisterType<aoptestimpl>()
.As<aoptest>()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(CheckPointInterceptor))
.PropertiesAutowired()
.PreserveExistingDefaults();
...
var container = builder.Build();
...
最后,让我们测试并使用代码:
using (var scope = container.BeginLifetimeScope())
{
//
// Create the registered AOPTest object, with the interceptor in between
//
AOPTest aOPTest = scope.Resolve<aoptest>();
//
// Call the method, with the [CheckPoint(Id = 10)] on it
//
aOPTest.DoSomething();
}
应导致:
- @Before 方法调用 DoSomething
- 使用cp id = 10在方法上找到CheckPointAttribute
- DoSomething
- @After 方法: DoSomething
REST异步控制器方法的ActionFilterAttributes
我们在上面看到了DynamicProxy的工作原理。
但是,如果我们尝试将其应用于“服务器”端.NET Core MVC REST控制器,该调用来自OWIN REST通道,该怎么办?
如下:
[HttpGet("index")]
[CheckPoint(Id = 10)]
public ActionResult Index()
{
Debug.Print("3. Index\n");
....
}
乍一看,这似乎可行。您必须将AutoFac连接到Microsoft Extensions DependencyInjection。
但是,一旦您仔细查看调用内部,就会陷入痛苦中:
public void Intercept(Castle.DynamicProxy.IInvocation invocation) {..}
由于ActionResult的异步行为,现在突然被调用了多次,并且没有棘手的、复杂的、繁琐的代码,您将无法使这项工作正常进行。
我无法以任何可接受的方式进行这项工作。但是,我碰上了这种模式找代码来解决问题:ActionFilterAttributes。
步骤1:定义筛选器操作OnActionExecuting和OnActionExecuted:
public class CheckPointActionAtrribute : Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute
{
//
// Your custom checkpoint Id for this CheckPoint attribute
//
public long Id { get; set; }
//
// @Before
//
public override void OnActionExecuting
(Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext context)
{
ControllerActionDescriptor actionDescriptor =
(ControllerActionDescriptor)context.ActionDescriptor;
Debug.Print($"1. @Before Method called
{actionDescriptor.ControllerName}.{actionDescriptor.ActionName}");
var controllerName = actionDescriptor.ControllerName;
var actionName = actionDescriptor.ActionName;
var parameters = actionDescriptor.Parameters;
var fullName = actionDescriptor.DisplayName;
//
// CheckPointActionAtrribute are not factored by the IOC
//
CheckPointClient checkPointClient = BootStapper.Resolve<checkpointclient>();
checkPointClient.CheckPoint(Id);
}
//
// @After
//
public override void OnActionExecuted(ActionExecutedContext context)
{
ControllerActionDescriptor actionDescriptor =
(ControllerActionDescriptor)context.ActionDescriptor;
Debug.Print($"3. @After method:
{actionDescriptor.ControllerName}.{actionDescriptor.ActionName}");
}
}
最后步骤2,应用特性:
[HttpGet("index")]
[CheckPointActionAtrribute(Id = 10)]
public ActionResult Index()
{
Debug.Print("3. CheckPointController.Index\n");
....
}
结果应该是这样的:
- @Before 方法调用 CheckPointController.Index
- CheckPointController.Index
- @After 方法: CheckPointController.Index
恩,完成了。十分简单。
问题:如果过滤器属性是如此简单,为什么还要烦恼第一个更复杂的DynamicProxy解决方案呢?
答:ActionFilterAttribute可用于(MVC)DotNet Core。如果您执行纯.NET Core或.NET Full,该怎么办?->您可以使用DynamicProxy。
希望在安装RabbitMQ 时,在checkpoint.check.request队列中运行上面的C#示例和/或下面的Java示例时看到一条CheckPoint消息:
checkpoint.check.request队列中的消息。注意:交换源,routingkey,app-id,时间戳,标头和类型以及主体有效负载
C#切面总结
我们从上面的面向切面的C#实现中学到了什么?
C#LTW是面向方面的
- 使用DynamicProxy和/或ActionFilterAttribute(.NET Core MVC)
- 您确实需要使用实现,接口和IOC设置进行程序清洁。
- 内部切面也可以在C#和LTW中使用。我没有使其工作,但是AutoFac和Castle支持此功能。看一下:
builder.RegisterType<..>()
.As<..>()
.EnableInterfaceInterceptors()
并添加.EnableClassInterceptors()。
C#CTW可以工作
- 但不包含在此示例中。我找不到任何C#IL切面的编织器。他们可能在那里...
使用RabbitMQ进行消息传递
对于下面的Java和C#示例,我们使用主题交换。主题交流的工作方式如下:
- 声明交换, ExchangeDeclare("X.Y.exchange");
- 声明队列 "A.B.C", QueueDeclare("A.B.C");
- 将声明的队列绑定到主题的声明的交换"S.T.*": QueueBind("A.B.C", "X.Y.exchange", "S.T.*");
具有交流和主题的定义队列
从此刻起,所有发送到交换的包含主题类型为“ ST *”消息的消息也将发送到队列。因此,您可以将一条消息发送到交换,并且如果在此主题上将多个队列绑定到交换,则它们都将收到该主题消息。注意:您发送到Exchange,然后从队列接收。
Java消息传递
对于Java中的RabbitMQ,我们使用org.springframework.boot/spring-boot-starter-amqp和org.springframework.amqp/spring-rabbit。
使用AMQP发送和接收队列消息:
首先,您需要声明您的交换和队列以及一些bean。我在单独的类中这样做:
public class ExchangeDefinition {
public static final String CHECKPOINT_EXCHANGE = "ricta.checkpoint.exchange";
public static final String KEY_CHECKPOINT_REQUEST = "checkpoint.check.request";
}
@Configuration
public class CheckPointExchangeConfig implements RabbitListenerConfigurer {
@Autowired
ConnectionFactory connectionFactory;
@Bean
public Exchange checkPointEventExchange() {
return new TopicExchange(CHECKPOINT_EXCHANGE);
}
@Override
public void configureRabbitListeners(final RabbitListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
}
@Bean
public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public MappingJackson2MessageConverter consumerJackson2MessageConverter() {
return new MappingJackson2MessageConverter();
}
@Bean
public DefaultMessageHandlerMethodFactory messageHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setMessageConverter(consumerJackson2MessageConverter());
return factory;
}
@Bean
public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(producerJackson2MessageConverter());
return rabbitTemplate;
}
}
然后,您需要声明并绑定您的队列。同样,在一个单独的类中:
public class QueueDefinition {
public static final String QUEUE_CHECKPOINT_REQUEST = KEY_CHECKPOINT_REQUEST;
}
@Configuration
public class CheckPointQueueConfig implements RabbitListenerConfigurer {
@Bean
public Queue queueCheckpointRequest() {
return new Queue(QueueDefinition.QUEUE_CHECKPOINT_REQUEST);
}
@Bean
public Binding checkPointRequestBinding
(Queue queueCheckpointRequest, Exchange checkPointEventExchange) {
return BindingBuilder
.bind(queueCheckpointRequest)
.to(checkPointEventExchange)
.with(KEY_CHECKPOINT_REQUEST)
.noargs();
}
@Autowired
DefaultMessageHandlerMethodFactory messageHandlerMethodFactory;
@Override
public void configureRabbitListeners(final RabbitListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory);
}
}
接下来,为了发送消息,我们使用RabbitTemplate:
{
...
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void sendToExhange(Message message) {
rabbitTemplate.send(ExchangeDefinition.CHECKPOINT_EXCHANGE,
ExchangeDefinition.KEY_CHECKPOINT_REQUEST, message);
}
...
}
再次注意:您发送到Exchange,并从队列中接收/收听。
为了接收消息,我们使用@RabbitListener:
{
....
@RabbitListener(queues = QUEUE_CHECKPOINT_REQUEST)
public void handleCheckPointMessage(Message message)
throws IOException {
....
}
....
}
因此,到此结束了用Java发送和接收RabbitMQ消息。
几点评论:
备注1:交换可以由多个senders使用。但是,队列的侦听器应仅为一个。如果您在同一队列中启动两个侦听器,您将体验到“循环”行为,并且RabbitMQ每次巡回都会向一个或另一个侦听器传递消息。分开定义Exchange和Queue,最好将Queue defs放在实现模块中,将Exchange defs放在API模块中。
备注2:在@RabbitListener上,一旦您的应用启动,并且对组件进行了扫描和分解,就会开始接收消息。这是完全不受控制的、隐含的,并且可以给出真正的怪异行为,在启动和设置应用程序的过程中,您已经收到一半的消息。有防止这种情况的方法。特别是在集成测试方案中,您需要更多控制权。我在这里使用一些特殊的高级代码将RabbitMQ配置为默认不启动侦听器,然后在代码中显式启动和停止侦听器:
{...
/**
* Props to stop listeners by startup in yml do not work.
* rabbitmq.listener.auto-startup: false
* rabbitmq.listener.simple.auto-startup: false
* So use this factory construction
* @param connectionFactory
* @return
*/
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory
(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory =
new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
//
//autoStartup = false, prevents handling messages immediately.
// You need to start each listener itself.
// Use BaseResponseMediaDTOQueueListener.startRabbitListener/stopRabbitListener
//
factory.setAutoStartup(false);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
return factory;
}
...
@Autowired
private RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry;
public boolean startRabbitListener(String rabbitListenerId) {
MessageListenerContainer listener =
rabbitListenerEndpointRegistry.getListenerContainer(rabbitListenerId);
if (listener != null) {
listener.start();
return true;
} else {
return false;
}
}
public boolean stopRabbitListener(String rabbitListenerId) {
MessageListenerContainer listener =
rabbitListenerEndpointRegistry.getListenerContainer(rabbitListenerId);
if (listener != null) {
listener.stop();
return true;
} else {
return false;
}
}
}
DotNET 消息
对于C#中的RabbitMQ,我们使用RabbitMQ.Client nuget包发送消息。我将其用于“拦截CheckPoint客户端”。由于某种原因,我脑子里有些奇怪的转折,说这个包是给想要发送消息的“客户”的。并且,在服务消息接收方(未包含在代码中)中,我去寻找“RabbitMQ.Server”。猜猜是什么,无处可寻。您还需要RabbitMQ.Client接收消息:-)。
通过RabbitMQ.Client发送队列消息:
using (var connection = ConnectionFactory.CreateConnection())
using (var channel = connection.CreateModel())
{
//
// At one place in time, you have to declare the exchange and topic queues, and bind them
//
var queue = channel.QueueDeclare(queue: "checkpoint.check.request",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.ExchangeDeclare("ricta.checkpoint.exchange",
ExchangeType.Topic);
channel.QueueBind(queue.QueueName,
"ricta.checkpoint.exchange",
"checkpoint.check.*"
);
string message = Id.ToString();
var body = Encoding.UTF8.GetBytes(message);
IBasicProperties props = channel.CreateBasicProperties();
props.AppId = "DEMO-APP";
DateTime now = DateTime.UtcNow;
long unixTime = ((DateTimeOffset)now).ToUnixTimeSeconds();
props.Timestamp = new AmqpTimestamp(unixTime);
props.Type = "application/json";
props.Headers = new Dictionary<string, object="">
{
{ "__TypeId__", "java.lang.String" }
};
channel.BasicPublish(exchange: "ricta.checkpoint.exchange",
routingKey: "checkpoint.check.request",
basicProperties: props,
body: body);
Console.WriteLine(" [x] Sent {0}", message);
}
通过RabbitMQ.Client以下方式接收队列消息:
{
var factory = new ConnectionFactory { HostName = "localhost" };
//
// create connection
//
_connection = factory.CreateConnection();
//
// create channel
//
_channel = _connection.CreateModel();
_channel.ExchangeDeclare("ricta.checkpoint.exchange", ExchangeType.Topic, true);
_channel.QueueDeclare("ricta.check.request", true, false, false, null);
_channel.QueueBind("ricta.check.request", "ricta.checkpoint.exchange",
"checkpoint.check.*", null);
_channel.BasicQos(0, 1, false);
//
// Create Consumer of messages
//
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (ch, ea) =>
{
//
// handle the received message
//
HandleMessage(ea);
_channel.BasicAck(ea.DeliveryTag, false);
};
...
private void HandleMessage(BasicDeliverEventArgs args)
{
//
// received message, app id and time
//
var content = System.Text.Encoding.UTF8.GetString(args.Body);
var appId = args.BasicProperties.AppId;
var checkTime = System.DateTimeOffset.FromUnixTimeMilliseconds
(args.BasicProperties.Timestamp.UnixTime).ToLocalTime().DateTime;
....
}
}
至此,以C#发送和接收RabbitMQ消息结束了。
几点评论:
备注1:交换可以由多个senders使用。但是,队列的侦听器应仅为一个。如果您在同一队列中启动两个侦听器,您将体验到“循环”行为,并且RabbitMQ每次巡回都会向一个或另一个侦听器传递消息。分开定义Exchange和Queue,最好将Queue defs放在实现模块中,将Exchange defs放在API模块中。
备注2:我没有在C#中仅给出样板代码,也没有费心创建生产控制代码,在类中分离,捕获所有异常并记录所有内容,等等。我建议不要那样做-:(。
结论,兴趣点和更多...
已解决
我们在本文中解决的问题是,在C#和Java中都可以构建具有许多模式的现代微服务代码。
我们在工作中亲眼看到了:
- 多层级联的Facade模式(面向切面,IOC,接口实现,客户端,消息传递)
- CQRS命令查询责任分离模式(带有通过消息传递的命令)
- 关注点概念分离(Aspect = API,实现接口,二进制文件分离)
- 具有Spring Boot和AutoFac容器的IOC模式
- 使用RabbitMQ进行消息传递
- 接口实现
- 面向切面
我们没有解决什么?
当然,世界其他地方的政治;-)。但是更详细。
- 在切面连接点中使用方法的参数。但是,它包含在随附的Java代码中,我发现在DotNet中是可行的;
- TDD测试驱动设计:为整个设置创建强大的测试代码。我将很快写另一篇文章,涉及单元测试和集成测试。但是,由于我们使用了许多关注点分离,因此使用Mockito,FakeIsEasy等现代模拟框架以及基于Gherkin的Cucumber和Specflow等现代集成语言,您将看到它是非常可测试的
- 功能标记/切换;如果您想要一个真正敏捷的概念,则需要此。
- 使用源存储库(例如Git)和组件存储库(例如NuGet,Nexus)对微服务代码和组件进行版本控制。
- 容器化,Docker,最后是Kubernetes。
- CI/Cd:使用TFS或BitBucket和Container注册表持续构建、测试和部署微服务
- 耐心...会做的所有事情,但也许今天不做:-)。
- 我忘记了什么?请告诉我。
我们学到了什么?
面向切面在C#和Java中均可使用,但实现方式相似但不相同。
(主题)消息传递在C#和Java中均可使用,但实现方式相似但不相同。
您可以创建带有模式的通用抽象设计,并以Java或C#实现它们。
关于微服务的坚定说明:我确实相信,要使它们成功,您需要所有这些模式,以及更多其他功能(文档,TDD,云,Ci/Cd,功能标记,容器化等),这意味着,您需要在组织内投入大量知识,并可能雇用一些有经验的人来进行设置。我保证您可以使用,但我也保证您,不使用所有这些模式和可靠的实现会比单一形式产生更大的混乱。更大的福报:-(。
为什么我确信微服务可以正常工作?
我可能已经在这里开始了一些关于宗教信仰的漫长争论。我没有。相反,我给您一个隐喻的问题,我的回答是:
问:为什么您认为现代的基于微组件的电子设备工作得如此好?(我学习、应用和管理过电子产品,所以请相信我,我在那里一点儿知道。)
我的简短回答:我坚信电子微组件可以工作,因为它们创建了真正可重用的非常小的独立组件,并带有一组出色的、标准化的工具和文档。现在,如果可以在诸如电子设备之类的复杂事物中做到这一点,那么我相信可以将其转换为与软件“相似的事物”。微服务是实现这一目标的关键部分。
我的较长的(关注的)答案:尽管我确实相信微服务可以提供现代电子微组件已经证明的几十年、甚至以后的几十年,但我还是有点担心我们还没有软件——土地。因为,电子产品已经掌握并教授了19世纪70年代/80年代/90年代初的概念,并且它们拥有一个快速发展的庞大产业,以实惠的价格提供了这些标准化的组件和工具以及出色的工具,规格和文档。
现在,我们在软件行业这方面做得如何?恩。我和你打赌:不一样。一方面,我们不会教学生微服务的概念。我们甚至还没有开始了解它们在软件行业中的确切地位。
我们是否就编写微服务规范达成任何规范?恩。
我们是否以合理的价格提供它们?恩。
我们是否有专门的工具来帮助我们掌握设计和实现中的微服务模式和标准?恩。
创建它们后,是否可以对其进行托管?是。Google,Microsoft,AWS是一个良好的开端。Kubernetes 。
设计、创建、构建、托管和维护微服务是否容易?嗯~一点都不。在这个无法想象的时间点复杂吗?你打赌
我们能否以合理的价格强大地托管它们?嗯,没事,我希望Microsoft,AWS(Google,Netflix?)是一个好的开始。
因此,我猜想,在漫长的隧道尽头的光线刺眼。但是你必须从某个地方开始,不是吗?
陷阱
现在,正如我在本文前面的几个地方提到的那样,版本地狱比以前要好一些,但是我向您保证,它仍然存在。而且,如果您尝试将所有概念与COTS组件(Maven,NuGet)结合起来,那么这将在某个时间的某个地方咬住您。我在那里没有很好的答案。我想,随便摇一摇吧。
但是,我确实知道一件事:为外部组件堆栈建立稳定的基准之后,请对其进行管理。小心。我多次应用的模式有效,它正在创建自己的托管组件存储库(NuGet服务器,Nexus等)。将您选择的和经过验证的基准版本化组件放在其中,然后让高层管理。其余的:仅使用其中的组件。在您的生产管道中,为所有开发人员,初级,中级和高级人员关闭Maven Central(maven.org)或NuGet Central(nuget.org)。给他们一个爱好农场,在某个地方和爱好时间,但不要在生产管道中。相信我,我去过那里,看到了,做到了。
如果您发现自己在争取交付、测试、错误修复和构建软件的过程中花费了数周的时间,却遇到了最奇怪的版本控制编译器和运行时错误,例如:“无法在组件YYY中找到方法XXX”或“无法找到或加载BBBB类”,以及您真的非常厌倦凌晨03:00的控制,而不是让它控制您。您可能会犯的最大错误是,重新构建那些安全的旧组件,并说:“我讨厌那些带有组件h ** ll的有趣的微服务”。我也不喜欢我的汽车的保险、车库、汽油和路税单,但我从来没有发现自己希望回来骑马和乘车...考虑到您的微服务是电动汽车。是的,目前他们的射程可能很短。是的,它们的价格昂贵。是,他们可能需要很长时间才能填满。但是在20至40年间,没人知道内燃机是什么了。单一项目也是如此。但这很可能在5-10年后就已经发生了。如果不是更早...