📜 ⬆️ ⬇️

Java and isomorphic React

image

To create isomorphic applications on React , Node.js is usually used as the server side. But, if the server is written in Java, then you should not give up the isomorphic application: Java includes an embedded javascript engine (Nashorn), which can cope with HTML server rendering with React.

The code for the application that demonstrates server rendering of React with a Java server is on GitHub . The article will be reviewed:
')

Java server


Consider the creation of a Java server in the style of microservice (a self-contained jar that does not require the use of any servlet containers). As a library for dependency management, we will use the CDI standard (Contexts and Dependency Injection), which came from the world of Java EE, but it can be used in Java SE applications. The CDI implementation, Weld SE, is a powerful and well-documented dependency management library. For CDI, there are many binders to other libraries, for example, the application uses CDI binders for JAX-RS and Netty. It is enough in the src / main / resources / META-INF directory to create the beans.xml file (declaration that this module supports CDI), mark the classes with standard attributes, initialize the container, and inject dependencies. Classes marked with special annotations will register automatically (manual registration is also available).

//  . public static void main(String[] args) { //  JUL     SLF4J. SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); LOG.info("Start application"); //  CDI  http://weld.cdi-spec.org/ final Weld weld = new Weld(); //  . weld.property(Weld.SHUTDOWN_HOOK_SYSTEM_PROPERTY, false); final WeldContainer container = weld.initialize(); //  Netty      JAX-RS,    CDI . final CdiNettyJaxrsServer nettyServer = new CdiNettyJaxrsServer(); ............... //  web . nettyServer.start(); .............. //   TERM   . try { final CountDownLatch shutdownSignal = new CountDownLatch(1); Runtime.getRuntime().addShutdownHook(new Thread(() -> { shutdownSignal.countDown(); })); try { shutdownSignal.await(); } catch (InterruptedException e) { } } finally { //    CDI . nettyServer.stop(); container.shutdown(); LOG.info("Application shutdown"); SLF4JBridgeHandler.uninstall(); } } //  ,    ""    @ApplicationScoped public class IncrementService { .............. } //   @NoCache @Path("/") @RequestScoped @Produces(MediaType.TEXT_HTML + ";charset=utf-8") public class RootResource { /** *   {@link IncrementService}. */ @Inject private IncrementService incrementService; .............. } 

For testing classes with CDI dependencies, the Arquillian extension for JUnit is used .

Modular test
 /** *   {@link IncrementResource}. */ @RunWith(Arquillian.class) public class IncrementResourceTest { @Inject private IncrementResource incrementResource; /** * @return  ,       CDI. */ @Deployment public static JavaArchive createDeployment() { return ShrinkWrap.create(JavaArchive.class) .addClass(IncrementResource.class) .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); } @Test public void getATest() { final Map<String, Integer> response = incrementResource.getA(); assertNotNull(response.get("value")); assertEquals(Integer.valueOf(1), response.get("value")); } .............. /** *    {@link IncrementService}.   RequestScoped: * Arquillian         . * @return   {@link IncrementService}. */ @Produces @RequestScoped public IncrementService getIncrementService() { final IncrementService service = mock(IncrementService.class); when(service.getA()).thenReturn(1); when(service.incrementA()).thenReturn(2); when(service.getB()).thenReturn(2); when(service.incrementB()).thenReturn(3); return service; } } 


We will configure the processing of web requests via the embedded web-server - Netty. For writing handler functions, we will use another standard, also derived from Java EE, JAX-RS. As an implementation of the JAX-RS standard, select the Resteasy library. A resteasy-netty4-cdi module is used to connect Netty, CDI and Resteasy . JAX-RS is configured using the heir class javax.ws.rs.core.Application. Usually, request handlers and other JAX-RS components are registered there. When using CDI and Resteasy, it is sufficient to indicate that JIX-RS components will use CDI-registered request handlers (marked with JAX-RS: Path annotation) and other JAX-RS components, called providers (marked with JAX-RS annotation: Provider) . More information about Resteasy can be found in the documentation .

Netty and JAX-RS Application
 public static void main(String[] args) { ............... //  Netty      JAX-RS,    CDI . //  JAX-RS   Resteasy http://resteasy.jboss.org/ final CdiNettyJaxrsServer nettyServer = new CdiNettyJaxrsServer(); //  Netty (  ). final String host = configuration.getString( AppConfiguration.WEBSERVER_HOST, AppConfiguration.WEBSERVER_HOST_DEFAULT); nettyServer.setHostname(host); final int port = configuration.getInt( AppConfiguration.WEBSERVER_PORT, AppConfiguration.WEBSERVER_PORT_DEFAULT); nettyServer.setPort(port); //  JAX-RS. final ResteasyDeployment deployment = nettyServer.getDeployment(); //     JAX-RS (   ). deployment.setInjectorFactoryClass(CdiInjectorFactory.class.getName()); //  ,   JAX-RS        . deployment.setApplicationClass(ReactReduxIsomorphicExampleApplication.class.getName()); //  web . nettyServer.start(); ............... } /** *          JAX-RS */ @ApplicationScoped @ApplicationPath("/") public class ReactReduxIsomorphicExampleApplication extends Application { /** *   CDI  Resteasy. */ @Inject private ResteasyCdiExtension extension; /** * @return        JAX-RS. */ @Override @SuppressWarnings("unchecked") public Set<Class<?>> getClasses() { final Set<Class<?>> result = new HashSet<>(); //   CDI  Resteasy      JAX-RS. result.addAll((Collection<? extends Class<?>>) (Object)extension.getResources()); //   CDI  Resteasy     JAX-RS. result.addAll((Collection<? extends Class<?>>) (Object)extension.getProviders()); return result; } } 


All static files (javascript bundles, css, images) are placed in the classpath (src / main / resources / webapp), they will fit into the resulting jar file. To access such files, a URL handler of the form {fileName:. *}. {Ext} is used, which loads the file from the classpath and gives it to the client.

Static Request Handler
 /** *     . * <p>       {filename}.{ext}</p> */ @Path("/") @RequestScoped public class StaticFilesResource { private final static Date START_DATE = DateUtils.setMilliseconds(new Date(), 0); @Inject private Configuration configuration; /** *     .    classpath. * @param fileName    . * @param ext  . * @param uriInfo URL ,    . * @param request   . * @return        404 -  . * @throws Exception   . */ @GET @Path("{fileName:.*}.{ext}") public Response getAsset( @PathParam("fileName") String fileName, @PathParam("ext") String ext, @Context UriInfo uriInfo, @Context Request request) throws Exception { if(StringUtils.contains(fileName, "nomin") || StringUtils.contains(fileName, "server")) { //    . return Response.status(Response.Status.NOT_FOUND) .build(); } //  ifModifiedSince .     classpath, //       . final ResponseBuilder builder = request.evaluatePreconditions(START_DATE); if (builder != null) { //   . return builder.build(); } //      classpath. final String fileFullName = "webapp/static/" + fileName + "." + ext; //  . final InputStream resourceStream = ResourceUtilities.getResourceStream(fileFullName); if(resourceStream != null) { //  ,     . final String cacheControl = configuration.getString( AppConfiguration.WEBSERVER_HOST, AppConfiguration.WEBSERVER_HOST_DEFAULT); //     . return Response.ok(resourceStream) .type(URLConnection.guessContentTypeFromName(fileFullName)) .cacheControl(CacheControl.valueOf(cacheControl)) .lastModified(START_DATE) .build(); } //   . return Response.status(Response.Status.NOT_FOUND) .build(); } } 


HTML server rendering on React


To build bundles when building a Java application, you can use the maven plugin frontend-maven-plugin . It loads and locally saves NodeJs of the required version locally, builds bundles using a webpack. It is enough to run the usual Java project building with the mvn command (or in the IDE that supports integration with maven). Client javascript, styles, package.json, webpack configuration file will be placed in the src / main / frontend directory, the resulting bundle in src / main / resources / webapp / static / assets.

Configure fronend-maven-plugin
 <plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <configuration> <nodeVersion>v${node.version}</nodeVersion> <npmVersion>${npm.version}</npmVersion> <installDirectory>${basedir}/src/main/frontend</installDirectory> <workingDirectory>${basedir}/src/main/frontend</workingDirectory> </configuration> <executions> <!--  nodejs  npm  . --> <execution> <id>nodeInstall</id> <goals> <goal>install-node-and-npm</goal> </goals> </execution> <!--   npm  src/main/frontend/package.json. --> <execution> <id>npmInstall</id> <goals> <goal>npm</goal> </goals> </execution> <!--     webpack. --> <execution> <id>webpackBuild</id> <goals> <goal>webpack</goal> </goals> <configuration> <skip>${webpack.skip}</skip> <arguments>${webpack.arguments}</arguments> <srcdir>${basedir}/src/main/frontend/app</srcdir> <outputdir>${basedir}/src/main/resources/webapp/static/assets</outputdir> <triggerfiles> <triggerfile>${basedir}/src/main/frontend/webpack.config.js</triggerfile> <triggerfile>${basedir}/src/main/frontend/package.json</triggerfile> </triggerfiles> </configuration> </execution> </executions> </plugin> 


To set up your own HTML page generator in JAX-RS, you need to create some kind of class, create a handler for it with annotations Provider, implementing the javax.ws.rs.ext.MessageBodyWriter interface, and return it as a response to the web request handler.
Server rendering is performed using the Nashorn engine built into Java javascript. This is a single-threaded scripting engine: to process several simultaneous requests, you need to use several cache engine instances, a free instance is taken for each request, HTML is rendered, then it is returned to the pool ( Apache Commons Pool 2 ).

 /** *    web-. */ public class ViewResult { private final String template; private final Map<String, Object> viewData = new HashMap<>(); private final Map<String, Object> reduxInitialState = new HashMap<>(); .............. } /** *   ,   {@link ViewResult}   HTML. * <p> *      React      HTML (React Isomorphic), *      ,    React. * </p> */ @Provider @ApplicationScoped public class ViewResultBodyWriter implements MessageBodyWriter<ViewResult> { .............. private ObjectPool<AbstractScriptEngine> enginePool = null; @PostConstruct public void initialize() { //   . final boolean useIsomorphicRender = configuration.getBoolean( AppConfiguration.WEBSERVER_ISOMORPHIC, AppConfiguration.WEBSERVER_ISOMORPHIC_DEFAULT); final int minIdleScriptEngines = configuration.getInt( AppConfiguration.WEBSERVER_MIN_IDLE_SCRIPT_ENGINES, AppConfiguration.WEBSERVER_MIN_IDLE_SCRIPT_ENGINES_DEFAULT); LOG.info("Isomorphic render: {}", useIsomorphicRender); if(useIsomorphicRender) { //     React  ,    // javascript . Javascript , //          javascript. final GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMinIdle(minIdleScriptEngines); enginePool = new GenericObjectPool<AbstractScriptEngine>(new ScriptEngineFactory(), config); } } @PreDestroy public void destroy() { if(enginePool != null) { enginePool.close(); } } .............. @Override public void writeTo( ViewResult t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { .............. if(enginePool != null && t.getUseIsomorphic()) { //  React  . try { //      javascript. final AbstractScriptEngine scriptEngine = enginePool.borrowObject(); try { // URL  ,  react-router     . final String uri = uriInfo.getPath() + (uriInfo.getRequestUri().getQuery() != null ? (String) ("?" + uriInfo.getRequestUri().getQuery()) : StringUtils.EMPTY); //    React. final String htmlContent = (String)((Invocable)scriptEngine).invokeFunction( "renderHtml", uri, initialStateJson); //     . enginePool.returnObject(scriptEngine); viewData.put(HTML_CONTENT_KEY, htmlContent); } catch (Throwable e) { enginePool.invalidateObject(scriptEngine); throw e; } } catch (Exception e) { throw new WebApplicationException(e); } } else { viewData.put(HTML_CONTENT_KEY, StringUtils.EMPTY); } //  HTML  . final String pageContent = StrSubstitutor.replace(templateContent, viewData); entityStream.write(pageContent.getBytes(StandardCharsets.UTF_8)); } /** *       javascript. */ private static class ScriptEngineFactory extends BasePooledObjectFactory<AbstractScriptEngine> { @Override public AbstractScriptEngine create() throws Exception { LOG.info("Create new script engine"); //  nashorn   javascript . final AbstractScriptEngine scriptEngine = (AbstractScriptEngine) new ScriptEngineManager().getEngineByName("nashorn"); try(final InputStreamReader polyfillReader = ResourceUtilities.getResourceTextReader(WEBAPP_ROOT + "server-polyfill.js"); final InputStreamReader serverReader = ResourceUtilities.getResourceTextReader(WEBAPP_ROOT + "static/assets/server.js")) { //     ,    nashorn,       . scriptEngine.eval(polyfillReader); //  ,    HTML     React. scriptEngine.eval(serverReader); } //   . ((Invocable)scriptEngine).invokeFunction( "initializeEngine", ResourceUtilities.class.getName()); return scriptEngine; } @Override public PooledObject<AbstractScriptEngine> wrap(AbstractScriptEngine obj) { return new DefaultPooledObject<AbstractScriptEngine>(obj); } } } 

The engine executes Javascript versions of ECMAScript 5.1 and does not support loading modules, therefore the server script, like the client script, will be assembled into bundles using a webpack. The server bundle and client bundle are built on the basis of a common code base, but they have different entry points. For some reason, Nashorn cannot execute the minimized bundle (collected by the webpack with the --optimize-minimize key) - crashes, so on the server side you need to execute the unminimized bundle. To build both types of bundles, you can use the Webpack plugin at the same time: unminified-webpack-plugin .

When you first request any page, or if there is no free engine instance, we will initialize a new instance. The initialization process consists of creating an instance of Nashorn and executing in it server scripts loaded from the classpath. Nashorn does not implement several ordinary javascript functions, such as setInterval, setTimeout, so you need to include the simplest script-polyfill. Then the code that forms the HTML pages is loaded directly (just like on the client). This process is not very fast, on a fairly powerful computer it takes a couple of seconds, so you need a cache of engine instances.

Polyphyl for Nashorn
 //   global  javascript . var global = this; //   window  javascript ,     , //       . var window = this; //    ,  Nashorn  console. var console = { error: print, debug: print, warn: print, log: print }; //  Nashorn  setTimeout,  callback -     . function setTimeout(func, delay) { func(); return 0; }; function clearTimeout() { }; //  Nashorn  setInterval,  callback -     . function setInterval(func, delay) { func(); return 0; }; function clearInterval() { }; 


HTML rendering on an already initialized engine is much faster. To get the HTML generated by React, we write the renderHtml function, which we put in the server entry point (src \ server.jsx). The current URL is transferred to this function, for processing it using react-router, and the initial redux state for the requested page (as JSON). The same state for redux, in the form of JSON, is placed on the page in the variable window.INITIAL_STATE. This is necessary so that the element tree built by React on the client matches the HTML generated on the server.

Js bundle server entry point:

 /** *   HTML   React. * @param {String} url URL  . * @param {String} initialStateJson    Redux     JSON. * @return {String} HTML,  React. */ renderHtml = function renderHtml(url, initialStateJson) { //  JSON    Redux. const initialState = JSON.parse(initialStateJson) //     react-router (   ). const history = createMemoryHistory() //   Redux    ,   . const store = configureStore(initialState, history, true) //       . const htmlContent = {} global.INITIAL_STATE = initialState //       URL   react-router. match({ routes: routes({history}), location: url }, (error, redirectLocation, renderProps) => { if (error) { throw error } //  HTML     React. htmlContent.result = ReactDOMServer.renderToString( <AppContainer> <Provider store={store}> <RouterContext {...renderProps}/> </Provider> </AppContainer> ) }) return htmlContent.result } 

Js bundle client entry point:

 //   Redux. const store = configureStore(initialState, history, false) //      HTML,  React. const contentElement = document.getElementById("content") //   HTML   React. ReactDOM.render(<App store={store} history={history}/>, contentElement) 

HTML / styles reboot support


For the convenience of developing the client side, you can customize the webpack dev server with the support of “hot” reloading of changed pages or styles. The developer starts the application, launches the webpack dev server on another port (for example, setting the npm run debug command in package.json) and gets the opportunity in most cases not to update the modified pages - the changes are applied on the fly, this applies to both HTML code and style code . To do this, in the browser, go to the previously configured address of the webpack dev server. The server builds bundles on the fly, the remaining requests are proxied to the application.

package.json:
 { "name": "java-react-redux-isomorphic-example", "version": "1.0.0", "private": true, "scripts": { "debug": "cross-env DEBUG=true APP_PORT=8080 PROXY_PORT=8081 webpack-dev-server --hot --colors --inline", "build": "webpack", "build:debug": "webpack -p" } } 

To configure the "hot" reboot, follow the steps below.

In the webpack settings file:


At the client entry point into the application, insert the module update handler. The handler loads the updated module and starts the HTML rendering process using React.

 //   HTML   React. ReactDOM.render(<App store={store} history={history}/>, contentElement) if (module.hot) { //  ""  . module.hot.accept("./containers/app", () => { const app = require("./containers/app").default ReactDOM.render(app({store, history}), contentElement) }) } 

In the module where the redux repository is created, insert the module update handler. This handler loads updated redux converters and replaces old ones with them.

 const store = createStore(reducers, initialState, applyMiddleware(...middleware)) if (module.hot) { //  ""  Redux-. module.hot.accept("./reducers", () => { const nextRootReducer = require("./reducers") store.replaceReducer(nextRootReducer) }) } return store 

In the Java application itself, you need to disable building bundles via frontend-maven-plugin and using React server rendering: now webpack dev server begins to answer for building scripts and styles, it does this very quickly and in memory, the processor and disk will not be loaded by rebuilding bundles To disable rebuilding using frontend-maven-plugin and React server rendering, you can provide a maven: frontendDevelopment profile (you can enable it in an IDE that supports integration with maven). If necessary, bundles are reassembled manually at any time using a webpack.

Source: https://habr.com/ru/post/327480/


All Articles