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 firedUse 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 tosrc/main/resources
(note:hikaricp.properties
ands3.properties
no longer exist, this information is now part of the defaultapplication.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 thecustomer.acl
contains additional rules they should be moved to the normalresources/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 mainlogback-spring.xml
config filethe remaining resources in the
share
andmodule/module
directories should be merged together in the standardresources
sub folder of that customer module.if a customer module contains a
resources/cms/web/less
directory its contents will be compiled toresources/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;