In my application I perform some business logic for example I have business logic methods:
@Override
@ByPassable(exceptions = {"InvalidIdentityException"})
public void validate(Model model) {
if (nonNull(model)) {
final boolean test = isOk(model.getIdentity());
if (test) {
throw new InvalidIdentityException("Invalid bla bla");
}
}
}
And Custom Exception Class:
public class InvalidIdentityException extends SomeException {
public InvalidIdentityException (final String message) {
super(message);
}
}
@ByPassable
on methods takes list of exceptions that can be bypassed so in this instance InvalidIdentityException
is thrown and it becomes bypassable
for near future when this method is re-executed.
I initiate a bean for my spring boot application that has the set of bypassable exceptions:
public class Config {
@Bean("bypassable-exceptions")
public Set<String> getBypassableExceptions() {
final Set<String> exceptions = new HashSet<>();
new Reflections(new MethodAnnotationsScanner())
.getMethodsAnnotatedWith(ByPassable.class).stream()
.filter(method -> method.getAnnotation(ByPassable.class).enabled())
.forEach(method -> {
final String[] exceptions = method.getAnnotation(ByPassable.class).exceptions();
exceptions.addAll(Arrays.asList(exceptions));
});
return exceptions;
}
}
Whenever an exception that is Bypassable
is thrown in a method, my application persists the Throwable
object as a Json in the database however I need to have an additional boolean property bypassable
on this throwable object that should be updated @BeforeThrowing
the exception as interception. Is this possible?
public class ExceptionAspect {
@Pointcut("@annotation(com.services.aop.ByPassable)")
public void byPassableExceptionMethods() {
}
@BeforeThrowing(pointcut = "byPassableExceptionMethods()", throwing = "exception")
public void beforeThrowingAdviceForByPassableExceptionMethods(final JoinPoint jp,
final Throwable exception) {
// check against the set of bypassable exceptions and update a custom property on the exception
class so when Throwable is persisted it is persisted with this customer property e.g. bypassable
= true
}
From the Spring reference documentation : AOP Concepts there is no advice type @BeforeThrowing
.
In Spring AOP , a method execution (joinpoint) can be adviced - before the method begins , after the method finishes ( with or without exception) or around ( before the method begins and after the method finishes). This also means that a logic within that method cannot be altered during runtime , only the input to or the result of the method execution can be manipulated.
As per the code logic shared , the exception is thrown based on a validation within a method and Spring AOP does not provide a handle to advice before the excpetion is thrown.
Having said that following are ways I can think of to achieve the same.
bypassable
can be set during exception instance creation time itself. This would be the simplest way.Following are the Spring AOP ways I came up to achieve the same.
@AfterThrowing
can be used as follows to set the bypassable.
@BeforeThrowing
can be simulated.
Note : Internal calls cannot be intercepted using Spring AOP . Relevant information from the reference documentation can be found under section.
Due to the proxy-based nature of Spring’s AOP framework, calls within the target object are, by definition, not intercepted.
For this reason , for demonstration purpose , the example code autowires own reference. The method to throw exception may be moved to another bean and intercepted similarly.
Following changes are done for the example.
Bypassable exceptions to have a common base class
public class BaseBypassableException extends RuntimeException {
private boolean bypassable;
public BaseBypassableException(String message) {
super(message);
}
public boolean isBypassable() {
return bypassable;
}
public void setBypassable(boolean bypassable) {
this.bypassable = bypassable;
}
}
Bypassable exception extends from the common base class
public class InvalidIdentityException extends BaseBypassableException {
public InvalidIdentityException(String message) {
super(message);
}
}
Method to advice modified as follows. ( Example has String
instead of Model
)
@Component
public class BypassableServiceImpl implements BypassableService {
@Autowired
BypassableService service;
@Override
@ByPassable(exceptions = {"InvalidIdentityException"})
public void validate(String model) {
if (null != model) {
final boolean test = !("Ok".equals(model));
if (test) {
service.throwException(new InvalidIdentityException("Invalid bla bla"));
}
}
System.out.println("Validate called : "+model);
}
@Override
public void throwException(BaseBypassableException exception) {
throw exception;
}
}
Aspect to advice both the methods. throwing
filters based on the exception type, so for the example I have not included the logic to check the bypassableExceptionNames
and the logic safely assumes the exception is of type BaseBypassableException
. Logic may be modified to include the check if required.
@Component
@Aspect
public class ExceptionAspect {
@Autowired
@Qualifier("bypassable-exceptions")
Set<String> bypassableExceptionNames;
@Pointcut("@annotation(com.services.aop.ByPassable)")
public void byPassableExceptionMethods() {
}
@AfterThrowing(pointcut = "byPassableExceptionMethods()", throwing = "exception")
public void afterThrowingAdviceForByPassableExceptionMethods(final JoinPoint jp,
final BaseBypassableException exception) {
System.out.println(jp.getSignature());
System.out.println("Before " + exception.isBypassable());
exception.setBypassable(true);
System.out.println("After " + exception.isBypassable());
System.out.println(exception);
}
@Before("execution(* com.services..*.*(..)) && args(exception)")
public void beforeThrowingAdviceForByPassableExceptionMethods(final JoinPoint jp,
final BaseBypassableException exception) {
System.out.println(jp.getSignature());
System.out.println("Before " + exception.isBypassable());
exception.setBypassable(true);
System.out.println("After " + exception.isBypassable());
System.out.println(exception);
}
}
Hope this helps