6

Im looking to create a Spring library project to share across an internal team.

At a very basic concept level The library will send message events to a queue and my plan is to standardise this within a team across several Spring Boot Microservices send messages the same way.

My pom in the library project looks something like this

<artifactId>my-library</artifactId>
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

etc...

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>2.0.1.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.0.16.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.0.2.Final</version>
    </dependency>
    <dependency>
        <groupId>javax.el</groupId>
        <artifactId>javax.el-api</artifactId>
        <version>3.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.web</groupId>
        <artifactId>javax.el</artifactId>
        <version>2.2.6</version>
    </dependency>

I have a service in the library project that looks like this

public class EventService {

  Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

  public void sendAuditEvent(AuditMessage auditMessage){

    Set<ConstraintViolation<AuditMessage>> violations = validator.validate(auditMessage);

    if(!isEmpty(violations)){
      log.error("Unable to send audit message");
      violations.stream().forEach( v-> log.error(v.getMessage()));
    }
    log.info("Found {} violations", violations.size());
    // etc blah blah
    return;
  }
}

When I import the library into another project my thinking is that I can Autowire the EventService. By adding it in the pom and then @ComponentScan({"my.library.package.eventlibrary.service"})

How do I prevent spring version locking? If the library is using spring 2.1.5.RELEASE today and the project that imports the library uses a different version would I not end up with potentially maven conflicts?

Also lets say the project that imports the library uses a lower version of hibernate api and the library has 6.0.16.Final. How would I prevent the project from using the newer one found one in the library classpath?

To clarify my question further is there a way I can separate the dependencies in the library from the project that uses it.

Robbo_UK
  • 11,351
  • 25
  • 81
  • 117
  • What is the minimum java version you want to support ? – Alexander Petrov Jun 15 '19 at 18:08
  • Using java 11 in the project – Robbo_UK Jun 16 '19 at 13:11
  • the quetion is not what version your module is supporting, the question is what is the minimum version of Java you would like your module to support. F.ex. what if the client module improting yours uses java 6 ? What is the minimum version you would like to support? – Alexander Petrov Jun 17 '19 at 10:24
  • ok I understand it will be java11 as a minimum – Robbo_UK Jun 17 '19 at 10:26
  • it depends how dynamic you want it to be. If you want this module to go with its own hibernate version you need ClassLoader issolation that jigsaw will supply you with. If you don't want to be able to define the specific version and would be willing to allow the client module to define it. Then maven is all you need. – Alexander Petrov Jun 17 '19 at 12:15

3 Answers3

3

Pre Java 9. You can exclude the spring dependencies using maven when you declare the dependency to your module, same goes on for Hibernate. But you can't tell to your module to use a different hibernate version in a WAR.

If you want to work around this you can develop your library as independent micro service expose interface in the form of REST or Websocket if you want full duplex communication or something else JMS whatever....

Post Java 9 you can use java modularity to define the exact dependencies for your jar module. Check Project Jigsaw https://www.baeldung.com/project-jigsaw-java-modularity.

In your case in order to have different versions of the same library (hibernate). You would need two separate class loaders. To achieve this you would need to use layering read here http://openjdk.java.net/projects/jigsaw/spec/sotms/#layers

And here is the source code of many examples including ones that use layers. Focus on them : https://github.com/accso/java9-jigsaw-examples/tree/master/jigsaw-examples

Alexander Petrov
  • 9,204
  • 31
  • 70
2

You can try to exclude all transitive dependencies that your library can bring to projects that will use it.

To do this you should replace spring-boot-starter-parent with spring-boot-dependencies in dependencyManagement section and use provided scope for all dependencies which the library needs to work with and which will be exactly used by the projects, that will work with the library.

For example, a pom.xml of your library can be looks like this:

<!-- ... -->
    <groupId>com.example</groupId>
    <artifactId>library</artifactId>
    <version>0.1.0</version>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <spring-boot.version>2.1.5.RELEASE</spring-boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
<!-- ... -->

Then you will be able to use your library in the different projects, that use for example the old Spring Boot:

<!-- ... -->
    <groupId>com.example</groupId>
    <artifactId>old-project</artifactId>
    <version>0.13.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.19.RELEASE</version>
        <relativePath/>
    </parent>
<!-- ... -->
    <dependencies>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>library</artifactId>
            <version>0.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
<!-- ... -->    

So this project will use hibernate-validator:5.3.6.Final from its spring-boot-starter-web.

Important notes - the code of your library should be 'compatible' with this version of Spring Boot. In other words, you should test your library with different versions of Spring Boot in which you are interested.

See my project as an example.

Cepr0
  • 28,144
  • 8
  • 75
  • 101
  • if I specify the property value `spring-boot-version` in the library and also in the project importing the library which one will the library pick? – Robbo_UK Jun 17 '19 at 08:36
  • The library doesn't use Spring Boot itself - it uses only several modules which versions are defined in the `spring-boot-dependencies`. And their scope is `provided`. So regardless of the `spring-boot-version` value in the library, those modules will give their versions from the project `spring-boot-starter-parent`. As I've provided in the answer example: the module `hibernate-validator` in the library has version `6.0.16.Final`, but in the project it has version `5.3.6.Final` that is defined in the `spring-boot-starter-parent:1.5.19.RELEASE`. – Cepr0 Jun 17 '19 at 12:23
  • Do I need to use the the `provided` ? in the library pom? – Robbo_UK Jun 17 '19 at 14:42
  • @Robbo_UK Of course yes. Sorry, I forgot to add it to the library pom.xml example. I've already corrected it. – Cepr0 Jun 17 '19 at 15:10
  • I like the answer.. trying it out now... could you expand a bit on why you need to use provided scope not sure I understand that part? – Robbo_UK Jun 18 '19 at 09:08
  • @Robbo_UK [This is much like compile, but indicates you expect the JDK or a container to provide the dependency at runtime](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Scope). More info: [Difference between maven scope compile and provided for JAR packaging](https://stackoverflow.com/a/6647178) – Cepr0 Jun 18 '19 at 09:17
  • @Cepr0 how is your answer solving the problem with the library using a lower version of hibernate than the parent. – Alexander Petrov Jun 21 '19 at 07:07
0

Might be not what you are looking for, but you can distribute your library as a spring-boot-starter auto configuration module (of course, if the clients are spring boot applications).

This way you can control your dependencies in an agile way and you give your clients more freedom in using the library.

In your particular case, if you need to send a message to a queue you for sure need to have a corresponding classes in classpath. With auto configuration you can have Class Conditions or Been Conditions based on which you can track if your clients have correct configurations in runtime. You can also fail the context loading if something is wrong (providing a meaningful error message).

Spring also provides tracking mechanisms of what could happen if a particular class/library is missing.

Sasha Shpota
  • 9,436
  • 14
  • 75
  • 148