playframework / play-ebean

Play Ebean module
Apache License 2.0
102 stars 69 forks source link

sbt-play-ebean 6.2.0 doesn't work with Play Framework 2.8.20 (NoClassDefFoundError) #388

Closed dEajL3kA closed 11 months ago

dEajL3kA commented 1 year ago

After upgrading sbt-play-ebean to version 6.2.0, the application based on Play Framework 2.8.20 does not work anymore:

(works fine with sbt-play-ebean version 6.0.0)

[error] java.lang.NoClassDefFoundError: sbt/internal/inc/PlainVirtualFileConverter
[error]         at play.ebean.sbt.PlayEbean$.$anonfun$ebeanEnhance$1(PlayEbean.scala:98)
[error]         at scala.Function1.$anonfun$compose$1(Function1.scala:49)
[error]         at sbt.internal.util.$tilde$greater.$anonfun$$u2219$1(TypeFunctions.scala:62)
[error]         at sbt.std.Transform$$anon$4.work(Transform.scala:67)
[error]         at sbt.Execute.$anonfun$submit$2(Execute.scala:281)
[error]         at sbt.internal.util.ErrorHandling$.wideConvert(ErrorHandling.scala:19)
[error]         at sbt.Execute.work(Execute.scala:290)
[error]         at sbt.Execute.$anonfun$submit$1(Execute.scala:281)
[error]         at sbt.ConcurrentRestrictions$$anon$4.$anonfun$submitValid$1(ConcurrentRestrictions.scala:178)
[error]         at sbt.CompletionService$$anon$2.call(CompletionService.scala:37)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
[error]         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[error]         at java.lang.Thread.run(Thread.java:750)
[error] Caused by: java.lang.ClassNotFoundException: sbt.internal.inc.PlainVirtualFileConverter
[error]         at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
[error]         at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
[error]         at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
[error]         at play.ebean.sbt.PlayEbean$.$anonfun$ebeanEnhance$1(PlayEbean.scala:98)
[error]         at scala.Function1.$anonfun$compose$1(Function1.scala:49)
[error]         at sbt.internal.util.$tilde$greater.$anonfun$$u2219$1(TypeFunctions.scala:62)
[error]         at sbt.std.Transform$$anon$4.work(Transform.scala:67)
[error]         at sbt.Execute.$anonfun$submit$2(Execute.scala:281)
[error]         at sbt.internal.util.ErrorHandling$.wideConvert(ErrorHandling.scala:19)
[error]         at sbt.Execute.work(Execute.scala:290)
[error]         at sbt.Execute.$anonfun$submit$1(Execute.scala:281)
[error]         at sbt.ConcurrentRestrictions$$anon$4.$anonfun$submitValid$1(ConcurrentRestrictions.scala:178)
[error]         at sbt.CompletionService$$anon$2.call(CompletionService.scala:37)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
[error]         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[error]         at java.lang.Thread.run(Thread.java:750)

I think this is a bug, since sbt-play-ebean version 6.2.0 is supposed to work with Play Framework 2.8.18+ 😢

mkurz commented 1 year ago

Which sbt version are you using in project/build.properties? I recommend 1.7.2

mkurz commented 1 year ago

See https://github.com/playframework/play-ebean/releases/tag/6.2.0-RC4

play-ebean 6.2.0-RC4 requires sbt 1.4 or newer

dEajL3kA commented 1 year ago

Thanks for response!

I have tried SBT version 1.4.9, 1.5.8, 1.6.2 as well as 1.7.3. But result is the same.

mkurz commented 1 year ago

Use 1.7.2, shut down sbt, remove the target folder (also project/target) and try again

dEajL3kA commented 1 year ago

Use 1.7.2, shut down sbt, remove the target folder (also project/target) and try again

Okay, as soon as I change SBT to 1.5.x or newer (including 1.7.2) in project/build.properties the build will fail with:


[error] java.lang.RuntimeException: found version conflict(s) in library dependencies; some are suspected to be binary incompatible:
[error]
[error]         * org.scala-lang.modules:scala-xml_2.12:2.1.0 (early-semver) is selected over {1.2.0, 1.1.1, 1.0.6}
[error]             +- com.github.sbt:sbt-native-packager:1.9.16 (sbtVersion=1.0, scalaVersion=2.12) (depends on 2.1.0)
[error]             +- org.scala-lang:scala-compiler:2.12.16              (depends on 1.0.6)
[error]             +- com.typesafe.sbt:sbt-native-packager:1.5.2 (scalaVersion=2.12, sbtVersion=1.0) (depends on 1.1.1)
[error]             +- com.typesafe.play:twirl-api_2.12:1.5.1             (depends on 1.2.0)
mkurz commented 1 year ago

Seems like you upgraded sbt-native-packer to 1.9.16 yourself. If you don't really need to do that, I would just remove it because Play 2.8 comes with 1.5.2 anyway. However if you really need to upgrade sbt-native-packager, you can instead put

ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always

in your project/plugins.sbt, see https://github.com/playframework/playframework/releases/tag/2.8.19

dEajL3kA commented 1 year ago

Thank you! I have removed sbt-native-packager 1.9.16 for now.

But build still fails, now with NoSuchMethodError:

[error] java.lang.NoSuchMethodError: sbt.internal.inc.Relations.classNames(Ljava/io/File;)Lscala/collection/immutable/Set;
[error]         at com.typesafe.play.sbt.enhancer.PlayEnhancer$.$anonfun$bytecodeEnhance$4(PlayEnhancer.scala:80)
[error]         at scala.collection.TraversableLike.$anonfun$flatMap$1(TraversableLike.scala:293)
[error]         at scala.collection.Iterator.foreach(Iterator.scala:943)
[error]         at scala.collection.Iterator.foreach$(Iterator.scala:943)
[error]         at scala.collection.AbstractIterator.foreach(Iterator.scala:1431)
[error]         at scala.collection.IterableLike.foreach(IterableLike.scala:74)
[error]         at scala.collection.IterableLike.foreach$(IterableLike.scala:73)
[error]         at scala.collection.AbstractIterable.foreach(Iterable.scala:56)
[error]         at scala.collection.TraversableLike.flatMap(TraversableLike.scala:293)
[error]         at scala.collection.TraversableLike.flatMap$(TraversableLike.scala:290)
[error]         at scala.collection.AbstractTraversable.flatMap(Traversable.scala:108)
[error]         at com.typesafe.play.sbt.enhancer.PlayEnhancer$.getClassesForSources$1(PlayEnhancer.scala:76)
[error]         at com.typesafe.play.sbt.enhancer.PlayEnhancer$.$anonfun$bytecodeEnhance$3(PlayEnhancer.scala:91)
[error]         at com.typesafe.play.sbt.enhancer.PlayEnhancer$.$anonfun$scopedSettings$3(PlayEnhancer.scala:51)
[error]         at scala.Function1.$anonfun$compose$1(Function1.scala:49)
[error]         at sbt.internal.util.$tilde$greater.$anonfun$$u2219$1(TypeFunctions.scala:62)
[error]         at sbt.std.Transform$$anon$4.work(Transform.scala:68)
[error]         at sbt.Execute.$anonfun$submit$2(Execute.scala:282)
[error]         at sbt.internal.util.ErrorHandling$.wideConvert(ErrorHandling.scala:23)
[error]         at sbt.Execute.work(Execute.scala:291)
[error]         at sbt.Execute.$anonfun$submit$1(Execute.scala:282)
[error]         at sbt.ConcurrentRestrictions$$anon$4.$anonfun$submitValid$1(ConcurrentRestrictions.scala:265)
[error]         at sbt.CompletionService$$anon$2.call(CompletionService.scala:64)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
[error]         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[error]         at java.lang.Thread.run(Thread.java:750)
[error] (common / Compile / manipulateBytecode) java.lang.NoSuchMethodError: sbt.internal.inc.Relations.classNames(Ljava/io
mkurz commented 1 year ago

Thats a different thing, starting with play-ebean 6.2 "Play enhancer got removed", see https://github.com/playframework/play-ebean/releases/tag/6.2.0-RC4

dEajL3kA commented 1 year ago

Does it mean that I have to remove sbt-play-enhancer from project/plugins.sbt?

Not sure what might be the consequences.

Well, I gave it a try, but doesn't seem to fix the NoSuchMethodError problem:

[error] java.lang.NoSuchMethodError: sbt.internal.inc.Relations.products(Ljava/io/File;)Lscala/collection/immutable/Set;
[error]         at com.typesafe.sbteclipse.core.EclipsePlugin$.$anonfun$copyManagedClasses$2(EclipsePlugin.scala:117)
[error]         at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:286)
[error]         at scala.collection.mutable.ResizableArray.foreach(ResizableArray.scala:62)
[error]         at scala.collection.mutable.ResizableArray.foreach$(ResizableArray.scala:55)
[error]         at scala.collection.mutable.ArrayBuffer.foreach(ArrayBuffer.scala:49)
[error]         at scala.collection.TraversableLike.map(TraversableLike.scala:286)
[error]         at scala.collection.TraversableLike.map$(TraversableLike.scala:279)
[error]         at scala.collection.AbstractTraversable.map(Traversable.scala:108)
[error]         at com.typesafe.sbteclipse.core.EclipsePlugin$.$anonfun$copyManagedClasses$1(EclipsePlugin.scala:116)
[error]         at scala.Function1.$anonfun$compose$1(Function1.scala:49)
[error]         at sbt.internal.util.$tilde$greater.$anonfun$$u2219$1(TypeFunctions.scala:62)
[error]         at sbt.std.Transform$$anon$4.work(Transform.scala:68)
[error]         at sbt.Execute.$anonfun$submit$2(Execute.scala:282)
[error]         at sbt.internal.util.ErrorHandling$.wideConvert(ErrorHandling.scala:23)
[error]         at sbt.Execute.work(Execute.scala:291)
[error]         at sbt.Execute.$anonfun$submit$1(Execute.scala:282)
[error]         at sbt.ConcurrentRestrictions$$anon$4.$anonfun$submitValid$1(ConcurrentRestrictions.scala:265)
[error]         at sbt.CompletionService$$anon$2.call(CompletionService.scala:64)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
[error]         at java.util.concurrent.FutureTask.run(FutureTask.java:266)
[error]         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
[error]         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
[error]         at java.lang.Thread.run(Thread.java:750)
mkurz commented 1 year ago

You need to upgrade sbt-eclipse to v6.0.0: https://github.com/sbt/sbt-eclipse/releases/tag/6.0.0 in your project/plugins.sbt

mkurz commented 1 year ago

// For sbt versions >= 1.4.0 addSbtPlugin("com.github.sbt" % "sbt-eclipse" % "6.0.0")

dEajL3kA commented 1 year ago

// For sbt versions >= 1.4.0 addSbtPlugin("com.github.sbt" % "sbt-eclipse" % "6.0.0")

Okay, thanks! This seems to fix the build.

But now I'm getting all sorts of errors (failed tests) at runtime. Probably result of missing sbt-play-enhancer ???

mkurz commented 1 year ago

Yes, you need to write getters and setter yourself now.

dEajL3kA commented 1 year ago

Could you give a simple example, please?

Suppose I have a model class that looks something like this:

@Entity
public class Certificate {

    @EmbeddedId
    public CertificateId id;

    @Lob
    public byte[] certificate;

    /* ... */
}

...do I need to add a getter and setter method for every property/field?

And do I need to change all code that uses the model class to use these getter/setter methods instead of direct field access?

Should the actual fields then become private?

mkurz commented 1 year ago

I don't know you codebase but usually yes you want to make the fields private and create getters and setters. Your IDE will probably help you with that.

Alternative: You can also set up lombok (https://projectlombok.org/) which can generate getters and setters for you. However if you do that yo have to be aware that the lombok generated getters and setters will not be availabe in twirl templates (meaning you can't can get/set on a instance enhanced by lombok) because twirl templates are actually scala code that is not aware of lombok annotions. You can workaround that by putting your models in a sbt sub-project and make your main project depend on it, this way the models in your sub-project will get compiled to bytecode first, and will then be availble in twirl templates in your main project as well.

dEajL3kA commented 1 year ago

Since we've always used the sbt-play-enhancer:

If I write explicit getter/setter methods in my model (@Entity) classes, do these methods need to do anything specific to fetch the value from DB and/or persists the value in the DB? Or does it come down to:

@Entity
public class Certificate {

    @Lob
    private byte[] certificate;

    public void setCertificate(final byte[] value) {
        certificate = value;
    }

    public byte[] getCertificate() {
        return certificate;
    }

   /* ... */
}

I will have a look at lombok.


The Ebean documentation (latest version) says that they don't need getter/setter methods 😕

Getters Setters

Ebean does NOT need getters and setters as it adds it's own accessor methods via enhancement.

Source: https://ebean.io/docs/best-practice/

dEajL3kA commented 1 year ago

So, we have a model class in our Play Framework application that looks like this (original source code):

@Entity
public class Customer {
    @Id
    public Long id;

    public String email;

    /* ... */
}

By applying a de-compiler on the final Customer.class file, from the last "working" build of our application, I have been able to figure out that the Ebean Play Enhancer apparently has translated the above source code into this:

public class Customer implements EntityBean {
    @PropertiesEnhancer$GeneratedAccessor
    public String getEmail() {
        return this._ebean_get_email();
    }

    @PropertiesEnhancer$GeneratedAccessor
    public void setEmail(final String newValue) {
        this._ebean_set_email(newValue);
    }

    protected String _ebean_get_email() {
        this._ebean_intercept.preGetter(14);
        return this.email;
    }

    protected void _ebean_set_email(final String newValue) {
        this._ebean_intercept.preSetter(true, 14, (Object)this._ebean_get_email(), (Object)newValue);
        this.email = newValue;
    }
}

Do I really have to write getter and setter method like the above myself, for every field, from now on ???

(And how could I have known that they need to be written exactly like this?)

Regards.

PromanSEW commented 1 year ago

The Ebean documentation (latest version) says that they don't need getter/setter methods

In Play app Play Ebean plugin runs enhancement manually. So "enhancing" now applying only to model classes, instead of all referencing classes to these fields. In my app for example I use only setters for correct update DB fields, getters are optional, using fields directly works fine.

dEajL3kA commented 1 year ago

The Ebean documentation (latest version) says that they don't need getter/setter methods

In Play app Play Ebean plugin runs enhancement manually. So "enhancing" now applying only to model classes, instead of all referencing classes to these fields. In my app for example I use only setters for correct update DB fields, getters are optional, using fields directly works fine.

But the problem is that the Enhancer has been deprecated and is not compatible with the newer SBT versions: https://github.com/playframework/play-ebean/issues/388#issuecomment-1680401361 https://github.com/playframework/play-ebean/issues/388#issuecomment-1680418309

Hence, if I want to switch to a newer SBT version, I have to drop the Enhancer from my plugins.sbt completely, which apparently means that I have to write the getter and setter methods myself. Unfortunately, as has been shown above, the getter and setter methods that (until now) had been generated by the Enhancer are actually quite complex. Is there any documentation what exactly must be written in those getter and setter methods in order to mimic what the Enhancer would have generated?

I could copy&paste the code from the de-compiler, but I'm not sure this the correct approach 😟

(BTW: I assume that my model classes will only continue to work correctly, if "my" getter and setter methods do the same as the Enhancer-generated getter and setter methods would have done. Right?)

PromanSEW commented 1 year ago

Play runs Ebean agent manually: https://github.com/playframework/play-ebean/blob/89571c425016c95c61db75e49a5b6f44119588e9/sbt-play-ebean/src/main/scala/play/ebean/sbt/PlayEbean.scala#L67

You should create setters for your models, and Play Ebean plugin will enhance your setters correctly:

public class Foo extends Model {
    public int count;

    public int addCount(int count) {
        return this.count += count;
    }
}

After enhancing:

public class Foo extends Model implements EntityBean {
    public int count;

    public int addCount(int count) {
        int var10002 = this._ebean_get_count() + count;
        this._ebean_set_count(var10002);
        return var10002;
    }
}

If you used setCount as setter, enhancing will convert this.count = count to this._ebean_set_count(count), as well, and will not create own setters anymore. Enhancing runs on compilation of project automatically as SBT task, or on recompilation in dev mode as well, and correctly builds jar with enhanced classes on dist. You can see enhanced classes in target\scala-2.13\classes\models at any time, if needed, to see what is under the hood.

dEajL3kA commented 1 year ago

Are you saying that this still is supposed to work, even when the Play Enhancer was removed from the plugins.sbt?

If so, then it means that "enhancing" pretty much still works?

If so, I'm still a bit confused what exactly I need to change in my existing code, in order to retain the "old" (correct) behavior?

PromanSEW commented 1 year ago

@dEajL3kA you should convert all you model fields assignments to setter calls at least. For example someModel.field = 100 to model.setField(100). Creating getters is not needed. Getting field values works without them.

dEajL3kA commented 1 year ago

This is a rather big project and finding all places will be difficult. If I add getter and setter methods, can I make the field itself private (or protected), so that I can easily find all places that still attempt direct field access?

PromanSEW commented 1 year ago

@dEajL3kA yes of course