jetty / jetty.project

Eclipse Jetty® - Web Container & Clients - supports HTTP/2, HTTP/1.1, HTTP/1.0, websocket, servlets, and more
https://eclipse.dev/jetty
Other
3.84k stars 1.91k forks source link

Add possibility to deploy every web application in separate JPMS layer #2895

Open PavelTurk opened 6 years ago

PavelTurk commented 6 years ago

I hope that soon jetty will be modularized according to JPMS specs. As the next step I suggest to add posibility to deploy every web application in separate JPMS layer. I explain the details on the following example. Lets' suppose that there are two jetty modules: main and extra (at this moment we don't know yet what modules there will be and how many). Besides there is one module of some library. Web application also has three modules. So we have the following diagram: jetty-jpms As we see there are two JPMS layers. Every web application is deployed in its own layer. However, I think that all them must be child to Jetty layer. In order Jetty could work with servlets there must be created special JPMS service via which Jetty can control instances of the servlets.

Additional information can be found here: 1) The book "Java 9 Modularity Patterns and Practices for Developing Maintainable Applications" - Chapter 6. Advanced Modularity Patterns - Container Application Patterns 2) Http Service Specification - this specification describes how such solution was implemented in OSGi.

joakime commented 6 years ago

This might be a good issue to make more generic and submit to https://github.com/eclipse-ee4j/servlet-api/ as well.

PavelTurk commented 6 years ago

Default ModuleFinder doesn't allow to add .war files to ModuleLayer. See my issue here . Alan Bateman suggested to implement custom ModuleFinder and I implemented it (I took a lot of code from jdk.internal).

Now I can put my .war archives on separate layer without placing it to module path, class path or to jetty layer. Now I can create two custom layers - one for jetty and another for my web application. Both layers are children of boot layer. And it works - jetty works with my web application. Although I thought that it wouldn't without JPMSServletService.

I can share the code I did if you say where I should put it.

sbordet commented 6 years ago

@PashaTurok I recommend you put your code in your GitHub profile, publicly, with a friendly license such as Apache 2, and link that repository from this issue. Thanks.

PavelTurk commented 6 years ago

Do I understand right - jetty loads classes from war file itself? I am asking as now I am testing my solution and it seems that when I load war file to separate JPMS layer then there are two instances for each class of war in JVM - one loaded by jetty and one in war layer.

sbordet commented 6 years ago

Of course Jetty loads classes from the war, that is mandated by the Servlet specification and it's the only way Servlet Containers can run web applications.

Why are you doing this layer separation? What benefit does it bring you?

PavelTurk commented 6 years ago

@sbordet Layers in JPMS allow dynamically create subsytems of the application where every subsytem has its own modules and every subsytem is isolated from others. So we can consider every java program as graph of dynamic subsytems. For example it can be used for programs that has plugins etc. See the book I mentioned in the issue.

The same idea can be applied and to web applications, where every web application has its own layer as it allows to use same frameworks both for native applications and web applications.

sbordet commented 6 years ago

@PashaTurok web applications are already isolated from others, so you don't need to add another isolation. Plus experience with years of Servlet Containers showed that they should be isolated with each other, but not really with the container that provides them services.

It's not the ModuleLayer that gives you isolation, it's the ClassLoader. And that is already present.

Plugins? Show me a real use case, otherwise we are in the realm of possibilities that nobody ever uses. Web applications can already be composed via web fragments.

PavelTurk commented 6 years ago

@sbordet As I remember developers of Jigsaw project worked about 10 years. 10 years! What for? Modularization? But why do we need it if we always can create N jars? Are N Jars not modularization? Only to modularize JDK? So many efforts and what for?

JPMS is new technology but it significantly affects the architecture of the whole application. From other side you cannot ignore it as it was possible to ignore OSGi because it is implemented in JVM. Of course we can always reinvent the bicycle but what for? Layers were developed for these purposes and this is not only my opinion see what say JDK developers (comment to the issue)

sbordet commented 6 years ago

Jigsaw was all about modularization of the JDK.

The fact that people wants to use it for their own applications or for EE applications works only if you have the same set of problems that the JDK had before Jigsaw, which is rare.

JPMS is new technology but it significantly affects the architecture of the whole application.

Maybe, but only if you want to use it. If it gives you a benefit, go for it. Otherwise don't use it. Seems to me that just because Oracle added a hammer into the JDK, now all the problems look like a nail to you and can only be solved with JPMS. It's not the case.

Alan Bateman's comment on the issue you linked is all about possibilities. Maybe Jakarta EE will decide that JPMS is a bad idea for EE applications, and that's it, we stay with the current model.

Like you said, it was possible to ignore OSGi, and it is possible to ignore JPMS. The JVM will work perfectly fine. We are already running Jetty on JDK 11 without JPMS, and we don't have any problem.

That said, Jetty always had a history of experimenting with new technologies. We are doing this on #2191 and will continue to do so.

We need to keep backwards compatibility and look at the future but with some pragmatism and engineering trade-off on resources, costs and benefits.

Community members like you play an important role by pushing a project towards exploring new things, so keep your comments coming. Thanks!

joakime commented 6 years ago

As I remember developers of Jigsaw project worked about 10 years. 10 years! What for? Modularization?

Jetty and the Servlet spec have been around longer, over 2x longer in fact. The Jigsaw project had a history of working in isolation and ignoring feedback and advice from outside the project. And don't forget, the Modularization that JPMS brings to the table was considered broken and unusable by many, even as recently as June 2017. (see RedHat / IBM rejection of Jigsaw in Java 9) In fact, if you compare JPMS of today to JPMS of Jan 2017 its quite stark the difference. (JPMS of Jan 2017 was solely focused on the modularization of the JVM itself, not applications)

PavelTurk commented 6 years ago

@panga left and I am alone :). The main problem here is Servlet specs is older and there is nothing about JPMS. So we are at the intersection of technologies. JavaEE is dead, and about Jakarta EE plans to use JPMS I didn't find any information.

So there are two ways: 1) forget about issue (servlet + jpms) until Jakarta EE takes any decisions and gives any solutions 2) implement something without waiting Jakarta EE that can be later used as reference implementation.

sbordet commented 6 years ago

@joakime wrote:

The Jigsaw project had a history of working in isolation and ignoring feedback and advice from outside the project.

Come on, not really true. Alan Bateman did a tremendous job answering to open source project issues, stackoverlow, etc.

sbordet commented 6 years ago

@PashaTurok we are interested in trying out solution 2, which may not work. You're already doing some work in that direction and we are collaborating.

PavelTurk commented 6 years ago

@sbordet At current moment I can create JPMS layer and deploy there .war as automatic module. But jetty reads classes not from that automatic module, but from .war file. We need a new way to connect jetty and war. I suggest the following approach: 1) Jetty module must provide JPMS service for registering war jpms module (we must assume that there can be only one war module in layer?). 2) Some code (framework, jetty) that creates layer for war application after creating this layer must get jetty JPMS service and call it passing as parameter either newly deployed war module or newly created layer. 3) Jetty starts internal deployment of the war application. 4) When war application must be stoped, via Jetty JPMS service we unregister war application and Jetty internally undeploy war application.

Something like this.

joakime commented 6 years ago

Be aware that Jetty will also need to bytecode scan classes on the server container as well for proper operation of a WebAppContext.

sbordet commented 6 years ago

I can create JPMS layer and deploy there .war as automatic module

We need to know the details. How do you do this? How do you make the module system look for the Automatic-Module-Name inside a .war? And in future, how do you make a .war module-info resolve modules in WEB-INF/lib? There is a packaging problem before a module problem.

But jetty reads classes not from that automatic module, but from .war file.

Please explain. A module is a runtime object created by the JVM that only exists in memory. A .war file is the only place where we can scan jars via JarFile.

What is your current problem? That you cannot find TLDs? If so, I already told you the solution, to use a URLClassLoader. If not, then you need to explain what the problem is.

PavelTurk commented 6 years ago

We need to know the details. How do you do this? How do you make the module system look for the Automatic-Module-Name inside a .war? And in future, how do you make a .war module-info resolve modules in WEB-INF/lib? There is a packaging problem before a module problem.

I wrote WarModuleFinder that allows to add .war arhives as JPMS module to layer. However, it is not tested because I can't make Jetty read classes from this module. About WEB-INF/lib I think it conflicts with JPMS principles. All jars from WEB-INF/lib must be independent JPMS modules.

Please explain. A module is a runtime object created by the JVM that only exists in memory. A .war file is the only place where we can scan jars via JarFile.

Yes, I absolutely agree with you. This is the code I use to deploy my war archive.

        Server server = new Server( 8080 );
        Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server);
        classlist.addBefore(
                "org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
                "org.eclipse.jetty.annotations.AnnotationConfiguration"
        );
        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath( "/" );
        File warFile = new File(
                "/home/Pavel/WebServerJettyJpms/jar/org.school.site.temp-0.1.0-SNAPSHOT.war" );
        if (!warFile.exists())
        {
            throw new RuntimeException( "Unable to find WAR File: "
                    + warFile.getAbsolutePath() );
        }
        webapp.setWar( warFile.getAbsolutePath() );
        webapp.setExtractWAR(true);
        webapp.setAttribute(
                "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
                ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\.jar$|.*/[^/]*taglibs.*\\.jar$" );
        server.setHandler( webapp );
        server.start();
        server.dumpStdErr(); 

However, with this code I even don't need to create JPMS layer and add .war module to it, as Jetty reads classes directly from "/home/Pavel/WebServerJettyJpms/jar/org.school.site.temp-0.1.0-SNAPSHOT.war".

What is your current problem? That you cannot find TLDs? If so, I already told you the solution, to use a URLClassLoader. If not, then you need to explain what the problem is.

I don't have any problems. This issue is different from issue #2191. I am trying implement we are interested in trying out solution 2, which may not work. if I understand it correctly.

sbordet commented 6 years ago

I don't think this is the right approach.

WebAppContext is the class that has the logic to create an isolating classloader for the web application, to read war files, etc.

We probably want a JPMSWebAppContext that has the different logic required for JPMS. @gregw?

janbartel commented 6 years ago

If you look at jetty's osgi implementation you'll see that we subclass some of the Configuration classes and other supporting classes that jetty uses at startup time to find resources and classes in an osgi-friendly way. We don't subclass WebAppContext and indeed the project has generally taken the view that its better not to subclass such a central class as WebAppContext if at all possible. That said, even in the osgi world, we ultimately are still dealing with urls, so that jasper still works, albeit with a few tricks and swizzles.

PavelTurk commented 6 years ago

Let's suppose we have two modules - jetty module and our module. Firstly all instances of server will be created in our module and secondly we can create N instances simultaneously. So it means, JPMS service (for controlling the servers) to outer world must be provided by our module.

In our module we will have references to the following main objects (as I understand with my little knowledge of jetty) - WebContext, Server, Module, ModuleLayer. So, we can do one of the following:

  1. Create context for JPMS module as @sbordet said. To create context we pass reference to Module/ModuleLayer.
  2. Do Server.deploy (Module/ModuleLayer).

The second variant contradicts current jetty principles. So, I don't know another way except @sbordet said.

PavelTurk commented 6 years ago

Thanks to @lukehutch . Now we can scan separate ModuleLayer with ClassGraph. See here. Now we have all tools to implement this feature.

PavelTurk commented 6 years ago

So I implemented some solution that works. What I did:

  1. I created two layers - jettyLayer and warLayer. WarLayer is the child of jettyLayer. The class loader of jettyLayer is the parent class loader for warLayer.
  2. For creating these layers I used this ModuleFinder - https://github.com/PashaTurok/warmodulefinder
  3. This is the module-info of my war module:
    module SchoolSiteTemp {
    requires slf4j.api;
    requires javax.servlet.api;
    exports org.school.site.temp; //servlet is in this package
    }

    We need to export the packages which contain servlets (filters/listeners) but no need to export packages with other used classes in servlets/filters/lsteners.

  4. This is the code I start jetty
Server server = new Server( 8080 );
Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server);
classlist.addBefore(
        "org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
        "org.eclipse.jetty.annotations.AnnotationConfiguration"
);
Module warModule = null;
List<Module> modules = component.getLayer()
        .modules()
        .stream()
        .filter(module -> module.getName().equals("SchoolSiteTemp"))
        .collect(Collectors.toList());
warModule = modules.get(0);
WebAppContext webapp = new WebAppContext();
webapp.setClassLoader(new JpmsWebAppClassLoader(this.getClass().getClassLoader(), webapp, warModule));
webapp.setContextPath( "/" );
String warModulePath = warModule
        .getLayer()
        .configuration()
        .findModule(warModule.getName())
        .map(ResolvedModule::reference)
        .orElseThrow(() -> new RuntimeException("should not happen"))
        .location().get().toString();
webapp.setWar(warModulePath);
webapp.setExtractWAR(true);
webapp.setAttribute(
        "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern",
        ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\.jar$|.*/[^/]*taglibs.*\\.jar$" );
server.setHandler( webapp );
server.start();
server.dumpStdErr();

So we see that all resources are taken directly from war file on file system.

  1. And this is the base of JpmsWebAppClassLoader

    public class JpmsWebAppClassLoader extends WebAppClassLoader {
    
    private Module warModule;
    
    public JpmsWebAppClassLoader(Context context, Module warModule) throws IOException {
        super(context);
        this.warModule = warModule;
    }
    
    public JpmsWebAppClassLoader(ClassLoader parent, Context context, Module warModule) throws IOException {
        super(parent, context);
        this.warModule = warModule;
    }
    
    protected Class<?> foundClass(final String name, URL url) throws ClassNotFoundException
    {
        return warModule.getClassLoader().loadClass(name);
    }
    }
  2. I added to JSP scriplet where I used class from another package, not from org.school.site.temp. Jasper gave me this: javax.servlet.ServletException: java.lang.IllegalAccessError: class org.apache.jsp.dynamic.about_jsp (in unnamed module @0x8991843) cannot access class org.school.site.temp.service.Outsider (in module SchoolSiteTemp) because module SchoolSiteTemp does not export org.school.site.temp.service to unnamed module @0x8991843 When I added exports org.school.site.temp.service to module-info JPS worked without problems.
  3. I suggest to do the following - create interface of JpmsHttpService and its default implementation and put it the module-info of the war file : provides JpmsHttpService with DefaultJpmsHttpService. This service must create instances of the serlets|filters|listeners but not give their classes what I suppose will let not to export all packages with them. I can't try it as I don't know if I can add a hook in their creating - if someone could help me it would be great. At the same time classes used in JSP scriplets must be exported to Jasper.
  4. I think this is the first working solution with Servlets and JPMS. Well done :)
PavelTurk commented 6 years ago

Alan Bateman showed the right way to implement WarModuleFinder see here If on one layer we have both .jar and .war modules then we need to do

ModuleFinder commonFinder = ModuleFinder.compose(jarFinder, warFinder);
Configuration cf = boot.configuration().resolve(commonFinder,  ModuleFinder.of(), Set.of(jars+wars)); 

@sbordet What do you think about # 7 of the previous post? Is this issue still actual or you decided not to go this way?

PavelTurk commented 6 years ago

Here is the information what JPMS developers suggest to do with jars in WEB-INF/lib of the war module, when war module is at own JPMS layer.

sbordet commented 6 years ago

@PashaTurok we want to land first the changes we have done in #2191.

Before committing to a certain solution, we would like to get some feedback from people running on the module-path.

This issue will be likely implemented in Jetty 10.x, since we will have available the Module API.

It may be possible that we add a JDK 9 module in Jetty 9.4.x with an experimental implementation of what described in this issue, but again it's a matter of time/resources.

PavelTurk commented 6 years ago

@sbordet Thank you for explanation. At least now you have all the information to implement this issue.

Only one question - is there a hook (spi) that jetty can use when it creates instances of servlets/filters/listeners?

sbordet commented 6 years ago

Can you be more specific? What is the problem you're trying to solve?

PavelTurk commented 6 years ago

@sbordet Now Jetty loads classes (of servlets/filters/listeners) from my application and creates instances. I want Jetty to use my factory in which I will create instances myself and return them to jetty.

sbordet commented 6 years ago

@PashaTurok you can use ServletContextHandler.getObjectFactory() and add your Decorator to it.

Jetty will first create an instance of the Servlet, then call the decorators, where you can return the same instance, but decorated (e.g. you can inject other objects based on your custom annotations - CDI does it in this way), or you can return a completely different instance you create.

ServletContextHandler context = ...;
context.getObjectFactory().addDecorator(new PashaDecorator());
PavelTurk commented 6 years ago

When this issue will be implemented we will need separate logging for every JPMS layer. See here https://issues.apache.org/jira/browse/LOG4J2-2464

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has been a full year without activit. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] commented 4 years ago

This issue has been closed due to it having no activity.

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has been a full year without activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has been a full year without activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has been a full year without activity. It will be closed if no further activity occurs. Thank you for your contributions.