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

Javalin + JTE Hot reload doesn't work #63

Closed
IvanPizhenko opened this issue Feb 26, 2021 · 18 comments
Closed

Javalin + JTE Hot reload doesn't work #63

IvanPizhenko opened this issue Feb 26, 2021 · 18 comments
Assignees
Labels
bug Something isn't working enhancement New feature or request

Comments

@IvanPizhenko
Copy link
Contributor

IvanPizhenko commented Feb 26, 2021

I've set up things for the hot reload in the my Javalin + JTE based application as follows:

class MyServer{

// ...

  private fun createTemplateEngine(): TemplateEngine {
    if (isDevSystem()) {
      val path = Path.of("src", "main", "jte")
      val codeResolver = DirectoryCodeResolver(path)
      logger.info("DEVELOPMENT SYSTEM DETECTED. JTE source folder: '${path.toAbsolutePath()}'")
      return TemplateEngine.create(codeResolver, ContentType.Html)
    } else {
      return TemplateEngine.createPrecompiled(ContentType.Html)
    }
  }

  private fun configureJavalin(): Javalin {
    JavalinJte.configure(createTemplateEngine())
    // ... more stuff here ...
  }

// ...

}

But hot reload just doesn't work. When I change JTE file and referesh page in browser, it just doesn't change.

I've create descendant class from the DirectoryCodeResolver and overridden interface methods to output stuff to log and forcibly report that file has changed even if it is not.

package mypackage

import gg.jte.resolve.DirectoryCodeResolver
import java.nio.file.Path
import mu.KotlinLogging

class CustomCodeResolver(val path: Path) : DirectoryCodeResolver(path) {
  private val logger = KotlinLogging.logger {}

  override fun resolve(name: String): String {
    val s = super.resolve(name)
    logger.info("***JTE*** resolve: $name, size=${s.length}")
    return s
  }

  override fun hasChanged(name: String): Boolean {
    val changed = super.hasChanged(name)
    logger.info("***JTE*** hasChanged: $name -> $changed")
    return true // force "always changed"
  }

  override fun resolveAllTemplateNames(): List<String> {
    logger.info("***JTE*** resolveAllTemplateNames")
    return super.resolveAllTemplateNames()
  }
}

But still even with forced "always changed" file it still doesn't work.
According to log outputs, this part works correctly - when I change JTE file and refresh browser, method from DirectoryCodeResolver reports file as changed, and my custom resolve() shows different file size.

However, rendering result still as if old version is used. Actually, rendering result corresponds to JTE template content faced on the first rendering - no matter how much times I change page and refresh browser, the page is always as it was first time, before any changes.

Please fix.

@casid casid self-assigned this Feb 26, 2021
@casid
Copy link
Owner

casid commented Feb 26, 2021

Hey @IvanPizhenko, do you maybe generate jte sources to produce a self-contained JAR? If so, can you try to clean your generated sources and retry the hot reload?

I had a similar issue and switched to precompiled templates after that .-)

@IvanPizhenko
Copy link
Contributor Author

IvanPizhenko commented Feb 26, 2021

Yes, I am generating such JAR, but I do run my application just via Gradle using ./gradlew run. It should not use that JAR. The idea is to have hot reloading on the developer machine, without need to manually precompile anything after it is changed (production version is of course precompiled). Is there a way to make this working without cleaning anything? My build process will each time generate that precompiled stuff, and I will have to clean that up each time - I believe that's not what I should do.
I was assuming it should work like this: just if I change source JTE template file it detects that and uses new file. This should depend only on JTE file, not anything else, and work anyway, no matter I have precompiled stuff or not. And only if I explicitly tell it - "just use precompiled stuff", only then it uses that precompiled stuff, otherwise ignores it if it pre-exists.

@IvanPizhenko
Copy link
Contributor Author

IvanPizhenko commented Feb 26, 2021

May it happen because precompilation puts classes into some "location 1" and on-demand compilation puts classes into "location2" which may be later on the classpath and so classes, even if on-demand compiled correctly, are loaded from location 1? Is there a way to tell JTE where to output classes during on-demand compilation?

@IvanPizhenko
Copy link
Contributor Author

IvanPizhenko commented Feb 26, 2021

Here's what I noticed - I have precompilation output in the build/resources/main, while JTE on-demand precompilation goes into build/jte-classes. Is there a way to tell JTE to write on-demand compilation outputs into the build/resources/main?

@IvanPizhenko
Copy link
Contributor Author

Found overloaded create() method, which allows to specify class directory. Trying it...

@IvanPizhenko
Copy link
Contributor Author

Specified that directory and checked that source and class generated in it are updated when I change JTE template. Still seeing the old one. What else this can be?

@casid
Copy link
Owner

casid commented Feb 27, 2021

Those precompiled class files are instantly available to the application class loader, since I guess build/resources/main is on your classpath. Such classes cannot be redifined by another class loader. That's why hot reload isn't working.

The only solution I can think of is to place on demand generated templates in a different package than precompiled ones. This way there is no chance they conflict with each other.

@casid casid added bug Something isn't working enhancement New feature or request labels Feb 27, 2021
@IvanPizhenko
Copy link
Contributor Author

IvanPizhenko commented Feb 27, 2021

Well, now classes really go to the same package. Is it possible to make that depending on Template Mode, package name will differ - i.e for example (in Kotlin notation): "gg.jte.generated.${templateMode.toString().toLowerCase()}"?

casid added a commit that referenced this issue Feb 27, 2021
… also sets different packages for on-demand or precompiled templates, so that hot reload works even if there are still old precompiled templates around.
@casid
Copy link
Owner

casid commented Feb 27, 2021

@IvanPizhenko I made the package for template classes fully configurable and set defaults as you suggested. Precompiled templates are generated to package gg.jte.generated.precompiled, on-demand templates to gg.jte.generated.ondemand.

I've written a test that resembles your situation (just with generated code, since that was easier to set-up):
https://github.com/casid/jte/blob/master/jte-hotreload-test/src/test/java/gg/jte/TemplateEngineTest.java

Would you like to give it a try if this works for your project?

You'd need to clone the jte repo and build 1.8.0-SNAPSHOT:

  • Run mvn clean install in the jte root directory
  • Run gradle publishToMavenLocal in jte-gradle-plugin directory

To use the locally built gradle plugin you'd need to temporarily add this to your project's settings.gradle file:

pluginManagement {
    repositories {
        mavenLocal()
        gradlePluginPortal()
    }
}

@IvanPizhenko
Copy link
Contributor Author

Great! Yes, I will try it and let you know.

@IvanPizhenko
Copy link
Contributor Author

IvanPizhenko commented Feb 27, 2021

Doing it now...
One small note: please do chmod +x mvnw and commit it. This should be done on Linux or Mac, don't know how to make git set "executable" bit on the file on Windows, if ever possible.

UPDATE Found a way to do it on Windows - read the full story here

TLDR:

# Run in the git-bash on Windows:
git update-index --chmod=+x 'name-of-shell-script'
git commit -m "....."

@IvanPizhenko
Copy link
Contributor Author

Hmm... Still doesn't work. I can see that sources and classes in the gg.jte.generated.ondemand do appear, and they are updated once I change JTE template file and refresh in the browser. But the old version still rendered.

@IvanPizhenko
Copy link
Contributor Author

IvanPizhenko commented Feb 27, 2021

Did one more test, and it started to work. The problem was that I've previously pointed class directory to the location on the classpath, i.e. build/resources/main. After pointing this to another directory, it finally has started to work 🔥🎆🚀:

      return TemplateEngine.create(codeResolver, Path.of("build", "jte-classes-dev-hot-reload"), ContentType.Html)

Please reflect this point in the documentation.

@IvanPizhenko
Copy link
Contributor Author

So when do you plan to publish 1.8.0?

@casid
Copy link
Owner

casid commented Feb 27, 2021

I made mvnw executable, good suggestion :-)

Glad you got it working. Yeah, whatever you put on the classpath will be picked up by the application class loader and won't be able to change, even with another classloader. Would you like to create a PR for the documentation adjustment? I'm not sure where to put it, maybe you have a location in mind where it would've been helpful to you.

Will probably release 1.8.0 this weekend. This version will mainly add an optional Kotlin module, that allows to have Kotlin instead of Java as expression language in templates (#59).

The IntelliJ plugin needs some more adjustments to support it, but I think I'm ready to release Kotlin support as a beta feature this weekend.

@IvanPizhenko
Copy link
Contributor Author

Posted PR #64

@casid
Copy link
Owner

casid commented Feb 28, 2021

@IvanPizhenko I just released version 1.8.0 👍

@casid casid closed this as completed Feb 28, 2021
@IvanPizhenko
Copy link
Contributor Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants