From 5759a223d2d73a94a4cab4bba4300748afafdc57 Mon Sep 17 00:00:00 2001 From: Danil-Bodry Date: Fri, 17 Apr 2026 13:57:48 +0300 Subject: [PATCH] + up --- docker-compose.yml | 18 +- .../java/su/xserver/iikocon/MainVerticle.java | 158 +++++++++--------- .../su/xserver/iikocon/config/AppConfig.java | 105 ++++++++++++ .../iikocon/config/DatabaseConfig.java | 26 +++ .../xserver/iikocon/config/RedisConfig.java | 22 +++ .../xserver/iikocon/config/ServerConfig.java | 16 ++ .../iikocon/service/DataBaseService.java | 46 +++++ .../iikocon/service/HealthCheckService.java | 44 +++++ .../xserver/iikocon/service/RedisService.java | 48 ++++++ src/main/resources/config.json | 22 +++ src/main/resources/log4j2.xml | 48 ++++++ 11 files changed, 466 insertions(+), 87 deletions(-) create mode 100644 src/main/java/su/xserver/iikocon/config/AppConfig.java create mode 100644 src/main/java/su/xserver/iikocon/config/DatabaseConfig.java create mode 100644 src/main/java/su/xserver/iikocon/config/RedisConfig.java create mode 100644 src/main/java/su/xserver/iikocon/config/ServerConfig.java create mode 100644 src/main/java/su/xserver/iikocon/service/DataBaseService.java create mode 100644 src/main/java/su/xserver/iikocon/service/HealthCheckService.java create mode 100644 src/main/java/su/xserver/iikocon/service/RedisService.java create mode 100644 src/main/resources/config.json create mode 100644 src/main/resources/log4j2.xml diff --git a/docker-compose.yml b/docker-compose.yml index bd76cb1..bd67d5a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,16 +53,16 @@ services: container_name: iiko-app restart: unless-stopped ports: - - "7104:8080" + - "7104:7104" depends_on: - db - redis environment: - DB_HOST: db - DB_PORT: 3306 - DB_NAME: app_db - DB_USER: app_user - DB_PASSWORD: app_pass - REDIS_HOST: redis - REDIS_PORT: 6379 - HTTP_PORT: 8080 + DATABASE__HOST: db + DATABASE__PORT: 3306 + DATABASE__DATABASE: app_db + DATABASE__USER: app_user + DATABASE__PASSWORD: app_pass + REDIS__HOST: redis + REDIS__PORT: 6379 + SERVER__PORT: 7104 diff --git a/src/main/java/su/xserver/iikocon/MainVerticle.java b/src/main/java/su/xserver/iikocon/MainVerticle.java index 1a28316..0e72f52 100644 --- a/src/main/java/su/xserver/iikocon/MainVerticle.java +++ b/src/main/java/su/xserver/iikocon/MainVerticle.java @@ -1,70 +1,80 @@ package su.xserver.iikocon; +import io.vertx.config.ConfigRetriever; +import io.vertx.config.ConfigRetrieverOptions; +import io.vertx.config.ConfigStoreOptions; import io.vertx.core.AbstractVerticle; import io.vertx.core.Promise; +import io.vertx.core.http.HttpServer; import io.vertx.core.json.JsonObject; -import io.vertx.ext.healthchecks.HealthChecks; -import io.vertx.ext.healthchecks.Status; import io.vertx.ext.web.Router; import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.SessionHandler; import io.vertx.ext.web.handler.StaticHandler; import io.vertx.ext.web.sstore.LocalSessionStore; import io.vertx.ext.web.sstore.SessionStore; -import io.vertx.ext.web.sstore.redis.RedisSessionStore; -import io.vertx.mysqlclient.MySQLConnectOptions; -import io.vertx.redis.client.Redis; -import io.vertx.redis.client.RedisAPI; -import io.vertx.redis.client.RedisOptions; -import io.vertx.sqlclient.Pool; -import io.vertx.sqlclient.PoolOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import java.util.Collections; +import su.xserver.iikocon.config.AppConfig; +import su.xserver.iikocon.service.DataBaseService; +import su.xserver.iikocon.service.HealthCheckService; +import su.xserver.iikocon.service.RedisService; public class MainVerticle extends AbstractVerticle { - private static final Logger log = LoggerFactory.getLogger(MainVerticle.class); - private Pool dbPool; + private final Logger log = LoggerFactory.getLogger("[MainVerticle]"); + + private DataBaseService db; + private RedisService redis; + private HttpServer httpServer; + private AppConfig config; + private UserService userService; @Override public void start(Promise startPromise) { - // Конфигурация из переменных окружения - JsonObject config = new JsonObject() - .put("db_host", System.getenv().getOrDefault("DB_HOST", "localhost")) - .put("db_port", Integer.parseInt(System.getenv().getOrDefault("DB_PORT", "3306"))) - .put("db_name", System.getenv().getOrDefault("DB_NAME", "admin_db")) - .put("db_user", System.getenv().getOrDefault("DB_USER", "admin_user")) - .put("db_password", System.getenv().getOrDefault("DB_PASSWORD", "admin_pass")) - .put("http_port", Integer.parseInt(System.getenv().getOrDefault("HTTP_PORT", "8080"))); - log.info("Starting with config: {}", config.encodePrettily()); + ConfigStoreOptions classpathStore = new ConfigStoreOptions() + .setType("file") + .setFormat("json") + .setConfig(new JsonObject().put("path", "config.json").put("hierarchical", true)) + .setOptional(false); - // Подключение к MariaDB - MySQLConnectOptions connectOptions = new MySQLConnectOptions() - .setHost(config.getString("db_host")) - .setPort(config.getInteger("db_port")) - .setDatabase(config.getString("db_name")) - .setUser(config.getString("db_user")) - .setPassword(config.getString("db_password")); + ConfigRetrieverOptions options = new ConfigRetrieverOptions() + .addStore(classpathStore); - PoolOptions poolOptions = new PoolOptions().setMaxSize(5); - dbPool = Pool.pool(vertx, connectOptions, poolOptions); + ConfigRetriever retriever = ConfigRetriever.create(vertx, options); - // Инициализация сервисов - userService = new UserService(dbPool); + retriever.getConfig() + .onSuccess(cfg -> { + config = AppConfig.from(cfg); - // Инициализация БД (создание таблицы users) - userService.initDatabase() - .onSuccess(v -> log.info("Database initialized successfully")) - .onFailure(err -> { - log.error("Failed to initialize database", err); - startPromise.fail(err); - return; - }); + db = new DataBaseService(vertx, config.database); + redis = new RedisService(vertx, config.redis); + + // Инициализация сервисов + userService = new UserService(db.getPool()); + + // Инициализация БД (создание таблицы users) + userService.initDatabase() + .onSuccess(v -> log.info("Database initialized successfully")) + .onFailure(err -> { + log.error("Failed to initialize database", err); + startPromise.fail(err); + return; + }); + + Router router = initRouter(); + + startHttp(router, startPromise); + + }) + .onFailure(startPromise::fail); + + } + + private Router initRouter() { // Настройка сессий (используем LocalSessionStore для простоты) SessionStore sessionStore = LocalSessionStore.create(vertx); @@ -95,27 +105,8 @@ public class MainVerticle extends AbstractVerticle { }); // Health Checks - HealthChecks hc = HealthChecks.create(vertx); - hc.register("database", promise -> - dbPool.getConnection().onComplete(ar -> { - if (ar.succeeded()) { - ar.result().close(); - promise.complete(Status.OK()); - } else { - promise.fail(ar.cause()); - } - }) - ); - - router.get("/health").handler(rc -> - hc.checkStatus().onComplete(ar -> { - if (ar.succeeded()) { - rc.response().end(ar.result().toJson().encodePrettily()); - } else { - rc.response().setStatusCode(503).end(ar.cause().getMessage()); - } - }) - ); + HealthCheckService healthCheckService = new HealthCheckService(vertx, redis, db); + healthCheckService.registerHealthCheck(router); // API маршруты AuthHandler authHandler = new AuthHandler(userService); @@ -169,26 +160,37 @@ public class MainVerticle extends AbstractVerticle { .setCachingEnabled(false) .setIndexPage("index.html")); - // Запуск HTTP сервера - int port = config.getInteger("http_port"); - vertx.createHttpServer() - .requestHandler(router) - .listen(port).onComplete(http -> { - if (http.succeeded()) { - log.info("HTTP server started on port {}", port); - startPromise.complete(); - } else { - log.error("Failed to start HTTP server", http.cause()); - startPromise.fail(http.cause()); - } + return router; + } + + private void startHttp(Router router, Promise startPromise) { + // Запуск HTTP-сервера + httpServer = vertx.createHttpServer(); + httpServer.requestHandler(router).listen(config.server.port, config.server.host) + .onSuccess(server -> { + log.info("HTTP server started on port {}", server.actualPort()); + startPromise.complete(); + }) + .onFailure(throwable -> { + log.error(throwable.getMessage()); + startPromise.fail(throwable); }); } @Override public void stop(Promise stopPromise) { - if (dbPool != null) { - dbPool.close(); - } - stopPromise.complete(); + this.httpServer.close() + .onSuccess(server -> { + log.info("Stop HTTP server"); + this.db.disconnect(); + this.redis.disconnect(); + this.vertx.close() + .onSuccess(vertx -> { + log.info("Stop Vert.x"); + stopPromise.complete(); + }) + .onFailure(throwable -> log.info(throwable.getMessage())); + }) + .onFailure(throwable -> log.info(throwable.getMessage())); } } diff --git a/src/main/java/su/xserver/iikocon/config/AppConfig.java b/src/main/java/su/xserver/iikocon/config/AppConfig.java new file mode 100644 index 0000000..385d171 --- /dev/null +++ b/src/main/java/su/xserver/iikocon/config/AppConfig.java @@ -0,0 +1,105 @@ +package su.xserver.iikocon.config; + +import io.vertx.core.json.JsonObject; +import java.util.Map; + +public class AppConfig { + + public ServerConfig server; + public DatabaseConfig database; + public RedisConfig redis; + + public static AppConfig from(JsonObject json) { + JsonObject resolved = json.copy(); + applyEnvOverrides(resolved); + return resolved.mapTo(AppConfig.class); + } + + private static void applyEnvOverrides(JsonObject config) { + Map env = System.getenv(); + + env.forEach((key, value) -> { + String path = envKeyToPath(key); + + if (path != null && exists(config, path)) { + setValue(config, path, value); + } + }); + } + + /** + * SERVER__MAX_POOL_SIZE -> server.maxPoolSize + */ + private static String envKeyToPath(String envKey) { + if (!envKey.contains("__")) { + return null; + } + + String[] levels = envKey.toLowerCase().split("__"); + + for (int i = 0; i < levels.length; i++) { + levels[i] = toCamelCase(levels[i]); + } + + return String.join(".", levels); + } + + private static String toCamelCase(String value) { + String[] parts = value.split("_"); + StringBuilder sb = new StringBuilder(parts[0]); + + for (int i = 1; i < parts.length; i++) { + sb.append(Character.toUpperCase(parts[i].charAt(0))) + .append(parts[i].substring(1)); + } + + return sb.toString(); + } + + private static boolean exists(JsonObject json, String path) { + String[] parts = path.split("\\."); + JsonObject current = json; + + for (int i = 0; i < parts.length - 1; i++) { + if (!current.containsKey(parts[i])) { + return false; + } + current = current.getJsonObject(parts[i]); + } + return current.containsKey(parts[parts.length - 1]); + } + + private static void setValue(JsonObject json, String path, String value) { + String[] parts = path.split("\\."); + JsonObject current = json; + + for (int i = 0; i < parts.length - 1; i++) { + current = current.getJsonObject(parts[i]); + } + + String key = parts[parts.length - 1]; + Object oldValue = current.getValue(key); + current.put(key, cast(value, oldValue)); + } + + private static Object cast(String value, Object oldValue) { + if (oldValue instanceof Integer) return Integer.parseInt(value); + if (oldValue instanceof Long) return Long.parseLong(value); + if (oldValue instanceof Boolean) return Boolean.parseBoolean(value); + if (oldValue == null) return value; + return value; + } + + public JsonObject json() { + return new JsonObject() + .put("server", server.json().getJsonObject("server")) + .put("database", database.json().getJsonObject("database")) + .put("redis", redis.json().getJsonObject("redis")); + } + + @Override + public String toString() { + return json().encode(); + } +} + diff --git a/src/main/java/su/xserver/iikocon/config/DatabaseConfig.java b/src/main/java/su/xserver/iikocon/config/DatabaseConfig.java new file mode 100644 index 0000000..e5acdf4 --- /dev/null +++ b/src/main/java/su/xserver/iikocon/config/DatabaseConfig.java @@ -0,0 +1,26 @@ +package su.xserver.iikocon.config; + +import io.vertx.core.json.JsonObject; + +public class DatabaseConfig { + public String host; + public int port; + public String database; + public String user; + public String password; + public int connectionTimeout; + public int maxPoolSize; + + public JsonObject json() { + return new JsonObject() + .put("database", new JsonObject() + .put("host", host) + .put("port", port) + .put("user", user) + .put("password", password) + .put("database", database) + .put("connectionTimeout", connectionTimeout) + .put("maxPoolSize", maxPoolSize) + ); + } +} diff --git a/src/main/java/su/xserver/iikocon/config/RedisConfig.java b/src/main/java/su/xserver/iikocon/config/RedisConfig.java new file mode 100644 index 0000000..c7163e5 --- /dev/null +++ b/src/main/java/su/xserver/iikocon/config/RedisConfig.java @@ -0,0 +1,22 @@ +package su.xserver.iikocon.config; + +import io.vertx.core.json.JsonObject; + +public class RedisConfig { + public String host; + public int port; + public String password; + public int maxPoolSize; + public int maxWaitingHandlers; + + public JsonObject json() { + return new JsonObject() + .put("redis", new JsonObject() + .put("host", host) + .put("port", port) + .put("password", password) + .put("maxPoolSize", maxPoolSize) + .put("maxWaitingHandlers", maxWaitingHandlers) + ); + } +} diff --git a/src/main/java/su/xserver/iikocon/config/ServerConfig.java b/src/main/java/su/xserver/iikocon/config/ServerConfig.java new file mode 100644 index 0000000..f4a7126 --- /dev/null +++ b/src/main/java/su/xserver/iikocon/config/ServerConfig.java @@ -0,0 +1,16 @@ +package su.xserver.iikocon.config; + +import io.vertx.core.json.JsonObject; + +public class ServerConfig { + public int port; + public String host; + + public JsonObject json() { + return new JsonObject() + .put("server", new JsonObject() + .put("port", port) + .put("host", host) + ); + } +} diff --git a/src/main/java/su/xserver/iikocon/service/DataBaseService.java b/src/main/java/su/xserver/iikocon/service/DataBaseService.java new file mode 100644 index 0000000..1730210 --- /dev/null +++ b/src/main/java/su/xserver/iikocon/service/DataBaseService.java @@ -0,0 +1,46 @@ +package su.xserver.iikocon.service; + +import io.vertx.core.Vertx; +import io.vertx.mysqlclient.MySQLConnectOptions; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.PoolOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import su.xserver.iikocon.config.DatabaseConfig; + +public class DataBaseService { + private final Logger log = LoggerFactory.getLogger("[DataBaseService]"); + private final DatabaseConfig config; + private final Pool pool; + + public DataBaseService(Vertx vertx, DatabaseConfig config) { + log.info("Initialization"); + this.config = config; + this.pool = Pool.pool(vertx, this.getMySQLConnectOptions(), this.getPoolOptions()); + } + + public Pool getPool() { + return this.pool; + } + + public void disconnect() { + this.pool.close(); + log.info("Connection is closed!"); + } + + private MySQLConnectOptions getMySQLConnectOptions() { + return new MySQLConnectOptions() + .setHost(this.config.host) + .setPort(this.config.port) + .setDatabase(this.config.database) + .setUser(this.config.user) + .setPassword(this.config.password); + } + + private PoolOptions getPoolOptions() { + return new PoolOptions() + .setMaxSize(this.config.maxPoolSize) + .setConnectionTimeout(this.config.connectionTimeout) + .setName(this.config.database); + } +} diff --git a/src/main/java/su/xserver/iikocon/service/HealthCheckService.java b/src/main/java/su/xserver/iikocon/service/HealthCheckService.java new file mode 100644 index 0000000..ad025c6 --- /dev/null +++ b/src/main/java/su/xserver/iikocon/service/HealthCheckService.java @@ -0,0 +1,44 @@ +package su.xserver.iikocon.service; + +import io.vertx.core.Vertx; +import io.vertx.ext.healthchecks.Status; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.healthchecks.HealthCheckHandler; + +import java.util.Collections; + +public class HealthCheckService { + + private final RedisService redisService; + private final DataBaseService dbService; + private final Vertx vertx; + + public HealthCheckService(Vertx vertx, RedisService redisService, DataBaseService dbService) { + this.vertx = vertx; + this.redisService = redisService; + this.dbService = dbService; + } + + public void registerHealthCheck(Router router) { + HealthCheckHandler healthCheckHandler = HealthCheckHandler.create(vertx); + + // Redis check + healthCheckHandler.register("redis", future -> redisService.getRedisApi().ping(Collections.emptyList()) + .onSuccess(response -> { + if ("PONG".equalsIgnoreCase(response.toString())) { + future.complete(Status.OK()); + } else { + future.tryFail("Unexpected Redis response: " + response); + } + }) + .onFailure(err -> future.tryFail("Redis ping failed: " + err.getMessage()))); + + // Database check + healthCheckHandler.register("database", future -> dbService.getPool().query("SELECT 1").execute() + .onSuccess(rs -> future.complete(Status.OK())) + .onFailure(t -> future.fail("Database is not reachable: " + t.getMessage()))); + + // Регистрируем endpoint /health + router.get("/health").handler(healthCheckHandler); + } +} diff --git a/src/main/java/su/xserver/iikocon/service/RedisService.java b/src/main/java/su/xserver/iikocon/service/RedisService.java new file mode 100644 index 0000000..b20aede --- /dev/null +++ b/src/main/java/su/xserver/iikocon/service/RedisService.java @@ -0,0 +1,48 @@ +package su.xserver.iikocon.service; + +import io.vertx.core.Vertx; +import io.vertx.redis.client.Redis; +import io.vertx.redis.client.RedisAPI; +import io.vertx.redis.client.RedisOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import su.xserver.iikocon.config.RedisConfig; + +public class RedisService { + private final Logger log = LoggerFactory.getLogger("[RedisService]"); + private final RedisConfig config; + private final Redis redis; + + public RedisService(Vertx vertx, RedisConfig config) { + log.info("Initialization"); + this.config = config; + this.redis = Redis.createClient(vertx, this.getRedisOptions()); + } + + public Redis getRedis() { + return this.redis; + } + + public RedisAPI getRedisApi() { + return RedisAPI.api(this.redis); + } + + public void disconnect() { + this.redis.close(); + log.info("Connection is closed!"); + } + + private RedisOptions getRedisOptions() { + RedisOptions options = new RedisOptions() + .addConnectionString(String.format("redis://%s:%s", + this.config.host, + this.config.port)) + .setMaxPoolSize(this.config.maxPoolSize) + .setMaxWaitingHandlers(this.config.maxWaitingHandlers); + + if (this.config.password != null) { + options.setPassword(this.config.password); + } + return options; + } +} diff --git a/src/main/resources/config.json b/src/main/resources/config.json new file mode 100644 index 0000000..05c3fa4 --- /dev/null +++ b/src/main/resources/config.json @@ -0,0 +1,22 @@ +{ + "server": { + "port": 8080, + "host": "0.0.0.0" + }, + "database": { + "host": "localhost", + "port": 3306, + "user": "user", + "password": "password", + "database": "database", + "connectionTimeout": 5000, + "maxPoolSize": 8 + }, + "redis": { + "host": "localhost", + "port": 6379, + "password": null, + "maxPoolSize": 6, + "maxWaitingHandlers": 6 + } +} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..b9844e8 --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,48 @@ + + + + + logs + iiko-app + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +