I am writing an interceptor for the service methods in my spring boot application like below:
@Aspect
public class MyAspect {
public MyAspect() {
}
@Pointcut("@within(org.springframework.stereotype.Service)")
public void applicationServicePointcut() {
}
@Before(value = ("applicationServicePointcut()"))
public void process(JoinPoint joinPoint) throws Exception {
...
}
}
One such service is as follows:
@Service
@Transactional
public class AService {
public ADTO create(ADTO aDTO) {
...
}
public ADTO update(ADTO aDTO) {
...
}
}
Another service can be as follows:
@Service
@Transactional
public class BService {
public BDTO create(BDTO bDTO) {
...
}
public BDTO update(BDTO bDTO) {
...
}
public void doSomething(String a, int b) {
...
}
}
Here my goal is to extract the value of a particular field from the associated method arguments. In order to do this, I can write a single function in aspect, where I can have multiple if-else blocks as follows:
String extractMyField(JoinPoint joinPoint) {
//extract the arguments of the associated method from joinpoint
//depending on the type of arguments, extract the value of myfield as follows
if(arg instance of ADTO) {
...
} else if (arg instance of BDTO) {
...
}
...
}
It can be seen in the above code snippet, there can be the following cases:
Also, I cannot change the DTO objects as well.
I am wondering if there can be a better approach to do this. Basically, I am looking for a way to make it easily extensible.
Could anyone please help here? Thanks.
EDIT
As of now, in order to process the method arguments, first I have been collecting the method arguments as follows:
Map<String, Object> getMethodArguments(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Map<String, Object> argumentNameValueMap = new HashMap<>();
if (methodSignature.getParameterNames() == null) {
return argumentNameValueMap;
}
for (int i = 0; i < methodSignature.getParameterNames().length; i++) {
String argumentName = methodSignature.getParameterNames()[i];
Object argumentValue = joinPoint.getArgs()[i];
Class argumentType = joinPoint.getArgs()[i].getClass();
//check if i th parameter is json serializable
if (Objects.isNull(methodSignature.getParameterTypes()) ||
isJsonSerializable(methodSignature.getParameterTypes()[i])) {
argumentNameValueMap.put(argumentName, mapper.convertValue(argumentValue, Map.class));
}
}
return argumentNameValueMap;
}
private boolean isJsonSerializable(Class parameterType) {
return
!Objects.isNull(parameterType) &&
!parameterType.getName().endsWith("HttpServletRequest") &&
!parameterType.getName().endsWith("HttpServletResponse") &&
!parameterType.getName().endsWith("MultipartFile");
}
Actually, the catch here is that the field I am looking for is named differently across DTOs. For example, in ADTO the field is named as id
whereas, in BDTO, it's named as aId
.
Ok, then let me suggest the following:
I assume, you can't change the DTO objects I assume, sometimes your methods won't even have DTO parameters, but you have a logic to "find" them if they exist
If you know what are the types of the intercepted DTOs are, you can create the interface:
interface DTOHandler {
Class<?> getDTOClass();
Object getMyField(Object dtoObject);
}
Implement this interface for each DTO
For example, if you plan to intercept 2 different types of DTOs:
class ADTO {
Integer myField;
}
class BDTO {
String myField;
}
Then implement:
class DTOAHandler implements DTOHandler {
Class<?> getDtoClass() {return ADTO.class;}
Object getMyField(Object obj) {return ((ADTO)obj).getMyField();}
}
// and do a similar implementation same for DTOB
Now define all these handlers as spring beans and inject a list of them into the aspect. Spring boot will inject them all as a list, the order doesn't matter
In the constructor of the aspect create a map of Class<?> -> DTOHandler. Iterate throw the list and create a key of the map as a call of 'getDTOClass' and the value as a handler itself:
public class MyAspect {
private Map<Class<?>, DTOHandler> handlersRegistry;
public MyAspect(List<DTOHandler> allHandlers) {
handlersRegistry = new HashMap<>();
for(DTOHandler h : allHandlers) {
handlersRegistry.put(h.getDTOClass(), h);
}
}
}
With this setup the extractMyField
will look as follows:
public void extractMyField(JoinPoint jp) {
Object myDTO = findDTOParameter(jp);
DTOHandler handler = this.handlersRegistry.get(myDTO.getClass());
if(handler != null) {
// there is an actual handler for this type of DTOs
Object myField = handler.getMyField(myDTO);
}
else {
// the aspect can't handle the DTO of the specified type
}
}
Update (based on Op's comment):
In order to actually find a parameter that has a 'myfield' value, you've used a pretty complicated logic (in the EDIT section of the question).
Instead you can create an annotation (runtime retention) that can be applied to the parameters.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface @DTO {
}
Then you can annotate with this annotation the parameters in the service to "help" your aspect to understand which of the parameters are actually DTOs:
@Service
@Transactional
public class AService {
public ADTO create(@DTO ADTO aDTO) {
...
}
public ADTO update(@DTO ADTO aDTO) {
...
}
}
Then, the resolution logic in the aspect will become something like:
// pseudocode
foreach p in 'parameters' {
if(isAnnotatedWith(p, DTO.class)) {
// it matches
}
}