简体   繁体   中英

Assign a unique id to every request in a spring-based web application

I created a spring-based Java web application,

for each request, an instance of 'controller class' will be created to handle the request.

in the business logic, I want to do some logging with a UNIQUE ID automatically assigned to each request so that I can track what the program exactly did.

the log may be like this(2 requests at same time):

[INFO] request #XXX: begin.
[INFO] request #XXX: did step 1
[INFO] request #YYY: begin.
[INFO] request #XXX: did step 2
[INFO] request #YYY: did step 1
[INFO] request #XXX: end.
[INFO] request #YYY: end.

from the log, I can realize: req #XXX: begin-step1-step2-end req #YYY: begin-step1-end

I hope the logging can be called easily everywhere in the code, so I don't want to add a parameter of "requestId" to every java function,

It's perfect if the logging tool can be called in a static way:

LOG.doLog("did step 1");

any idea of how can I do this? thank you :)

You can also try using MDC class of Log4j. The MDC is managed on a per thread basis. If you are using a ServletRequestListner then you can set the unique Id in the requestInitialized.

import org.apache.log4j.MDC;
import java.util.UUID;

public class TestRequestListener implements ServletRequestListener {    
protected static final Logger LOGGER = LoggerFactory.getLogger(TestRequestListener.class);

 public void requestInitialized(ServletRequestEvent arg0) {
    LOGGER.debug("++++++++++++ REQUEST INITIALIZED +++++++++++++++++");

    MDC.put("RequestId", UUID.randomUUID());

 }

 public void requestDestroyed(ServletRequestEvent arg0) {
    LOGGER.debug("-------------REQUEST DESTROYED ------------");
    MDC.clear(); 
 }
}

Now anywhere in the code if you do a log either debug, warn or error. Whatever you had put in the MDC will be printed out. You need to configure you log4j.properties. Notice the %X{RequestId}. This referes to the key name which is inserted in the requestInitialized() above.

log4j.appender.A1.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSSS} %p %C %X{RequestId} - %m%n

I also found this link to be helpful -> What is the difference between Log4j's NDC and MDC facilities?

You have three different problems to solve:

  1. Generate an unique id for each request
  2. Store the id and access it everywhere in the code
  3. Log the id automatically

I would suggest this approaches

  1. Use a Servlet filter or a ServletRequestListener (as suggested by M. Deinum) or a Spring Handler Interceptor to intercept the request in a general way, there you can create a unique id, maybe with an UUID

  2. You can save the id as an attribute of the request, in this case the id would propagate just in the controller layer, not in the services. So you can solve the problem using a ThreadLocal variable or asking Spring to do the magic with the RequestContextHolder : the RequestContextHolder will allow you to access the request of that specific thread, and the request attributes as well, in the service layer. The RequestContextHolder use ThreadLocal variable to store the request. You can access the request in this way:

     ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); // Extract the request HttpServletRequest request = attr.getRequest();
  3. There is an interesting article ( 2018 alternative ), if you are using log4j, on the customization of the pattern layout of the logger. However, youncan simply create a proxy of your logging system interface and append manually the id to every logged string.

You can also use the "Fish Tagging" in Log4j 2. It is the same idea like MDC and NDC (thread-basis) as described inhttps://logging.apache.org/log4j/2.x/manual/thread-context.html

Here you can use either the Thread Context Stack or the Thread Context Map. An example for the Map looks like this:

//put a unique id to the map
ThreadContext.put("id", UUID.randomUUID().toString()

//clear map
ThreadContext.clearMap();

And for the pattern in log4j2.xml you can also use the %X{KEY} tag.

To put a new id to the map (for example for every incoming request) you can do that in an ServletRequestListener implementation how Sharadr described it.

If you don't mind using spring 4.1.3 or later, you can wrap your request into a custom subclass of ContentCachingRequestWrapper class.

public class MyHTTPServletRequestWrapper extends ContentCachingRequestWrapper {

    private UUID uuid;

    public MyHTTPServletRequestWrapper (HttpServletRequest request) {
        super(request);
        uuid = UUID.randomUUID();
    }

    public UUID getUUID() {
        return uuid;
    }
}

In your spring controller, add the request to the method's param, and cast it to your custom wrapper:

@RequestMapping(value="/get", method = RequestMethod.GET, produces="application/json")
    public @ResponseBody String find(@RequestParam(value = "id") String id, HttpServletRequest request) {
        MyHTTPServletRequestWrapper wrappedRequest = (WGHTTPServletRequestWrapper)request;
        System.out.println(wrappedRequest.getUUID());
...
}

You will need to user filter though, to connect the dots:

public class RequestLoggingFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest requestToCache = new MyHTTPServletRequestWrapper((HttpServletRequest) request);
System.out.println(((WGHTTPServletRequestWrapper)requestToCache).getUUID());
        chain.doFilter(requestToCache, response);

    }
}

You will find that the printlin from RequestLoggingFilter.doFilter() and from controllers getId() will produce the same UUID.

You will just need to play around to use the UUID in appropriate places, but at least you have the value in your controller where is the start of your business logic.

You could use the @Scope annotation to load a new instance of the service for each request.

See official documentation here

If you want to inject this in a non-request aware service, you should use proxyMode option. more info here (from this stack question)

You must use a database sequence (if your database is ORACLE) as follows

create sequence "sequence_Name";

String uniqueString=sessionFactory.getCurrentSession(). createSQLQuery("SELECT \"sequence_Name\".nextval FROM dual").list().get(0);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM