2024 DevOps Lifecycle: Share your expertise on CI/CD, deployment metrics, tech debt, and more for our Feb. Trend Report (+ enter a raffle!).
Kubernetes in the Enterprise: Join our Virtual Roundtable as we dive into Kubernetes over the past year, core usages, and emerging trends.
A framework is a collection of code that is leveraged in the development process by providing ready-made components. Through the use of frameworks, architectural patterns and structures are created, which help speed up the development process. This Zone contains helpful resources for developers to learn about and further explore popular frameworks such as the Spring framework, Drupal, Angular, Eclipse, and more.
Micro Frontends Architecture
Deployment of Spring MVC App on a Local Tomcat Server
Over the past few years, many new frontend frameworks have been released, offering developers a wide range of options to choose the one that best fits their projects. In this article, we will analyze Astro, an open-source project released with an MIT license. The first version, v1.0, was released in August 2022 as a web framework tailored for high-speed and content-focused websites. One year later, in August 2023, they released Astro 3.0 with a lot of new features like view transitions, faster-rendering performance, SSR enhancements for serverless, and optimized build output, which we will cover later in the article. On October 12, 2023, they announced Astro 3.3 with exciting updates, such as the <Picture/> component for image handling. Astro.js is a multi-page application website framework. It indicates that Astro renders web pages on the server, which explains why it is so fast: while you navigate between pages, you will continually get those pages. Although Astro can also lazy load client-side JavaScript if our web pages require interactivity, I’ll go into more detail about this in the upcoming chapter. Astro Features Island Architecture Island Architecture, pioneered by Etsy’s frontend Architect Katie Sylor-Miller, is a revolutionary concept in web development. It involves the separation of a website’s static elements, such as images and text, which can be server-rendered and delivered without JavaScript, from the interactive components that require JavaScript for interactivity. By prioritizing these interactive elements, a web page can load its important features first, enhancing the user experience. Astro fully embraces this Island architecture, and it divides the user interface (UI) into smaller, isolated components known as “Astro Islands.” What sets Astro apart from other frameworks is its utilization of partial hydration, offering compatibility with a variety of UI libraries, including React, Svelte, Vue, and more. Users have the flexibility to mix and match these libraries to render islands in the browser through partial hydration. Astro optimizes your website’s performance by shipping code without JavaScript. For instance, even if you create a highly interactive React component, Astro will deliver only HTML and CSS, reserving interactivity until it’s activated. This is where partial hydration plays a vital role in enhancing web interactivity. Hydration, in this context, means adding JavaScript to HTML code to make it interactive. Partial hydration selectively loads individual components as needed, keeping the rest of the page as static HTML. The Island architecture encourages the creation of small, modular interactivity components. One of Astro’s standout features is the precise control it offers over when to introduce interactivity: <MyComponent client:load /> – loads JavaScript simultaneously with HTML <MyComponent client:idle /> – loads JavaScript when the browser has no other tasks to do <MyComponent client: visible /> – loads JavaScript only when visible to the user <MyComponent client:media /> – loads JavaScript only for specific screen width <MyComponent client:only /> – only client-side rendering For example, high-priority islands may include elements like buttons, tags, and navigation for immediate user interaction. Medium-priority islands could have features like a light/dark mode switch. By segregating the UI into static and interactive elements, Astro ensures a swift and efficient user experience by loading interactive components only when necessary. The Island architecture approach not only speeds up performance but significantly benefits SEO rankings on search engines. It enhances user experiences, minimizes boilerplate code, and provides robust support for various CSS libraries and frameworks. Framework-Agnostic Astro allows us to construct our website using our preferred framework: React, Vue, Svelte, SolidJS, Preact, Alpine, Lit, Web components, etc., and it’s not limited to just one; we can work with multiple frameworks simultaneously. We can have React and Vue components coexisting in the same codebase. If one day we would like to emigrate from React to Vue or vice versa, we can do that gradually. If we don’t have dynamic parts in our project, we can build our web only with Astro, providing a lightweight and efficient solution. View Transitions API Chrome and Astro have joined forces to introduce the View Transitions API, a revolutionary tool for web developers. With the help of this API, creating smooth state transitions will become easier. Previously, this was a difficult operation that involved handling scroll position variations and CSS animations. This method was rapidly adopted by the Astro framework, allowing it to deliver the magic of page transitions without the typical complexity and performance limitations. View Transitions are now supported in Astro 3.0, enabling the use of shared elements between routes and providing access to additional capabilities like custom animations. Understanding the Project Structure and Rendering Flexibility In Astro, the project structure includes essential elements, including components, layouts, pages, and styles. Let’s dive into each of these components: Components Reusable chunks of code that can be integrated throughout your website. By default, these components carry the .astro file extension. However, the flexibility of Astro allows you to incorporate non-Astro components crafted with popular libraries such as Vue, React, Preact, or Svelte. Layouts Reusable components, but they serve as wrappers for your code, providing structure and organization to your web pages. Pages Specialized components that hold responsibility for routing, data loading, and templating. The framework employs file-based routing to generate web pages. Moreover, you can also use dynamic routing for more customized URL paths. Every file you create in this folder returns to a URL. For example, a file called about would give us availability for /about URLs. Styles The “styles” folder serves as the repository for your website’s styles. Astro seamlessly accommodates various styling options, including Sass, Scoped CSS, CSS Modules, and Tailwind CSS. Beyond these structural components, Astro offers several additional capabilities. It provides a global object called “Astro,” granting access to valuable properties like props, cookies, params, redirection, and more. One notable feature is the absence of boilerplate code. When defining a component, you are relieved of the need to write out the export function. In the code snippet below, you can observe the inclusion of JavaScript code enclosed between three dashes, followed by HTML code. HTML --- import GreetingHeadline from './GreetingHeadline.astro'; const name = "Astro"; --- <h1>Greeting Card</h1> <GreetingHeadline greeting="Hi" name={name} /> <p>I hope you have a wonderful day!</p> By default, Astro runs as a static site generator. This means that all the content is converted to static HTML pages, a strategy known for its optimization of website speed. However, it’s worth noting that web development can occasionally demand a more dynamic approach. Even though Astro started as a static site generator, now it facilitates both static site generation (SSG) and server-side rendering (SSR) based on your specific project requirements. And you can pick which pages will use which approach. We can add the following code to astro.config.mjs if most or all of your site should be server-rendered: HTML import { defineConfig } from 'astro/config'; import nodejs from '@astrojs/node'; export default defineConfig({ output: 'server', adapter: nodejs(), }); Or instead of'server', if we write 'hybrid,' it will be pre-rendered to HTML by default. We should use the hybrid one when most of our pages are static. Conclusion Astro is an innovative and adaptable option in the rapidly evolving front-end framework world. With its unique approach to “Island Architecture” and the ability to embrace multiple UI libraries, Astro offers developers the ability to work with their preferred tools and even combine them, giving rise to a seamless user experience. Furthermore, another compelling reason to make the switch to Astro is that your content-rich static sites will be significantly faster using Astro since less JavaScript is served. For example, Astro sites can load 40% faster with 90% less JavaScript compared to Next.js. Because it only hydrates what’s needed and leaves the rest as static HTML. This selective hydration, paired with Astro’s island architecture for interactive components, means you can build lightning-fast websites. The increase in performance will result in improved SEO and user experience for your Astro site. Whether you’re prioritizing performance, SEO, or transitioning between frameworks, Astro stands as a remarkable framework of choice for high-speed, content-focused websites.
In this blog, you will learn how to monitor a Spring Boot application using Ostara. Ostara is a desktop application that monitors and manages your application. Enjoy! Introduction When an application runs in production (but also your other environments), it is wise to monitor its health. You want to make sure that everything is running without any problems, and the only way to know this is to measure the health of your application. When something goes wrong, you hopefully will be notified before your customer notices the problem, and maybe you can solve the problem before your customer notices anything. In a previous post, it was explained how to monitor your application using Spring Actuator, Prometheus, and Grafana. In this post, you will take a look at an alternative approach using Spring Actuator in combination with Ostara. The setup with Ostara is a bit easier; therefore, it looks like a valid alternative. The proof of the pudding is in the eating, so let’s try Ostara! The sources used in this blog are available on GitHub. Prerequisites The prerequisites needed for this blog are: Basic Spring Boot 3 knowledge; Basic Linux knowledge; Java 17 is used. Create an Application Under Test First, you need to create an application that you can monitor. Navigate to Spring Initializr and add the Spring Web and Spring Boot Actuator dependencies. Spring Web will be used to create two dummy Rest endpoints, and Spring Boot Actuator will be used to enable the monitor endpoints. See a previous post in order to get more acquainted with Spring Boot Actuator. The post is written for Spring Boot 2, but the contents are still applicable for Spring Boot 3. Add the git-commit-id-plugin to the pom file in order to be able to generate build information. Also, add the build-info goal to the executions of the spring-boot-maven-plugin in order to generate the information automatically during a build. See a previous post if you want to know more about the git-commit-id-plugin. XML <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>build-info</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>pl.project13.maven</groupId> <artifactId>git-commit-id-plugin</artifactId> <version>4.9.10</version> <executions> <execution> <id>get-the-git-infos</id> <goals> <goal>revision</goal> </goals> </execution> </executions> <configuration> <dotGitDirectory>${project.basedir}/.git</dotGitDirectory> <prefix>git</prefix> <verbose>false</verbose> <generateGitPropertiesFile>true</generateGitPropertiesFile> <generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename> <format>properties</format> <gitDescribe> <skip>false</skip> <always>false</always> <dirty>-dirty</dirty> </gitDescribe> </configuration> </plugin> </plugins> </build> Enable the full git information to the actuator endpoint in the application.properties. Properties files management.info.git.mode=full Add a Rest controller with two dummy endpoints. Java @RestController public class MetricsController { @GetMapping("/endPoint1") public String endPoint1() { return "Metrics for endPoint1"; } @GetMapping("/endPoint2") public String endPoint2() { return "Metrics for endPoint2"; } } Build the application. Shell $ mvn clean verify Run the application. Shell $ java -jar target/myostaraplanet-0.0.1-SNAPSHOT.jar Verify the endpoints. Shell $ curl http://localhost:8080/endPoint1 Metrics for endPoint1 $ curl http://localhost:8080/endPoint2 Metrics for endPoint2 Verify the actuator endpoint. Shell $ curl http://localhost:8080/actuator | python3 -mjson.tool ... { "_links": { "self": { "href": "http://localhost:8080/actuator", "templated": false }, "health": { "href": "http://localhost:8080/actuator/health", "templated": false }, "health-path": { "href": "http://localhost:8080/actuator/health/{*path}", "templated": true } } } Add Security The basics are in place now. However, it is not very secure. Let’s add authorization to the actuator endpoint. Beware that the setup in this paragraph is not intended for production usage. Add the Spring Security dependency to the pom. XML <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> Add the credentials and role to the application.properties file. Again, do not use this for production purposes. Properties files spring.security.user.name=admin spring.security.user.password=admin123 spring.security.user.roles=ADMIN Add a WebSecurity class, which adds the security layer to the actuator endpoint. Java @Configuration @EnableWebSecurity public class WebSecurity { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authz -> authz .requestMatchers("/actuator/**").hasRole("ADMIN") .anyRequest().permitAll()) .httpBasic(Customizer.withDefaults()); return http.build(); } } Build and start the application. Verify whether the actuator endpoint can be accessed using the credentials as specified. Shell $ curl http://localhost:8080/actuator -u "admin:admin123" | python3 -mjson.tool ... { "_links": { "self": { "href": "http://localhost:8080/actuator", "templated": false }, "health": { "href": "http://localhost:8080/actuator/health", "templated": false }, "health-path": { "href": "http://localhost:8080/actuator/health/{*path}", "templated": true } } } Install Ostara Navigate to the Ostara website and click the Download Ostara button. Choose the platform you are using (Linux 64bit, in my case), and the file Ostara-0.12.0.AppImage is downloaded. Double-click the file, and Ostara is started. That’s all! Monitor Application By default, only a limited set of actuator endpoints are enabled. Ostara will function with this limited set, but less information will be visible as a consequence. In order to see the full set of capabilities of Ostara, you enable all actuator endpoints. Again, beware of how much you expose in production. Properties files management.endpoints.web.exposure.include=* management.endpoint.health.show-details=always Before you continue using Ostara, you are advised to disable sending usage statistics and error information. Navigate to the settings (right top corner), choose Privacy, and disable the tracking options. In the left menu, choose Create Instance and fill in the fields as follows: Actuator URL Alias: MyFirstInstance Application Name: MyFirstApp Disable SSL Verification: Yes (for this demo, no SSL connection is used) Authentication Type: Basic Username and Password: the admin credentials Click the Test Connection button. This returns an unauthorized error, which appears to be a bug in Ostara because the credential information is correct. Ignore the error and click the Save button. Ostara can connect to the application, and the dashboard shows some basic status information. You can explore all the available information for yourself. Some of them are highlighted below. Info The Info page shows you the information which you made available with the help of the git-commit-id-plugin. App Properties The App Properties page shows you the application properties. However, as you can see in the below screenshot, all values are masked. This is the default Spring Boot 3 behavior. This behavior can be changed in application.properties of the Spring Boot Application. You can choose between always (not recommended), when-authorized or never. Properties files management.endpoint.configprops.show-values=when-authorized management.endpoint.env.show-values=when-authorized Build and start the application again. The values are visible. Metrics The Metrics page allows you to enable notifications for predefined or custom metrics. Open the http.server.requests metric and click the Add Metric Notification. Fill in the following in order to create a notification when EndPoint1 is invoked more than ten times, and click the Save button: Name: EndPoint 1 invoked > 10 times Type: Simple Tags: /endPoint1 Operation: Greater Than Value: 10 Invoke EndPoint1 more than ten times in a row. Wait for a minute, and the notification appears at the top of your main screen. Loggers The Loggers page shows you the available loggers, and you are able to change the desired log level. This is an interesting feature when you need to analyze a bug. Click the DEBUG button for the com.mydeveloperplanet.myostaraplanet.MetricsController. A message is shown that this operation is forbidden. The solution is to disable the csrf protection for the actuator endpoints. For more information about csrf attacks, see this blog. Add the following line to the WebSecurity class. Java public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authz -> authz .requestMatchers("/actuator/**").hasRole("ADMIN") .anyRequest().permitAll()) .csrf(csrf -> csrf .ignoringRequestMatchers("/actuator/**") ) .httpBasic(Customizer.withDefaults()); return http.build(); } Also, add some logging statements to the EndPoint1 code in order to verify the result. Java @RequestMapping("/endPoint1") public String endPoint1() { logger.debug("This is DEBUG message"); logger.trace("This is a TRACE message"); return "Metrics for endPoint1"; } Build and restart the application. Enable the DEBUG logging again for the MetricsController and invoke EndPoint1. The DEBUG statement is shown in the logs. Shell 2023-09-10T15:06:04.511+02:00 DEBUG 30167 --- [nio-8080-exec-8] c.m.myostaraplanet.MetricsController : This is DEBUG message Multiple Instances When you have multiple instances of your application, you can create another instance to monitor. Start another instance of the application on port 8081. Shell $ java -jar -Dserver.port=8081 target/myostaraplanet-0.0.1-SNAPSHOT.jar Hover over MyFirstApp and click the three dots menu. Choose Add Instance and fill in the following: Actuator URL Alias: MySecondInstance Clicking the Test Connection button is successful this time. Click the Save button. The second instance is added, and the application dashboard view shows the summary information. Conclusion Ostara is a good alternative for monitoring Spring Boot applications. The installation only requires you to download a file and start it. Ostara gives you a clear visual view, and the notifications notify you when something is wrong. It is also capable of starting thread profiling on your instance and downloading heap dumps. Compared to Grafana, Grafana has some more fancy graphs than Ostara. However, Ostara is not only a visualization tool; but you can also interact with your application and receive notifications when something goes wrong.
To log, or not to log? To log! Nowadays, we can’t even imagine a modern software system without logging subsystem implementation, because it’s the very basic tool of debugging and monitoring developers can’t be productive without. Once something gets broken or you just want to know what’s going on in the depths of your code execution, there’s almost no other way than just to implement a similar functionality. With distributed systems, and microservices architectures in particular, the situation gets even more complicated since each service can theoretically call any other service (or several of them at once), using either REST, gRPC, or asynchronous messaging (by means of numerous service buses, queues, brokers, and actor-based frameworks). Background processing goes there as well, resulting in entangled call chains we still want to have control over. In this article we will show you how to implement efficient distributed tracing in .NET quickly, avoiding the modification of low-level code as much as possible so that only generic tooling and base classes for each communication instrument are affected. Ambient Context Is The Core: Exploring The AsyncLocal Let’s start with the root which ensures the growth of our tree - that is, where the tracing information is stored. Because to log the tracing information, we need to store it somewhere and then get it somehow. Furthermore, this information should be available throughout the execution flow - this is exactly what we want to achieve. Thus, I’ve chosen to implement the ambient context pattern (you’re probably familiar with it from HttpContext): simply put, it provides global access to certain resources in the scope of execution flow. Though it’s sometimes considered an anti-pattern, in my opinion, the dependency injection concerns are a bit out of… scope (sorry for the pun), at least for a specific case where we don’t hold any business data. And .NET can help us with that, providing the AsyncLocal<T> class; as opposed to ThreadLocal<T>, which ensures data locality in the scope of a certain thread, AsyncLocal is used to hold data for tasks, which (as we know) can be executed in any thread. It’s worth mentioning that AsyncLocal works top down, so once you set the value at the start of the flow, it will be available for the rest of the ongoing flow as well, but if you change the value in the middle of the flow, it will be changed for the flow branch only; i.e., data locality will be preserved for each branch separately. If we look at the picture above, the following consequent use cases can be considered as examples: We set the AsyncLocal value as 0in the Root Task. If we don’t change it in the child tasks, it will be read as 0 in the child tasks’ branches as well. We set the AsyncLocalvalue as 1 in the Child Task 1. If we don’t change it in the Child Task 1.1, it will be read as 1 in the context of _Child Task 1 _and Child Task 1.1, but not in theRoot Task or Child Task 2 branch - they will keep 0. We set the AsyncLocal value as 2 in the Child Task 2. Similarly to #2, if we don’t change it in the Child Task 2.1, it will be read as 2 in the context of Child Task 2 and Child Task 2.1, but not in the Root Task or Child Task 1 branch - they will be 0 for Root Task, and 1 for Child Task 1 branch. We set the AsyncLocal value as 3 in the Child Task 1.1. This way, it will be read as 3 only in the context of Child Task 1.1, and not others’ - they will preserve previous values. We set the AsyncLocal value as 4 in the Child Task 2.1. This way, it will be read as 4 only in the context of Child Task 2.1, and not others’ - they will preserve previous values. OK, words are cheap: let’s get to the code! C# using Serilog; using System; using System.Threading; namespace DashDevs.Framework.ExecutionContext { /// /// Dash execution context uses to hold ambient context. /// IMPORTANT: works only top down, i.e. if you set a value in a child task, the parent task and other execution flow branches will NOT share the same context! /// That's why you should set needed properties as soon you have corresponding values for them. /// public static class DashExecutionContext { private static AsyncLocal _traceIdentifier = new AsyncLocal(); public static string? TraceIdentifier => _traceIdentifier.Value; /// /// Tries to set the trace identifier. /// /// Trace identifier. /// If existing trace ID should be replaced (set to true ONLY if you receive and handle traced entities in a constant context)! /// public static bool TrySetTraceIdentifier(string traceIdentifier, bool force = false) { return TrySetValue(nameof(TraceIdentifier), traceIdentifier, _traceIdentifier, string.IsNullOrEmpty, force); } private static bool TrySetValue( string contextPropertyName, T newValue, AsyncLocal ambientHolder, Func valueInvalidator, bool force) where T : IEquatable { if (newValue is null || newValue.Equals(default) || valueInvalidator.Invoke(newValue)) { return false; } var currentValue = ambientHolder.Value; if (force || currentValue is null || currentValue.Equals(default) || valueInvalidator.Invoke(currentValue)) { ambientHolder.Value = newValue; return true; } else if (!currentValue.Equals(newValue)) { Log.Error($"Tried to set different value for {contextPropertyName}, but it is already set for this execution flow - " + $"please, check the execution context logic! Current value: {currentValue} ; rejected value: {newValue}"); } return false; } } } Setting the trace ID is as simple as DashExecutionContext.TrySetTraceIdentifier(“yourTraceId”)with an optional value replacement option (we will talk about it later), and then you can access the value with DashExecutionContext.TraceIdentifier. We could implement this class to hold a dictionary as well; just in our case, it was enough (you can do this by yourself if needed, initializing a ConcurrentDictionary<TKey, TValue> for holding ambient context information with TValue being AsyncLocal). In the next section, we will enrich Serilog with trace ID values to be able to filter the logs and get complete information about specific call chains. Logging Made Easy With Serilog Dynamic Enrichment Serilog, being one of the most famous logging tools on the market (if not the most), comes with an enrichment concept - logs can include additional metadata of your choice by default, so you don’t need to set it for each write by yourself. While this piece of software already provides us with an existing LogContext, which is stated to be ambient, too, its disposable nature isn’t convenient to use and reduces the range of execution flows, while we need to process them in the widest range possible. So, how do we enrich logs with our tracing information? Among all the examples I’ve found that the enrichment was made using immutable values, so the initial plan was to implement a simple custom enricher quickly which would accept the delegate to get DashExecutionContext.TraceIdentifier value each time the log is written to reach our goal and log the flow-specific data. Fortunately, there’s already a community implementation of this feature, so we’ll just use it like this during logger configuration initialization: C# var loggerConfiguration = new LoggerConfiguration() ... .Enrich.WithDynamicProperty(“X-Dash-TraceIdentifier”, () => DashExecutionContext.TraceIdentifier) ... Yes, it's as simple as that - just a single line of code with a lambda, and all your logs now have a trace identifier! HTTP Headers With Trace IDs for ASP.NET Core REST API and GRPC The next move is to set the trace ID in the first place so that something valuable is shown in the logs. In this section, we will learn how to do this for REST API and gRPC communication layers, both server and client sides. Server Side: REST API For the server side, we can use custom middleware and populate our requests and responses with a trace ID header (don’t forget to configure your pipeline so that this middleware is the first one!). C# using DashDevs.Framework.ExecutionContext; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Serilog; using System.Threading.Tasks; namespace DashDevs.Framework.Middlewares { public class TracingMiddleware { private const string DashTraceIdentifier = "X-Dash-TraceIdentifier"; private readonly RequestDelegate _next; public TracingMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext httpContext) { if (httpContext.Request.Headers.TryGetValue(DashTraceIdentifier, out var traceId)) { httpContext.TraceIdentifier = traceId; DashExecutionContext.TrySetTraceIdentifier(traceId); } else { Log.Debug($"Setting the detached HTTP Trace Identifier for {nameof(DashExecutionContext)}, because the HTTP context misses {DashTraceIdentifier} header!"); DashExecutionContext.TrySetTraceIdentifier(httpContext.TraceIdentifier); } httpContext.Response.OnStarting(state => { var ctx = (HttpContext)state; ctx.Response.Headers.Add(DashTraceIdentifier, new[] { ctx.TraceIdentifier }); // there’s a reason not to use DashExecutionContext.TraceIdentifier value directly here return Task.CompletedTask; }, httpContext); await _next(httpContext); } } } Since the code is rather simple, we will stop only on a line where the response header is added. In our practice, we’ve faced a situation when in specific cases the response context was detached from the one we’d expected because of yet unknown reason, and thus the DashExecutionContext.TraceIdentifier value was null. Please, feel free to leave a comment if you know more - we’ll be glad to hear it! Client Side: REST API For REST API, your client is probably a handy library like Refit or RestEase. Not to add the header each time and produce unnecessary code, we can use an HttpMessageHandler implementation that fits the client of your choice. Here we’ll go with Refit and implement a DelegatingHandler for it. C# using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using DashDevs.Framework.ExecutionContext; namespace DashDevs.Framework.HttpMessageHandlers { public class TracingHttpMessageHandler : DelegatingHandler { private const string DashTraceIdentifier = "X-Dash-TraceIdentifier"; protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (!request.Headers.TryGetValues(DashTraceIdentifier, out var traceValues)) { var traceId = DashExecutionContext.TraceIdentifier; if (string.IsNullOrEmpty(traceId)) { traceId = Guid.NewGuid().ToString(); } request.Headers.Add(DashTraceIdentifier, traceId); } return await base.SendAsync(request, cancellationToken); } } } Then you just need to register this handler as a scoped service in the ConfigureServices method of your Startup class and finally add it to your client configuration as follows. C# public void ConfigureServices(IServiceCollection services) { ... services.AddScoped(); ... services.AddRefitClient(). ... .AddHttpMessageHandler(); ... } Server Side: gRPC For gRPC, the code is generated from Protobuf IDL (interface definition language) definitions, which can use interceptors for intermediate processing. For the server side, we’ll implement a corresponding one that checks the request headers for the trace ID header. C# using DashDevs.Framework.ExecutionContext; using Grpc.Core; using Grpc.Core.Interceptors; using System; using System.Linq; using System.Threading.Tasks; namespace DashDevs.Framework.gRPC.Interceptors { public class ServerTracingInterceptor : Interceptor { private const string DashTraceIdentifier = "X-Dash-TraceIdentifier"; public override Task UnaryServerHandler(TRequest request, ServerCallContext context, UnaryServerMethod continuation) { ProcessTracing(context); return continuation(request, context); } public override Task ClientStreamingServerHandler(IAsyncStreamReader requestStream, ServerCallContext context, ClientStreamingServerMethod continuation) { ProcessTracing(context); return continuation(requestStream, context); } public override Task ServerStreamingServerHandler(TRequest request, IServerStreamWriter responseStream, ServerCallContext context, ServerStreamingServerMethod continuation) { ProcessTracing(context); return continuation(request, responseStream, context); } public override Task DuplexStreamingServerHandler(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context, DuplexStreamingServerMethod continuation) { ProcessTracing(context); return continuation(requestStream, responseStream, context); } private void ProcessTracing(ServerCallContext context) { if (string.IsNullOrEmpty(DashExecutionContext.TraceIdentifier)) { var traceIdEntry = context.RequestHeaders.FirstOrDefault(m => m.Key == DashTraceIdentifier.ToLowerInvariant()); var traceId = traceIdEntry?.Value ?? Guid.NewGuid().ToString(); DashExecutionContext.TrySetTraceIdentifier(traceId); } } } } To make your server calls intercepted, you need to pass a new instance of the ServerTracingInterceptor to the ServerServiceDefinition.Intercept method. The ServerServiceDefinition, in turn, is obtained by a call of the BindService method of your generated service. The following example can be used as a starting point. C# ... var server = new Server { Services = { YourService.BindService(new YourServiceImpl()).Intercept(new ServerTracingInterceptor()) }, Ports = { new ServerPort("yourServiceHost", Port, ServerCredentials.Insecure) } }; server.Start(); ... Client Side: GRPC ChannelExtensions.Intercept extension method comes to the rescue here - we will call it after channel creation, but at first we’re to implement the interceptor itself in the form of Func like it’s shown below. C# using DashDevs.Framework.ExecutionContext; using Grpc.Core; using System; namespace DashDevs.Framework.gRPC.Interceptors { public static class ClientInterceptorFunctions { private const string DashTraceIdentifier = "X-Dash-TraceIdentifier"; public static Func TraceHeaderForwarder = (Metadata source) => { var traceId = DashExecutionContext.TraceIdentifier; if (string.IsNullOrEmpty(traceId)) { traceId = Guid.NewGuid().ToString(); } source.Add(DashTraceIdentifier, traceId); return source; }; } } The usage is quite simple: Create the Channel object with specific parameters. Create your client class object and pass the Intercept method result of a Channel from p.1 using the InterceptorFunctions.TraceHeaderForwarder as a parameter for the client class constructor instead of passing the original Channel instance instead. It can be achieved with the following code as an example: C# … var channel = new Channel("yourServiceHost:yourServicePort", ChannelCredentials.Insecure); var client = new YourService.YourServiceClient(channel.Intercept(ClientInterceptorFunctions.TraceHeaderForwarder)); ... Base Message Class vs. Framework Message Metadata in Asynchronous Communication Software The next question is how to pass the trace ID in various async communication software. Basically, one can choose to use either framework-related features to pass trace ID further or go in a more straightforward manner with a base message. Both have pros and cons: The base message approach is ideal for communication where no features are provided to store contextual data, and it’s the least error-prone overall due to simplicity. On the other hand, if you have already defined a set of messages, backward compatibility may break if you just add another field depending on the serialization mechanism (so if you are to go this way, it’s better to do this from the very beginning and consider among other infrastructure features during design sessions), not mentioning that it may affect much code, which is better to be avoided. Setting framework metadata, if available, is a better choice, because you can leave your message processing code as it is with just a minor improvement, which will be automatically applied to all messaging across the whole system. Also, some software may provide features for additional monitoring of this data (e.g., in the dashboard). Next, we will provide you with some real-world examples. Amazon SQS One of the most widely used message queues is Amazon Simple Queue Service. Fortunately, it provides message metadata (namely, message attributes) out of the box, so we will gladly use it. The first step is to add trace ID to messages we send, so you can do something like this. C# public async Task SendMessageAsync(T message, CancellationToken cancellationToken, string? messageDeduplicationId = null) { var amazonClient = new AmazonSQSClient(yourConfig); var messageBody = JsonSerializer.Serialize(message, yourJsonOptions); return await amazonClient.SendMessageAsync( new SendMessageRequest { QueueUrl = "yourQueueUrl", MessageBody = messageBody, MessageDeduplicationId = messageDeduplicationId, MessageAttributes = new Dictionary() { { "X-Dash-TraceIdentifier", new MessageAttributeValue() { DataType = "String", StringValue = DashExecutionContext.TraceIdentifier, } } } }, cancellationToken); } The second step is to read this trace ID in a receiver to be able to set it for ambient context and continue the same way. C# public async Task> GetMessagesAsync(int maxNumberOfMessages, CancellationToken token) { if (maxNumberOfMessages < 0) { throw new ArgumentOutOfRangeException(nameof(maxNumberOfMessages)); } var amazonClient = new AmazonSQSClient(yourConfig); var asyncMessage = await amazonClient.ReceiveMessageAsync( new ReceiveMessageRequest { QueueUrl = "yourQueueUrl", MaxNumberOfMessages = maxNumberOfMessages, WaitTimeSeconds = yourLongPollTimeout, MessageAttributeNames = new List() { "X-Dash-TraceIdentifier" }, }, token); return asyncMessage.Messages; } Important note (also applicable to other messaging platforms): If you read and handle messages in the background loop one by one (not several at once) and wait for the completion of each one, calling the DashExecutionContext.TrySetTraceIdentifier with trace ID from metadata before message handling method with your business logic, then the DashExecutionContext.TraceIdentifier value always lies in the same async context. That’s why in this case it’s essential to use the override option in the DashExecutionContext.TrySetTraceIdentifiereach time: it’s safe since only one message is processed at a time, so we don’t get a mess anyhow. Otherwise, the very first metadata trace ID will be used for all upcoming messages as well, which is wrong. But if you read and process your messages in batches, the simplest way is to add an intermediate async method where the DashExecutionContext.TrySetTraceIdentifier is called and separate message from a batch is processed, so that you preserve an execution flow context isolation (and therefore trace ID) for each message separately. In this case, the override is not needed. Microsoft Orleans Microsoft Orleans provides its own execution flow context out of the box, so it’s extremely easy to pass metadata by means of the static RequestContext.Set(string key, object value) method, and reading it in the receiver with a RequestContext.Get(string key). The behavior is similar to AsyncLocal we’ve already learned about; i.e., the original caller context always preserves the value that is projected to message receivers, and getting responses doesn’t imply any caller context metadata changes even if another value has been set on the other side. But how can we efficiently interlink it with other contexts we use? The answer lies within Grain call filters. So, at first, we will add the outgoing filter so that the trace ID is set for calls to other Grains (which is an actor definition in Orleans). C# using DashDevs.Framework.ExecutionContext; using Microsoft.AspNetCore.Http; using Orleans; using Orleans.Runtime; using System; using System.Threading.Tasks; namespace DashDevs.Framework.Orleans.Filters { public class OutgoingGrainTracingFilter : IOutgoingGrainCallFilter { private const string TraceIdentifierKey = "X-Dash-TraceIdentifier"; private const string IngorePrefix = "Orleans.Runtime"; public async Task Invoke(IOutgoingGrainCallContext context) { if (context.Grain.GetType().FullName.StartsWith(IngorePrefix)) { await context.Invoke(); return; } var traceId = DashExecutionContext.TraceIdentifier; if (string.IsNullOrEmpty(traceId)) { traceId = Guid.NewGuid().ToString(); } RequestContext.Set(TraceIdentifierKey, traceId); await context.Invoke(); } } } By default, the framework is constantly sending numerous service messages between specific actors, so it’s mandatory to move them out of our filters because they’re not subjects for tracing. Thus, we’ve introduced an ignore prefix so that these messages aren’t processed. Also, it’s worth mentioning that this filter is working for the pure client side, too. For example, if you’re calling an actor from the REST API controller by means of the Orleans cluster client, the trace ID will be passed from the REST API context further to the actors’ execution context and so on. Then we’ll continue with an incoming filter, where we get the trace ID from RequestContext and initialize our DashExecutionContext with it. The ignore prefix is used there, too. C# using DashDevs.Framework.ExecutionContext; using Orleans; using Orleans.Runtime; using System.Threading.Tasks; namespace DashDevs.Framework.Orleans.Filters { public class IncomingGrainTracingFilter : IIncomingGrainCallFilter { private const string TraceIdentifierKey = "X-Dash-TraceIdentifier"; private const string IngorePrefix = "Orleans.Runtime"; public async Task Invoke(IIncomingGrainCallContext context) { if (context.Grain.GetType().FullName.StartsWith(IngorePrefix)) { await context.Invoke(); return; } DashExecutionContext.TrySetTraceIdentifier(RequestContext.Get(TraceIdentifierKey).ToString()); await context.Invoke(); } } } Now let’s finish with our Silo (a Grain server definition in Orleans) host configuration to use the features we’ve already implemented, and we’re done here! C# var siloHostBuilder = new SiloHostBuilder(). ... .AddOutgoingGrainCallFilter() .AddIncomingGrainCallFilter() ... Background Processing Another piece of software you can use pretty often is a background jobs implementation. Here the concept itself prevents us from using a base data structure (which would look like an obvious workaround), and we’re going to review the features of Hangfire (the most famous background jobs software) which will help us to reach the goal of distributed tracing even for these kinds of execution units. Hangfire The feature which fits our goal most is the job filtering, implemented in the Attribute form. Thus, we need to define our own filtering attribute which will derive from the JobFilterAttribute, and implement the IClientFilter with IServerFilter interfaces. From the client side, we can access our DashExecutionContext.TraceIdentifier value, but not from the server. So, to be able to reach this value from the server context, we’ll pass our trace ID through the Job Parameter setting (worth mentioning that it’s not the parameter of a job method you write in your code, but a metadata handled by the framework). With this knowledge, let’s define our job filter. C# using DashDevs.Framework.ExecutionContext; using Hangfire.Client; using Hangfire.Common; using Hangfire.Server; using Hangfire.States; using Serilog; using System; namespace DashDevs.Framework.Hangfire.Filters { public class TraceJobFilterAttribute : JobFilterAttribute, IClientFilter, IServerFilter { private const string TraceParameter = "TraceIdentifier"; public void OnCreating(CreatingContext filterContext) { var traceId = GetParentTraceIdentifier(filterContext); if (string.IsNullOrEmpty(traceId)) { traceId = DashExecutionContext.TraceIdentifier; Log.Information($"{filterContext.Job.Type.Name} job {TraceParameter} parameter was not set in the parent job, " + "which means it's not a continuation"); } if (string.IsNullOrEmpty(traceId)) { traceId = Guid.NewGuid().ToString(); Log.Information($"{filterContext.Job.Type.Name} job {TraceParameter} parameter was not set in the {nameof(DashExecutionContext)} either. " + "Generated a new one."); } filterContext.SetJobParameter(TraceParameter, traceId); } public void OnPerforming(PerformingContext filterContext) { var traceId = SerializationHelper.Deserialize( filterContext.Connection.GetJobParameter(filterContext.BackgroundJob.Id, TraceParameter)); DashExecutionContext.TrySetTraceIdentifier(traceId!); } public void OnCreated(CreatedContext filterContext) { return; } public void OnPerformed(PerformedContext filterContext) { return; } private static string? GetParentTraceIdentifier(CreateContext filterContext) { if (!(filterContext.InitialState is AwaitingState awaitingState)) { return null; } var traceId = SerializationHelper.Deserialize( filterContext.Connection.GetJobParameter(awaitingState.ParentId, TraceParameter)); return traceId; } } } The specific case here is a continuation. If you don’t set the DashExecutionContext.TraceIdentifier, enqueue a regular job, and then specify a continuation. Then your continuations will not get the trace ID of a parent job. But in case you do set the DashExecutionContext.TraceIdentifier and then do the same, even though your continuations will share the same trace ID, in the particular case it may be considered as simple luck and a sort of coincidence, considering our job filter implementation and AsyncLocal principles. Thus, checking the parent is a must. Now, the final step is to register it globally so that it’s applied to all the jobs. C# GlobalJobFilters.Filters.Add(new TraceJobFilterAttribute()); Well, that’s it - your Hangfire jobs are now under control, too! By the way, you can compare this approach with the Correlate integration proposed by Hangfire docs. Summary In this article, we’ve tried to compose numerous practices and real-world examples for distributed tracing in .NET so that they can be used for most of the use cases in any software solution. We don’t cover automatic request/response and message logging directly here - it’s the simplest part of the story, so the implementation (i.e., if and where to add automatic request/response/message logging, and all other possible logs as well) should be made according to the specific needs. Also, in addition to tracing, this approach fits for any other data that you may need to pass across your system. As you can see, the DashExecutionContext class, relying on AsyncLocal features, plays the key role in transferring the trace identifier between different communication instruments in the scope of a single service, so it’s crucial to understand how it works. Other interlink implementations depend on the features of each piece of software and should be carefully reviewed to craft the best solution possible, which can be automatically applied to all incoming and outgoing calls without modifications to existing code. Thank you for reading!
Most applications in the real world will accumulate a large amount of features and code in the long run. Multi-module projects are a good approach to structuring the application without having to go down the complex path of microservices. The following five tips can help to better organize such Spring Boot projects in the long run. #1 Find a Proper Module Structure In general, the use of two modules, "base" and "web," is a good starting point for Spring Boot applications. The "base" module describes the basic setup, for example, database settings, and provides utility classes. Standards defined here then apply to all further modules. In "web," all modules are combined, and the executable application is built — our executable "fat jar." The structure of the intermediate modules should be less technical and more domain-driven. First, here is a negative example of partitioning an online store. Poor example of technical structuring While a purely technical separation is possible, it offers little advantage: for a developer to perform a typical task ("add a filter to the search"), all the artifacts involved must be gathered from multiple modules. With a domain-driven subdivision, however, customization would be limited to one module for many tasks. Good example of domain-driven modularization When the application is extended over time, it is easy to add more modules with a domain-driven structure, e.g., "blog." With a technical separation, this makes little sense, and at some point, you will end up with a big ball of mud. #2 Minimize Dependencies Each module should contain all artifacts that it needs to provide its own functionality in order to minimize dependencies on other modules. This includes classes, templates, dependencies, resources, and so on. An interesting question is whether the ORM layer should be stored in a central module or split among the respective modules. Nowadays, the support for separation is very good, so that actually nothing speaks against it. Each module can contain its own JPA entities and Flyway / Liquibase changelogs. When running an integration test within a module, a partial database schema would be created based on the current and all referenced modules. A useful library that supports separating the modules is Springify Multiconfig. It allows each module to contain its own application-mc-xx.yml file to store specific configuration of that module. Without this library, the entire config must be in the application.yml of the "base" module. #3 Continuous Improvement A bad architecture is better than none, but the best structure of the modules is never clear from the beginning. Since all dependencies are within one application, the partitioning can be adjusted with a single pull request if necessary. With microservices, an extensive, multi-stage procedure would usually be required. It is always worthwhile to question the current status and further improve it in the long term. #4 Gradle API vs. Implementation While developing the code of a module, the directly and indirectly referenced modules of the current project are always available. When external libraries are declared as a dependency, there are two possibilities in Gradle: with api (formerly compile) the library is also visible indirectly, whereas with implementation a library is only available within the current module. In other words, if a module is referenced and a library there is included with implementation, it will be hidden and cannot be used in the code. In the final app, all dependencies end up in the single "fat jar" again, but during the build process, invalid accesses would already be prevented. Following this approach, the libraries available during development should remain cleaner. #5 Use Separate Test Jars Each module has its own tests to validate the functionality it contains. Sometimes, however, test data is created, or utility methods are provided that are needed in the modules that build on top of them — for example, to create test users. In general, classes and data that are only needed for tests should not be contained in the application that is later running in production. For Maven, Gradle, and Kotlin Script, there is a way to create separate test jars for each module. These can then be referenced specifically for the test code. More backgrounds on the implementation are here for Maven and here for Gradle. Conclusion Multi-module projects are getting increased attention right now, as microservices come with many challenges in practice, and the extra effort is only really worth it for larger development teams. By following best practices, well-structured applications can be developed for the real world. If later on you want to switch to microservices, individual modules can be broken out as standalone apps.
ML.NET is a cross-platform, open-source machine learning framework for .NET developers. For those just starting in machine learning, understanding ML.NET's catalogs is essential. It provides various "catalogs," which are collections of algorithms and transformations for different types of machine learning tasks. Each catalog in ML.NET is designed for specific types of machine learning tasks, offering various algorithms and methods suitable for those tasks. ML.NET Catalogs and Use Cases Here's an overview of some of the key catalogs in ML.NET and examples of real-world scenarios where they might be used: 1. Data Loading and Transformation Purpose: To load, transform, and manipulate data Use case: Preprocessing health records data to normalize and encode it before feeding it into a predictive model Library: Microsoft.ML.Data 2. Binary Classification Purpose: For tasks where you predict one of two outcomes Use case: Screening health records to identify patients at risk for developing diabetes: the ML.NET model analyzes patient data, such as age, weight, blood pressure, and family history, to classify each patient as either "at risk" or "not at risk" for diabetes. This enables healthcare providers to offer targeted lifestyle advice or preventive treatments to those identified as high-risk, potentially preventing the onset of the disease, or simply predicting whether an email is spam or not based on its content. Library: Microsoft.ML.Trainers.FastTree 3. Multiclass Classification Purpose: For tasks where you predict one outcome from more than two possible outcomes Use case: Categorizing news articles into predefined topics like sports, politics, or technology Library: Microsoft.ML.Trainers.LbfgsMaximumEntropy 4. Regression Purpose: To predict a continuous value Use case: Estimating the price of a house based on features like size, location, and age Library: Microsoft.ML.Trainers.FastTree 5. Clustering Purpose: For grouping similar items together Use case: Segmenting customers into different groups based on purchasing behavior for targeted marketing Library: Microsoft.ML.Trainers.KMeans 6. Anomaly Detection Purpose: For identifying unusual data points or events Use case: Detecting fraudulent transactions in a credit card transaction dataset or detecting any sudden spikes or drops in blood sugar/pressure levels that deviate from the patient's typical pattern Library: Microsoft.ML.Trainers.RandomizedPca 7. Ranking Purpose: For tasks where you want to rank items in a certain order Use case: Prioritizing patient waitlists in a hospital or clinic based on the urgency of medical conditions by analyzing patient data, including symptoms, medical history, and severity of conditions: by accurately ranking patients, the system ensures that those in most need of urgent care are attended to first, optimizing the allocation of medical resources and improving patient care efficiency. Library: Microsoft.ML.Trainers.FastTree 8. Recommendation Purpose: For recommending items to users Use case: Suggesting products to a user on an e-commerce website based on their browsing history or it can recommend personalized patient care plans in a hospital or clinic Library: Microsoft.ML.Trainers.MatrixFactorization 9. Time Series and Sequence Prediction Purpose: For predicting future values in a time series Use case: Forecasting stock prices or electricity demand based on historical data or in an intensive care unit to forecast patients' critical events or health deteriorations hours in advance, allowing medical staff to step in proactively, saving lives and improving patient outcomes Library: Microsoft.ML.TimeSeries 10. Text Analytics and Natural Language Processing Purpose: For analyzing and processing textual data Use case: Sentiment analysis on customer reviews to gauge overall customer satisfaction Library: Microsoft.ML.Transforms.Text 11. Image Classification and Object Detection Purpose: For tasks related to image processing Use case: Identifying and classifying defects in manufacturing products using images from an assembly line or it can help radiologists in the analysis of medical imaging, such as MRI or CT scans to identify potential areas of concern, like tumors or fractures, and to find their locations and sizes within the scans Library: Microsoft.ML.Vision 12. Model Explainability Purpose: To understand and interpret model decisions Use case: Justifying why a particular loan or job application was approved/accepted or rejected by a predictive model Library: Microsoft.ML.Model.OnnxConverter Each catalog in ML.NET provides specific algorithms and methods tailored for these types of tasks, helping developers implement machine learning in their applications effectively.
This is the second and final part in our exploration of must-know OOP patterns and covers the composite bridge pattern, iterator pattern, and lock design pattern. Find part one here, covering extension, singleton, exception shielding, and object pool patterns. Object-oriented design is a fundamental part of modern software engineering that all developers need to understand. Software design patterns like object-oriented design serve as universally applicable solutions to common problems. However, if you don’t have much experience with these object-oriented patterns, you can fall into suboptimal, ad-hoc solutions that violate key software engineering principles like code reusability and separation of concerns. On the other hand, misuse and overuse can result in a tangled, overly complex codebase that’s hard to understand and navigate. In this article, we’ll explore our final three must-know object-oriented programming patterns (composite bridge, iterator, and lock) and show how to use these design patterns in your software development. With an array of example programming languages, we’ll show how to apply the composite bridge, iterator, and lock patterns effectively, compare them to ad hoc solutions, and demonstrate some common antipatterns that result from misuse or overuse. Composite Bridge The Composite Bridge pattern is a combination of two object-oriented design patterns (Composite and Bridge), and each has distinct benefits in designing flexible, decoupled, and reusable code. The Bridge pattern separates an abstraction from its implementation, allowing both to evolve independently. This is useful when an abstraction is going to be implemented in several distinct ways, and you want to keep your codebase adaptable to future changes. The Composite pattern, on the other hand, allows you to treat a group of objects as a single instance of the object itself, simplifying the interaction with collections of objects. This pattern is particularly useful when you want to apply the same operations over a group of similar kinds of objects using the same piece of code. However, at times, simpler constructs like basic inheritance might be a better choice. For example, implementing interfaces may not always be the best approach. If you only need to work with a single object, calling the method directly is a more straightforward and understandable solution. Without C# public void Log(Exception exception) { raygunClient.Send(exception); fileLogger.WriteException(exception); … dbLogger.InsertException(exception); } The code snippet (in C#) above represents a method for logging exceptions that utilizes multiple logging systems: Raygun, file logging, and a database logger. However, it directly calls each logging mechanism inside the Log function. This approach is not only monolithic but also rigid and tightly coupled. It means every time a new logging mechanism is added or removed, the Log method needs to be altered. In this setup, the Log method must be made directly aware of all the different logging mechanisms. So, the Log method and the individual logging systems are tightly coupled. If you wanted to add another logger, you’d need to modify the Log method to incorporate it. Similarly, if a logging system needed to be removed or replaced, you’d have to alter the Log method. This is inflexible, makes the system harder to maintain, and goes against the design principle of separation of concerns. Plus, this direct method calling approach doesn’t promote code reusability. If a different part of your application needed to use the same group of loggers, you would have to duplicate this code. This can lead to issues with code maintenance and consistency across your application. With The above code lacks the flexibility and reusability of decoupled design patterns like the Composite Bridge. Instead, we introduce an ILogger interface which exposes a Log method. This interface acts as an abstraction for our logging system, following the Bridge design pattern. Any class that implements this interface promises to provide a Log function, effectively creating a bridge between the generic logging operation (Log) and its specific implementation (_raygunClient.Send in RaygunLogger). Then, we have a RaygunLogger class that implements the ILogger interface, providing an actual implementation for logging exceptions. This class encapsulates the logging details for the Raygun system, making the concrete implementation invisible to other parts of the system. We can also create other specific loggers, like a FileLogger or DbLogger, each implementing the ILogger interface and providing their unique logging implementations. The ApplicationLogger class uses the Composite design pattern to treat a group of ILogger objects (_loggers) as a single ILogger. This means we can add as many loggers as we need to the ApplicationLogger, and the operation will be delegated to each logger automatically. The ApplicationLogger doesn’t need to know the specifics of each, just that they will handle the Log method. This arrangement is highly flexible. To add, remove or replace a logging system, you just need to manipulate the _loggers list in the ApplicationLogger, with no need to alter any other code. The Bridge pattern ensures each logger can evolve independently, while the Composite pattern lets us handle multiple loggers transparently with a single piece of code. This decoupled and extensible design makes your logging system much easier to maintain and evolve over time. C# public interface ILogger { void Log(Exception exception); } public class RaygunLogger : ILogger /*Bridge pattern*/ { private RaygunClient _raygunClient; public RaygunLogger(string apiKey) { _raygunClient = new RaygunClient(apiKey); } public void Log(Exception exception) { _raygunClient.Send(exception); /*Bridges Log to Send*/ } } public class ApplicationLogger /*Composite pattern*/ { private List<ILogger> _loggers; /*Store different types of loggers*/ public ApplicationLogger() { _loggers = new List<ILogger>(); } public void AddLogger(ILogger logger) { _loggers.Add(logger); } public void Log(Exception exception) { foreach (var logger in _loggers) { logger.Log(exception); /*Send to all different loggers*/ } } } Antipattern The flip side is that these patterns tend to be abused, and often, developers keep introducing unnecessary abstractions. We don’t need to add this many layers to just log into the console, assuming that this is the only thing required in the following application: C# public interface IWriter { void Write(string message); } public class ConsoleWriter : IWriter { public void Write(string message) { Console.WriteLine(message); } } public class CompositeWriter { private List<IWriter> _writers; public CompositeWriter() { _writers = new List<IWriter>(); } public void AddWriter(IWriter writer) { _writers.Add(writer); } public void Write(string message) { foreach (var writer in _writers) { writer.Write(message); } } } class Program { static void Main(string[] args) { CompositeWriter writer = new CompositeWriter(); writer.AddWriter(new ConsoleWriter()); writer.Write("Hello, World!"); } } Instead Rather, just call the method directly in such cases: class Program { static void Main(string[] args) { Console.WriteLine("Hello, World!"); } } Iterator The iterator pattern offers a consistent way to traverse the elements of a collection or an aggregate object without exposing the internal details of the collection itself. This pattern is often used in conjunction with the Composite pattern to traverse a complex tree-like structure. It allows processing items in a sequence without needing to understand or handle the complexities of the collection’s underlying data structure. This can lead to cleaner and more readable code. However, iterator comes with caveats. In some cases, using an iterator can reveal too much about the underlying structure of the data, making it harder to change the data structure in the future without also changing the clients that use the iterator. This can limit the reusability of the code. Furthermore, multi-threaded applications can face issues with the iterator pattern. If one thread is iterating through a collection while another thread modifies the collection, this can lead to inconsistent states or even exceptions. So, we have to carefully synchronize access to the collection to prevent such issues, often at the cost of performance. Without The following Python code employs a traditional approach to iterate over the ‘index’ list, which holds the indices of ‘data’ list elements in the desired order. It then prints the ‘data’ elements according to these indices using a while loop. The implementation is straightforward but breaks encapsulation and decouples data that should be kept together, making it error-prone when reused or maintained. C# data = ['a', 'b', 'c', 'd', 'e'] index = [3, 0, 4, 1, 2] i = 0 while i < len(index): print(data[index[i]]) i += 1 With On the other hand, the following improved design leverages the iterator pattern to achieve the same goal but in a more elegant and Pythonic way. Here, an IndexIterator class is defined, which takes the ‘data’ and ‘index’ lists as parameters in its constructor. It implements the Python’s iterator protocol by providing iter() and next() methods. The iter() method simply returns the instance itself, allowing the class to be used in for-each loops. The next() method retrieves the next item in the ‘index’ list, uses this to get the corresponding item from the ‘data’ list, and then increments the current position. If the end of the ‘index’ list is reached, it raises the StopIteration exception, which signifies the end of iteration to the for-each loop. Finally, an instance of IndexIterator is created with ‘data’ and ‘index’ as parameters, and a for-each loop is used to iterate over the items. This makes the code cleaner and the iteration process more transparent, showcasing the power and utility of the iterator pattern. C# class IndexIterator: def __init__(self, data, index): self.data = data self.index = index self.current = 0 def __iter__(self): return self def __next__(self): if self.current < len(self.index): result = self.data[self.index[self.current]] self.current += 1 return result else: raise StopIteration C# data = ['a', 'b', 'c', 'd', 'e'] index = [3, 0, 4, 1, 2] for item in IndexIterator(data, index): print(item) Antipattern The iterator can, of course, be misused. For example, the lack of true encapsulation in Python allows direct modifications of the ‘index’ list in the iterator after its creation. This compromises the state of the iterator because the ‘current’ pointer doesn’t get reset. As a result, the iterator’s behavior becomes unpredictable and inconsistent. The engineer might expect that after reversing the index list, the iterator would start from the beginning of the newly ordered list. However, due to the previously advanced ‘current’ pointer, it instead points to the second last element of the revised list. C# iterator = IndexIterator(data, index) # Display the first item print(next(iterator)) # Misuse the iterator by changing the index list directly # Remember, Python does not offer encapsulation with private fields iterator.index.reverse() # The behavior of the iterator has been compromised now; it will the second-last item, not the first in reverse print(next(iterator)) Instead The code below corrects this by encapsulating the reverse operation within the IndexIterator class. A reverse method is added that not only reverses the order of the ‘index’ list but also resets the ‘current’ pointer to the beginning of the list. This ensures the iterator’s state remains consistent after the reverse operation. In the revised code, the developer creates an IndexIterator instance, retrieves the first item, reverses the ‘index’ list using the encapsulated reverse method, and then retrieves the next item. This time, the iterator works as expected, proving the advantage of the iterator pattern in preserving the iterator’s internal state and protecting it from unintended modifications. C# class IndexIterator: … def reverse(self): self.index.reverse() self.current = 0 C# # Create an iterator object iterator = IndexIterator(data, index) print(next(iterator)) # The encapsulated method correctly modifies the state of the iterator iterator.reverse() # Now, indeed the first item in reverse is displayed print(next(iterator)) Lock The Lock design pattern is a crucial element in multi-threaded programming that helps maintain the integrity of shared resources across multiple threads. It serves as a gatekeeper, allowing only one thread at a time to access or modify a particular resource. When a thread acquires a lock on a resource, it effectively prevents other threads from accessing or modifying it until the lock is released. This exclusivity ensures that concurrent operations don’t lead to inconsistent or unpredictable states of the shared resource (commonly referred to as data races or race conditions). However, improper use of the Lock design pattern can lead to a variety of problems, with deadlocks being one of the most notorious. Deadlocks occur when two or more threads indefinitely wait for each other to release a lock, effectively freezing the application. For example, if thread A holds a lock that thread B needs and thread B, in turn, holds a lock that thread A needs, neither thread can proceed, leading to a deadlock. So, it’s essential to design your locking strategies carefully. To mitigate these risks, one common strategy is to implement try-locking with timeouts. In this approach, a thread will try to acquire a lock, and if unsuccessful, it will wait for a specified timeout period before retrying. This method prevents a thread from being indefinitely blocked if it can’t immediately acquire a lock. Another strategy is to carefully order the acquisition and release of locks to prevent circular waiting. Despite the potential for these complexities, the Lock design pattern is a powerful tool for ensuring thread safety in concurrent programming, but it shouldn’t be overused. Without In the Ruby on Rails application code below, we’re dealing with a user login system where users receive a bonus on their first login of the year. The grant_bonus method is used to check whether it’s the user’s first login this year and, if so, grants a bonus by updating their balance. However, this approach is susceptible to a race condition, known as a check-then-act scenario. If two requests for the same user occur simultaneously, they could both pass the first_login_this_year? check, leading to granting the bonus twice. We need a locking mechanism to ensure the atomicity of the grant_bonus operation. C# # user.rb (User model) class User < ApplicationRecord def first_login_this_year? last_login_at.nil? || last_login_at.year < Time.zone.now.year end def grant_bonus if first_login_this_year? update(last_login_at: Time.zone.now) bonus = 50 update(balance: balance + bonus) # Add bonus to the user's balance else bonus = 0 end end end # sessions_controller.rb class SessionsController < ApplicationController def login user = User.find_by(email: params[:email]) if user && user.authenticate(params[:password]) bonus = user.grant_bonus render json: { message: "Login successful! Bonus: $#{bonus}. New balance: $#{user.balance}" } else render json: { message: "Invalid credentials" }, status: :unauthorized end end end With To remedy the race condition, the updated code employs a locking mechanism provided by ActiveRecord’s transaction method. It opens a database transaction, and within it, the reload(lock: true) line is used to fetch the latest user record from the database and lock it, ensuring that no other operations can modify it concurrently. If another request attempts to grant a bonus to the same user simultaneously, it will have to wait until the first transaction is complete, preventing the double bonus issue. By encapsulating the check-then-act sequence in a transaction, we maintain the atomicity of the operation. The term ‘atomic’ here means that the operation will be executed as a single, unbroken unit without interference from other operations. If the transaction succeeds, the user’s last login date is updated, the bonus is added to their balance, and the updated balance is safely committed to the database. If the transaction fails at any point, none of the changes are applied, ensuring the data integrity. C# # user.rb (User model) class User < ApplicationRecord def first_login_this_year? last_login_at.nil? || last_login_at.year < Time.zone.now.year end def grant_bonus self.transaction do reload(lock: true) if first_login_this_year? update(last_login_at: Time.zone.now) bonus = 50 update(balance: balance + bonus) # Add bonus to the user's balance else bonus = 0 end end end end Antipattern 1: Cyclical Lock Allocation A common lock antipattern and pitfall in multi-threading involves a cyclical lock allocation. The controller locks product1 and then product2. If two requests simultaneously attempt to compare product1 and product2, but in opposite orders, a deadlock may occur. The first request locks product1 and then tries to lock product2, which is locked by the second request. The second request, meanwhile, is waiting for product1 to be unlocked by the first request, resulting in a cyclic wait scenario where each request is waiting for the other to release a lock. C# Rails.application.routes.draw do get '/product/:id1/other/:id2', to: 'products#compare' end class ProductsController < ApplicationController def compare product1 = Product.find(params[:id1]) product1.mutex.lock() product2 = Product.find(params[:id2]) product2.mutex.lock() # compare_product might throw an exception results = compare_product(product1, product2) product2.mutex.unlock() product1.mutex.unlock() render json: {message: results} end end Instead This example demonstrates a better approach using try_lock, a non-blocking method for acquiring a lock. If the lock is unavailable, it will not block execution and instead returns false immediately. This can prevent deadlocks and provide an opportunity to handle the scenario when a lock can’t be acquired. Even better, the revised example includes a timeout for acquiring the second lock. If the lock cannot be acquired within the specified timeout, the code catches the Timeout error and logs it using Raygun’s error tracking. This additional exception handling further safeguards against deadlocks by setting an upper limit on how long a thread will wait for a lock before it gives up. Finally, in both lock acquisition scenarios, the code structure makes use of Ruby’s begin-ensure-end construct to ensure that once a lock is acquired, it will always be released, even if an exception occurs during the execution of the critical section. This is an essential part of using locks to avoid leaving resources locked indefinitely due to unexpected errors. C# # Try acquiring lock for product1 if product1.mutex.try_lock begin # Successfully acquired lock for product1 # Now, try acquiring lock for product2 with a timeout of 5 seconds if product2.mutex.try_lock(5) begin # Successfully acquired lock for both product1 and product2 # Perform the critical section operations ensure product2.mutex.unlock end else # Failed to acquire lock for product2 within 5 seconds # Handle the timeout situation Raygun.track_exception(Timeout::Error.new('Lock timeout occurred on second product\'s lock'), custom_data: { product_ids: [product1.id, product2.id] }) end ensure product1.mutex.unlock end else # Failed to acquire lock for product1 # Handle the situation where the lock cannot be acquired immediately Raygun.track_exception(Timeout::Error.new('Lock timeout occurred on first product\'s lock'), custom_data: { product_ids: [product1.id, product2.id] }) end Antipattern 2: Removing Locks Improper lock removal, often as a misguided attempt at boosting performance, is a common antipattern in concurrent environments. Locks help preserve data integrity, preventing unpredictable outcomes from race conditions, and overzealous or premature lock removal can spur these very conditions. While managing locks might introduce some overhead, they are crucial for ensuring data consistency. Remove locks with caution and back with thorough testing. Instead of arbitrary lock removal, utilize performance monitoring tools like Raygun APM to pinpoint performance bottlenecks and guide optimization efforts. Wrap-up In this two-part exploration, we’ve dived into key design patterns, going deep on extension, singleton, exception shielding, object pool, composite bridge, iterator, and lock. These patterns provide robust and versatile solutions to common challenges. Done right, they can help you adhere to principles of code reusability, separation of concerns, and overall software engineering principles. However, it’s absolutely critical to be disciplined about when these patterns are implemented. Misuse or over-application can lead to confusion and dysfunction instead of simplicity and clarity. With consistent good habits, you’ll get a strong sense of when a pattern adds value and when it might obscure the essence of the code. The key is to strike a balance between robust design patterns and clean, simple code, leading to more efficient and resilient software development. Happy coding!
As most technologies or dependencies evolve fast, it's sometimes hard to make the initial setup or upgrade smoothly. The goal of this article is to provide a summary of the Maven setup for the Querydsl framework, depending on the used technology. After that, let's see a short overview of the Querydsl solution. In This Article, You Will Learn How to setup Querydsl with Spring Boot 2.x (i.e Java EE) and Spring Boot 3.x (i.e. Jakarta EE) What is a Maven classifier How is the Maven classifier used in Querydsl build Usage of Eclipse Transformer Plugin Querydsl Setup There are several possibilities to set up Querydsl framework in a Spring Boot application. The correct approach depends on the technologies used. Before we get into it, let's start with the recommended official setup. Official Setup Querydsl framework has a nice documentation site. The Maven integration is described in Chapter 2.1.1 where the recommended setup is based on the following: querydsl-jpa and querydsl-apt dependencies and usage of apt-maven-plugin plugin. The querydsl-apt dependency isn't mentioned on the official site, but such dependency is needed for the generation of metadata Q classes (see Metadata article). If we don't use querydsl-apt dependency then we get the error like this: Plain Text [INFO] --- apt:1.1.3:process (default) @ sat-jpa --- error: Annotation processor 'com.querydsl.apt.jpa.JPAAnnotationProcessor' not found 1 error The full working Maven setup based on the official recommendation is like this: XML <dependencies> ... <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> </dependency> ... </dependencies> <build> <plugins> <plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources/java</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> </dependency> </dependencies> </plugin> </plugins> </build> </project> This setup is working, and the Maven build is successful (see the log below). Unfortunately, several errors can be found there. In our case, the logs contain e.g. error: cannot find symbol import static com.github.aha.sat.jpa.city.City_.COUNTRY. Plain Text [INFO] ---------------------< com.github.aha.sat:sat-jpa >--------------------- [INFO] Building sat-jpa 0.5.2-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ sat-jpa --- [INFO] Deleting <local_path>\sat-jpa\target [INFO] [INFO] --- apt:1.1.3:process (default) @ sat-jpa --- <local_path>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityRepository.java:3: error: cannot find symbol import static com.github.aha.sat.jpa.city.City_.COUNTRY; ^ symbol: class City_ location: package com.github.aha.sat.jpa.city <local_path>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityRepository.java:3: error: static import only from classes and interfaces import static com.github.aha.sat.jpa.city.City_.COUNTRY; ^ <local_path>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityRepository.java:4: error: cannot find symbol import static com.github.aha.sat.jpa.city.City_.NAME; ^ symbol: class City_ location: package com.github.aha.sat.jpa.city ... 19 errors [INFO] [INFO] --- resources:3.2.0:resources (default-resources) @ sat-jpa --- ... [INFO] [INFO] Results: [INFO] [INFO] Tests run: 51, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] --- jar:3.2.2:jar (default-jar) @ sat-jpa --- [INFO] Building jar: <local_path>\sat-jpa\target\sat-jpa.jar [INFO] [INFO] --- spring-boot:2.7.5:repackage (repackage) @ sat-jpa --- [INFO] Replacing main artifact with repackaged archive [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 15.680 s [INFO] Finished at: 2023-09-20T08:43:59+02:00 [INFO] ------------------------------------------------------------------------ Let's focus on how to fix this issue in the next parts. Setup for Java EE With Spring Boot 2.x Once I found this StackOverflow issue, I realized that the querydsl-apt plugin is no longer needed. The trick lies in using querydsl-apt a dependency with a jpa classifier instead of using the apt-maven-pluginplugin. Note: the querydsl-apt plugin seems to be deprecated since Querydsl 3 (see the following). With that, the simplified Maven setup looks like this: XML <dependencies> ... <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> <classifier>jpa</classifier> <scope>provided</scope> </dependency> ... </dependencies> Note: Once we specify the classifier, we also need to specify a version of the dependency. Therefore, we cannot rely on the version defined in Spring Boot anymore. The logs from the Maven build are clean now. Plain Text [INFO] Scanning for projects... [INFO] [INFO] ---------------------< com.github.aha.sat:sat-jpa >--------------------- [INFO] Building sat-jpa 0.5.2-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ sat-jpa --- [INFO] Deleting <local_path>\sat-jpa\target [INFO] [INFO] --- resources:3.2.0:resources (default-resources) @ sat-jpa --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Using 'UTF-8' encoding to copy filtered properties files. [INFO] Copying 1 resource [INFO] Copying 2 resources ... [INFO] [INFO] --- jar:3.2.2:jar (default-jar) @ sat-jpa --- [INFO] Building jar: <local_path>\sat-jpa\target\sat-jpa.jar [INFO] [INFO] --- spring-boot:2.7.5:repackage (repackage) @ sat-jpa --- [INFO] Replacing main artifact with repackaged archive [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 15.148 s [INFO] Finished at: 2023-09-20T08:56:42+02:00 [INFO] ------------------------------------------------------------------------ Setup for Jakarta With Spring Boot 3.x As Spring Boot 3 relies on Jakarta instead of Java EE specification, we need to adjust our Maven setup a little bit. The change is described in Upgrade to Spring Boot 3.0. This article is based on this. Basically, we just need to use jakarta classifier instead of jpa classifier. XML <dependencies> ... <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>${querydsl.version}</version> <classifier>jakarta</classifier> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> <classifier>jakarta</classifier> <scope>provided</scope> </dependency> ... </dependencies> The maven build output is the same as in the Java EE setup. Maven Classifiers Let's shed light on the Querydsl solution once we understand Maven classifier usage. Querydsl Solution Querydsl generates metadata Q classes for every entity in order to be able to write queries easily. The querydsl-apt dependency achieves it with an instance of JPAAnnotationProcessor (see e.g., Java Annotation Processing and Creating a Builder for more details on annotation processing). The exact implementation of the annotation processor depends on the technology used. The desired processor is defined in javax.annotation.processing.Processor file located in the used querydsl-apt dependency. The content of this file has to have the full classpath to the desired annotation processor, e.g. com.querydsl.apt.jpa.JPAAnnotationProcessor. Let's go back to the classifiers for a while. Querydsl supports several classifiers (e.g., JPA, jdo, roo, etc.), and each of them needs a different treatment based on the used technology. Therefore, Querydsl needs to specify the supported annotations for each technology. For JPA, Querydsl supports these classifiers: jpa classifier for the old Java EE (with javax.persistence package) and jakarta classifier for a new Jakarta EE (with jakarta.persistence package) as you already know. Purpose of Maven Classifier The purpose of the Maven classifier is explained on the official site as follows: The classifier distinguishes artifacts that were built from the same POM but differ in content. It is some optional and arbitrary string that — if present — is appended to the artifact name just after the version number. As a motivation for this element, consider for example a project that offers an artifact targeting Java 11 but at the same time also an artifact that still supports Java 1.8. The first artifact could be equipped with the classifier jdk11 and the second one with jdk8 such that clients can choose which one to use. Please check, e.g., this guide for more information about the Maven classifier usage. In our case, all the available classifiers for querydsl-apt dependency are depicted below. They can also be listed online here. Similarly, you can also see all the classifiers for querydsl-jpa dependency here. When com.querydsl.apt.jpa.JPAAnnotationProcessor class is de-compiled from querydsl-apt-5.0.0-jpa.jar and querydsl-apt-5.0.0-jakarta.jar dependencies then we can see the only difference (see depicted below) is in the used imports (see lines 5-11). As a result, the JPAAnnotationProcessor is capable of handling different annotations in our classes (see lines 16-20). Use of Maven Classifier All Maven classifiers supported by Querydsl are defined in descriptors element specified in pom.xml file (see lines 11-20) as: XML <plugin> <artifactId>maven-assembly-plugin</artifactId> <executions> <execution> <id>apt-jars</id> <goals> <goal>single</goal> </goals> <phase>package</phase> <configuration> <descriptors> <descriptor>src/main/general.xml</descriptor> <descriptor>src/main/hibernate.xml</descriptor> <descriptor>src/main/jdo.xml</descriptor> <descriptor>src/main/jpa.xml</descriptor> <descriptor>src/main/jakarta.xml</descriptor> <descriptor>src/main/morphia.xml</descriptor> <descriptor>src/main/roo.xml</descriptor> <descriptor>src/main/onejar.xml</descriptor> </descriptors> <outputDirectory>${project.build.directory}</outputDirectory> </configuration> </execution> </executions> </plugin> This configuration is used in order to build multiple JARs defined by descriptors (see above). Each descriptor defines all the specifics for the technology. Usually, the XML descriptor just specifies the source folder (see line 11 in jpa.xml). XML <assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"> <id>jpa</id> <formats> <format>jar</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <fileSets> <fileSet> <directory>src/apt/jpa</directory> <outputDirectory>/</outputDirectory> </fileSet> <fileSet> <directory>${project.build.outputDirectory}</directory> <outputDirectory>/</outputDirectory> </fileSet> </fileSets> </assembly> However, the definition of Jakarta EE is a little bit more complicated. The key part in jakarta.xml is unpacking of JAR (see line 16) and using the jakarta classifier (see line 18) in order to activate the Eclipse Transformer Plugin. XML <assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"> <id>jakarta</id> <formats> <format>jar</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <moduleSets> <moduleSet> <useAllReactorProjects>true</useAllReactorProjects> <includes> <include>${project.groupId}:${project.artifactId}</include> </includes> <binaries> <unpack>true</unpack> <includeDependencies>false</includeDependencies> <attachmentClassifier>jakarta</attachmentClassifier> <outputDirectory>/</outputDirectory> </binaries> </moduleSet> </moduleSets> <fileSets> <fileSet> <directory>src/apt/jpa</directory> <outputDirectory>/</outputDirectory> </fileSet> </fileSets> </assembly> Note: the value in the id element is used as the classifier, see here. Eclipse Transformer Plugin The last piece in the puzzle lies in the usage of the already-mentioned org.eclipse.transformer.maven plugin. Eclipse Transformer provides tools and runtime components that transform Java binaries, such as individual class files and complete JARs and WARs, mapping changes to Java packages, type names, and related resource names. The org.eclipse.transformer.maven plugin is defined on lines 171-187 in querydsl-apt dependency as: XML <plugin> <groupId>org.eclipse.transformer</groupId> <artifactId>org.eclipse.transformer.maven</artifactId> <version>0.2.0</version> <executions> <execution> <id>jakarta-ee</id> <goals> <goal>run</goal> </goals> <phase>package</phase> <configuration> <classifier>jakarta</classifier> </configuration> </execution> </executions> </plugin> Note: See this blog for more information about Eclipse Transformer plugin usage. Conclusion This article has covered Querydsl setups for Java EE and Jakarta EE. The rest explained the usage of the Maven classifier by Querydsl. The used source code (even though it wasn't a lot here) is available here. Disclaimer: The article is based on my investigation when I tried to figure out the solution. Please let me know of any inaccuracies or misleading information.
In this document, I am going to put together a step-by-step process of connecting your Salesforce instance with Google BigQuery using Cloud Composer DAGs that are provided by Google Cortex Framework. Steps To Be Performed on the Salesforce Account For this setup, I’ve used a free version of the Salesforce Developer account (free version). You can do so by logging in at their site. 1. Create a Profile in Salesforce That has the following permissions: Apex REST Services and API Enabled. View All permissions for all objects that you would like to replicate. Is ideally not granted any permissions related to user interface login. For this setup, I am using the profile System Administrator. The recommended approach is to create a profile specifically for this task with the above-mentioned permissions. Below is how and where you can create and update the permissions. Logging into your salesforce account with a user that has permission to create a profile and then go to Setup. In the left-hand menu, go to Administration → Users → Profiles. For permissions on object to replication, you can manage that in the Field-Level Security. 2. Create or Use Existing Salesforce Profile The next step is to create or use an existing user in Salesforce and assign it to the profile that has been created in Step #1. Note down the following details: Username Password Security Token Caution: In case you don’t have the security token, you can use the below steps to generate it. However, in case you have existing applications using this security token for authentication, they would need to be updated with the new token to make it work. If you are in a production system, please confirm with your Administrator before doing this! Profile Avatar → Settings → My Personal Information → Reset My Security Token 3. Create a Connected App The next step is to create a Connected App, which will be used for establishing the connection to the outside world, i.e., Cortex Framework’s SFDC->RAW Ingestion module. This will be done with the profile and user ID you created in the above steps. Setup → App Manager → New Connected App → Provide the App Name (in this case GCPBQ) In the API (Enable OAuth Settings) → Select Oauth Scopes (In this case Full Access) → Callback URL (In this case OAuth/callback.) Enable the checkbox box ‘Require Secret for Web Server Flow’ and ‘Require Secret for Refresh Token Flow.’ You will need the Consumer Key/Client ID in the later steps. To do so, click on ‘Manage Consumer Details’ → It will open a new page and will ask for a verification code that will be sent to your email address. 4. Assign the Connected App The last step here is to assign the connected app you created above to the profile created in Step #1. Setup → Profile → Edit → Connected App Access → checkbox your connected app (in this case GCPBQ) Steps To Be Performed on Your Google Cloud Account 1. Deploy the Cortex Framework for Salesforce Data Deploy the Cortex Framework for Salesforce data with Test Data as false. Follow the steps mentioned in the readme of this git repo. This will get the DAGs in the GCS bucket you’ve mentioned in the config.json. These DAGs will later be used for extracting data from your Salesforce instance to your BigQuery ‘RAW’ dataset. Make sure to deploy to a target bucket that is NOT your Cloud Composer DAG bucket. This way, you can inspect the generated DAGs to ensure correctness before they actually get executed. 2. Create a Cloud Composer Instance The next step is to install some dependencies for Salesforce to execute the Python scripts in the DAGs provided by the Data Foundation framework. Refer to this link for details. Here is the command to install the required dependencies: $ gcloud composer environments update <ENVIRONMENT_NAME> \ --location <LOCATION> \ --update-pypi-package <PACKAGE_NAME><EXTRAS_AND_VERSION> For Airflow version 1.10 use - “apache-airflow-backport-providers-salesforce>=2021.3.3” For Airflow version 2.x use - “apache-airflow-providers-salesforce~=5.2.0” You can find the required libraries based on your composer version in this document. 3. Create a Secret to Sore Your Salesforce Connection Information Create a secret to store your Salesforce connection information in the Google Cloud Secret Manager. Follow this document for details. The secret name should have a prefix airflow-connections-<your secret name>. In this case, it should be named airflow-connections-salesforce-conn. The value of this secret is: http://<username>:<password>@https%3A%2F%2F<instance-name>.lightning.force.com?client_id=<client_id>&security_token=<secret-token> Replace the username, password, and security token from Step #2 in the section “Steps to be performed in the Salesforce account.” To get the instance name of your Salesforce account, refer to this document. 4. Create the Connection in the Composer Environment for Bigquery Details about different connections for the composer settings are mentioned in this document. Go to the Airflow webserver UI. Go to Admin → Connections → Add a new record. Provide the following details: Connection Id: sfdc_cdc_bq Connection Type: Google Cloud Project ID: <your project ID where the bq datasets are created> Keyfile JSON: <JSON Keyfile for the service account, which has BigQuery Data Editor and BigQuery Job User roles. Refer to this document on how to create the JSON Key for the service account. Similarly, you need to create another connection, sfdc_reporting_bq. 5. Enable Secret Manager as a Backend in Cloud Composer Refer to this doc for more details regarding Secrets Manager. Go to your composer instance and then go to Airflow Configuration Overrides and maintain below two secrets: Key: backend Value:airflow.providers.google.cloud.secrets.secret_manager.CloudSecretManagerBackend 2. Key: backend_kwargs Value: {"project_id": <gcp project id>”, "connections_prefix":"airflow-connections", "variables_prefix":"airflow-variables", "sep":"-"} 6. Allow the Composer Service Account to Access the Secrets Look for the service account for your composer instance and make sure it has the Secret Manager Secret Accessor permission. The default is the GCE service account. 7. Copy Generated Files After the inspection and ensuring correctness, copy generated files from the generated DAG bucket to your actual Cloud Composer DAG bucket. Once the composer instance is created, use the below command to copy the DAGs and data from the output bucket, which was used in the config.json file for Cortex deployment to the composer bucket: gsutil -m cp -r gs://<output bucket>/dags/ gs://<composer dag bucket>/ gsutil -m cp -r gs://<output bucket>/data/ gs://<composer sql bucket>/ 8. Go Into Airflow UI and Start the RAW DAGs You can navigate to Airflow UI from your cloud composer instance (will open a new page). You’ll see all the dags that were copied in the previous step. You can adjust the frequency of the DAGs based on the requirement, and it will run as per the schedule. 9. Checkout Your BigQuery RAW Datasets Once the DAGs are executed, you’ll be able to see the data in RAW and CDC datasets within your BigQuery in the target project. This document only focuses on the integration option using Salesforce API. Refer to the Cortex framework for the Salesforce blog to see what analytics content is available out of the box for you to use to unlock richer insights and make smart business decisions. Google Cloud is a top choice for customers seeking data-driven insights, and data integration is a first step in the journey. Massive scalability, reliability, security, and AI-rich features make Google Cloud an ideal platform to accomplish your business goals and objectives and pave the way for an innovation-driven expedition. DISCLAIMER: The opinions and viewpoints are solely mine in this article and do not reflect my employer's official position or views. This article should not be considered an official endorsement or statement from my employer.
Working with big data is not that easy. Every component should provide the means and tooling to actually make sense of the data that is going to be used by the end user. This is where full-featured UI component libraries and rich row features prove to be handy. In this article, I will look at the top five Angular Grid Row features to consider for your next project. Row selection Multi-row layout Role UI actions Row pinning Row dragging In Brief – What Is Grid Row Feature in Angular? Grid row features refer to the functionalities and capabilities that a certain Angular UI library provides so developers can manage and manipulate rows within a grid component efficiently and easily. Here are my top five Angular Grid Row features that you must have: 1. Row Selection With row selection, a row selector column precedes all other columns within the row. When a user clicks on the row selector, the row will either become selected or deselected, enabling the user to select multiple rows of data. The sample below demonstrates the three types of Grid's row selection behavior. Use the buttons below to enable each of the available selection modes. A brief description will be provided on each button interaction through a Snackbar message box. Use the switch button to hide or show the row selector checkbox. Based on the components library that you use or the plan that you have for creating such a Grid feature, you should definitely consider three different modes of selection - none, single, and multiple. Let's look at a concrete example: the Ignite UI Angular library. Single row selection can be easily set up; the only thing you need to do is to set [rowSelection] = '"single"' property. This gives you the opportunity to select only one row within a Grid. You can select a row by clicking on a cell or pressing the space key when you focus on a cell of the row, and of course you can select a row by clicking on the row selector field. When row is selected or deselected rowSelectionChanging event is emitted. HTML <!-- selectionExample.component.html --> <igx-grid [data]="remote | async" [rowSelection]="'single'" [autoGenerate]="true" (rowSelectionChanging)="handleRowSelection($event)" [allowFiltering]="true"> </igx-grid> TypeScript /* selectionExample.component.ts */ public handleRowSelection(args) { if (args.added.length && args.added[0] === 3) { args.cancel = true; } } To enable multiple row selection in the igx-grid just set the rowSelection property to multiple. This will enable a row selector field on each row and in the Grid header. HTML <!-- selectionExample.component.html --> <igx-grid [data]="remote | async" [primaryKey]="'ProductID'" [rowSelection]="'multiple'" (rowSelectionChanging)="handleRowSelection($event)" [allowFiltering]="true" [autoGenerate]="true"> </igx-grid> TypeScript <!-- selectionExample.component.ts --> public handleRowSelection(event: IRowSelectionEventArgs) { // use event.newSelection to retrieve primary key/row data of latest selected row this.selectedRowsCount = event.newSelection.length; this.selectedRowIndex = event.newSelection[0]; } 2. Multi-Row Layout Multi-row Layout extends the rendering capabilities of the igxGridComponent. The feature allows splitting a single data record into multiple visible rows. Multi-row Layout can be implemented on top of the grid layout W3 specification and should conform to its requirements. That's the case with the Ignite UI Angular library, it was achieved through the declaration of Multi-row Layout igx-column-layout component. Each igx-column-layout component should be considered as a block containing one or multiple igx-column components. Some of the grid features work on block level (those are listed in the "Feature Integration" section below). For example, the virtualization will use the block to determine the virtual chunks, so for better performance, split the columns into more igx-column-layout blocks if the layout allows it. There should be no columns outside of those blocks and no usage of IgxColumnGroupComponent when configuring a multi-row layout. IgxColumnComponent exposes four @Input properties to determine the location and span of each cell: colStart: Column index from which the field is starting. This property is mandatory. rowStart: Row index from which the field is starting. This property is mandatory. colEnd: Column index where the current field should end. The amount of columns between colStart and colEnd will determine the amount of spanning columns to that field. This property is optional. If not, set defaults to colStart + 1. rowEnd: Row index where the current field should end. The amount of rows between rowStart and rowEnd will determine the amount of spanning rows to that field. This property is optional. If not, set defaults to rowStart + 1. HTML <igx-column-layout> <igx-column [rowStart]="1" [colStart]="1" [rowEnd]="3" field="ID"></igx-column> </igx-column-layout> <igx-column-layout> <igx-column [rowStart]="1" [colStart]="1" [colEnd]="3" field="CompanyName"></igx-column> <igx-column [rowStart]="2" [colStart]="1" [colEnd]="2" field="ContactName"></igx-column> <igx-column [rowStart]="2" [colStart]="2" [colEnd]="3" field="ContactTitle"></igx-column> </igx-column-layout> <igx-column-layout> <igx-column [rowStart]="1" [colStart]="1" [colEnd]="3" field="Country"></igx-column> <igx-column [rowStart]="1" [colStart]="3" [colEnd]="5" field="Region"></igx-column> <igx-column [rowStart]="1" [colStart]="5" [colEnd]="7" field="PostalCode"></igx-column> <igx-column [rowStart]="2" [colStart]="1" [colEnd]="4" field="City"></igx-column> <igx-column [rowStart]="2" [colStart]="4" [colEnd]="7" field="Address"></igx-column> </igx-column-layout> <igx-column-layout> <igx-column [rowStart]="1" [colStart]="1" field="Phone"></igx-column> <igx-column [rowStart]="2" [colStart]="1" field="Fax"></igx-column> </igx-column-layout> 3. Row UI Actions The grid component in Ignite UI for Angular provides the ability to use ActionStrip and utilize CRUD for row/cell components and row pinning. The Action Strip component can host predefined UI controls for these operations. Its main purpose is to provide an overlay area containing one or more actions, allowing additional UI and functionality to be shown on top of a specific target container upon user interaction, e.g., hover. The container should be positioned relatively as the Action Strip attempts to overlay it and is itself positioned absolutely. Despite overlapped by an Action Strip, the main interactions and user access to the target container remain available. Based on the implementation that you take, you might need to initialize and position the Action Strip correctly; it needs to be inside a relatively positioned container as in the case of Ignite UI Angular Action strip: HTML <div style="position:relative; width:100px; height:100px;"> <igx-action-strip> <button igxButton (click)="makeTextBold()"> <igx-icon>format_bold</igx-icon> </button> </igx-action-strip> <div> For scenarios where more than three action items will be shown, it is best to use IgxActionStripMenuItem directive. Any item within the Action Strip marked with the *igxActionStripMenuItem structural directive will be shown in a dropdown, revealed upon toggling the more button i.e., the three dots representing the last action 4. Row Pinning One or multiple rows can be pinned to the top or bottom of the Angular UI Grid. Row Pinning allows end-users to pin rows in a particular order, duplicating them in a special, visible area even when they scroll the Grid vertically. The Material UI Grid has a built-in row pinning UI, which is enabled by initializing an igxActionStrip component in the context of the Grid. In addition, you can define custom UI and change the pin state of the rows via the Row Pinning API. Based on the UI consistency and ease of you that you are trying to achieve, you can implement a built-in row-pinning UI. In the example below, such a functionality is enabled by adding an igxActionStrip component with the GridPinningActions component. The action strip is automatically shown when hovering a row and will display a pin or unpin button icon based on the state of the row it is shown for. An additional action allowing to scroll the copy of the pinned row into view is shown for each pinned row as well. HTML <igx-grid [data]="data" [autoGenerate]="false"> <igx-column *ngFor="let c of columns" [field]="c.field" [header]="c.field"> </igx-column> <igx-action-strip #actionStrip> <igx-grid-pinning-actions></igx-grid-pinning-actions> <igx-grid-editing-actions></igx-grid-editing-actions> </igx-action-strip> </igx-grid> 5. Row Dragging Angular Grid Row dragging provides users with a row drag handle with which they can initiate the dragging of a row. Row dragging feature is tightly coupled with the Grid Row implementation as a whole. It lets users pass the data of a grid record onto another surface, which has been configured to process/render this data in a particular way. If you are a developer who wants to achieve such functionality, first define and answer the questions that may come from an end-user, what would he want and expect? Be able to click on a grid row and drag it in order to provide its content as input to another piece of UI. Have a clear indication as I drag a row whether I can drop it on the underlying area or not. See a ghost of the dragged row while dragging. I do not want the ghost to have selected or active classes applied while dragging. Be able to cancel the dragging by pressing the Esc key while dragging is performed. When I drag a row that is in edit mode, I want to exit edit mode and save the changes that are made. If I am dragging a row that is selected or has a selected cell, no selection-related classes should be copied to the ghost. User Interface example: IgxGrid example: Wrap Up There are different Grid row features and functionalities in Angular UI libraries available on the market. But to me, the must-have features are precisely Row selection, Multi-row layout, Role UI actions, Row pinning, and Row dragging. With them, users can more easily manage and manipulate tabular data.
"Is it working now?" asked the Product Owner. "Well... I hope so. You know this bug, it can not really be reproduced locally, therefore I can not really test if it works now. The best I can do is deploy it to prod and wait." The answer did not make the Product Owner particularly happy, but he also knew the bug appears only when an API called by his application has a quick downtime exactly at the time when the user clicks on a specific button. The daily stand-up, which was the environment of the small conversation, went on, and nobody wanted to dedicate much time or attention to the bug mentioned - except for Jack, the latest addition to the team, who was concerned about this "hit deploy and roll the dice" approach. Later that day, he actually reached out to Bill - the one who fixed the bug. "Can you tell me some details? Can not we write some unit tests or so?" "Well, we can not. I actually did not really write much code. Still, I have strong faith, because I added @Retryable to ensure the API call is being re-tried if it fails. What's more, I added @Cacheable to reduce the amount of calls fired up against the API in the first place. As I said in the daily, we can not really test it, but it will work on prod." With that Bill wanted to close this topic and focus on the new task he picked up, but Jack was resistant: "I would still love to have automated tests on that," stated Jack. "On what? You should not unit-test Spring. Those guys know what they are doing." "Well, to be honest, I am not worried about Spring not working. I am worried about us not using it the right way." The Challenge This is the point: when we can abandon Jack and Bill, as we arrived at in the main message of this article, I have seen the following pattern multiple times. Someone resolves an issue by utilizing some framework-provided, out-of-the-box functionality. In many cases, it is just applying an annotation to a method or a class and the following sequence happens: The developer argues there is nothing to write automated tests for, as it is a standard feature of the framework that is being used. The developer might or might not at least test it manually (and like in the example above, the manual test might happen on a test environment, or might happen only on a prod environment). At some point, it breaks, and half of the team does not know why it is broken, the other half does not know why it used to work at all. Of course, this scenario can apply to any development, but my observation is that framework-provided features (such as re-try something, cache something, etc.) are really tempting the developers to skip writing automated tests. On a side note, you can find my more generic thoughts on testing in a previous article. Of course, I do not want to argue for testing a framework itself (although no framework is bug-free, you might find actual bugs within the framework). But I am strongly arguing for testing that you are using the framework properly. In many cases it can be tricky, therefore, in this tutorial, you will find a typically hard-to-test code, tips about how to rework and test it, and the final reworked version of the same code. Code That Is Hard To Test Take a look at the following example: Java @Slf4j @Component public class Before { @Retryable @Cacheable("titlesFromMainPage") public List<String> getTitlesFromMainPage(final String url) { final RestTemplate restTemplate = new RestTemplate(); log.info("Going to fire up a request against {}", url); final var responseEntity = restTemplate.getForEntity(url, String.class); final var content = responseEntity.getBody(); final Pattern pattern = Pattern.compile("<p class=\"resource-title\">\n(.*)\n.*</p>"); final Matcher matcher = pattern.matcher(content); final List<String> result = new ArrayList<>(); while (matcher.find()) { result.add(matcher.group(1).trim()); } log.info("Found titles: {}", result); return result; } } It is fair to say it is tricky to test. Probably your best shot would be to set up a mock server to respond to your call (for example, by using WireMock) as follows: @WireMockTest public class BeforeTest { private static final Before BEFORE_INSTANCE = new Before(); @Test public void testCall(final WireMockRuntimeInfo wmRuntimeInfo) { stubFor(get("/test-url").willReturn(ok( "<p class=\"resource-title\">\nFirst Title\n.*</p><p class=\"resource-title\">\nOther Title\n.*</p>"))); final var titles = BEFORE_INSTANCE.getTitlesFromMainPage("http://localhost:"+wmRuntimeInfo.getHttpPort()+"/test-url"); assertEquals(List.of("First Title", "Other Title"), titles); } } Many of the average developers would be happy with this test. Especially, after noticing, that this test generates 100% line coverage. And some of them would entirely forget to add @EnableCaching ... ... or add @EnableRetry ... ... or to create a CacheManager bean That would not only lead to multiple rounds of deployment and manual testing, but if the developers are not ready to admit (even to themselves) that it is their mistake, such excuses like, "Spring does not work," are going to be made. Let’s Make Life Better! Although my plan is to describe code changes, the point is not only to have nicer code but also to help developers be more reliable and lower the number of bug tickets. Let's not forget, that a couple of unforeseen bug tickets can ruin even the most carefully established sprint plans and can seriously damage the reputation of the developer team. Just think about the experience that businesses have: They got something delivered that does not work as expected The new (in progress) features are likely not to be delivered on time because the team is busy fixing bugs from previous releases. Back to the code: you can easily identify a couple of problems like the only method in the class is doing multiple things (a.k.a., has multiple responsibilities), and the test entirely ignores the fact that the class is serving as a Spring bean and actually depending on Spring's features. Rework the class to have more methods with less responsibility. In cases you are depending on something brought by annotations, I would suggest having a method that serves only as a proxy to another method: it will make your life seriously easier when you are writing tests to find out if you used the annotation properly. Step 1 probably led you to have one public method which is going to be called by the actual business callers, and a group of private methods (called by each other and the public method). Let's make them default visibility. This enables you to call them from classes that are in the same package - just like your unit test class. Split your unit test based on what aspect is tested. Although in several cases it is just straightforward to have exactly one test class for each business class, nobody actually restricts you to have multiple test classes for the same test class. Define what you are expecting: for example, in the test methods, when you want to ensure that retry is working, you do not care about the actual call (as for how the business result is created, you will test that logic in a different test anyway). There you have such expectations as: if I call the method and it throws an exception once, it will be called again. If it fails X times, an exception is thrown. You can also define your expectations against cache: you expect subsequent calls on the public method to lead to only one call of the internal method. Final Code After performing Steps 1 and 2, the business class becomes: Java @Slf4j @Component public class After { @Retryable @Cacheable("titlesFromMainPage") public List<String> getTitlesFromMainPage(final String url) { return getTitlesFromMainPageInternal(url); } List<String> getTitlesFromMainPageInternal(final String url) { log.info("Going to fire up a request against {}", url); final var content = getContentsOf(url); final var titles = extractTitlesFrom(content); log.info("Found titles: {}", titles); return titles; } String getContentsOf(final String url) { final RestTemplate restTemplate = new RestTemplate(); final var responseEntity = restTemplate.getForEntity(url, String.class); return responseEntity.getBody(); } List<String> extractTitlesFrom(final String content) { final Pattern pattern = Pattern.compile("<p class=\"resource-title\">\n(.*)\n.*</p>"); final Matcher matcher = pattern.matcher(content); final List<String> result = new ArrayList<>(); while (matcher.find()) { result.add(matcher.group(1).trim()); } return result; } } On a side note: you can, of course, spit the original class even to multiple classes. For example: One proxy class which only contains @Retryable and @Cacheable (contains only getTitlesFromMainPage method) One class that only focuses on REST calls (contains only getContentsOf method) One class that is responsible for extracting the titles from HTML content (contains only extractTitlesFrom method) One class which orchestrates fetching and processing the HTML content (contains only getTitlesFromMainPageInternal method) I am convinced that although in that case, the scope of the classes would be even more strict, the overall readability and understandability of the code would suffer from having many classes with 2-3 lines of business code. Steps 3 and 4 lead you to the following test classes: Java @ExtendWith(MockitoExtension.class) public class AfterTest { @Spy private After after = new After(); @Test public void mainFlowFetchesAndExtractsContent() { doReturn("contents").when(after).getContentsOf("test-url"); doReturn(List.of("title1", "title2")).when(after).extractTitlesFrom("contents"); assertEquals(List.of("title1", "title2"), after.getTitlesFromMainPage("test-url")); } @Test public void extractContent() { final String htmlContent = "<p class=\"resource-title\">\nFirst Title\n.*</p><p class=\"resource-title\">\nOther Title\n.*</p>"; assertEquals(List.of("First Title", "Other Title"), after.extractTitlesFrom(htmlContent)); } } Java @WireMockTest public class AfterWireMockTest { private final After after = new After(); @Test public void getContents_firesUpGet_andReturnsResultUnmodified(final WireMockRuntimeInfo wmRuntimeInfo) { final String testContent = "some totally random string content"; stubFor(get("/test-url").willReturn(ok(testContent))); assertEquals(testContent, after.getContentsOf("http://localhost:" + wmRuntimeInfo.getHttpPort() + "/test-url")); } } Java @SpringBootTest public class AfterSpringTest { @Autowired private EmptyAfter after; @Autowired private CacheManager cacheManager; @BeforeEach public void reset() { after.reset(); cacheManager.getCache("titlesFromMainPage").clear(); } @Test public void noException_oneInvocationOfInnerMethod() { after.getTitlesFromMainPage("any-test-url"); assertEquals(1, after.getNumberOfInvocations()); } @Test public void oneException_twoInvocationsOfInnerMethod() { after.setNumberOfExceptionsToThrow(1); after.getTitlesFromMainPage("any-test-url"); assertEquals(2, after.getNumberOfInvocations()); } @Test public void twoExceptions_threeInvocationsOfInnerMethod() { after.setNumberOfExceptionsToThrow(2); after.getTitlesFromMainPage("any-test-url"); assertEquals(3, after.getNumberOfInvocations()); } @Test public void threeExceptions_threeInvocationsOfInnerMethod_andThrows() { after.setNumberOfExceptionsToThrow(3); assertThrows(RuntimeException.class, () -> after.getTitlesFromMainPage("any-test-url")); assertEquals(3, after.getNumberOfInvocations()); } @Test public void noException_twoPublicCalls_InvocationsOfInnerMethod() { assertEquals(0, ((Map)cacheManager.getCache("titlesFromMainPage").getNativeCache()).size()); after.getTitlesFromMainPage("any-test-url"); assertEquals(1, after.getNumberOfInvocations()); assertEquals(1, ((Map)cacheManager.getCache("titlesFromMainPage").getNativeCache()).size()); after.getTitlesFromMainPage("any-test-url"); assertEquals(1, after.getNumberOfInvocations()); assertEquals(1, ((Map)cacheManager.getCache("titlesFromMainPage").getNativeCache()).size()); } @TestConfiguration public static class TestConfig { @Bean public EmptyAfter getAfter() { return new EmptyAfter(); } } @Slf4j public static class EmptyAfter extends After { @Getter private int numberOfInvocations = 0; @Setter private int numberOfExceptionsToThrow = 0; void reset() { numberOfInvocations = 0; numberOfExceptionsToThrow = 0; } @Override List<String> getTitlesFromMainPageInternal(String url) { numberOfInvocations++; if (numberOfExceptionsToThrow > 0) { numberOfExceptionsToThrow--; log.info("EmptyAfter throws exception now"); throw new RuntimeException(); } log.info("Empty after returns empty list now"); return List.of(); } } } Note that the usage of various test frameworks is separated: the class that actually tests if Spring features are used correctly has SpringRunner, but is not aware of WireMock and vice-versa. There is no "dangling" extra configuration in the test classes, which is used only by a fraction of the test methods in a given test class. On a side note to AfterSpringTest: Usage of @DirtiesContext on the class could be an alternative to manually resetting the cache in reset() method, but doing the clean-up manually is a more performant way. My advice on this question is: Do a manual reset if the scope of what to reset is small (this is normally the case in unit tests). Reset the beans by annotation if many beans are involved or cleaning the context would require complex logic (this is the case in many integration and system tests). You can find the complete code on GitHub. Final Thoughts After all the reworking and creating extra test classes, what would happen now if you delete @EnableRetry or @EnableCaching from the configuration? What would happen if someone would delete or even modify @Retryable or @Cacheable on the business method? Go ahead and try it out! Or trust me when I say unit tests would fail. And what would happen if a new member joins the team to work on such code? Based on the tests, he would know what is the expected behavior of the class. Tests are important. Quality tests can help you to produce code that others can better understand, can help you to be more reliable, and identify bugs faster. Tests can be tricky, and tests can be hard to write. But never forget, that if someone says, "That can not be tested," in the overwhelming majority of cases, it only means "I don't know how to test it and not caring to figure it out."
Justin Albano
Software Engineer,
IBM
Thomas Hansen
CTO,
AINIRO.IO