Search code examples
javaspringspring-bootmicroservices

Change Return type of Overridden Methods?


The below is the "ControllerAdvice" and it acts as the Global Exception Handler for Multiple MicroServices.

// Custom Exception Handler
@ExceptionHandler(value = CustomException.class)
public ResponseEntity<ErrorResponse> customException(CustomException ex) {
    ErrorResponse error = new ErrorResponse(ex.getErrorDescription(), ex.getErrorMessage());
    return new ResponseEntity<>(error, ex.getStatus());
}

@ExceptionHandler(value = Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse globalException(Exception ex) {
        
    return new ErrorMessage("INTERNAL_SERVER_ERROR", "500");    
}

Now for One of my MicroService, I need to customize the Error Response coming from the above Exception Handler i.e I need to Wrap the "ErrorResponse" coming from the Global Exception Handler under another Object called "ErrorResponseWrapper"

class ErrorResponseWrapper extends ErrorResponse {

   String type;

   // getter & setters
}

Now in My Microservice where I need to Customise the error Response, I am just Overriding the GlobalExceptionHandler methods

@RestControllerAdvice
public class ControllerException extends GlobalExceptionHandler {

    // This Works FINE...
    @Override
    public ErrorResponseWrapper globalException(Exception ex) {
        // 
        ErrorResponse error = super.globalException(ex);

        // New Response Wrapper
        ErrorResponseWrapper wrapper = new ResponseWrapper();
        wrapper.setType("SOMETHING");
        
        return wrapper;
    }

//Issue is here:- 
//   I get Compilation Error: ReturnType is Not Compatible with the GlobalException return type.

    @Override
    public ResponseEntity<ErrorResponseWrapper> customException(CustomException ex) {
        
        ErrorResponse error = super.customException(ex).getBody();

        // New Response Wrapper
        ErrorResponseWrapper wrapper = new ResponseWrapper();
        wrapper.setType("SOMETHING");
    
        return new ResponseEntity<>(wrapper,ex.getStatus());
}

}

It works fine with the First Overridden method, why is it different from the ResponseEntity? Is there anything I am doing wrong, or how do I achieve this?


Solution

  • Your problem is generics, and that they are invariant by default, which trips up loads of people and may not immediately come across as logical or correct (but, it is - keep reading).

    There are these 3 concepts, called invariant / covariant / contravariant.

    Covariant

    covariant means that a subtype of a thing is just as good as the thing.

    Java basic typing is covariant. Here:

    Integer x = 5;
    Number n = x; // This compiles and runs just great.
    

    Note how the type of x is Integer, which is a subtype of Number, and yet that is fine here. That's because covariance is applied here.

    Contravariance

    Contravariance is the inverse: A supertype is a fair standin. This is actually part of java: When overriding methods, you can use supertypes for parameters and that is fine. This compiles:

    public class Parent {
        public void foo(String x) {}
    }
    
    public class Child extends Parent {
        @Override public void foo(Object x) {}
    }
    

    The reason this is okay should be obvious: Because common sense: All strings are also objects therefore the foo method that Child defines can handle all possible cases of invoking foo whenever an instance of Child is treated as if its a Parent. It can handle all of these (that'd be when x is a String), and more to boot.

    Generics and variance

    Generics is invariant by default. That's a head twister! Let's check it out:

    List<String> list1 = new ArrayList<String>();
    List<Object> list2 = list1; // DOES NOT COMPILE!!
    

    That's bizarre, isn't it? But it makes sense! After all, if the above code works, list1 and list2 are both pointing at the same actual list; therefore if you make a change to this one list via the list1 ref, you'd be able to see the change when using the list2 ref. But, therein lies the problem: I can run list2.add(new Object()), but that would mean that list1.get() returns an Object and not a String.

    That is why generics are invariant: Because the universe works that way.

    This applies everywhere. Thus, we get to the crux of your problem here:

    ResponseEntity<ErrorResponseWrapper> is not a subtype of ResponseEntity<ErrorResponse>!

    Fortunately, generics are quite flexible. You can have covariance and even contravariance if you want it; just, ask for it. Here, now it works fine:

    public class Parent {
        public ResponseEntity<? extends ErrorResponse> foo() {}
    }
    
    public class Child extends Parent {
        @Override public ResponseEntity<? extends ErrorResponseWrapper> foo() {}
    }
    

    That does work fine. ? extends is java-ese for: I want generics, but covariant. The compiler will then help you out and prevent you from breaking the world. Let's try it:

    List<String> list1 = new ArrayList<>();
    List<? extends Object> list2 = list1; // now it compiles!
    list2.add(new Object()); // but this does not!
    

    Calling .add() on any List<? extends Y> where Y can be anything you please does not compile, unless you pass a literal null and only because null is every type. That is on purpose - it lets you assign that list-of-strings to the ref.

    The fix

    Go back to the original type and update the return type. If you can't do that, there is nothing you can do here.