In modern OO languages and frameworks, a lot of patterns, e.g., aspect-orientation, facade, IOC, CQRS are used. This article describes some of these concepts and patterns, the possible solutions and gives an example implementation in Java and C#.
Introduction
This article provides a state of the art view of aspect orientation in the context of modern platforms for micro-services. It describes the pros and cons of aspect orientation and the pros and cons of implementations for it. It does not describe in depth the concept of Aspect-orientation. There are other articles out there that do this job fine.
See related articles:
How Is This Article Built Up?
- Examples in Java and C# of final usage
- Aspect orientation in modern micro-services, why and how?
- High level design and rationale (pros and cons of the design)
- Java implementation details
- C# implementation details
Using the Code in Java (11+, Spring boot 2.2+, Spring Boot AOP, AspectJ)
@CheckPoint(10)
public void annotatedCheckPointMethod(@CheckPointParameter String user) {
....code ...
}
Using the Code in C# (7, .NET MVC Core 2.1+, Autofac, Caste Core Dynamic Proxy)
[CheckPoint(10)]
public void AnnotatedCheckPointMethod([CheckPointParameter] String user)
{
....code ...
}
Concept: The Basic Principles
What Is Aspect-Oriented Programming?
For what aspect orientation is, I refer to [1].
How Does Aspect-Orientation Work?
In this article's implementation, we use CTW (Compile Time Weaving) and LTW (Load Time Weaving).
LTW weaving "weaves" at run-time an in-between object that implements the same interface, and does the aspect work, and then delegates to the implementation code.
CTW weaving "weaves" at compile-time, the aspect code in the proper place. Both C# and Java use intermediate (byte or IL code) to compile to. So typically, first the native language compiler generates the byte code, then the aspect compiler weaves in the aspect in the byte code.
Why Use Aspect Orientation?
In modern, micro-service oriented application landscapes, there will typically not be one, or two or three services, but rather a dozen or even hundreds of micro-services. Typically, if we want to modularize this software, there are at least these possibilities:
- Use a distributed service call
- Use a binary library
- Copy code
- Use aspect orientation
Note: This view of splitting is oversimplified, incomplete, and yes, an implementation may very well do a distributed call or a distributed service call can be hidden with a client facade pattern in binary library. So, I know, this view of the world is faulty, but any split in views on all systems has exceptions and is debatable. I only use it to show how aspects can be used.
1. Use a Distributed Service Call
We split-off the re-usable part in a separate service, and call that service from all other services that need this functionality. This is not the fastest, and most scalable and robust solution in all cases, for example, for something like logging. Typically, before you know it, this becomes the "God" service pattern that everyone warns for in micro service land not to use...
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. Use a Binary Library
We split-off the re-usable part in a separate binary library (jar, dll, nuget, maven, etc.), and call that API from all other services that need this functionality.
public void DistributedCheckPointMethod(String user)
{
libApi.SendCheckPoint("before", 10, user);
...code...
libApi.SendCheckPoint("after", 10, user);
}
3. Copy-Paste Code
Just copy-paste the code across all services. There are certainly pros for this approach. That is why it is still used a lot :-((. It is simple, easily adjustable to small exceptions, and often scalable in performance. This used to be considered a real bad design in all circumstances.
Note: In modern OO-designs, there are patterns like Data Transfer Objects, that can be copy-pasted and tailored to the specific needs of that usage, without being considered a "bad" design per-se, if used wisely.
public void DistributedCheckPointMethod(String user)
{
List<string> props = new List<string>(){user};
rest.CheckPoint(place, id, props);
...code...
rest.CheckPoint("after", 10, props);
}
4. Use Aspect Orientation
Whereas this sounds like a complete new option, it typically is on top of the first three solutions. The pro of this, is that the API footprint is really the almost most minimal API that you can have.
Note: My favorite solution for Command like aspects is, to combine aspects with a binary library and a facade client/proxy approach and the CQRS pattern with a queue, which I will explain later in this article.
The "client" service code typically only defines a tag (annotation) like:
@CheckPoint(10)
...
@FeatureFlag("Feature1")
...
So, the API is defined as "one word with optionally parameters". Whatever the call or implementation may be, is hidden in the aspect code. This way, implementation and API are split very clean, and the chance of having API changes due to implementation changes is really very minimal. Don't forget, your API call may end up in a few hundred places per service, and in a few dozen or more services. And, yes, if you use a aspect for this, and a (simple but faulty design) that calls a distributed service, you still have created that "SPOF God" service. But, at least, you have the most minimal API on that, and called a minimal number of times in the code, so implementation changes have typically a lot less impact.
And of course, if you want something done for all methods in you class, you define one class level aspect:
[Log("SomeClass")]
public class SomeClass
{
....methods go here, and all methods do logging...
}
High Level Design
Packaging of application and aspect library. The Aspect and Client are packages in a (binary) library
As we can see here, there are four facade patterns applied:
- Facade CheckPoint aspect, that hides CheckPoint Aspect behaviour from the app
SomeClassUsingCheckPoints
. - Within the CheckPoint Aspect, the Facade
CheckPointClient
, that hides the sending of messages from the Checkpoint aspect. - Within the Client facade, there is a checkpoint queue, that hides from the client that there is a CheckPoint Service.
- Within the queue, there is a checkpoint Service, that hides from the queue what is done with the message.
In other words, separation of concern is applied:
- The concern of the app is to do something on app level (
annotatedCheckPointMethod
) - The concern of the aspect is to intercept and handle checkpoints
- The concern of the client is to send a message
- The concern of the queue is to receive and send messages
- The concern of the service is to handle checkpoint messages.
- So, everything has one concern.
And, note that the CheckPoint
API for the app is very clean, and even binary separated from the app.
Finally, the queue part delivers the CQRS CommandQueryResponsibilitySeparation. The fact that we need a checkpoint is a Command: Create / sent a checkpoint (command).
Now, we can see that in applying the combined facade, client, queue and CQRS patterns in this example, we achieve separation of concern, loose coupling and lots of possibilities for flexibility and re-use. The aspect orientation delivers us a very clean API split, and does an excellent facade job.
General Implementation Details
In general, for both Java and .NET, I'll leave out the CheckPoint
server/vice side.
As this article is about aspect orientation mainly, I'll focus the implementation on the "client side".
For the Queuing messaging implementation, I have chosen RabbitMQ. This is both rather commonly accepted and well supported by Java and C#, and Windows and Linux and the "cloud".
Note: Before running the code, you have to install RabbitMQ first on your system. See https://www.rabbitmq.com/download.html.
Java Implementation Details
Java Aspects
There are a couple of remarks on aspects in Java I address here before we dive in the code. Then you can decide up-front to choose one or the other and dive into the details of that.
Remark 1: I did find both Compile Time and Load Time Weaving in Java.
Remark 2: Both implementations are using Spring Boot.
Remark 3: Load Time Weaving uses "plain" Spring AOP. One of the "by design features" (you could call it a bug, or constraint, or whatever) is that an aspect only works on an interface call, because then you have the com.sun proxy in there. It does not work on an implementation class. As a result, you need to define your aspects on the interface. And, if you then, inside your aspect-larded method, call another implementation method, that aspect is not executed.
Pros LTW
- Works out of the box with most Spring AOP versions
Cons LTW
- Performance is not that good. I choose the cheaper
@Before
aspect, instead of @Around
, the most expensive performance savvy aspect; - Internal aspects don't work.
Remark 4: Compile Time Weaving uses Spring AOP, together with a aspectjrt and a pre-aspect compiler. However, I ended up in jar mvn version hell again. I could only make it work with some private development branch of the aspectj-maven-plugin compiler (1.12.6, com.nickwongdev), together with the latest greatest release 2.x train in march 2020, of Spring Boot (Hoxton.SR3):
<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>
Pros CTW
- Better performance
- "Internal" implementation aspects do work
Cons CTW
- More setup needed
- Version release train constraints on versions between Spring Boot and aspectj-maven-plugin compiler
LTW Java Proxy
The following steps will implement your LTW Aspects.
First, define the aspect annotation:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPoint {
long value() default 0;
}
Next, define the @Before
aspect implementation:
@Configurable
@Aspect
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public class CheckPointAspect {
@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...
}
}
Next, define a (test) interface using the @CheckPoint
. Make sure you define your attributes on the interface methods:
public interface CheckPointTest {
@CheckPoint(id=10)
void doCheck();
}
Then, the implementing test class:
@Component
public class CheckPointTestImpl implements CheckPointTest {
@Override
public void doCheck() {
log.info("2. CheckPointTestImpl.doCheck");
}
CTW Java Aspect Compiler
The following steps will implement your CTW Aspects.
Actually, there is completely no difference in coding. So, all coding is, as above, exactly the same as for LTW.
The only difference is in the pom project configuration: the org.aspectjrt
dependency, and the maven aspectj-maven-plugin
compiler 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>
Note 1: When you do LTW Spring AOP, the aspect class is loaded and managed by the container of Spring Boot, so your wiring is automagically done for you. No such thing with CTW. The Aspect compiler and your aspects and Spring Boot IOC don't play nice out of the box. You have to connect each aspect manually. Otherwise, your @Autowiring
will fail.
....
public static CheckPointAspect aspectOf() {
return SpringApplicationContextHolder.getApplicationContext().
getBean(CheckPointAspect.class);
}
...
@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;
}
}
Note: One of my favorite components is Lombok. It saves a lot of boiler plate code, and the @Builder
pattern is perfect. However, the maven aspectj compiler does not play nice with Lombok. As the aspectj runs before Lombok, you don't have the builders and getters/setters yet, and the aspectj compiler does not understand your code anymore. I only could make it work by excluding the DTO package:
<sources>
<source>
<basedir>src/main/java</basedir>
<excludes>
<exclude>nl/bebr/xdat/checkpoint/api/*.*</exclude>
</excludes>
</source>
</sources>
Java Aspect Wrap Up
What did we learn from the previous aspect-oriented Java implementations?
Java CTW and LTW spect-orientation works
- in Java 11, using Spring Boot
- you do need to program clean with implementations, interfaces and IOC setup
Java LTW works
- is relatively simple with only Spring AOP dependency
- has limitations: performance, not all internal aspects work
Java CTW works
- Has better performance
- internal aspects do work
- needs manual connection to IOC container
- interferes with other compilers, like Lombok
- has cumbersome large, not very stable version tree dependencies (Spring boot 2.5.x only, with branched compiler version)
DotNET C# Implementation Details
DotNET Aspects
There are a couple of remarks on aspects in C#.
Remark 1: I did not find a Compile Time byte code aspect weaving implementation for .NET. There is IL weaving in .NET, but I could not find something like Fody weaving for aspects. So I'll only implement the LTW proxy solution.
Remark 2: There is rather a big difference between .NET and Java going on in syntax and sync/async in the method signature. This has impact in .NET on the intercepting and aspects. I got the intercepting with AutoFac working on standard classes with plain sync methods , but that falls apart on async HTTP Rest controller methods. So, for intercepting on REST controllers, I used another solution I came across, ActionFilterAttributes
. I use that for intercepting REST Controller methods. And for the other classes, we use the intercepting mechanism of a combination of AutoFac and Castle Core Dynamic proxies. And, yes, we could use ActionFilterAttributes
everywhere.
C# Dynamic Proxy
Remark 1: "Maven, Nuget, DotNet, dll, jar, Spring, it's all a version hell..." You have to find out which version of Castle, AutoFac, AutoFac.Extra.DynamicProxy
, ... work together in a way that does the job.
This did it for me in May 2020, but if you use other components or want a higher version, you have to twut around a lot probably, as I did for some frustrating hours:
Autofac : 5.1.0
Autofac.Extras.DynamicProxy : 5.0.0
Castle.Core: 4.4.0
FakeItEasy: 6.0.0
How does the DynamicProxy
in this C# work?
First, define a interface. Make sure you define your attributes on the interface methods:
public interface AOPTest
{
[CheckPoint(Id = 10)]
void DoSomething();
[CheckPointActionAtrribute(Id = 10)]
void DoAction();
}
Then, implement the interface:
public class AOPTestImpl : AOPTest
{
public void DoSomething()
{
Debug.Print("3. DoSomething\n");
}
public void DoAction()
{
}
}
Then, define the interceptor.
Note, in both Java and .NET, there is some "invocation point: defined.
public class CheckPointInterceptor : Castle.DynamicProxy.IInterceptor
{
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)
{
checkPointClient.CheckPoint(theCheckPoint.Id);
Debug.Print($"2. CheckPointAttribute on method found with cp id =
{theCheckPoint.Id}\n");
}
invocation.Proceed();
Debug.Print($"4. @After method: {invocation.Method.Name}");
}
}
Next, register the objects with AutoFac:
var builder = new ContainerBuilder();
...
builder.RegisterType<aoptestimpl>()
.As<aoptest>()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(CheckPointInterceptor))
.PropertiesAutowired()
.PreserveExistingDefaults();
...
var container = builder.Build();
...
Finally, let's test and use the code:
using (var scope = container.BeginLifetimeScope())
{
AOPTest aOPTest = scope.Resolve<aoptest>();
aOPTest.DoSomething();
}
should result in:
@Before
method called DoSomething
CheckPointAttribute
on method found with cp id = 10
DoSomething
@After
method: DoSomething
ActionFilterAttributes for REST Async Controller Methods
We saw the DynamicProxy
above at work.
But, what if we try to apply that to a "server" side .NET Core MVC REST controller, where the call comes from the OWIN REST channel?
Like:
[HttpGet("index")]
[CheckPoint(Id = 10)]
public ActionResult Index()
{
Debug.Print("3. Index\n");
....
}
Then at first glimpse, this seems to work. You do have to connect AutoFac to the Microsoft Extensions DependencyInjection
.
But, as soon as you look closer inside the invocation, you end up in misery:
public void Intercept(Castle.DynamicProxy.IInvocation invocation) {..}
is now all of a sudden called multiple times, due to the async behavior of ActionResult
, and no way you can make this work without tricky, complex, cumbersome code.
I couldn't make this work in any acceptable way. But, I bumped into this pattern looking for code to solve the problems: ActionFilterAttributes
.
Step 1: Define filter actions OnActionExecuting
and OnActionExecuted
:
public class CheckPointActionAtrribute : Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute
{
public long Id { get; set; }
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;
CheckPointClient checkPointClient = BootStapper.Resolve<checkpointclient>();
checkPointClient.CheckPoint(Id);
}
public override void OnActionExecuted(ActionExecutedContext context)
{
ControllerActionDescriptor actionDescriptor =
(ControllerActionDescriptor)context.ActionDescriptor;
Debug.Print($"3. @After method:
{actionDescriptor.ControllerName}.{actionDescriptor.ActionName}");
}
}
Final Step 2, apply the attribute:
[HttpGet("index")]
[CheckPointActionAtrribute(Id = 10)]
public ActionResult Index()
{
Debug.Print("3. CheckPointController.Index\n");
....
}
Result should be this:
@Before
method called CheckPointController.Index
CheckPointController.Index
@After
method: CheckPointController.Index
Eh voila, done. Easy-peasy.
Question: If filter attributes are that simple, why bother the first, much more elaborate, DynamicProxy
solution?
Answer: ActionFilterAttribute
is available for (MVC) DotNet Core. What if you do plain .NET Core or .NET Full? -> You can use the DynamicProxy
.
Hopefully, when your RabbitMQ is installed, you see a CheckPoint
message when running the C# examples above and/or the Java examples below, in the checkpoint.check.request
queue:
Message in checkpoint.check.request queue. Note: The exchange source, routingkey, app-id, timestamp, headers and type, and body payload
C# Aspect Wrap Up
What did we learn from the previous aspect-oriented C# implementations?
C# LTW Aspect-Orientation works
C# CTW can work
- but is not included in this examples. I could not find any C# IL aspect weaver. They may be out there however...
Messaging with RabbitMQ
For below examples in Java and C#, we use a topic exchange. Topic exchanges work as follows:
- Declare an exchange,
ExchangeDeclare("X.Y.exchange");
- Declare a queue
"A.B.C", QueueDeclare("A.B.C");
- Bind the declared Queue to the declared Exchange for topic
"S.T.*": QueueBind("A.B.C", "X.Y.exchange", "S.T.*");
Definition queue with exchange and topic
From this moment on, all messages sent to the exchange containing messages with topic type "S.T.*" are also sent to the Queue. So, you can send one message to an exchange, and if multiple queues are bind on this topic to the exchange, they all receive that topic message. Note: You send to an Exchange, and Receive from a Queue.
Java Messaging
For RabbitMQ in Java, we use org.springframework.boot/spring-boot-starter-amqp
and org.springframework.amqp/spring-rabbit
.
Sending and receiving queue messages with AMQP:
First, you need to declare your exchanges and queues and some beans. I did this in separate classes:
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;
}
}
Then, you need to declare and bind your Queues. Again, in a separate class:
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);
}
}
Next, for sending a message, we use RabbitTemplate
:
{
...
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void sendToExhange(Message message) {
rabbitTemplate.send(ExchangeDefinition.CHECKPOINT_EXCHANGE,
ExchangeDefinition.KEY_CHECKPOINT_REQUEST, message);
}
...
}
Note again: You send to an Exchange, and Receive/Listen from a Queue.
For receiving messages, we use @RabbitListener
:
{
....
@RabbitListener(queues = QUEUE_CHECKPOINT_REQUEST)
public void handleCheckPointMessage(Message message)
throws IOException {
....
}
....
}
So, that concludes sending and receiving RabbitMQ messages in Java.
A few remarks:
Remark 1: Exchanges can be used by multiple senders. However, listeners to Queues should be only one. If you start up two listeners on the same queue, you will experience Round Robin behaviour, and RabbitMQ will deliver messages per tour to either the one or the other listener. Separate your Exchange and Queue definitions, and preferably, put your Queue defs in an implementation module, and your exchange defs in an API module.
Remark 2: On the @RabbitListener
, as soon as your app starts, and your components are scanned and factored, receiving messages will start. This is quite uncontrolled, and implicit and can give real weird behaviour, where you receive messages already halfway during startup and setup of your app. There are ways to prevent this. Especially, in integration testing scenarios, you want more control. I use some special advanced code here to configure RabbitMQ to default not starting listeners, and then explicitly starting and stopping listeners in your code:
{...
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory
(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory =
new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
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 Messaging
For RabbitMQ in C#, we use the RabbitMQ.Client
nuget package to sent messages. I used that for the "intercepting CheckPoint client". For some reason, I made some real weird twist in my head that this package is for "clients" that want to sent messages. And, on the service message receiving side, which is not included in the code, I went looking for the "RabbitMQ.Server
". Guess what, nowhere to be found, of course. You need the RabbitMQ.Client
as well to receive messages:-).
Sending queue messages with RabbitMQ.Client
:
using (var connection = ConnectionFactory.CreateConnection())
using (var channel = connection.CreateModel())
{
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);
}
Receiving queue messages with RabbitMQ.Client
:
{
var factory = new ConnectionFactory { HostName = "localhost" };
_connection = factory.CreateConnection();
_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);
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (ch, ea) =>
{
HandleMessage(ea);
_channel.BasicAck(ea.DeliveryTag, false);
};
...
private void HandleMessage(BasicDeliverEventArgs args)
{
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;
....
}
}
So, that concludes sending and receiving RabbitMQ messages in C#.
A few remarks:
Remark 1: Exchanges can be used by multiple senders. However, listeners to Queues should be only one. If you start up two listeners on the same queue, you will experience Round Robin behaviour, and RabbitMQ will deliver messages per tour to either the one or the other listener. Separate your Exchange and Queue definitions, and preferably, put your Queue defs in an implementation module, and your exchange defs in an API module.
Remark 2: I did give only boiler-plate code in C#, and did not bother creating production harnessed code, separate in classes, catching all exceptions and logging all, etc. I suggest not to do it like that -:(.
Conclusions, Points of Interests and More...
Addressed
What we addressed in this article is that building modern micro-service code with a lot of patterns is possible in both C# and Java.
We saw at work, hands-on:
- Multi-level cascaded Facade patterns (Aspect-orientation, IOC, Interface-Implementation, Client, Messaging)
- CQRS Command Query Responsibility Separation pattern (with commands via messaging)
- Separation of concern concepts (Aspect = API, implementation-interface, binaries separation)
- IOC pattern with Spring Boot and AutoFac containers
- Messaging using RabbitMQ
- Interface-Implementation
- Aspect-orientation
What Did We Not Address?
Of course, the rest of the world politics;-). But more detailed.
- Using parameters of the methods in your aspect joinpoints. It is included in the Java code attached however, and I saw that is possible in DotNet;
- TDD Test Driven Design: Creating robust test code for this whole setup. I will write another article on that soon, addressing unit testing and integration testing. However, because we used a lot of separation of concern, you will see that it is very testable, using modern mocking frameworks like Mockito, FakeIsEasy, etc., and modern integration languages like Gherkin based Cucumber and Specflow
- Feature flagging/toggling; If you want a truly agile concept, you need this; See [2] Roland Roos, Feature Flags
- Versioning your micro-service code and components with source repos, e.g., Git and Component repos, e.g., NuGet, Nexus.
- Containerization, Docker, and finally: Kubernetes.
- Ci/Cd: Continuous building, testing and deploying your micro-services with e.g. TFS, or BitBucket and Container Registries
- Patience... will do all that, but maybe not today:-).
- What did I forget? Please let me know.
What Did We Learn?
Aspect orientation works in both C# and Java, with similar but not the same implementations.
(Topic) Messaging works in both C# and Java, with similar but not the same implementations.
You can create a generic, abstract design with patterns, and implement them in either Java or C#.
A firm note on micro-services: I do believe, that you need all of these patterns to make them a success, and many more (Documentation, TDD, Cloud, Ci/Cd, Feature flagging, Containerization, ..) That means, you need to invest in a lot of knowledge within your organization, and probably hire a few experienced people to set it up. I guarantee you it works, but I also guarantee you, that not using all of these patterns and robust implementations will create an even bigger mess than monoliths. Fubar bigger :-(.
Why am I convinced that micro-services do work?
I might have started some lengthy argumentation of religious belief here. I did not. Instead, I give you a metaphorical question, and my answer to it:
Question: Why do you think modern micro-component based electronics work as good as they do? (I studied, applied and managed Electronics, so trust me, I know a little bit there.)
My short answer: I am convinced that Electronic micro-components work, because they created truly re-usable very tiny independent components, with a splendid, normalized set of tooling and documentation. Now, if that is possible in something complex as electronics, I am convinced is that is transposable to something "similar complex" as software. And micro-services are a key part in creating just that.
My longer (concerned) answer: Although I do believe micro-services can deliver what modern electronical micro-components have proven to be for decades now, and for decades more to come, I'm slightly worried that we are not there yet in software-land. Because, electronics have mastered and taught the concepts from say the 19-seventies/eighties/early nineties, and they have a fast and big industry, delivering these standardized components and toolings with splendid tools, specs and documentation, at affordable pricing.
Now how are we doing in the software industry on this part? Mwhhhaaa. I bet you: different. For one thing, we don't teach our students micro-services concepts. We haven't even started knowing what they exactly are in the software industry.
Did we agree on any norms on writing specs on micro-services? Mwhhhaaa.
Do we deliver them at affordable pricing? Mwhhhaaa.
Do we have specialized tooling that helps us mastering micro-services patterns and standards in our designs and implementations? Mwhhhaaa.
Once we created them, can we host them robustly? Yes. Google, Microsoft, AWS are a good start. Kubernetes rocks.
Is it easy to design, create, build and host and maintain micro-services? No way. At this point-in-time beyond imagination complex? You bet.
Can we host them robustly at affordable pricing? Mwah. Yeen. Noy. Microsoft, AWS (Google, Netflix?) are a good start I hope...
So, light is glaring at the beginning of a long, long tunnel, I suspect. But you have to start somewhere, don't you?
Gotchas
Now, as I mentioned earlier in this article in several places, version hell is a bit better than it used to be, but I guarantee you, it is still there. And if you try to combine all concepts with COTS components (Maven, NuGet), this will bite you in the Royal Butt at some time at some place. I don't have a good answer there. Waddle your way through it, I suppose.
I do know one thing however: Once you have established a stable baseline for your external component stack, manage it. Carefully. A pattern I applied multiple times, that works, is creating your own managed component repositories (NuGet server, Nexus, whatever). Put your chosen and proven baseline versioned components in there, and let the seniors manage it. The rest: only take components of what is in there. Close down Maven Central (maven.org), or NuGet central (nuget.org) in your production pipelines, for all your developers, juniors, intermediates, and seniors. Give them a hobby farm somewhere and hobby time, but not in your production pipeline. Trust me, I have been there, seen that, done it.
If you find yourselves fighting to deliver, test, bugfix and build your software for weeks getting the most weird versioning compiler and runtime errors like: "cannot find method XXX in component YYY", or "cannot find or load class BBBB", and if you're really, really tired of that at 03.00 in the morning, take control, instead of letting it control you. The biggest mistake you can make is going back to building those safe old monoliths, saying: "I hate those funny micro-services with their component-h**ll". I don't like the insurance-, garage-, gasoline- and roadtax-bills of my car either, but I never find myself wishing back for a horse and carriage... Consider your micro-services being electrical cars. Yes, they may have short range at this moment. Yes, they are expensive to buy. Yes, they may take long to fill-up. But in 20-40 years, no one knows what combustion engines are anymore. Same holds for monoliths. But that may well be already in 5-10 years from now. If not earlier...
History
- 7th April, 2020: Initial version