This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.

Module migration

This guide explains how to migrate an existing Tocco module from HiveApp to Spring Boot.

Note

We use a custom Maven Artifact Repository. To be able to use it the credentials need to be stored in the ~/.gradle/gradle.properties file:

  • nice2_repo_username=...

  • nice2_repo_password=...

The values of the maven project are stored in ~/.m2/settings.xml.

Create new module

Note

This section describes how to manually create a new module. However there is a new gradle task as well which does this job: gradlew createNewModule -P moduleType=core -P moduleName=name. The module type can be one of core, optional or customer. This script creates the folder structure, a build.gradle, module-info.java and also all necessary entries in settings.gradle and the build.gradle/module-info.java files of other modules. For customer modules a main class and an application.properties file is created additionally.

Apart from a few base modules, all modules should be created in either the core, optional or customer directories. Simply create a new directory named after the module (e.g. reporting) and create an empty build.gradle file inside it.

The new module needs to be registered in the settings.gradle file in the root directory of the project:

include '{core, optional, customer}:<module-name>'

We use the Java Platform Module System for modularization. First we need to create the module descriptor module-info.java in the main source directory src/main/java:

open module nice.core.reporting {

}

At this point it is recommended to refresh the Gradle project in IDEA in order to have IDE support for the next steps.

Note

IDEA should not be refreshed before the module-info.java file has been created. Otherwise the project files might become corrupt (module visibility not working properly for the new module) and the .idea folder has to be deleted and restored.

The module name should be in the following format: nice.{core, optional, customer}.<module-name>. Note that the module must be open. This allows other modules to access the module classes by reflection, which is necessary for the discovery of the beans by spring. It would also be possible to only open certain packages to certain modules using the opens <package> to <module> syntax.

If a customer module is migrated a main class must be created additionally:

@SpringBootApplication(scanBasePackages = "ch.tocco.nice2", exclude = LiquibaseAutoConfiguration.class)
public class Nice2Application {

    public static void main(String[] args) {
            SpringApplication.run(Nice2Application.class, args);
    }
}

In addition the module and main class must be specified in the build.gradle (see https://docs.gradle.org/current/userguide/application_plugin.html#sec:application_modular):

ext {
    customerModuleName = 'nice.customer.test'
    customerMainClass = 'ch.tocco.nice2.customer.test.Nice2Application'
}

If the new module is a core module it should also be added to the build.gradle in the customer folder, so that it will be included for all customers (the corresponding customer/src/main/java/module-info.java file needs to be updated as well). If it is an optional module, it needs to be included in the build.gradle (and module-info.java) of the test customer directly. This will make sure that the StartupTest integration test will pickup the new module and makes sure that the spring configuration is correct.

Adding sources

In the next step the sources files may be added to the new module. All .java files should be placed in the src/main/java directory, all .groovy files must be placed in the src/main/groovy (which also supports java files).

The packages should be named according to the following scheme:

  • ch.tocco.nice2.{optional, customer}.<module-name>.api for classes that should be exported to other modules

  • ch.tocco.nice2.{optional, customer}.<module-name>.impl for internal classes that will not be exported

  • ch.tocco.nice2.{optional, customer}.<module-name>.test for test fixtures (test classes that should be available in other modules)

A module no longer contains the api, impl and module sub-modules. The api packages (which should be accessible by other projects) need to be declared in the module-descriptor:

exports <package-name>;

Only exported packages may be read by other modules.

The test sources belong in the src/test/java (or src/test/groovy) directories. Test sources typically do not have a module-info.java class, which means that internal classes of other modules are also accessible in tests. However it is possible to add a module descriptor if the modularization is required for integration tests.

Note

Spock tests are no longer supported. Spock is based on JUnit but we mainly use TestNG and to avoid an unnecessarily complicated Gradle configuration it is easier to just migrate them to normal Groovy tests as there were very few Spock tests anyway.

After all source files have been added the imports in the java files need to be fixed (as most classes will have been moved to another package).

‘Test-Jars’ / Test Fixtures

Test classes that should be available in other modules should be placed in the src/testFixtures/java folder.

They can then be referenced in a build.gradle in the following way: testImplementation(testFixtures(project(":boot")))

It is also possible to declare dependencies specifically for the test fixtures using testFixturesApi (transitive) or testFixturesImplementation. See the manual for details.

Support for test fixtures must be enabled in build.groovy:

apply plugin: 'java-test-fixtures'

Adding dependencies

After moving all the source files, some dependencies will most likely be missing. Dependencies may be declared in the build.gradle file of the module:

dependencies {
    api project(":core:model:entity")

    implementation project(":core:reporting")

    testImplementation "org.testng:testng"
}

See also the gradle documentation for more details.

  • The api dependencies are transitive and are automatically available for all modules that depend on this module. This should be used if the dependency is required by the public API of a class in an exported package (see gradle docs for details).

  • implementation should be used for all other dependencies that are only used internally.

  • Each (non-test) dependency also requires an entry in the module-info.java file: requires transitive nice.core.model.entity; (transitive only for api dependencies). See the gradle documentation for details.

When adding project dependencies keep in mind that it’s not necessary to add every single dependency because of the transitive api dependencies.

External dependencies should be referenced without an explicit version number. Library versions are managed in the dependencyManagement block in the root build.gradle.

Dependencies which should be available in all modules (like guava for example) should be declared in the dependencies block of the root build.gradle. The corresponding module-info.java entry should be made in the boot module (transitive) which is available in all modules.

Some external dependencies might be problematic, if they have not been modularized properly:

  • If the library is not a module and doesn’t have an automatic module name

  • Split packages: a certain package may only be used by one library. This often happens with javax.* packages.

The extraJavaModuleInfo Gradle plugin may be used to fix these issues (see root build.gradle).

Adding resources

Normal classpath resources can be placed in the src/main/resources directory as usual. Keep in mind that the modularization is also applied to the resources and make sure that the correct packages are used.

The resources that used to be in the module sub-module are handled differently. They should be placed in the resources directory of the module (using the same internal structure as before). During the build these resources will be moved to the src/main/resources/META-INF directory. This is necessary because the META-INF directory is excluded from modularization. Otherwise the compiler would complain about using the same ‘package’ (e.g. model.entities) in multiple modules.

The paths that are moved automatically are defined by the ext.resourceIncludePattern property of the root build.gradle. Additional paths can be added for a specific module by adding the following to its build.gradle:

resourceIncludePattern << '...'

Migrating the hivemodule.xml

The first step would be running the HiveappModuleMigrator class which takes three arguments:

  • path to hivemodule.xml file that should be migrated

  • path to the new module that is being migrated

  • base package name of the new module

This script creates spring configuration classes for contributions that can be easily migrated. It also creates a file called hivemodule.replaced.xml which only contains the contributions and services which still need to be migrated manually.

Note

The script generates multiple files. However they should all be merged into a single class named <module-name>Configuration which must be annotated with @Configuration. The configuration class should be placed in the root impl package.

The remaining elements should be migrated in the following order:

Configuration-Point

There are a few different options how to migrate configuration points:

<contribution configuration-id="nice2.persist.core.HibernateBootstrapContributions">
  <contribution implementation="service:GeolocationTypesContribution"/>
</contribution>

The above example only contributes a service. The only thing to do here is to annotate the setter method with @Autowired where the configuration should be injected. Instead of the using a setter it’s also possible to use the constructor for injection.

Note

If there are no contributions that match the setter that is marked with @Autowired spring will throw an exception. To avoid this the annotation attribute required may be set to false.

The approach above only works if the different contributions implement the same interface. If the contributions do not implement a common interface, an annotation can be used instead (have a look at this commit to see how to use annotations for this).

<contribution configuration-id="nice2.reporting.Reports">
  <report id="report_name"
          outputTemplate="template_name"
          synchronize="true"
          label="report.label">
  </report>
</contribution>

For the above case a contribution class that contains these properties needs to be created (often such a class already exists and can be reused). A list of this class can then be autowired into the target (as described above). Note that the class must be in an exported package, as it needs to be accessible to modules that want to contribute. Consider extending the HiveappModuleMigrator for such cases.

<contribution configuration-id="Functions">
  <function name="DATETIMEADD" function="service:DatetimeAddFunction"/>
</contribution>

This example is a mix of the first two examples, it contains both a service and some additional information. There are two different ways to migrate these cases:

  • Using a contribution class like in the second example

  • Using a custom annotation. Have a look at the @Listener annotation for an example how to do this.

Services

It is usually sufficient to annotate the service implementation with the @Component annotation. If the service was a “threaded” HiveApp service the @ThreadScope annotation must be added as well to achieve the same behaviour.

Note

If no scope is specified, the default scope singleton is used. It’s also possible to use @Scope("prototype") to get a new instance when this dependency is injected. See also this article about the implications of using different scopes.

<set-configuration configuration-id="ServicePointCategoryExtractors" property="categoryExtractors"/>

If a configuration-point is injected into the service the setter has to be annotated with @Autowired or the property has to be moved into the constructor. Note that the injection order of several @Autowired methods is undefined. If the order is important they should be merged into one method or moved into the constructor.

<set property="enabled" value="${nice2.metrics.enabled}"/>

Setter for properties can be removed and replaced with the @Value("${..}") annotation directly on the field.

Note

If there are circular dependencies in the beans, this can be solved by placing the @Lazy annotation on the problematic constructor or method parameter. See also this article about other options how to solve this issue. This approach can also be used when an interface should be autowired, but there aren’t any implementations yet (will be added in a later module).

Note

In contrast to HiveMind a service does not have to implement an interface to be a bean. In fact an interface should only be used if:

  • the service must be exported form the module, but the implementation details should remain hidden in the module

  • there are multiple implementations

Contributions

All configuration points for these contributions should have already been migrated, otherwise the migration order is wrong.

How contributions are migrated depends on how the corresponding configuration point was migrated.

  • If only a service is contributed it is sufficient to add the @Component annotation to the contribution class (or the qualifier annotation in case it is used)

  • If there is an additional metadata annotation it needs to be placed on the class as well

  • If a custom contribution class is used, an instance of this class needs to be returned from a method that is annotated with @Bean and is in class that is annotated with @Configuration

Note

It’s easy to overlook a detail in the hivemodule.xml file, therefore it makes sense to search the file for terms like threaded (missing @ThreadScope annotation?), initialize-method (missing @PostConstruct annotation?), <set property (missing @Value annotation?) or <set-configuration (missing @Autowired annotation?).

Also keep in mind that HiveMind automatically calls a method named initializeService if it exists on a service, even when there is no initialize-method in the hivemodule.xml file.

Miscellaneous

application.properties

An application.properties file is supported per default by Spring. There should only be one application.properties file on the classpath (that means only one file per customer module in the resources directory).

The following properties should be dropped during the migration:

  • jmx.*

  • email.hostname

  • hiveapp.concurrent.defaults.thread-pool.core-pool-size

  • hiveapp.concurrent.defaults.thread-pool.max-pool-size

  • i18n.default.country

  • i18n.timezone.default

  • textresources.default.locale

To locally override properties an application-{profile}.properties file can be used, where {profile} corresponds to the active Spring profile (for example development). This is the replacement of the application.local.properties file.

Lazy initialization

Per default all spring beans are initialized lazily because the property spring.main.lazy-initialization has been set to true in the application.properties.

To enable eager loading of all beans this property must be set to false. To force eager loading only for certain beans they must be annotated with @Lazy(false)

Logging

Spring uses Logback by default and the default configuration is located at customer/src/main/resources/logback-spring.xml. By default these files just include the default logging configuration that is part of the boot module.

To customize the logging for a certain customer an logback-customer.xml file may optionally be created to override the default.

The logback config supports the <springProfile> tag to customize the logging depending on the current run environment.

The logging config for tests is defined in the logback-test.xml contained by the test fixture of the boot module (which is included in the main build.gradle for all modules).

Nice Version

The current-version.txt file no longer exists, the version number is now defined in the default.properties file of the boot module.

EventEmitter

Usages of EventEmitter<Listener> emitter can simply be replaced with List<Listener> listener and emiter.emitter().listenerMethod() with listeners.forEach(l -> l.listenerMethod()) (a thread safe collection should be used to store the listeners).

Alternatively usages of the EventEmitter can also be replaced by Spring’s ApplicationEvent but require a bit of refactoring:

  • Create a new event that extends the ApplicationEvent

  • Inject the ApplicationEventPublisher into the bean where events need to be fired

  • Use the @EventListener annotation to receive the published events.

See here for more details.

NPM

If NPM packages need to be installed for the javascript components, the npm.gradle script in the root directory needs to be included for that module: apply from: '../../npm.gradle'.

The package.json file needs to be placed in the resources/resources/webapp directory. npm install is called during the build and the contents of the node_modules directory are accessible under the /js path: loadJs('/js/tocco-login/dist/...');.

Customer modules

All optional modules that should be available for that customer need to be referenced in the build.gradle file. An entry is also required in the module-info.java to make sure that the module is loaded at startup. In addition the ‘base’ customer project (:customer / nice.customer) should be included, which contains all core modules.

Each customer project needs a Nice2Application main class (which can be copied from another customer). This is a spring configuration class and can be used for bean definitions (when migrating the hivemodule.xml file) instead of creating a separate @Configuration annotated class. Otherwise the migration process of the *.java and hivemodule.xml files remains the same.

In the new project the customer modules will have exactly the same structure as all other modules (in the old project the structure was different). The module resources should be migrated in the following way:

  • /etc/*.properties files should be moved to src/main/resources (note: hikaricp.properties and s3.properties no longer exist, this information is now part of the default application.properties file - see existing customers for an example)

  • the customer.acl file, which includes all the ACL rules of the available modules, has been moved the base customer project. If the customer.acl contains additional rules they should be moved to the normal resources/acl directory/files.

  • if the customer uses a custom Logback configuration it should be moved to src/main/resources/logback-customer.xml, this file will be included from the main logback-spring.xml config file

  • the remaining resources in the share and module/module directories should be merged together in the standard resources sub folder of that customer module.

  • if a customer module contains a resources/cms/web/less directory its contents will be compiled to resources/cms/web/css during the build. Note that it may be necessary to adjust some imports in the *.less files as the path has changed

Database changes

The table nice_task_config needs to be emptied because the storage format of the task data has changed.

Because the module names have changed, the ids of the already executed changesets need to be updated with the following SQL statements:

  • update databasechangelog set id = replace(id, 'nice2.optional.', 'nice.optional.') where id like '%/nice2.optional.%';

  • update databasechangelog set id = replace(id, 'nice2.customer.', 'nice.customer.') where id like '%/nice2.customer.%';

  • update databasechangelog set id = replace(id, 'nice2.', 'nice.core.') where id like '%/nice2.%';

  • update databasechangelog set md5sum = null;

The first execution of the db refactoring will take longer than usual because the checksum needs to be recreated.

After the db refactoring has been successfully executed for the first time, the table should be cleanup up. If a changelog no longer exists in the repository the value of its md5sum column will still be null. These rows should be deleted, otherwise the db refactoring will be very slow:

  • delete from databasechangelog where md5sum is null;