Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

io.micronaut.validation.Validated does not work on final classes #1291

Closed
David-16 opened this issue Apr 21, 2021 · 7 comments
Closed

io.micronaut.validation.Validated does not work on final classes #1291

David-16 opened this issue Apr 21, 2021 · 7 comments

Comments

@David-16
Copy link

David-16 commented Apr 21, 2021

Using Micronaut @Validated appears to generate proxies which extend the class

Compiling the following:

@io.micronaut.validation.Validated
@org.immutables.value.Value.Immutable
@com.fasterxml.jackson.databind.annotation.JsonSerialize(as = ImmutableFoobar.class)
public interface Foobar {
    String getFoo();
}

Results in following:

[ERROR] /blah/blah/target/generated-sources/annotations/ImmutableFoobar.java:[28,13] 
error: Cannot apply AOP advice to final class. Class must be made non-final to support proxying: ImmutableFoobar

Note the compile error is likely from Micronaut's pre-processor. it goes away if not using @JsonSerialize but the usecase here is to use this as a payload so not really an option

Micronaut is a compile time oriented dependency injector and does not rely on reflection, so I get why Micronaut probably needs to extend the class to make a proxy

I also get why Immutables makes the class final (at least I think). But the constructor is private so extending it is already hampered by that, plus everything is basically final within the class anyway, so I do not think making it final really buys much in terms of safety

I guess I could generate the Immutable class, add it to my source file, make it non-final, comment out the @Immutable annotation, and repeat this exercise if/when the class changes

Is there any middle ground?

@elucash
Copy link
Member

elucash commented Apr 22, 2021

does not rely on reflection, so I get why Micronaut probably needs to extend the class to make a proxy

modern reflection also uses runtime bytecode generation in combination with or instead of native calls. The only difference is that micronaut tries to use compile-time bytecode rewriting/generation.

Whereas I'm a bit upset that "modern" frameworks resort to such a strange and uncivilized solution like generating proxies around validated objects i.e. this is not a service endpoint or something worth intercepting. How are they suppose to handle new java record types which are final by design? (rhetorical question, anyway).

There are some precedents of us adding feature flags like finalInstanceFields ,transientDerivedFields, protected(private)NoargConstructor, etc just to accommodate some old-school reflection frameworks. But just adding another flag to dispense with other finals seems like counter-productive in terms of java language where final keyword is used to disallow subclassing/overriding to prevent misuse.

Here's some of the alternatives (no guarantee that these will work, but):

  1. It is might be possible to use (but not for all scenarios i guess) to use @Value.Modifiable, @JsonDeserialize(as = Modifiable.class). Together with Style(create="new" modifiable will not be generated as final, will have plain constructor, it is easy to convert to immutable and back: toImmutable``Modifiable.from. It's harder with recursive immutability, but some of that doable too.

  2. I dunno if this will work, but what about @JsonDeserialize(builder = Builder.class

@Immutable interface X {
  int attr();
  class Builder extends ImmutableX.Builder {}
}
  1. Just don't use exotic validation. Use precondition checking https://immutables.github.io/immutable.html#precondition-check-method
@Immutable interface X {
   int attr();
   @Value.Check default void validate() {
     if (attr() < 0) throw or report something
   }
}
  1. For JavaBean validation see (lengthy) discussion in validation: support for javax.validation API #26 . Basically you can invoke it manually from precondition @Check or by using Style.validationMethod=VALIDATION_API

@David-16
Copy link
Author

David-16 commented Apr 28, 2021

I will not argue, but that is an unexpected stance from someone working on this library. I mean, why does Immutables extend my class? Why not just inject random byte code? I.e., why not use Lombok? (Which does have a non final option, though I am no Lombok exert). All leading and rhetorical questions

I completely understand your frustration, though. I write frameworks and I hate things like this, too, so I know where you are coming from

Feel free to close this if it will not be considered. In meantime, I will try the proposed solutions and see if anything stands out

@elucash
Copy link
Member

elucash commented May 30, 2021

unexpected stance from someone working on this library

Just to clarify, we doing legitimate annotation processing by providing generating types which extends user written types or act as supertypes to the user written types. We're completely follow JLS and compilation related spec (including annotation processing JSR etc): we analyze type and method signatures and generate standalone Java files. You define structure of a datatype, Immutables processor generates an implementation. It just cannot be criticized by "why does Immutables extend my class", it's legit, you've added annotation to generate derived implementation – that's exactly what annotation processor were designed for.
Lombok is a Java compiler plugin (disguised as annotation processor, but it's not), it manipulates ASTs in a way which is not in any way specified in any Java language or compilation related specification. If you use Lombok – you're coding in non-compliant Java dialect (having IDE plugin doesn't make it any more legitimate).
Bytecode pre/postprocessing used by validation is something which is borderline ok. AOP advise might decide to intercept calls to constructor/factories or invokation of service methods. The AOP implementation / bytecode processor actually can rewrite even final classes. The main problem is that data-carrier types are often supposed to be final, newly added Java records are final and so on. I think that particular modus operandi is just mistake on the part of Micronaut Validation. Immutables are also full of mistakes here or there, but I think we've got the core functionality right.
But yeah – all that "magic" is all the same crap, no difference for unsuspecting stranger.

@David-16
Copy link
Author

David-16 commented Jun 4, 2021

I thought the Lombok thing would get to you, which is why your stance on final is still a bit odd to me particularly given your statement:

we are doing legitimate annotation processing

To which I say: what do you think Micronaut is doing?

I think Micronaut does what Immutables does: generate real code I can look at

They are generating compile time code using preprocessors. They could have gone the Lombok route and totally bypassed JLS restrictions as you point out (which is why I brought Lombok up in a provoking manner) or the Spring route and bypassed it using reflection or runtime proxies

But, since they generate source code instead of inject bytecode, they have to follow the rules. And so, in this particular case, they cannot extend and wrap a final class to instrument it with generated JSR validation code. Thus the ask for an option to make the class not be final (which I still think does nothing to invalidate the immutable nature of the class). However, as I said, I understand your stance, but hopefully this helps you understand where I am coming from

I suspect you might see more of this as java web frameworks like Micronaut, Quarkus, Helidon, and even Spring move away from runtime reflection and towards compiling stuff ahead of time because how else are you really going to implement validation during pre-compile time other than extending the class and doing aspect oriented injection stuff?

@elucash
Copy link
Member

elucash commented Jun 4, 2021

ok, I stand corrected about their AOP, it was known to me that Micronaut uses annotation processor for dependency injection and wiring, but I was under wrong impression that for AOP they use bytecode post-(compilation) or pre-(classloading)processing. If AOP uses annotation processing then it's changes very little if anything as my major concern is "how" it is used that it requires data types to be non-final. I went on to actually check what is going on there.
first there this (Javadoc excerpt)

io.micronaut.validation
Annotation Type Validated

@Documented
 @Retention(value=RUNTIME)
 @Target(value={TYPE,METHOD})
 @Around
 @Type(value=ValidatingInterceptor.class)
public @interface Validated
Around advice that ensures an objects methods are validated.
Since:
1.0
Author:
Graeme Rocher

That caught my attention Around advice that ensures an objects methods are validated.. After reading this I've started to google for more detailed documentation, but I've found couple of pages but none of those matches the issue described in this Github ticket. At least in my head, so I'm not sure that I'm understand things.

  1. Is this @Validated annotation should really be applied to datatypes or rather to the types/classes/services which have methods receiving datatypes? From the Javadoc documentation and from some github/stackoverflow snippets I've seen people put @io.micronaut.validation.Validated of a bean which receives data object, not the data object itself. As I've mentioned it's OK to proxy/extend service object to apply around advices, and there's no need to intrude into data objects or extends them, only controller/services objects or their methods are to be annotated.
    Excerpt from https://docs.micronaut.io/latest/guide/index.html#beanValidation
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.validation.Validated;

import javax.validation.Valid;
import java.util.Collections;

@Validated 
@Controller("/email")
public class EmailController {

    @Post("/send")
    public HttpResponse send(@Body @Valid Email email) { 
        return HttpResponse.ok(Collections.singletonMap("msg", "OK"));
    }
}
// Annotate the controller with Validated
// Annotate the POJO to validate with @Valid

If that is the case, then immutable final classes have nothing to do with this feature annotation, right?

  1. What kind of validation engine Micronaut uses, I mean actual one, under the hood. Is it just Bean Validation (JSR 330) implementation like Hibernate Validator? If that is the case, then why insist of using Micronaut's annotation but not just take advantage of BeanValidation integration we already have?

For JavaBean validation see (lengthy) discussion in #26 . Basically you can invoke it manually from precondition @Check or by using Style.validationMethod=VALIDATION_API

Regardless of the integration path, you should clearly understand when and what calls will be "intercepted" and how actual validation will kick in. Will it be during immutable construction, or before passing it to controller/or another adviced method.

My apologies if I've got something wrong again, but I currently don't understand the issue after looking at Micronaut documentation.

@David-16
Copy link
Author

David-16 commented Jun 4, 2021

First, I misspoke about Micronaut generating source code during pre-compilation phase; it was just my IDE de-compiling stuff and me not paying attention. Micronaut seems to generate hints from the annotations that would otherwise be read at run time and generates a bunch of helper classes for each kind of validation. That much I kind of knew before but I never stared at the classes before because they are just generated junk for the most part. However, after doing said staring, I cannot see anywhere where the class is actually extended. Maybe I should look at the source of the pre-processor to see what it is doing when it checks for final

Regarding #1, you raise a good point. My issue arose while using a swagger code generator which spits out source code. It chooses to annotate the datatype; not the controller. Let me try reversing that to align with the examples in the documentation and see if that does not resolve things before I waste your time on this. If it works, I will move my beef there

Regarding #2, I do not think Micronaut has their own validator. The docs suggest adding io.micronaut.beanvalidation:micronaut-hibernate-validator to your project (see https://docs.micronaut.io/latest/guide/#datavalidation). So I assume this uses Hibernate under the covers as the name suggests, but I have not dug into it much. As part of my tests, I will force a validation error and check the stacktrace.

@David-16
Copy link
Author

David-16 commented Jun 4, 2021

Okay, seems like this was a waste of your time, for which I apologize

The swagger codegen tool I was using was annotating the model classes so I assumed that was the correct thing to do. Seems like that is not the case. Adding annotations on the model class does not seem to enforce validation. Will have to follow up with codegen project to see why they are doing this

The minimum that seems to be required is to annotate the rest controller method argument with @Valid and annotate the class of the type in the aforementioned method with @Introspected so Micronaut will process it and generate the hints

Example that does the correct thing by enforcing payload size:

@Controller("/v1")
public class BasicRestService {
    @Post("/hello")
    public String sayHello(@Valid final Payload payload) {
        return String.format("Hola, %s!\n", payload.getName());
    }
}

@Introspected
@Immutable
@JsonDeserialize(as = ImmutablePayload.class)
public interface Payload {
    @Size(min = 1, max = 11)
    String getName();
}

I did learn that if you do not include io.micronaut.beanvalidation:micronaut-hibernate-validator then it will default to some other implementation, but I do not know what it is

Again, sorry to waste your time by not doing proper due diligence

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants