Не получается вынести внешние настройки за пределы jar-файла

Jmix version: 2.3.4
Jmix Studio plugin version: 2.3.3-241
IntelliJ version: GIGA IDE 2024.1.1 (Community Edition)

Здраствуйте!

Добрался до этапа развертывания, для начала решил попробовать с jar-файлом. Сразу же решил вынести настройки во внешний файл, а именно сделать файл my.properties, в котором указать настройки бд, путь доступа к шаблонам email, настройки других баз для синхронизации и т.п.

Судя по документации Spring boot, если положить файл с расширением .properties (я пробовал и просто application.properties, без переименования), то он сам просканирует папки и применит этот файл. Однако, данный способ не сработал.

Сделал кастомный класс:

@Configuration
@PropertySource(value = "file:${home}/my.properties", ignoreResourceNotFound = true)
public class AppConfig {
}

И добавил в главный класс:

@Bean
@Primary
AppConfig appConfig() {
    return new AppConfig();
}

Не знаю нужно ли было делать второе, но на всякий случай, и вроде ничего не сломалось.

Окей, скопировал из resources файл application.properties и переименовал его в my.properties. Для начала поменял в нём простую настройку server.port=8095 на 8097 и попробовал запустить через консоль java -jar -Dhome=D:/proga D:/proga/myApp-1.0.jar.

Судя по логам файл он нашёл, но проигнорировал его настройки и запустился, используя внутренний application.properties. Я удалил из внутреннего файла application.properties строку server.port, порт сменился по дефолту на 8080, пересобрал jar-файл (дальше не буду говорить что после каждого изменения пересобирал проект) и запустился через консоль той-же командой, увидел в консоли что он принял порт из внешнего файла и стал 8097.

Таким образом я сделал вывод, что внутренний файл application.properties имеет приоритет над внешним, и в случае одинаковых настроек, берёт из внутреннего, проверил это ещё на нескольких настройках. Хотя в документации Spring написано ровно обратное.

Попробовал полностью стереть содержимое application.properties, собрать jar и запуститься только на внешних настройках, вылетает ошибка:

2024-10-08T15:52:34.658+03:00  INFO 19924 --- [           main] org.quartz.core.QuartzScheduler          : JobFactory set to: org.springframework.scheduling.quartz.SpringBeanJobFactory@60431728
2024-10-08T15:52:34.690+03:00  INFO 19924 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler quartzScheduler_$_NON_CLUSTERED shutting down.
2024-10-08T15:52:34.690+03:00  INFO 19924 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler quartzScheduler_$_NON_CLUSTERED paused.
2024-10-08T15:52:35.150+03:00  INFO 19924 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler quartzScheduler_$_NON_CLUSTERED shutdown complete.
2024-10-08T15:52:35.150+03:00  WARN 19924 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'quartz_QuartzService': Unsatisfied dependency expressed through field 'scheduler': Error creating bean with name 'quartzScheduler' defined in class path resource [org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.class]: Couldn't retrieve trigger: Bad value for type long : \x
2024-10-08T15:52:35.150+03:00  INFO 19924 --- [           main] i.j.d.impl.JmixEntityManagerFactoryBean  : Closing JPA EntityManagerFactory for persistence unit 'main'
2024-10-08T15:52:35.165+03:00  INFO 19924 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2024-10-08T15:52:35.165+03:00  INFO 19924 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
2024-10-08T15:52:35.165+03:00  INFO 19924 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2024-10-08T15:52:35.181+03:00  INFO 19924 --- [           main] .s.b.a.l.ConditionEvaluationReportLogger :

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2024-10-08T15:52:35.212+03:00 ERROR 19924 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'quartz_QuartzService': Unsatisfied dependency expressed through field 'scheduler': Error creating bean with name 'quartzScheduler' defined in class path resource [org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.class]: Couldn't retrieve trigger: Bad value for type long : \x

Полный стак-трейс:

Спойлер
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'quartz_QuartzService': Unsatisfied dependency expressed through field 'scheduler': Error creating bean with name 'quartzScheduler' defined in class path resource [org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.class]: Couldn't retrieve trigger: Bad value for type long : \x
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:788) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:768) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:145) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:509) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1439) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:599) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:971) ~[spring-context-6.1.13.jar!/:6.1.13]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:625) ~[spring-context-6.1.13.jar!/:6.1.13]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.2.10.jar!/:3.2.10]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.2.10.jar!/:3.2.10]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.2.10.jar!/:3.2.10]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.2.10.jar!/:3.2.10]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.2.10.jar!/:3.2.10]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) ~[spring-boot-3.2.10.jar!/:3.2.10]
        at ru.sfi.SFIApplication.main(SFIApplication.java:33) ~[!/:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:569) ~[na:na]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:91) ~[SFI-1.0.jar:1.0]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:53) ~[SFI-1.0.jar:1.0]
        at org.springframework.boot.loader.launch.JarLauncher.main(JarLauncher.java:58) ~[SFI-1.0.jar:1.0]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'quartzScheduler' defined in class path resource [org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.class]: Couldn't retrieve trigger: Bad value for type long : \x
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1806) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:785) ~[spring-beans-6.1.13.jar!/:6.1.13]
        ... 27 common frames omitted
Caused by: org.quartz.JobPersistenceException: Couldn't retrieve trigger: Bad value for type long : \x
        at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveTrigger(JobStoreSupport.java:1538) ~[quartz-2.3.2.jar!/:na]
        at org.quartz.impl.jdbcjobstore.JobStoreSupport$12.execute(JobStoreSupport.java:1527) ~[quartz-2.3.2.jar!/:na]
        at org.quartz.impl.jdbcjobstore.JobStoreCMT.executeInLock(JobStoreCMT.java:245) ~[quartz-2.3.2.jar!/:na]
        at org.quartz.impl.jdbcjobstore.JobStoreSupport.executeWithoutLock(JobStoreSupport.java:3800) ~[quartz-2.3.2.jar!/:na]
        at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveTrigger(JobStoreSupport.java:1524) ~[quartz-2.3.2.jar!/:na]
        at org.quartz.core.QuartzScheduler.getTrigger(QuartzScheduler.java:1505) ~[quartz-2.3.2.jar!/:na]
        at org.quartz.impl.StdScheduler.getTrigger(StdScheduler.java:508) ~[quartz-2.3.2.jar!/:na]
        at org.springframework.scheduling.quartz.SchedulerAccessor.addTriggerToScheduler(SchedulerAccessor.java:300) ~[spring-context-support-6.1.13.jar!/:6.1.13]
        at org.springframework.scheduling.quartz.SchedulerAccessor.registerJobsAndTriggers(SchedulerAccessor.java:244) ~[spring-context-support-6.1.13.jar!/:6.1.13]
        at org.springframework.scheduling.quartz.SchedulerFactoryBean.afterPropertiesSet(SchedulerFactoryBean.java:507) ~[spring-context-support-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1853) ~[spring-beans-6.1.13.jar!/:6.1.13]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1802) ~[spring-beans-6.1.13.jar!/:6.1.13]
        ... 37 common frames omitted
Caused by: org.postgresql.util.PSQLException: Bad value for type long : \x
        at org.postgresql.jdbc.PgResultSet.toLong(PgResultSet.java:3328) ~[postgresql-42.6.2.jar!/:42.6.2]
        at org.postgresql.jdbc.PgResultSet.getLong(PgResultSet.java:2540) ~[postgresql-42.6.2.jar!/:42.6.2]
        at org.postgresql.jdbc.PgResultSet.getBlob(PgResultSet.java:456) ~[postgresql-42.6.2.jar!/:42.6.2]
        at org.postgresql.jdbc.PgResultSet.getBlob(PgResultSet.java:442) ~[postgresql-42.6.2.jar!/:42.6.2]
        at com.zaxxer.hikari.pool.HikariProxyResultSet.getBlob(HikariProxyResultSet.java) ~[HikariCP-5.0.1.jar!/:na]
        at org.quartz.impl.jdbcjobstore.StdJDBCDelegate.getObjectFromBlob(StdJDBCDelegate.java:3190) ~[quartz-2.3.2.jar!/:na]
        at org.quartz.impl.jdbcjobstore.StdJDBCDelegate.selectTrigger(StdJDBCDelegate.java:1780) ~[quartz-2.3.2.jar!/:na]
        at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveTrigger(JobStoreSupport.java:1536) ~[quartz-2.3.2.jar!/:na]
        ... 48 common frames omitted

Возвращаешь настройки во внутренний файл и запускаешься через IDEA - всё работает с этими же настройками, собираешь jar-файл с заполненными внутренними настройками, jar запускается (без внешних).

Подскажите, что не так и есть ли элегантное решение хранить внешние настройки (email, ldap, BD, пути к шаблонам эл. почты и т.п) в одной папке с jar-файлом?

p.s.: гуглил вопрос в такой формулировке, везде пишут просто положите файл .properties в одну папку с jar-файлом и будет всё супер. Через консоль может вариант, т.к. они точно имеют высший приоритет, но слишком дофига параметров, на целый лист.

UPDATE 1:
Не знаю надо ли отдельную тему с вопросом задавать, пока не понял связаны они или нет.

Разрабатывал с использованием базы Postgres, всё changelog удалил. Через IDEA всё запускается, data store говорит что база соответствует сущностям. Перед упаковкой в jar-файл меняю адрес базы на новый в том самом application.properties (т.к. он в любом случае поменяется), соответственно я имею jar-файл с программой и новую чистую БД.
То, есть, судя по моей логике и документации, при первом запуске приложения (jar-файла) должна просмотреться база, понять что там пусто и сгенерить новый changelog для всех-всех сущностей.
Однако, программа создаёт только таблицы, которые указаны в init-changelog’ах и при дальнейшем запуске сваливается с ошибкой, т.к. пытается обратиться к таблицам, которых не существует (@PostConstruct методы). И при просмотре таблиц в БД видно, что ни одной моей таблицы нет.
Если проделать такой же трюк с новой базой через IDEA, то генерится новый changelog и всё благополучно запускается.

Вопрос: так задумано и что делать чтобы через jar-файл создавались все таблицы, или не задумано?)

Здравствуйте,

Данные шаги не нужны, аннотации @Configuration достаточно для того чтобы поднялся бин с properties.

И добавил в главный класс:

@Bean
@Primary
AppConfig appConfig() {
    return new AppConfig();
}

Не знаю нужно ли было делать второе, но на всякий случай, и вроде ничего не сломалось.

Предложанные варианты из этой статьи, должны работать без проблем:

https://www.baeldung.com/spring-properties-file-outside-jar

Попробовал один из вариантов, он работает. Проделал следующие шаги:

Собрал Jar c productionMode=true:

gradle bootJar -Pvaadin.productionMode=true

Перешёл в директорию с jar:

cd build/libs  

Запустил jar с указанием пути к файлу с настройками:

java -jar jmix-properties-test-0.0.1-SNAPSHOT.jar --spring.config.location=file:///Users/nikitashchienko/dev/jmix-properties-test/external-application.properties

Результат:
image

Прикладываю пример проекта:
jmix-properties-test.zip (104.1 КБ)

С Уважением,
Никита

По второму вопросу:

Вы удалили файлы changelogs, которые были сгенерированы студией в ваших ресурсах?

С Уважением,
Никита

Несколько раз всё перепроверил, в конечном итоге всё собралось и запустилось. Есть предположение что ошибка с quartz была связана с каким то кэшем, потому что помогла полная очистка и перезагрузка зависимостей.

Да, на этапе разработки их накопилось несколько десятков.
Нашёл выход: настроить подключение к новой базе и сгенерировать changelog без запуска проекта. Запаковать проект в jar и подключиться к новой базе, тогда приложение генерит таблицы и стартует.
Если не добавлять changelog с созданием всех таблиц в Jar, то сам он там не генерируется.

Changelogs должны быть в проекте, чтобы создалась база данных. Важный момент, Changelogs генерируются именно студией, нашим плагином, а после запуска приложения, liquibase смотрит на сгенерированные файлы и только их применяет.

Вы можете удалить все changelogs, а потом сгенерировать их заново, тогда студия создаст минимальное количество файлов, чтобы не хранить экcперементальные changelogs. Это приемлемый вариант, во время разработки.

С Уважением,
Никита

Проблема в том, что changelog не генерируется, если база соответствует модели. То есть чтобы запаковать в jar корректный changelog, надо подключиться к пустой базе через IDE, сгенерировать changelog и потом уже паковать.

Да, верно, changelogs не будет генерироваться если база соответствует модели. Но и просто так удалять changelogs не нужно, это можно делать только во время разработки, чтобы избавиться от экспериментальных changelogs, в БД есть таблица в которой хранятся записи выполненных changelogs.

Если у вас тестовые данные в бд, вы можете пересоздать бд с помощью студии и заново сгенерировать changelogs.

PS: Пересоздания бд соответсвенно удаляет все данные.