package com.doopp.reactor.guice; import com.doopp.reactor.guice.annotation.Controller; import com.doopp.reactor.guice.json.HttpMessageConverter; import com.doopp.reactor.guice.publisher.*; import com.doopp.reactor.guice.view.TemplateDelegate; import com.doopp.reactor.guice.websocket.AbstractWebSocketServerHandle; import com.doopp.reactor.guice.websocket.WebSocketServerHandle; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import io.netty.channel.ChannelOption; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.netty.DisposableServer; import reactor.netty.http.server.*; import javax.net.ssl.KeyManagerFactory; import javax.ws.rs.*; import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; import java.io.File; import java.io.FileInputStream; import java.lang.reflect.Method; import java.net.URL; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.KeyStore; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; public class ReactorGuiceServer { private String host = "127.0.0.1"; private int port = 8083; private int sslPort = 8084; final private String version = "0.12.4"; // handle private HandlePublisher handlePublisher = new HandlePublisher(); // static file publisher private StaticFilePublisher staticFilePublisher = new StaticFilePublisher(); // websocket private WebsocketPublisher websocketPublisher = new WebsocketPublisher(); private Injector injector; // error log print private boolean printError = false; // Cross-Origin Resource Sharing private boolean crossOrigin = false; // api gateway model default disabled private ApiGatewayDispatcher apiGatewayDispatcher; private final Map<String, Class<? extends Filter>> filters = new HashMap<>(); private final Set<String> basePackages = new HashSet<>(); private final Set<Module> modules = new HashSet<>(); static final Set<String> classNames = new HashSet<>(); private SslContext sslContext = null; private static Map<String, FileSystem> jarPathFS = new HashMap<>(); public static ReactorGuiceServer create() { return new ReactorGuiceServer(); } public ReactorGuiceServer bind(String host, int port) { this.host = host; this.port = port; return this; } public ReactorGuiceServer bind(String host, int port, int sslPort) { this.host = host; this.port = port; this.sslPort = sslPort; return this; } public ReactorGuiceServer basePackages(String... basePackages) { Collections.addAll(this.basePackages, basePackages); this.setClassNames(); return this; } public ReactorGuiceServer createInjector(Module... modules) { Collections.addAll(this.modules, modules); return this; } public ReactorGuiceServer importInjector(Injector injector) { assert injector!=null : "A Injector instance is required"; this.injector = injector; return this; } public ReactorGuiceServer addFilter(String path, Class<? extends Filter> clazz) { this.filters.put(path, clazz); return this; } public ReactorGuiceServer addResource(String uriBegin, String resourcePath) { staticFilePublisher.addResourceLocations(uriBegin, resourcePath); return this; } public ReactorGuiceServer setHttpMessageConverter(HttpMessageConverter httpMessageConverter) { assert httpMessageConverter!=null : "A HttpMessageConverter instance is required"; handlePublisher.setHttpMessageConverter(httpMessageConverter); return this; } public ReactorGuiceServer setTemplateDelegate(TemplateDelegate templateDelegate) { assert templateDelegate!=null : "A TemplateDelegate instance is required"; handlePublisher.setTemplateDelegate(templateDelegate); return this; } public ReactorGuiceServer setApiGatewayDispatcher(ApiGatewayDispatcher apiGatewayDispatcher) { assert apiGatewayDispatcher!=null : "A ApiGatewayDispatcher instance is required"; this.apiGatewayDispatcher = apiGatewayDispatcher; return this; } public ReactorGuiceServer printError(boolean printError) { this.printError = printError; return this; } public ReactorGuiceServer crossOrigin (boolean crossOrigin) { this.crossOrigin = crossOrigin; return this; } public ReactorGuiceServer setHttps (File jksFile, String jksPassword, String jksSecret) { try { // ssl KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(new FileInputStream(jksFile), jksPassword.toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, jksSecret.toCharArray()); sslContext = SslContextBuilder.forServer(keyManagerFactory).build(); } catch(Exception e) { e.printStackTrace(); sslContext = null; } // SelfSignedCertificate cert = new SelfSignedCertificate(); // SslContextBuilder serverOptions = SslContextBuilder.forServer(cert.certificate(), cert.privateKey()); return this; } public ReactorGuiceServer setTestHttps () { try { SelfSignedCertificate cert = new SelfSignedCertificate(); SslContextBuilder serverOptions = SslContextBuilder.forServer(cert.certificate(), cert.privateKey()); sslContext = serverOptions.build(); } catch(Exception e) { e.printStackTrace(); sslContext = null; } // SelfSignedCertificate cert = new SelfSignedCertificate(); // SslContextBuilder serverOptions = SslContextBuilder.forServer(cert.certificate(), cert.privateKey()); return this; } public void launch() { // 如果 injector 没有设定,就使用自动扫描的注入 if (this.injector==null) { modules.add(new AutoImportModule()); this.injector = Guice.createInjector(modules); } Consumer<HttpServerRoutes> httpServerRoutesConsumer = this.routesBuilder(); // 启动服务 DisposableServer disposableServer = HttpServer.create() .tcpConfiguration(tcpServer -> { if (sslContext==null) { return tcpServer.option(ChannelOption.SO_KEEPALIVE, true) // .secure(s -> s.sslContext(sslContext)) // .option(ChannelOption.SO_BACKLOG, 128) .host(this.host) .port(this.port); } else { return tcpServer.option(ChannelOption.SO_KEEPALIVE, true) .secure(s -> s.sslContext(sslContext)) // .option(ChannelOption.SO_BACKLOG, 128) .host(this.host) .port(this.sslPort); } }) .route(httpServerRoutesConsumer) // .host(this.host) // .port(this.port) .wiretap(true) .bindNow(); // if (sslContext!=null) { // DisposableServer sslDisposableServer = HttpServer.create() // .tcpConfiguration(tcpServer -> { // if (sslContext!=null) { // return tcpServer.option(ChannelOption.SO_KEEPALIVE, true) // .secure(s -> s.sslContext(sslContext)) // .host(this.host) // .port(this.sslPort); // } // return tcpServer; // }) // .route(httpServerRoutesConsumer) // // .host(this.host) // // .port(this.port) // .wiretap(true) // .bindNow(); // // Channel ch80 = disposableServer.channel().ss; // Channel ch443 = sslDisposableServer.channel(); // // ch80.closeFuture().sync(); // ch443.closeFuture().sync(); // } System.out.printf("\n>>> KReactor Server Running http://%s:%d/ ... \n\n", this.host, this.port); disposableServer.onDispose().block(); } private Consumer<HttpServerRoutes> routesBuilder() { // routes return routes -> { for (String className : classNames) { if (injector == null) { continue; } Object handleObject; Class<?> handleClass; try { handleClass = Class.forName(className); if (handleClass.isInterface()) { continue; } } catch(ClassNotFoundException e) { continue; } // 拿到根路径 Path pathAnnotation = handleClass.getAnnotation(Path.class); String rootPath = (pathAnnotation == null) ? "" : pathAnnotation.value(); // if websocket if (AbstractWebSocketServerHandle.class.isAssignableFrom(handleClass) && pathAnnotation != null) { handleObject = injector.getInstance(handleClass); System.out.println(" WS " + rootPath + " → " + className); routes.get(rootPath, (req, resp) -> httpPublisher(req, resp, null, o -> websocketPublisher.sendMessage(req, resp, (WebSocketServerHandle) handleObject, o) )); continue; } // if is not controller if (!handleClass.isAnnotationPresent(Controller.class)) { continue; } handleObject = injector.getInstance(handleClass); // methods for handle Method[] handleMethods = handleClass.getMethods(); // loop methods for (Method method : handleMethods) { // if have request path if (method.isAnnotationPresent(Path.class)) { String requestPath = rootPath + method.getAnnotation(Path.class).value(); // GET if (method.isAnnotationPresent(GET.class)) { System.out.println(" GET " + requestPath + " → " + className + ":" + method.getName()); routes.get(requestPath, (req, resp) -> httpPublisher(req, resp, method, o -> handlePublisher.sendResult(req, resp, method, handleObject, o) )); } // POST else if (method.isAnnotationPresent(POST.class)) { System.out.println(" POST " + requestPath + " → " + className + ":" + method.getName()); routes.post(requestPath, (req, resp) -> httpPublisher(req, resp, method, o -> handlePublisher.sendResult(req, resp, method, handleObject, o) )); } // DELETE else if (method.isAnnotationPresent(DELETE.class)) { System.out.println("DELETE " + requestPath + " → " + className + ":" + method.getName()); routes.delete(requestPath, (req, resp) -> httpPublisher(req, resp, method, o -> handlePublisher.sendResult(req, resp, method, handleObject, o) )); } // UPDATE else if (method.isAnnotationPresent(PUT.class)) { System.out.println(" PUT " + requestPath + " → " + className + ":" + method.getName()); routes.put(requestPath, (req, resp) -> httpPublisher(req, resp, method, o -> handlePublisher.sendResult(req, resp, method, handleObject, o) )); } if (crossOrigin) { // OPTION routes.options(requestPath, (req, resp) -> httpPublisher(req, resp, method, o -> Mono.empty())); } } } } // is is api gateway server if (this.apiGatewayDispatcher!=null) { ApiGatewayPublisher apiGatewayPublisher = new ApiGatewayPublisher(this.apiGatewayDispatcher); System.out.println(" Any /** → /** <api gateway model>"); routes.route(apiGatewayPublisher::checkRequest, (req, resp) -> httpPublisher(req, resp, null, o -> apiGatewayPublisher.sendResponse(req, resp, websocketPublisher, o) )); } // static server else { System.out.println(" GET /** → /public/* <static files>"); routes.route(req->true, (req, resp) -> httpPublisher(req, resp, null, o -> staticFilePublisher.sendFile(req, resp) )); } }; } private Publisher<Void> httpPublisher(HttpServerRequest req, HttpServerResponse resp, Method method, Function<Object, Mono<?>> handle) { // response header if (req.isKeepAlive()) { resp.header(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } resp.header(HttpHeaderNames.SERVER, "RGS/" + this.version); // cross domain if (crossOrigin) { resp.header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Headers", "X-Requested-With, accept, origin, content-type") .header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); } // result return doFilter(req, resp, new RequestAttribute()) .flatMap(handle) .onErrorMap(throwable -> { if (this.printError) { throwable.printStackTrace(); } // if handle @Products is json , RETURN StatusMessageException if (handlePublisher.methodProductsValue(method).contains(MediaType.APPLICATION_JSON)) { if (throwable instanceof StatusMessageException) { resp.status(((StatusMessageException) throwable).getCode()); resp.header(HttpHeaderNames.CONTENT_TYPE, MediaType.APPLICATION_JSON); return throwable; } else { resp.status(HttpResponseStatus.INTERNAL_SERVER_ERROR); resp.header(HttpHeaderNames.CONTENT_TYPE, MediaType.APPLICATION_JSON); return new StatusMessageException(HttpResponseStatus.INTERNAL_SERVER_ERROR, throwable.getMessage()); } } // else return other Exception else { if (throwable instanceof StatusMessageException) { resp.status(((StatusMessageException) throwable).getCode()); resp.header(HttpHeaderNames.CONTENT_TYPE, MediaType.TEXT_PLAIN); return new Exception(throwable.getMessage()); } else { resp.status(HttpResponseStatus.INTERNAL_SERVER_ERROR); resp.header(HttpHeaderNames.CONTENT_TYPE, MediaType.TEXT_PLAIN); return throwable; } } }) .onErrorResume(throwable -> { // is json if (throwable instanceof StatusMessageException) { if (handlePublisher.getHttpMessageConverter()==null) { return Mono.just("{\"err_code\":500, \"err_msg\":\"A Message Converter instance is required\", \"data\":null}"); } return Mono.just( handlePublisher.getHttpMessageConverter().toJson(throwable) ); } // string else { return Mono.just(throwable.getMessage()); } }) .flatMap(o -> { if (o instanceof Mono<?>) { return ((Mono<?>) o).then(); } return (o instanceof String) ? resp.sendString(Mono.just((String) o)).then() : resp.sendObject(Mono.just(o)).then(); }); } private Mono<Object> doFilter(HttpServerRequest req, HttpServerResponse resp, RequestAttribute requestAttribute) { // loop filter map for (String key : this.filters.keySet()) { // choice filter if (req.uri().length() >= key.length() && req.uri().startsWith(key)) { return this.injector.getInstance(this.filters.get(key)).doFilter(req, resp, requestAttribute); } } return Mono.just(requestAttribute); } // private File getUriFile(URI uri) { // final java.nio.file.Path resource; // if (uri.toString().startsWith("jar:")) { // Map<String, String> env = new HashMap<>(); // String[] array = uri.toString().split("!"); // FileSystem fs = FileSystems.newFileSystem(URI.create(array[0]), env); // resource = fs.getPath(array[1]); // fs.close(); // } else { // resource = Paths.get(uri); // } // return resource.toFile(); // } private void setClassNames() { // Set<String> handleClasses = new HashSet<>(); for(String basePackage : basePackages) { try { // URL resource = this.getClass().getResource("/" + basePackage.replace(".", "/")); // System.out.println("resource.toString() : " + resource.toString()); // System.out.println("resource.toURI() : " + resource.toURI()); // System.out.println("resource.getFile() : " + resource.getFile()); // System.out.println("resource.getPath() : " + resource.getPath()); java.nio.file.Path resourcePath = classResourcePath("/" + basePackage.replace(".", "/")).block(); if (resourcePath==null) { continue; } // System.out.println(resourcePath); Files.walkFileTree(resourcePath, new SimpleFileVisitor<java.nio.file.Path>() { @Override public FileVisitResult visitFile(java.nio.file.Path file, BasicFileAttributes attrs) { String uriPath = file.toUri().toString(); // System.out.println("file.toUri() : " + file.toUri()); // System.out.println("file.toRealPath() : " + file.toRealPath()); // System.out.println("file.toAbsolutePath() : " + file.toAbsolutePath()); if (uriPath.endsWith(".class")) { int startIndexOf = uriPath.indexOf(basePackage.replace(".", "/")); int endIndexOf = uriPath.indexOf(".class"); if (startIndexOf<0) { return FileVisitResult.CONTINUE; } String classPath = uriPath.substring(startIndexOf, endIndexOf); String className = classPath.replace("/", "."); classNames.add(className); } return FileVisitResult.CONTINUE; } }); } catch(Exception e) { e.printStackTrace(); } } // Set<String> handleClasses = new HashSet<>(); // Set<File> dirList = new HashSet<>(); // for(String basePackage : basePackages) { // URL scanUrl = this.getClass().getResource("/" + basePackage.replace(".", "/")); // File uriFile = getUriFile(scanUrl.toURI()); // if (uriFile.isDirectory()) { // dirList.add(uriFile); // } // int ii=1; // while(true) { // File ff = dirList.iterator().next(); // File[] fff = ff.listFiles(); // if (fff==null) { // continue; // } // for (File file : fff) { // if (file.isDirectory()) { // dirList.add(file); // ii++; // } // else if (file.getName().endsWith(".class")) { // handleClasses.add(file.getName()); // } // } // if (ii>=dirList.size()) { // break; // } // } // } // return handleClasses; // URL scanUrl = this.getClass().getResource("/" + this.basePackages.iterator().next().replace(".", "/")); // // // init result // Set<String> handleClassesName = new HashSet<>(); // if (this.handlePackages.size()<1) { // return handleClassesName; // } // URL resourceUrl = this.getClass().getResource("/" + this.handlePackages.iterator().next().replace(".", "/")); // if (resourceUrl==null) { // return handleClassesName; // } // String resourcePath = resourceUrl.getPath(); // // if is jar package // if (resourcePath.contains(".jar!")) { // try (JarFile jarFile = new JarFile(resourcePath.substring(5, resourcePath.lastIndexOf(".jar!") + 4))) { // Enumeration<JarEntry> entries = jarFile.entries(); // while (entries.hasMoreElements()) { // JarEntry jar = entries.nextElement(); // String name = jar.getName(); // for (String packageName : this.handlePackages) { // if (name.contains(packageName.replace(".", "/")) && name.contains(".class")) { // int beginIndex = packageName.length() + 1; // int endIndex = name.lastIndexOf(".class"); // String className = name.substring(beginIndex, endIndex); // handleClassesName.add(packageName + "." + className); // } // } // } // } // catch (IOException e) { // e.printStackTrace(); // } // } // // if is dir // else { // for (String packageName : this.handlePackages) { // String path = "/" + packageName.replace(".", "/"); // resourcePath = this.getClass().getResource(path).getPath(); // File dir = new File(resourcePath); // File[] files = dir.listFiles(); // if (files == null) { // continue; // } // for (File file : files) { // String name = file.getName(); // int endIndex = name.lastIndexOf(".class"); // String className = name.substring(0, endIndex); // handleClassesName.add(packageName + "." + className); // } // } // } // return handleClassesName; } // private static FileSystem classFs = null; public static Mono<java.nio.file.Path> classResourcePath(String resourceUri) { URL resource = ReactorGuiceServer.class.getResource(resourceUri); return Mono.fromCallable(()->{ if (resource.getProtocol().equals("jar")) { String[] jarPathInfo = resource.getPath().split("!"); if (jarPathInfo[0].startsWith("file:")) { jarPathInfo[0] = java.io.File.separator.equals("\\") ? jarPathInfo[0].substring(6) : jarPathInfo[0].substring(5); } if (jarPathFS.get(jarPathInfo[0])==null || !jarPathFS.get(jarPathInfo[0]).isOpen()) { java.nio.file.Path jarPath = Paths.get(jarPathInfo[0]); jarPathFS.put(jarPathInfo[0], FileSystems.newFileSystem(jarPath, null)); } return jarPathFS.get(jarPathInfo[0]).getPath(jarPathInfo[1]); } return Paths.get(resource.toURI()); }) .subscribeOn(Schedulers.elastic()) .onErrorResume(t->Mono.error(new StatusMessageException(404, "Not Found"))); } }