Заготовка для UI проектов со Spring Boot Web и FreeMarker
Оглавление:
- О проекте в общем
- Внешний вид страниц для desktop
- Экраны для мобильных устройств
- Создание maven wrapper
- ModelAndView
- Тестирование
- Покрытие тестами
- Сборка
- Запуск
- Использование
- Проброс HTTPS через Apache web server
- Разное
- О Freemarker
- Макросы Freemarker
- HTTPS
О проекте в общем
Git репозиторий https://github.com/cherepakhin/hr_admin.git.
Java 17:
export JAVA_HOME=/usr/lib/jvm/java-1.17.0-openjdk-amd64
Открыть http://127.0.0.1:8088/hr_admin/
URL для разработки http://127.0.0.1:8088/hr_admin/employees/
Развернуто на https://v.perm.ru/hr_admin/.
Основная цель ТОЛЬКО FRONTEND.
- В качестве template использован Freemarker (spring-boot-starter-freemarker).
- Простой CRUD. База данных H2.
- Использован Tailwind - CSS-фреймворк для оформления интерфейсов.
- При тестировании использован AssertJ.
- Использован GigaChat.
- тесты в BDD стиле с Mockito в EmployeeControllerTest.java
- DataJpaTest в EmployeeRepositoryTest.java
Внешний вид страниц для desktop
Основной экран в виде карточек:

Список сотрудников в виде таблицы с сортировкой и СВЕРНУТОЙ панелью:

Список сотрудников в виде таблицы с сортировкой и РАЗВЕРНУТОЙ панелью:

Диалог ввода сотрудника:

Диалог изменение данных сотрудника:

Список должностей:

Редатирование должности:

Экраны для мобильных устройств






Toolbar на больших экранах слева, на маленьких снизу. Сделано с помощью CSS.
На самом деле, в программе есть две панели: sidebar.ftl и bottom-navigation.ftl.
Внимание на свойство md (middle device).
Для больших экранов работает sidebar.ftl:
id="sidebar" class="hidden md:flex ..." - по умолчанию скрыто (hidden), но на открыто на средних и больше экранах (md:flex).
Для маленьких экранов используется bottom-navigation.ftl: class="md:hidden (md - middle device) - скрыто на больших экранах.
Совет из интернета: Часто к md:flex добавляют другие утилиты для тонкой настройки. Например:
- md:shrink-0 — запрещает элементу сжиматься на средних и больших экранах.
- md:w-48 — задаёт ширину 48 rem на средних экранах и выше.
Другой способ адаптации к мобильным устройствам использовать свойство Device из Spring MVC пример в проекте https://github.com/cherepakhin/device-resolution.
Суть в том, что в контроллере мы можем определить, на каком устройстве работает пользователь и в зависимости от этого отдавать разные представления.
Создание maven wrapper
mvn -N wrapper:wrapper -Dmaven=3.9.9
./mvnw --version
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: /home/vasi/.m2/wrapper/dists/apache-maven-3.9.9/3477a4f1
Java version: 17.0.17, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "6.14.0-37-generic", arch: "amd64", family: "unix"
ModelAndView
В стандартном подходе Spring Boot Web должны возвращаться ModelAndView:
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("catalog/products")
public class ProductController {
@GetMapping("list")
ModelAndView list() {
return new ModelAndView("catalog/products/list",
Map.of("products", this.productRepository.findAll()),
HttpStatus.OK);
}
}
В этом проекте возвращаются имена view.
Внедрение значений в html файлы осуществляется через Model EmployeeController.java и
возврат имени view (не ModelAndVew)
@RequestMapping(value = "/employees/new", method = RequestMethod.GET)
public String showCreateForm(Model model) {
log.info("showCreateForm");
model.addAttribute("employee", new Employee());
model.addAttribute("positions", positionRepository.findAll());
log.info("/employees/new: from page={}", currentIndexPage);
return "create_employee"; // имя template из src/main/resources/templates
}
Пример использования positions из примера выше в форме src/main/resources/templates/create_employee.ftlh:
<!-- Position -->
<div>
<label for="position_id">Должность</label>
<select
id="position_id"
name="position.id"
class="border border-gray-300 shadow-sm py-2 px-3 focus:ring-0 focus:ring-sky-500 focus:outline-none combo-btn"
style="width: 12em; padding-left: 1em; padding-right: 1em;"
required>
<#list positions as pos> <!-- Пример использования positions -->
<option value="${pos.id}">${pos.name}</option>
</#list>
</select>
</div>
При этом подходе есть особенности тестирования. Пример:
@Test
public void shouldShowEditFormForExistingEmployee() throws Exception {
// Given
Position position = new Position(1L, "Developer");
Employee employee = new Employee("John", "Doe", "Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. ", position);
employee.setId(1L);
given(employeeRepository.findById(1L)).willReturn(Optional.of(employee));
given(positionRepository.findAll()).willReturn(List.of(position));
// When & Then
mockMvc.perform(get("/employees/edit/1"))
.andExpect(status().isOk())
.andExpect(view().name("edit_employee"))
.andExpect(model().attributeExists("employee", "positions"))
.andExpect(model().attribute("employee", employee));
}
Обращение из файлов шаблонов (*.ftlh) к ресурсам src/resource/STATIC/ происходит через переменную springMacroRequestContext.contextPath.
Нужно для минимизации запросов к внешним ресурсам интернета (и прочим вопросам безопаснотси).
Пример из src/resource/templates/positions.ftlh:
<head>
<!-- Tailwind CSS via CDN -->
<script src="${springMacroRequestContext.contextPath}/js/tailwind.js"></script>
<!-- Alpine.js for interactivity -->
<!--<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>-->
<script src="${springMacroRequestContext.contextPath}/js/cdn.min.js" defer></script>
...
Тестирование
export JAVA_HOME=/usr/lib/jvm/java-1.17.0-openjdk-amd64
./mvnw clean test
При тестировании с MockMVC, подстановка bean в модель страницы можно выполнить вручную с помощью .flashAttr:
ResultActions result = mockMvc.perform(post("/employees/")
.flashAttr("positions", asList(position1)) // подстановка в модель аттрибута positions как asList(position1) !!!
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("firstName", "0123456789_0123456789_0123456789_0123456789") // > 15 символов
.param("lastName", "LastName")
.param("email", "Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. ")
.param("position.id", "1"));
Покрытие тестами
Подключен плагин JaCoCo report для создания отчета покрытия тестами.
Для генерации отчета выполнить (ОБЯЗАТЕЛЬНО проверить версию Java! export JAVA_HOME=/usr/lib/jvm/java-1.17.0-openjdk-amd64 ):
./mvnw jacoco:report
Отчет в target/site/jacoco/index.html.
Сборка
./mvnw clean package
Собранный FAT jar будет в target/hr-admin-0.0.3.jar
Использован org.springframework.boot:spring-boot-maven-plugin .
Так же есть Jenkinsfile
В Jenkinsfile клонирование из ветки main:
sh 'rm -rf hr_admin; git clone https://github.com/cherepakhin/hr_admin'
клонирование из ветки v0.0.7:
git clone -b v0.0.7 https://github.com/cherepakhin/hr_admin
Запуск
/usr/lib/jvm/java-1.17.0-openjdk-amd64/bin/java -jar target/hr-admin-0.0.3.jar
Запуск на другом порту:
/usr/lib/jvm/java-1.17.0-openjdk-amd64/bin/java -jar target/hr-admin-0.0.3.jar --server.port=8088
Использование
Открыть http://127.0.0.1:8088/hr_admin/employees/
(см. application.yaml)
Для запуска выполнить на v.perm.ru:
/usr/lib/jvm/java-17-openjdk-amd64/bin/java -jar ./hr-admin-0.0.3.jar --server.port=8088
Открыть https://v.perm.ru:8088/
(проверено 29/03/26 на другом компьютере. Открывается с предупреждением о сертификате.)
Ниже более лучший вариант без заморочек с сертификатами.
Проброс через Apache HTTPS
Размещено на https://v.perm.ru/hr_admin/
Работа через HTTPS сделана с помощью проксирования через apache2. В приложении никаких настроек на HTTPS нет.
В hr_admin/https/ другая (неудавшаяся) настройка на HTTPS.
Через Apache proxy настройка проще. Примеры смотри в hr_admin/https/000-default-le-ssl.conf
Настройка apache2 (см. # Проксирование запросов для Spring Boot приложения hr_admin (работает по HTTP на localhost:8088)):
root@v:/etc/apache2/sites-enabled# cat 000-default-le-ssl.conf
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName v.perm.ru
ServerAdmin Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.
DocumentRoot /var/www/main/
LogLevel debug
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
# Настройка HTTPS для Apache2
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/v.perm.ru/cert.pem
SSLCertificateChainFile /etc/letsencrypt/live/v.perm.ru/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/v.perm.ru/privkey.pem
SSLCACertificateFile /etc/letsencrypt/live/v.perm.ru/chain.pem
Include /etc/letsencrypt/options-ssl-apache.conf
# Проксирование запросов для Spring Boot приложения hr_admin (работает по HTTP на localhost:8088)
ProxyPreserveHost On
ProxyPass /hr_admin http://192.168.1.20:8088/hr_admin
ProxyPassReverse /hr_admin http://192.168.1.20:8088/hr_admin
# Передача информации о протоколе (важно для Spring Boot)
RequestHeader set X-Forwarded-Proto https
# Дополнительные настройки безопасности и логирования
Protocols h2 http/1.1
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
ErrorLog ${APACHE_LOG_DIR}/spring-proxy-error.log
CustomLog ${APACHE_LOG_DIR}/spring-proxy.log combined
</VirtualHost>
</IfModule>
Логи на сервере смотреть в v.perm.ru:/home/vasi/temp/hr_admin/hr_admin.log.
Разное
SQL запросы логируются. Для этого сделена настройка:
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Не переносить слова whitespace-nowrap:
<span class="ml-3 whitespace-nowrap">Скрыть панель</span>
В контроллерах использовать RequestMapping вместо GetMapping. Почему?
Потому что здесь не простой REST Controller, а управление страницами. Методы те же, но нужна дополнительная функциональность типа "redirect".
О Freemarker
https://habr.com/ru/articles/420549/
Пример 1:
<ul>
<#list father as item>
<li>${item}</li>
</#list>
</ul>
Map<String, Object> root = new HashMap<>();
....
root.put("father", Arrays.asList("Alexander", "Petrov", 47));
Пример 2:
Шаблон hello_test.ftl:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello ${name}!</title>
</head>
<body>
<input type="text" placeholder="${name}">
<table>
<#list persons as row>
<tr>
<#list row as field>
<td>${field}</td>
</#list>
</tr>
</#list>
</table>
</body>
</html>
@Component
public class CommandLine implements CommandLineRunner {
@Autowired
private Configuration configuration;
public void run(String... args) {
Map<String, Object> root = new HashMap<>();
// для ${name}
root.put("name", "Fremarker");
// для <#list persons
List<List> persons = new ArrayList<>();
persons.add(Arrays.asList("Alexander", "Petrov", 47));
persons.add(Arrays.asList("Slava", "Petrov", 13));
root.put("persons", persons);
try {
Template template = configuration.getTemplate("hello_test.ftl");
Writer out = new OutputStreamWriter(System.out);
try {
template.process(root, out);
} catch (TemplateException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Результат:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello Fremarker!</title>
</head>
<body>
<input type="text" placeholder="Fremarker">
<table>
<tr>
<td>Alexander</td>
<td>Petrov</td>
<td>47</td>
</tr>
<tr>
<td>Slava</td>
<td>Petrov</td>
<td>13</td>
</tr>
</table>
</body>
Макросы FreeMarker
Пример 1
Объявление макроса textInput в файле "ui.ftl":
<#macro textInput id value="-">
<input type="text" id="${id}" value="${value}">
</#macro>
Ключевое слово <#macro> , </#macro>.
textInput имя макроса.
id и value - параметры макроса (value="-" - значение по умолчанию).
Подключение макроса через import:
(ключевое слово #import)
<#import "ui.ftl" as ui/>
"ui" - алиас для использования.
В шаблоне макрос "textInput" из импорта "ui" вызывается так:
<@ui.textInput id="name" value="${name}"/>
Пример 2:
Файл макрос__ОВ__ ui.ftl:
<#-- textInput macro for html input -->
<#macro textInput id placeholder="" value="">
<input type="text" id="${id}" placeholder="${placeholder}" value="${value}">
</#macro>
<#-- table macro for html table -->
<#macro table id rows>
<table id="${id}">
<#list rows as row> <#-- ЦИКЛ!!! -->
<tr>
<td>${row?index + 1}</td> <#-- ВЫЧИСЛЕНИЕ!!! -->
<#list row as field> <#-- ЦИКЛ!!! -->
<td>${field}</td>
</#list>
</tr>
</#list>
</table>
</#macro>
Использование ui.ftl в hello.ftl:
<#import "ui.ftl" as ui/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello ${name}!</title>
</head>
<body>
<@ui.textInput id="name" placeholder="Enter name" value="${name}"/>
<@ui.table id="table1" rows=persons/>
</body>
</html>
Расширения файлов freemarket должны быть ftlh. С расширением .ftlh автоматически активируется защита от XSS-атаксов (HTML-автоэкранирование).
HTTPS
keytool -genkeypair -alias tomcat -keyalg RSA -keystore keystore.p12 -storetype PKCS12 -validity 365 -storepass changeit
$ keytool -genkeypair -alias tomcat -keyalg RSA -keystore keystore.p12 -storetype PKCS12 -validity 365 -storepass changeit
Enter the distinguished name. Provide a single dot (.) to leave a sub-component empty or press ENTER to use the default value in braces.
What is your first and last name?
[Unknown]: Cherepakhin
What is the name of your organizational unit?
[Unknown]: Vasili OU
What is the name of your organization?
[Unknown]: Vasili Organization
What is the name of your City or Locality?
[Unknown]: Perm
What is the name of your State or Province?
[Unknown]: Perm
What is the two-letter country code for this unit?
[Unknown]: RU
Is CN=Cherepakhin, OU=Vasili OU, O=Vasili Organization, L=Perm, ST=Perm, C=RU correct?
[no]: yes
Generating 3072-bit RSA key pair and self-signed certificate (SHA384withRSA) with a validity of 365 days
for: CN=Cherepakhin, OU=Vasili OU, O=Vasili Organization, L=Perm, ST=Perm, C=RU
Создастся файл ./keystore.p12
Сохраните его в src/main/resources:
cp keystore.p12 src/main/resources
Добавьте настройки в application.yaml.
# Включить HTTPS
server:
port: 8443
ssl:
key-store: classpath:keystore.p12
key-store-password: changeit
key-store-type: PKCS12
key-alias: tomcat
Пароль не вводил.
Информация о сертификате при запуске с v.perm.ru https://www.misterpki.com/keytool-list-certs/:
Common Name (CN) Cherepakhin
Organization (O) Vasili Organization
Organizational Unit (OU) Vasili OU
Common Name (CN) Cherepakhin
Organization (O) Vasili Organization
Organizational Unit (OU) Vasili OU
Issued On Wednesday, April 1, 2026 at 10:15:07PM
Expires On Thursday, April 1, 2027 at 10:15:07PM
Просмотр keystore:
$ cat https.sh
$ keytool -list -keystore src/main/resources/keystore.p12
Enter keystore password: нет пароля
***************** WARNING WARNING WARNING *****************
* The integrity of the information stored in your keystore *
* has NOT been verified! In order to verify its integrity, *
* you must provide your keystore password. *
***************** WARNING WARNING WARNING *****************
Keystore type: PKCS12
Keystore provider: SUN
Your keystore contains 1 entry
Alias name: tomcat
Creation date: Apr 3, 2026
Entry type: PrivateKeyEntry
Certificate chain length: 0
keytool -list -v -alias tomcat -keystore src/main/resources/keystore.p12
pass: пусто
Alias name: tomcat
Creation date: Apr 3, 2026
Entry type: PrivateKeyEntry
Certificate chain length: 0
$ openssl s_client -connect v:443 -showcerts
CONNECTED(00000003)
Can't use SSL_get_servername
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = E7
verify return:1
depth=0 CN = v.perm.ru
verify return:1
---
Certificate chain
0 s:CN = v.perm.ru
i:C = US, O = Let's Encrypt, CN = E7
a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: Feb 16 16:24:34 2026 GMT; NotAfter: May 17 16:24:33 2026 GMT
-----BEGIN CERTIFICATE-----
....
-----END CERTIFICATE-----
1 s:CN = v.perm.ru
i:C = US, O = Let's Encrypt, CN = E7
a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
v:NotBefore: Feb 16 16:24:34 2026 GMT; NotAfter: May 17 16:24:33 2026 GMT
-----BEGIN CERTIFICATE-----
....
-----END CERTIFICATE-----
2 s:C = US, O = Let's Encrypt, CN = E7
i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
a:PKEY: id-ecPublicKey, 384 (bit); sigalg: RSA-SHA256
v:NotBefore: Mar 13 00:00:00 2024 GMT; NotAfter: Mar 12 23:59:59 2027 GMT
-----BEGIN CERTIFICATE----
....
-----END CERTIFICATE-----
---
Server certificate
subject=CN = v.perm.ru
issuer=C = US, O = Let's Encrypt, CN = E7
...
От gigachat:
Запрос:
Работает https сервер apache2 на linux. Есть доступ к нему по https. Есть cert.pem chain.pem fullchain.pem keystore.p12 privkey.pem. Нужно запустить spring boot приложение на этом сервере. Какие настройки нужно сделать в application.yaml этого приложения?
Ответ:
Для запуска Spring Boot-приложения по HTTPS с использованием ваших сертификатов и ключей, настройки в application.yaml будут следующими.
Пример конфигурации для application.yaml
server:
port: 8443 # или любой другой порт, который вы хотите использовать
ssl:
enabled: true
key-store: /path/to/keystore.p12
key-store-type: PKCS12
key-store-password: your_keystore_password # укажите пароль от keystore.p12
key-alias: your_key_alias # если есть, иначе можно не указывать
Пояснения
key-store: путь к вашему файлу keystore.p12.
key-store-type: тип хранилища ключей. Для .p12 — это PKCS12.
key-store-password: пароль для доступа к keystore.
key-alias: псевдоним ключа внутри keystore (если он есть, иначе можно не указывать).
Если вы хотите использовать отдельные файлы (privkey.pem и fullchain.pem)
Spring Boot также поддерживает конфигурацию через отдельные файлы ключа и сертификата:
server: <----- МОЙ ВАРИАНТ
port: 8443
ssl:
enabled: true
key-store-type: PKCS12
key-store: /path/to/keystore.p12
key-store-password: your_keystore_password
# Либо так, если не используете keystore:
# server:
# ssl:
# key-store-type: PKCS12
# key-store: classpath:keystore.p12
# key-store-password: your_keystore_password
# Или для отдельных файлов (Spring Boot 2.2+):
# server:
# ssl:
# key-store-type: PEM
# ssl-certificate: file:/path/to/fullchain.pem
# ssl-key: file:/path/to/privkey.pem
Важно: поддержка отдельных PEM-файлов появилась в Spring Boot 2.2+. Если у вас более старая версия, используйте только keystore.
Рекомендация
Если у вас уже есть keystore.p12, лучше использовать его, так как это наиболее универсальный и поддерживаемый способ для Spring Boot.
Запустите приложение.
Теперь доступно по: https:// :8443
Если хотите, чтобы HTTP → перенаправлялся на HTTPS, добавьте конфигурацию:
SecurityConfig.java:
package net.guides.springboot2.freemarker.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.requiresChannel(channel -> channel
.requestMatchers(AntPathRequestMatcher.antMatcher("/secure/**"))
.requiresSecure()
)
.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll()
)
.portMapper(mapper -> mapper
.http(8080).mapsTo(8443)
);
return http.build();
}
}
Перенаправление трафика через apache2
000-default-le-ssl.conf:
<IfModule mod_ssl.c>
<VirtualHost *:443>
# The ServerName directive sets the request scheme, hostname and port that
# the server uses to identify itself. This is used when creating
# redirection URLs. In the context of virtual hosts, the ServerName
# specifies what hostname must appear in the request's Host: header to
# match this virtual host. For the default virtual host (this file) this
# value is not decisive as it is used as a last resort host regardless.
# However, you must set it for any further virtual host explicitly.
ServerName v.perm.ru
ServerAdmin Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.
DocumentRoot /var/www/main/
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular
# modules, e.g.
LogLevel debug
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to
# include a line for only one particular virtual host. For example the
# following line enables the CGI configuration for this host only
# after it has been globally disabled with "a2disconf".
#Include conf-available/serve-cgi-bin.conf
#ServerName v.perm.ru
SSLEngine on
#SSLProxyEngine On
#ProxyPreserveHost on
#ProxyRequests off
SSLCertificateFile /etc/letsencrypt/live/v.perm.ru/cert.pem
#SSLCertificateFile /etc/letsencrypt/live/v.perm.ru/cert.pem
SSLCertificateChainFile /etc/letsencrypt/live/v.perm.ru/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/v.perm.ru/privkey.pem
SSLCACertificateFile /etc/letsencrypt/live/v.perm.ru/chain.pem
Include /etc/letsencrypt/options-ssl-apache.conf
# Проксирование запросов на Spring Boot (работает по HTTP на localhost:8088)
# Снаружи доступно по https://v.perm.ru/hr_admin
# 192.168.1.20:8888 - адрес машнины во внутренней сети с запущенным сервисом
ProxyPreserveHost On
ProxyPass /hr_admin http://192.168.1.20:8088
ProxyPassReverse /hr_admin http://192.168.1.20:8088
# Передача информации о протоколе (важно для Spring Boot)
RequestHeader set X-Forwarded-Proto https
# Дополнительные настройки безопасности и логирования
Protocols h2 http/1.1
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
ErrorLog ${APACHE_LOG_DIR}/spring-proxy-error.log
CustomLog ${APACHE_LOG_DIR}/spring-proxy.log combined
</VirtualHost>
Полностью в hr_admin/https/000-default-le-ssl.conf
Ссылки
FreeMarker шаблоны (habr)
Альтернатива Freemarker - Velocity.
Примечания
При тестировании с mock, если используется any...(), то остальные поля должны быть заданы в виде типа eq(...)
Пример:
verify(this.employeeRepository, times(1)).findByFiltersAndSort(eq("firstName1"), eq("lastName1"), eq("email1"), any());
SVG иконки (пример стрелки вниз)
SVG иконки (пример стрелки влево)
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>Arrow-down SVG Icon</title><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 20V4m-7 9l7 7l7-7"/></svg>
Стили
- py-2 = padding 8 px (*4)
- px-3 = padding 12 px (*4)
- mb-2 = margin buttom 8 px (*4)
- w-full - растянуть на всю доступную ширину
- max-w-6xl - определение максимальной ширины
- justify-center - динамическое центрирование по вертикали
- justify-end - прижать к правому краю
Прижать кнопки к правой стороне
text-right - стиль для выравнивания текста
justify-end - работает по другому принципу, чем text-right.
<tr id="position-${position.id}" class="border-b border-gray-100 hover:bg-gray-50 transition">
<td class="px-4 py-2 font-medium">${position.name}</td>
<td class="px-4 py-2 text-right"> <!-- !!!!!!!!!!!!!!!!!!!!!! -->
<!-- Edit Action -->
<a ...</a>
<!-- Delete Action with Custom Modal -->
<button ... </button>
</td>
</tr>
Тестирование снаружи
Screen shot tool URL https://iotools.cloud/tool/website-screenshot/
TODO
Мелкий шрифт на пагинации на телефоне.
Шрифты https://tailwindcss.com/docs/font-size.
Freemaker assign value:
<#assign value="\t\tweather\n\n">
<#compress>
${value}
Today is a wonderful day.
1 2 3 4 5
</#compress>
Удаление
Выполнение DELETE запроса (совет отсюда):
<script th:inline="javascript">
function sendDelete(url) {
var xhttp = new XMLHttpRequest();
xhttp.open("DELETE", url, true);
xhttp.onload = function () {
let responseURL = xhttp.responseURL;
console.log("Redirecting to:", responseURL);
window.location.replace(responseURL);
};
xhttp.send();
}
</script>
Использование на страницах:
<a type="button" th:with="url = @{<your_url>}" th:onclick="sendDelete([[${url}]])">Delete</a>
Ветки
v0.0.5 - работает CRUD (особенно удаление) позиций и сотрудников.
Разное
Нормально работает tools из браузера
Просмотреть скриншоты сайта:
- https://convertico.com/website-screenshot/
- для проверки ДОСТУПНОСТИ из web https://convertico.com/website-screenshot/
- https://anytools.pro/ru/img/screenshot
- https://screenshot.guru/
- https://iotools.cloud/tool/website-screenshot/
PageSpeedInsights
PageSpeedInsights - Инструмент тестирования скорости загрузки и качества html разметки https://pagespeed.web.dev/analysis/https-v-perm-ru-hr_admin/wlfhga6g6l?hl=ru&form_factor=desktop
Некоторые результаты работы PageSpeedInsights:







Просмотр commits на github https://github.com/cherepakhin/hr_admin/commits/v0.0.5
Диалоги
Простой диалог с сообщением:
<!-- Delete Action with Confirmation -->
<a href="/${springMacroRequestContext.contextPath}/positions/delete/${position.id}"
onclick="return confirm('Вы уверены, что хотите удалить ${position.name} ?');"
class="action-delete inline-flex items-center justify-center w-5 h-5 rounded-full hover:bg-red-100 transition text-red-600"
viewBox="0 0 24 24"
title="Удалить">
<svg class="action-icon" xmlns="https://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</a>
Убрать скругления углов:
.modal-container: border-radius: 0
.btn-cancel: border-radius: 0
.btn-delete: border-radius: 0
Диалог подтверждения удаления позиции:

HTML код страницы с диалогом подтверждения удаления и комментариями
Стиль "gap-4" - уместить 4 элемента
Включено логирование SQL запросов в application.yaml:
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: trace
Log:
Hibernate:
insert
into
employees
(email, first_name, last_name, position_id, id)
values
(?, ?, ?, ?, default)
23:54:07.281+05:00 TRACE 16885 --- [ main] org.hibernate.orm.jdbc.bind : binding parameter (1:VARCHAR) <- [Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. ]
23:54:07.282+05:00 TRACE 16885 --- [ main] org.hibernate.orm.jdbc.bind : binding parameter (2:VARCHAR) <- [Name 9]
23:54:07.283+05:00 TRACE 16885 --- [ main] org.hibernate.orm.jdbc.bind : binding parameter (3:VARCHAR) <- [Lastname 9]
23:54:07.293+05:00 TRACE 16885 --- [ main] org.hibernate.orm.jdbc.bind : binding parameter (4:BIGINT) <- [3]
23:54:07.301+05:00 INFO 16885 --- [ main] n.g.s.f.initializer.DataInitializer : Test data added.
Запуск как сервис в Linux
Описано здесь Autostart сервиса в linux.
Создать файл hr_admin.service в /etc/systemd:
[Unit]
Description=HR admin
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=vasi
Group=vasi
ExecReload=/bin/kill -HUP
ExecStart=/home/vasi/temp/hr_admin.sh
SyslogIdentifier=hr_admin
Restart=always
[Install]
WantedBy=multi-user.target
/home/vasi/temp/hr_admin.sh:
/usr/lib/jvm/java-17-openjdk-amd64/bin/java -jar /home/vasi/temp/hr-admin-0.0.5.jar --server.port=8088
Перечитать сервисы:
sudo systemctl daemon-reload
Включить сервис:
systemctl enable hr_admin.service
Запустить сервис:
systemctl start hr_admin.service
Подключение к MCP сервису для VS Code
Создать каталог .codeassistant в каталоге проекта. В нем создать файл mcp.json:
{
"mcpServers": {
"github": {
"type": "stdio",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"-e",
"GITHUB_TOOLSETS",
"-e",
"GITHUB_READ_ONLY",
"ghcr.io/github/github-mcp-server"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ТОКЕН_GITHUB",
"GITHUB_TOOLSETS": "",
"GITHUB_READ_ONLY": ""
}
}
}
}
Задание RULES для gigacode агента
Правил находятся в файле .gigacode/rules/hr_admin_rules.md
# Правила проекта
## Архитектура
- Используй слоистую архитектуру: controller, service, repository, model, dto.
- Бизнес-логика должна находиться в service-слое.
- Controller не должен напрямую обращаться к repository.
- названия методов должны быть глаголами в форме глагол + объект.
## Тестирование
- Для всех новых публичных методов создавай unit-тесты.
- Для API-изменений добавляй интеграционные тесты.
- Используй существующий стиль тестов проекта.
## Безопасность
- Не читай и не изменяй файлы .env, credentials.json, secrets.yaml.
- Не выводи секреты и токены в ответах.
- При изменении авторизации указывай возможные риски.
(скачал откуда-то)
Итоговые правила (prompt: "покажи rules для агента"):
# HR Admin Rules
## Архитектура
- Используй слоистую архитектуру: controller, service, repository, model, dto
- Бизнес-логика должна находиться в service-слое
- Controller не должен напрямую обращаться к repository
- Названия методов должны быть глаголами в форме "глагол + объект" (например: `createEmployee`, `deletePosition`)
## Тестирование
- Для всех новых публичных методов создавай unit-тесты
- Для API-изменений добавляй интеграционные тесты
- Используй существующий стиль тестов проекта (JUnit 5 + Mockito)
- Тесты пишутся в snake_case или camelCase для методов (например: `shouldListEmployeesWithPagination`, `create_employee_and_redirect`)
## Безопасность
- Не читай и не изменяй файлы `.env`, `credentials.json`, `secrets.yaml`
- Не выводи секреты и токены в ответах
- При изменении авторизации указывай возможные риски
## Код-стайл (AGENTS.md)
- Индентация: 4 пробела
- Максимальная длина строки: 120 символов
- Используй IntelliJ IDEA default code style для Java
- Используем Lombok: `@RequiredArgsConstructor` для DI, `@Slf4j` для логирования
- Предпочитаем инициализацию final полей через конструктор
- Избегай `var` — предпочитай явные типы
- Избегай мутаций объектов, особенно в foreach и Stream API
- Используй ранний выход (early return) где возможно
- Избегай методов с `throws` — используй unchecked exceptions
- Контроллеры называются `...Controller`, сервисы — `...Service`, репозитории — `...Repository`
- Для шаблонов используем FreeMarker с расширением `.ftlh`
- View-имена храним в интерфейсе `NamesView`
Из GigaIde в облаке https://gigaide-dc9f0332-248f-4c32-954e-d029312d17ee.containerapps.ru/proxy/8088/hr_admin/
Ввод голосом
На мобильных телефонах значения в текстовые поля можно вводить голосом при нажатии на иконку микрофона в правом верхнем углу клавиатуры.