+ up
This commit is contained in:
@@ -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<Void> 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<Void> 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<Void> 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()));
|
||||
}
|
||||
}
|
||||
|
||||
105
src/main/java/su/xserver/iikocon/config/AppConfig.java
Normal file
105
src/main/java/su/xserver/iikocon/config/AppConfig.java
Normal file
@@ -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<String, String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
26
src/main/java/su/xserver/iikocon/config/DatabaseConfig.java
Normal file
26
src/main/java/su/xserver/iikocon/config/DatabaseConfig.java
Normal file
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
22
src/main/java/su/xserver/iikocon/config/RedisConfig.java
Normal file
22
src/main/java/su/xserver/iikocon/config/RedisConfig.java
Normal file
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/main/java/su/xserver/iikocon/config/ServerConfig.java
Normal file
16
src/main/java/su/xserver/iikocon/config/ServerConfig.java
Normal file
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
48
src/main/java/su/xserver/iikocon/service/RedisService.java
Normal file
48
src/main/java/su/xserver/iikocon/service/RedisService.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user