Spring Boot: Show all logging events for one Web request only

In this post I show how you for a single Web request can make your Spring Boot application dump all log statements on all categories (TRACE, DEBUG, INFO, WARN, ERROR). Everything – but only for the specific Web request that you care about. I show the technique using Spring Boot 1.4. 

Here’s an example. Imagine a call to:

http://localhost:8080/greetings/duke?trace=on

The trace query parameter is completely unknown to the actual GreetingController. But it instructs your application to log everything related to that request:

Screen Shot 2016-08-24 at 20.50.01
With this amount of information you can go ahead and troubleshoot those hard to understand production issues! Compare that to what you will get without the trace support:

Screen Shot 2016-08-24 at 20.52.04
The best thing: once you have implemented support for the trace query parameter, you can use it on any Web request to your application: Servlets, JSP’s, SOAP endpoints, REST endpoints and so on. In fact: they don’t even know about it.

Spring Boot implementation

I have prepared a GitHub example – consult that to see all source code in its entirety and full context. 

Here is what you need to do:

  1. Create a ThreadLocal(ish) object that knows when everything should be logged
  2. Create a Web filter that manages this ThreadLocal(ish) object
  3. Hook into the logging provider so that it can use the ThreadLocal(ish) object to decide wether or not to log a logging event

Spring Boot’s default logging provider is Logback. So I have used that in this example when implementing step three. Here it goes.

Step 1 of 3: Class ThreadLoggingSupport

public class ThreadLoggingSupport {

    private static final Map<Long, Boolean> THREAD_TO_ENABLED = new HashMap<>();

    public static void logEverything(boolean enabled) {
        THREAD_TO_ENABLED.put(Thread.currentThread().getId(), enabled);
    }

    public static boolean shouldLogEverything() {
        return Optional.ofNullable(THREAD_TO_ENABLED.get(Thread.currentThread().getId()))
                .orElse(false);
    }

    public static void cleanup() {
        THREAD_TO_ENABLED.remove(Thread.currentThread().getId());
    }
}

The purpose of this class is to allow code in the Web layer (next step) to communicate with the logging system (step 3).

(I deliberately chose not to use a ThreadLocal here: since the use of it in step 3 would have made it bind to all threads that uses Logback – and not only the Web container threads.)

Step 2 of 3: Class ThreadLoggingFilterBean

@Component
public class ThreadLoggingFilterBean extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            boolean logEverythingForThisRequest = "on".equalsIgnoreCase(request.getParameter("trace"));
            ThreadLoggingSupport.logEverything(logEverythingForThisRequest);
            chain.doFilter(request, response);
        } finally {
            ThreadLoggingSupport.cleanup();
        }
    }

}

This is an ordinary Spring GenericFilterBean. All Web requests goes through this filter. Notice how it pulls out the trace query parameter value. If it has the value on then it will tell the log system to go nuts – logging everything it receives.

Step 3 of 3: Class ThreadLoggingInitializer

@Component
public class ThreadLoggingInitializer {

    private static org.slf4j.Logger LOG = LoggerFactory.getLogger(ThreadLoggingInitializer.class);

    @EventListener
    public void handleContextRefresh(ContextRefreshedEvent event) {
        LoggerContext loggerContext = ((Logger) LoggerFactory.getLogger("")).getLoggerContext();
        loggerContext.addTurboFilter(new TurboFilter() {
            @Override
            public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {
                return (ThreadLoggingSupport.shouldLogEverything()) ? FilterReply.ACCEPT : FilterReply.NEUTRAL;
            }
        });
        LOG.info("ThreadLogging support initialized");
    }
}

This is where we enter Logback specifics: We register a global log filter (a TurboFilter) that knows when to:

  • Force the logging of a specific logging event, or
  • Ignore the specific logging event

All logging events go through this filter – so it is important to avoid putting expensive code here.

Here we simply check the ThreadLocal(ish) state and use that to decide wether the current logging event should be logged or ignored. Ignoring it here means that this filter wont interfere in the decision. For more information about Logback and its filter support, refer to the online manual [1].

Closing remarks

Here I used Web requests as an example. But you can easily apply this technique to incoming JMS messages, scheduled jobs and so on.

Also, the technique was explained using Spring Boot. But you can easily apply it to other Web frameworks as well.

Be creative – bend that spoon.

References

[1] Logback – Chapter 7: Filters
http://logback.qos.ch/manual/filters.html

Tagged with: ,
Posted in Spring

Spring Boot: Sessions actuator endpoint

This post shows how you can implement a custom Spring Boot Actuator endpoint that prints information about all active HttpSessions:

Screen Shot 2016-08-22 at 22.23.45

HttpSession meta data is prefixed with @ signs: id, creation time and last accessed time. The other values are a raw dump of all the HttpSession attributes.

You can use this endpoint during development to inspect active sessions. Or you can use it on production systems when troubleshooting customer issues. Whatever you choose, make sure that you secure such an endpoint appropriately: You could expose some very sensitive data.

For an introduction to custom Spring Boot Actuator endpoints, refer to my previous post: Spring Boot: Introduce your own insight endpoints.

Implementation

I have prepared a GitHub example based on Spring Boot 1.4 – find it here. Consult that to see the code in it’s entirety and full context. Here’s the main principles that makes this possible:

  • Create a class to act as a registry of all active HttpSessions
  • Create an HttpSession listener that registers and de-registers the HttpSessions
  • Create a Spring Boot Actuator endpoint that dumps the internal state of all of the HttpSessions

Step 1 of 3: Class SessionRegistry

@Service
public class SessionRegistry {

    private final Map<String, HttpSession> httpSessionMap = new ConcurrentSkipListMap<>();

    public void addSession(HttpSession httpSession) {
        this.httpSessionMap.put(httpSession.getId(), httpSession);
    }

    public void removeSession(HttpSession httpSession) {
        this.httpSessionMap.remove(httpSession.getId());
    }

    public List<HttpSession> getSessions() {
        return new ArrayList<>(httpSessionMap.values());
    }
}

The httpSessionMap is a concurrent version of Map: to avoid ConcurrentModificationExceptions.

Step 2 of 3: Class SessionListener

@Component
public class SessionListener implements HttpSessionListener {

    @Autowired
    private SessionRegistry sessionRegistry;

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        sessionRegistry.addSession(se.getSession());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        sessionRegistry.removeSession(se.getSession());
    }
}

This is a Spring’ified version of a traditional Servlet container component: HttpSessionListener. This class maintains the registry.

Step 3 of 3: Class SessionActuatorEndpoint

This is the interesting part – where we create a Spring Boot Actuator endpoint:

@Component
public class SessionsActuatorEndpoint extends AbstractMvcEndpoint {

    @Autowired
    private SessionRegistry sessionRegistry;

    public SessionsActuatorEndpoint() {
        super("/sessions", true/*sensitive*/);
    }

    @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public SessionStateActuatorEndpointResponse listActiveSessions() {
        return new SessionStateActuatorEndpointResponse("Active HTTP sessions", sessionRegistry.getSessions());
    }

    @JsonPropertyOrder({"info", "numberOfActiveSessions", "sessions"})
    public static class SessionStateActuatorEndpointResponse {

        @JsonProperty
        private String info;

        @JsonProperty
        private int numberOfActiveSessions;

        @JsonProperty
        private List<Map<String, String>> sessions;

        public SessionStateActuatorEndpointResponse(String info, List<HttpSession> httpSessions) {
            this.info = info;
            this.numberOfActiveSessions = httpSessions.size();
            this.sessions = httpSessions.stream()
                    .map(this::getSessionState)
                    .collect(Collectors.toList());
        }

        private Map<String, String> getSessionState(HttpSession httpSession) {
            Map<String, String> sessionState = new LinkedHashMap<>();
            sessionState.put("@session_id", httpSession.getId());
            sessionState.put("@session_creation_time", formatDateTime(httpSession.getCreationTime()));
            sessionState.put("@session_last_accessed_time", formatDateTime(httpSession.getLastAccessedTime()));
            for (Enumeration<String> attributeNames = httpSession.getAttributeNames(); attributeNames.hasMoreElements(); ) {
                String attributeName = attributeNames.nextElement();
                Object attributeValue = httpSession.getAttribute(attributeName);
                sessionState.put(attributeName, attributeValue instanceof String || attributeValue instanceof Number ?
                        attributeValue.toString() : ReflectionToStringBuilder.toString(attributeValue));
            }
            return sessionState;
        }

        private String formatDateTime(long epoch) {
            Instant instant = Instant.ofEpochMilli(epoch);
            ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
            return zonedDataTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
        }
    }

}

The thing that makes this a Spring Boot Actuator endpoint is that it declares AbstractMvcEndpoint as its superclass. Also take note of the constructor: this is where you register the path at which this endpoint can be found (‘/sessions‘).

The inner class SessionStateActuatorEndpointResponse represents the JSON response structure. I’ve selected some meta data that I find interesting and added that in the output – prefixed with ‘@’ characters. Other than that, it simply dumps all HttpSession attributes. Notice that it uses commons ReflectionToStringBuilder for object structures other than Strings and Numbers.

 

Tagged with:
Posted in Spring

Spring Boot: Introduce your own insight endpoints

In this post I show how you can develop custom Spring Boot Actuator HTTP endpoints for obtaining detailed insight into your Spring Boot application’s runtime behavior.

Here’s an example:

Screen Shot 2016-08-18 at 20.28.21

The above output is simple. Yet it can be useful to have in many applications. Like the “official” Spring Boot Actuator info output.

If you don’t find this concrete example interesting, then I’m sure that you can find something in your Spring Boot applications that you would find interesting to have insight into. In fact, I recommend that you think about areas of your application that you would like to have insight into. Then provide endpoints that gives you this visibility.

When you develop a new feature, I bet you add unit tests and integration tests etc. So, while you are at it: add insight HTTP endpoints too.

Do this, so that you better understand the application’s behavior. And do it, because when your system is in production and poop hits the fan: then insight HTTP endpoints may be there to save your b…ehind.

About Spring Boot Actuator

Spring Boot Actuator offers a suite of HTTP endpoints that provide standard insight into a Spring Boot application. With them you can: show thread stacks, beans in the ApplicationContext, latest HTTP requests, information about the current application version, performance metrics and much more.

These insight “tools” are extremely useful. But:

The Spring Boot Actuator HTTP endpoints don’t know anything that’s specific about your application

 

You can have much more insight

The fantastic Spring Boot team has, in traditional Spring framework spirit, ensured that the actuator framework is open for extension by us. And it is very easy!

I have created an example on GitHub – ready for pull and play. Consult that for the full picture. But for your convenience – here is how I implemented the active users HTTP endpoint:

@Component
public class ActiveUsersActuatorEndpoint extends AbstractMvcEndpoint {

    @Autowired
    private ActiveUsersService activeUsersService;

    public ActiveUsersActuatorEndpoint() {
        super("/activeusers", false /* sensitive */);
    }

    @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ActiveUsersResponse listActiveUsers() {
        return new ActiveUsersResponse("Active users right now", activeUsersService.listActiveUsers());
    }

    @JsonPropertyOrder({"info", "activeUsers"})
    public static class ActiveUsersResponse {

        @JsonProperty
        private String info;

        @JsonProperty
        private List&lt;String&gt; activeUsers;

        public ActiveUsersResponse(String info, List&lt;String&gt; activeUsers) {
            this.info = info;
            this.activeUsers = activeUsers;
        }
    }

}

This example extends a Spring Boot Actuator specific class: AbstractMvcEndpoint. They have their own AbstractEndpoint as well – but that’s not so cool: you cannot use the flexible @RequestMapping annotation with that.

See the Spring Boot Reference Guide for further details on custom actuator endpoints [1].

Don’t use the normal Spring MVC controllers for this

Instead of registering your custom insight HTTP endpoints the Spring Boot Actuator way, you could also just expose plain Spring MVC endpoints. But that’s not a wise approach.

Here’s why:

  • You would produce a parallel insight tool framework (alongside Spring Boot Actuator). That can be confusing for new developers on the application. Also, they may wonder why you don’t just do it “the standard way” (as described in this post).
  • If you register your own insight HTTP endpoints with Spring Boot Actuator – then they “follow along” with the location of the standard actuator endpoints. This includes the root URL context and the port number. So – should you choose to expose Spring Boot Actuator on 9090 instead of 8080 – then your custom endpoints would “follow along”.

References

[1] Spring Boot Reference Guide: Adding custom endpoints:
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready-customizing-endpoints-programmatically

Tagged with:
Posted in Spring

Spring Boot: Enhance your logging

In this post I show how you can tune your Spring Boot application’s logging output – such that it has even more information when you need to troubleshoot. The method is actually so generic that you can apply it to other types of Java applications as well – for example Java EE applications.

What you have today

Firstly: what is wrong with the default logging we get in Spring Boot? Nothing, actually – but it can be better. As an example, let us consider the log output from a RESTful resource invocation to /greetings/duke which returns a plain text greeting:

2016-08-16 21:57:56.269 INFO 29817 --- [o-auto-1-exec-1] com.moelholm.GreetingController : Request received. PathVariable is: [duke]
2016-08-16 21:57:56.271 INFO 29817 --- [o-auto-1-exec-1] com.moelholm.GreetingRepository : Retrieving standard greeting from the "datastore"
2016-08-16 21:57:56.271 INFO 29817 --- [o-auto-1-exec-1] com.moelholm.GreetingService : Formatting greeting for [duke]

There are 3 log lines: from GreetingController, GreetingService and GreetingRepository. Now imagine a system with 30 simultaneous active users that perform such invocations:

  • The log lines would be mixed from the different concurrent threads – making it rather difficult to reason about the sequence of events. The thread names do help us – but even they get reused: the typical web container re-uses the same threads for serving different requests. Effectively meaning that if we filter the logs for, say o-auto-1-exec-1, we would get all log lines ever served by that thread.
  • It is not possible to relate the log lines to the actual users. We really don’t know which user caused what log lines.

What you can get

This is better:

2016-08-16 22:17:34.408 [userId:tux | requestId:3e21b7f3-3ba9-49b9-8390-4ab8987f995f] INFO 30158 --- [o-auto-1-exec-1] com.moelholm.GreetingController : Request received. PathVariable is: [duke]
2016-08-16 22:17:34.409 [userId:tux | requestId:3e21b7f3-3ba9-49b9-8390-4ab8987f995f] INFO 30158 --- [o-auto-1-exec-1] com.moelholm.GreetingRepository : Retrieving standard greeting from the "datastore"
2016-08-16 22:17:34.409 [userId:tux | requestId:3e21b7f3-3ba9-49b9-8390-4ab8987f995f] INFO 30158 --- [o-auto-1-exec-1] com.moelholm.GreetingService : Formatting greeting for [duke]

Here we have the same 3 log lines as before. But this time we can see that they belong to the same HTTP request: the request with id 3e21b7f3-xxxxx. We can also see that it is the user tux that caused these log lines.

The naive solution would be for you to prepend the userId and requestId to all log lines. But that’s never going to happen. You will forget it. If not you – then your colleague. And it can even be difficult to get such information from subcomponents – for example the GreetingRepository: how does it know about the requestId? Don’t even consider using ThreadLocal‘s now🙂.

The solution to get such “omni present” logging data is: Mapped Diagnostic Context (MDC). MDC is a feature that is supported in the most modern Java logging frameworks, for example: Log4j, Log4j2, Logback and more.

If you want to dig further into MDC, then take a look at Logbacks online documentation [1].

How you get that in Spring Boot

It’s easy. First you tell Spring Boot what MDC data to render:

logging.pattern.level=%X{mdcData}%5p

Put this in src/main/resources/application.properties. Or supply it in any other way supported by the flexible configuration mechanism in Spring Boot. This property tells Spring Boot to render the MDC data variable mdcData just before the priority field in the log output. The priority field is the logging level (DEBUG, INFO, …) that you are used to see.

Then you maintain the MDC data variable mdcData using your logging API – here using SLF4J (over the default Logback provider in Spring Boot):

@Component
public class RequestFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            // Setup MDC data:
            String mdcData = String.format("[userId:%s | requestId:%s] ", user(), requestId());
            MDC.put("mdcData", mdcData); //Variable 'mdcData' is referenced in Spring Boot's logging.pattern.level property 
            chain.doFilter(request, response);
        } finally {
           // Tear down MDC data:
           // ( Important! Cleans up the ThreadLocal data again )
           MDC.clear();
        }
    }

Here we maintain the MDC data in a Servlet Filter. That is an excellent place to maintain it – if we work with HTTP requests (REST, SOAP, JSP, Servlet, Thymeleaf, …). If you have JMS listeners in your application – then you’ll need to maintain the MDC data there too. Same applies with Quartz Jobs. And so on.

You decide what you want in the MDC data.

Anything goes: URL, session id, request id, user id, ip address, …

I chose to exemplify the use of MDC data with a user id and an HTTP request id generated when receiving inbound HTTP requests. I like the idea of generating a unique request id (for HTTP, JMS, etc). Especially if you serve it back to the caller’s when Exceptions occur. Given such an ID you can easily find all relevant log output related to that problem. I also like the user id: it easily gives you an overview of what a certain user has been doing in the application. But please decide what works for you – I am certain you can find additional useful data to put in the MDC.

I prepared a GitHub example for your convenience – take a look at that for a working example. Or if you just want to look at the above code examples in their full context.

Not using Spring Boot?

Not using Spring Boot? Then consult your logging frameworks documentation. The entire concept, MDC, is not at all tied to Spring Boot. In fact: MDC existed in Log4j long before Spring Boot was created.

References

[1] Logback – Chapter 8: Mapped Diagnostic Context
http://logback.qos.ch/manual/mdc.html

 

 

Tagged with: ,
Posted in Java EE, Spring

Spring Boot: Enhance your thread dumps

In this post I show how you can add extra information to your applications’ thread dumps.

I will use Spring Boot as an example environment – but the idea is very generic: You can, for example, easily apply this technique to any other Java EE application server environment you may have. Here is an example of an enhanced thread dump output (out of context and heavily edited) from the dump tool in a Spring Boot application (JSON format):

{
    threadName: "http-nio-8080-exec-1_[started:2016-08-15T22:38:04.275+02:00 | user:johndoe | uri:/slowgreetings/duke]_http-nio-8080-exec-1",
    threadId: 24,
    // ... and so on and so forth
    blockedCount: 0
}

( You will see the same information in the thread dumps you take by any other means – for example using the JDK jstack tool)

Notice the threadName: normally that would only show http-nio-8080-exec-1. But here it additionally shows:

  • uri: The HTTP request URI being processed by the thread
  • started: The date and time at which the request was received
  • user: The user principal who is executing the request

I like these 3 pieces of information when I’m debugging a production problem where the application seems to “hang”. You can put anything that you want into the thread name as well – these are just some ideas for your inspiration.

To get such enhanced thread dumps you will need to modify the thread names temporarily. The above example is relevant when the JVM is receiving inbound HTTP client requests. So in that case you will need to update the thread name as early as possible upon receiving a request. But you could also add Quartz job information instead when jobs start. Or JMS information when you receive messages. Or whatever you can imagine.

Spring Boot code example

I have prepared a super simplistic example on GitHub.

It is a Spring Boot application that exposes a single REST endpoint that you can poke from the browser (accepts a simple GET request). In addition to that endpoint I’ve also included a Servlet Filter: this is where I add the enhanced thread information:

 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
     String threadName = Thread.currentThread().getName();
     try {
         Thread.currentThread().setName(String.format(
             "%1$s_[started:%2$s | user:%3$s | uri:%4$s]_%1$s",
             threadName, timeNow(), user(), uri(request)));
         chain.doFilter(request, response);
     } finally {
         Thread.currentThread().setName(threadName);
     }
 }

There is really no magic here. Just modify the thread name with whatever information you can imagine. Don’t forget to reset the name in a finally block (so that the information belonging to a certain request doesn’t accidentally survive to another request).

In fact, since this is an ordinary Servlet Filter: you can take this recipe directly to your Java EE application as well. If you are using another stack: I bet you can find a similar “single point of entry” where you can place the functionality. Otherwise consider upgrading to Spring Boot😉.

Do you know why I prepended and appended the original thread name to the modified name? I appended the name so that it still appears in the Spring Boot console output (otherwise we would only see the last part of the thread name: which is the extra information we have added). And I prepended it – so that it is still the first thing you see in the thread dumps.

 

 

Tagged with: ,
Posted in Java EE, Spring

Spring Boot: Custom auto-configuration JARs

Custom auto-configuration JAR:

A shared JAR module containing Spring beans that can be automatically activated in one or more Spring Boot applications.

Auto-configuration JARs are extensively used by the official Spring Boot starter modules you are using in your every-day Spring Boot applications. But did you know that you easily can create such functionality yourself too?

Here’s how to do it. From your shared Java project, start by creating a Spring @Configuration:

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(EndpointHandlerMapping.class)
public class DumpAutoConfiguration {

@Bean @Qualifier("title")
public String dumpUiTitle() { return "UI Dump" ; }

}

Then tell Spring Boot that this is an auto-configuration JAR by adding a META-INF/spring.factories file to the classpath:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.moelholm.tools.actuatorui.dump.DumpAutoConfiguration

Done.

About the example

Did you notice the annotations @ConditionalOnWebApplication and @ConditionalOnClass? They are used to ensure that the configuration is only activated if the target Spring Boot application environment satisfies some expected circumstances.

The Spring Boot application that includes this JAR will now have a new bean in the ApplicationContext: dumpUiTitle. This bean can be injected like any other bean:

@Autowired @Qualifier("title") private String title;

The example code above is inspired by the actuator-ui-dump JAR I developed a while ago. This JAR registers a new Spring MVC controller that renders a UI on top of the dump actuator endpoint. For details about the actuator-ui-dump: see my previous post.

It is very different from a typical JAR file. A typical JAR file is passive: it only contributes classes and resources that you then need to manually use from your application. The Spring Boot custom auto-configuration JAR files contributes services to the application. It could be Spring MVC HandlerInterceptors, Spring AOP aspects or whatever else you can do with a typical Spring application. And all your Spring Boot applications need to do is: add the JAR to the classpath.

The Spring Boot reference manual has more detailed information on the subject – see [1]. If you want to see the example code above in it’s actual context – then see the source code on GitHub [2].

References

[1] Spring Boot reference manual – on auto-configuration:
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-understanding-auto-configured-beans

[2] Source code for the actuator-ui-dump module on GitHub:
https://github.com/nickymoelholm/tools/tree/master/actuator-ui-dump

Tagged with:
Posted in Spring

Spring Boot: UI for the dump endpoint

I am really happy with the actuator functionality in Spring Boot applications. It offers some fantastic tools for gaining insight into your application. Some of the REST endpoints that you get – such as health and metrics – are easy to read. Despite the fact that they render as JSON, my guess is that your human eyes still can comprehend it.

But have you ever taken a look at the output from Spring Boot’s dump endpoint?

That’s really not so friendly to read. At least not for my kind of eyes. And perhaps thats okay – if you got some tooling that can parse the thread stack info and show it nicely to you. But chances are that you don’t have that – like I didn’t.

So, a while back,  I decided to create a Spring Boot plugin that you just include on the application classpath. Like you do with the Spring Boot starters. This is what you get:

dump-ui

Red lines indicate methods that owns an intrinsic lock (think synchronized in Java land). I added a little gray line above each such event: it tells you what kind of class the intrinsic lock is associated with.

I have added this plugin to the Maven Central. Find it here.

The code is OSS – find it at GitHub. It is also here that you will find some extra documentation on how you can configure the plugin – so check it out if you want to give it a spin.

By the way: It was very interesting to make such a Spring Boot plugin. It uses auto configuration and other cool stuff. If you think that sounds interesting, then look at the code: it is really simple😉.

Tagged with:
Posted in Spring
Archives
Subscribe

Get notified per email when new posts are published

Follow

Get every new post delivered to your Inbox.