4

Consider a UrlValidator method annotation that tests if a given url is valid before calling a method.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UrlValdator{
    String value();
}

This is working fine when routes are static and known ahead of time. For example:

@UrlValidator("http://some.known.url")
public void doSomething();

But this is not very flexible. For example, what if the route was implicit in the doSomething() method signature? Could I somehow access it form the Spring Expression Language, or some other means? For example, this doesn't work but is what I'm shooting for

@UrlValidator("#p1")
public void doSomething(String url)

or

@UrlValidator("#p1.url")
public void doSomething(Request request)

Is it possible to make annotations dynamic this way?

Related

This is the closest I've found, but the thread is old and the accepted answer is quire cumbersome/hard to follow. Is there a minimal working example/updated way to do this?

Adam Hughes
  • 14,601
  • 12
  • 83
  • 122
  • You could try spring AOP, creating pointcut around @UrlValidator annotated controller methods. – Michal Jun 15 '18 at 21:14
  • I am using spring AOP already, but don't understand how a pointcut would help me achieve this. Can you provide an example – Adam Hughes Jun 15 '18 at 21:43

2 Answers2

8

I'm not entirely sure if that's what you had in mind, but i can suggest using Spring AOP as it can give you a lot of flexibility.

Since you've mentioned in one of the comments that you're already using Spring AOP, I'm going to assume that you've added spring-boot-starter-aop as a dependency and that you've enabled support for handling components marked with @Aspect by annotating one of your config classes with @EnableAspectJAutoProxy

For example, having defined annotations as such:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EnsureUrlValid {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface UrlToVerify {
}

I can use them in a sample spring component as follows:

@Component
public class SampleComponent {

    private static final Logger logger = LogManager.getLogger(SampleComponent.class);

    @EnsureUrlValid
    public void fetchData(String url) {
        logger.info("Fetching data from " + url);
    }

    @EnsureUrlValid
    public long fetchData(Long id, @UrlToVerify String url) {
        logger.info("Fetching data for user#" + id + " from " + url);
        // just to show that a method annotated like this can return values too
        return 10L;
    }

    @EnsureUrlValid
    public void fetchDataFailedAttempt() {
        logger.info("This should not be logged");
    }
}

And here's a sample "processor" of the EnsureUrlValid annotation. It looks for the annotated methods, tries to extract the passed-in url and depending on whether the url is valid or not, it proceeds with invoking the method or throws an exception. It's simple but it shows that you have complete control over the methods that you've annotated.

@Aspect
@Component
public class UrlValidator {

    @Around(value = "@annotation(EnsureUrlValid)")
    public Object checkUrl(ProceedingJoinPoint joinPoint) throws Throwable {
        final Optional<String> urlOpt = extractUrl(joinPoint);
        if (urlOpt.isPresent()) {
            final String url = urlOpt.get();
            if (isUrlValid(url)) {
                return joinPoint.proceed();
            }
        }
        throw new RuntimeException("The passed-in url either could not be resolved or is not valid");
    }

    private Optional<String> extractUrl(JoinPoint joinPoint) {
        Object[] methodArgs = joinPoint.getArgs();

        Object rawUrl = null;
        if (methodArgs.length == 1) {
            rawUrl = methodArgs[0];
        }
        else if (methodArgs.length > 1) {
            // check which parameter has been marked for validation
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            Parameter[] parameters = method.getParameters();
            boolean foundMarked = false;
            int i = 0;
            while (i < parameters.length && !foundMarked) {
                final Parameter param = parameters[i];
                if (param.getAnnotation(UrlToVerify.class) != null) {
                    rawUrl = methodArgs[i];
                    foundMarked = true;
                }
                i++;
            }
        }

        if (rawUrl instanceof String) { // if rawUrl is null, instanceof returns false
            return Optional.of((String) rawUrl);
        }
        // there could be some kind of logic for handling other types

        return Optional.empty();
    }

    private boolean isUrlValid(String url) {
        // the actual validation logic
        return true;
    }
}

I hope it's somewhat helpful.

Joe Doe
  • 889
  • 8
  • 4
2

Short answer: Yes.

Long answer: ElementType specifies the target of the annotation, which can be the following: ANNOTATION_TYPE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE, and TYPE_PARAMETER. Were are interested in PARAMETER here. Since we want from the compiler the run our code, RetentionPolicy.RUNTIME is fine for the retention type. Next we have to add @Constraint annotation, which according to the documentation:

Marks an annotation as being a Bean Validation constraint.

This means, Spring will pick up your parameter and validate it in runtime. The last thing we have to do is to implement the validation itself which implies creating a class which implements ConstraintValidator interface.

Putting it all together:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UrlValidatorImplementation.class)
public @interface UrlValidator{
    String message() default "Invalid url";
}

Implementation of the UrlValidatorImplementation class:

public class UrlValidatorImplementation implements ConstraintValidator<UrlValidator, String> {
    @Override
    public void initialize(UrlValidator annotation) {
        // initialization, probably not needed
    }

    @Override
    public boolean isValid(String url, ConstraintValidatorContext context) {
        // implementation of the url validation
    }
}

Usage of the annotation:

public void doSomething(@UrlValidator url) { ... }
Ervin Szilagyi
  • 14,274
  • 2
  • 25
  • 40
  • This certainly looks like what I had in mind. Not that it's super necessary, but what about the case when the field is on an object. For example `@UrlValidator(obj.url)`. That's probably not feasible right? Only reason I ask is because a few of the URLs I want to validate are on objects being passed into methods. Perhaps a refactor is in order :0 – Adam Hughes Jun 16 '18 at 01:34
  • `ConstraintValidator` - instead of `String` you pass in any `Object` you want. Also, the validation method will have the following signature: `public boolean isValid(Object object, ConstraintValidatorContext context)` – Ervin Szilagyi Jun 16 '18 at 11:46