HOAB

History of a bug

SpringBoot OSGI and templated OSGI modules with external constrant Jetty

Rédigé par gorki Aucun commentaire

Problem :

A very specific and particular problem.

I want to deploy  a SpringBoot application, in an OSGI environment, in a external Jetty server…

So I found solution do deploy Springboot in OSGI : here or here. Very good by the wat.

And also to deploy Springboot as war for an external Tomcat…

Too easy, my external Jetty server was rejecting Servlet 3.0 application startup… (hard coding for : 

context.setAttribute("org.eclipse.jetty.containerInitializers", initializers);

Yes, it's better, starting a 1,25 days of try and success !

Solution :

My first web application was deployed with Spring Dispatcher Servlet with a BundleActivator

My second web application was deployed with Spring DispatcherServlet AND a SpringBootApplication intialized with the ressource loader of the BundleActivator (as in the OSGI example) : component-scan is working ! 

My third web application was switching to an initialization Servlet 3.0 with an extension of ServletContainerInitializer, thanks to harded jetty configuration, it's not working

My third and a half remove all web.xml content to keep only a context listener as my Jetty was doing nothing : 

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    <!-- ServletContainerInitializer can not be used here -->
    <!-- XXXXXXX is putting in hard : context.setAttribute("org.eclipse.jetty.containerInitializers with only Jasper -->

    <!-- At the end, it's possible to load SpringBoot with this context listener -->
    <listener>
        <listener-class>
            com.hexagon.introscope.extension.MyContextLoaderListener
        </listener-class>
    </listener>

</web-app>

My fourth web application was trying to load the Springboot application file as a YAML file (why so simple !), resolved with a “hack” :

  • The ServletContext has a way to store the configuration directory, during the initialization, I saved it somewhere : HpaContextLoaderListener.getConfigDirectory() 

The listener loader : 

package com.xxxxx.extension;

import org.eclipse.jetty.webapp.WebAppContext;
import org.springframework.web.context.WebApplicationContext;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import java.io.File;
import java.nio.file.Path;

public class MyContextLoaderListener implements ServletContextListener {

    private static File configDirectory;

    public static File getConfigDirectory() {
        return configDirectory;
    }

    public void contextInitialized(ServletContextEvent event) {
        ServletContext servletContext = event.getServletContext();
        MySpringBootHexagonActivator app = new MySpringBootHexagonActivator();

        try {
            configDirectory = servletContext.getAttribute("introscope.em"));
            
            InstantiateHelper.getValueByMethod(servletContext, "setExtendedListenerTypes", new Class[]{boolean.class}, true);

            app.onStartup(event.getServletContext());
        } catch (ServletException e) {
            throw new RuntimeException(e);
        }
    }

    public void contextDestroyed(ServletContextEvent event) {
    }
}

 

@SpringBootApplication
public class MySpringBootHexagonActivator extends SpringBootServletInitializer {

    protected SpringApplicationBuilder createSpringApplicationBuilder() {
        SpringApplicationBuilder builder = super.createSpringApplicationBuilder();
        Map<String, Object> properties = new HashMap<>();
        Path propertyFile = Path.of(MyContextLoaderListener.getConfigDirectory().getPath(), "application-extension.yml");
        properties.put("spring.config.location", propertyFile.toString());
        builder.properties(properties);
        builder.resourceLoader(HpaHexagonActivator.getResourceResolver());

        return builder;
    }
}

The HpaHexagonActivator.getResourceResolver() is the trick from SpringBoot OSGI example to load classes from Bundle classpath. I store the ressources resolver in a static variable that I can access from everywhere.

A word on that one : 

InstantiateHelper.getValueByMethod(servletContext, "setExtendedListenerTypes", new Class[]{boolean.class}, true);

I'm in OSGI, Jetty controls the listener class, and Springboot is unknown. You can authorized additional listeners but in my case, Jetty is not exported by the other OSGI package, not able to call it simply so… reflection : InstantiateHelper is basically a helper class to call methods by reflection.

 

At the end : 

  1. Springboot starts
  2. Springboot scans package offered by BundleActivator ressource locator
  3. Springboot can use an external configuration file, in YAML
  4. Springboot starts REST & Database
  5. It was hard.

Of course, lot of tries between these big steps !

The goal is to extend a 3rd party product without having source code.

Spring, Autowire, Proxy Class

Rédigé par gorki Aucun commentaire

Le problème :

A l'origine, suite à une réorganisation des classes et un nettoyage des configurations Spring, une des classes (appelons-là ClasseA) n'est plus "Autowired" sur ClasseB.

Alors que :

- les dépendances entre configurations Spring sont OK

- le component scan est OK

- les traces de Spring montrent bien une instanciation de la classe ClasseA et une tentative d'"autowiring" sur la classe ClasseB.

Puis, plus rien, pas de trace d'erreur si ce n'est que l'autowiring indique qu'il n'a pas trouvé de candidat.

 

Solution :

Ce n'est pas très simple.

Première étape :

- le tag qui a fait marcher le test :

<tx:annotation-driven proxy-target-class="true" transaction-manager="transactionManager" />

Je ne voyais pas le lien entre les transactions spring et ma classe qui n'avait aucun rapport avec les transactions.

En réalité c'est le proxy-target-class="true" qui a fait marcher le test. Grâce à ce lien, il est indiqué que cet attribut s'applique partout !

 

Deuxième étape deux nouveaux tests :

- suppression du proxy-target-class:  le test est KO (revenu à l'état premier)

<tx:annotation-driven transaction-manager="transactionManager" /> 

- ajout d'un tag général dédié aux proxies : le test est KO, il manque cette classe org.aspectj.util.PartialOrder$PartialComparable.

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

<aop:config proxy-target-class="true" />

 

Troisième étape, en ajoutant le jar aspectjweaver (moins lourd que le aspecttools), ça a marché.

Donc c'était bien l'instanciation de ma classe via un JDK Proxy plutôt que via un CGLIB qui a changé la donne.

 

Pour aller plus avant :

- ma classe hérite de org.apache.commons.configuration.CompositeConfiguration

- j'ai essayé d'en extraire une interface pour utiliser les JDK Proxy plutôt que les CGI : échec sur les premiers tests. J'ai laissé filé.

- un mystère que je n'ai pas creusé non plus : pourquoi le fait d'utiliser le proxy-target-class sur le tag aop:config demande plus de fonctionnalités (PartialOrder manquant) que sur tx:annotation-driven ?

- les JDK proxy sont conseillés car ils implique une programmation par interface (pour ceux qui aiment...)

 

Moralité ( il est mort alité...) :

- d'où l'importance de ne pas laisser faire la configuration Spring à n'importe qui, de bien comprendre les tags et leur portées.

- contrôler la portée de vos component-scan, ça aide d'avoir des tests rapides...

 

Mise à jour :

Le problème a aussi une autre conséquence qui bloque le comportement Autowired:

- Si une classe CLASSE_REELLE (@Component ) implémente une interface INTERFACE_GENIALE

- Une classe B (de test par exemple) essaye d'injecter A directement

- aop:config n'est pas configuré pour empêcher l'utilisation des proxy Java ! Spring décide par défaut d'utiliser les proxy dès qu'une interface est présente. (cf ce lien :  JDK- and CGLIB-based proxies)

Conséquence : Spring ne qu'injecter des variables de types INTERFACE_GENIALE et pas de type CLASSE_REELLE. Seules les interfaces sont injectables.

 

Fil RSS des articles de ce mot clé