0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Java約215行で15分以内に読める!Tomcat無しでサーブレットを実行する最小コンテナを自作してみた第3回jsp導入

Posted at

前回まで

Javaによる簡易HTTPサーバの作り方
Java100行弱で10分以内に読める!Tomcat無しでサーブレットを実行する最小コンテナを自作してみた
Java100行弱で10分以内に読める!Tomcat無しでサーブレットを実行する最小コンテナを自作してみた第2回web.xml導入

今回の肝

コンテナ起動時に自動的にJSPをサーブレット(classファイル)へコンパイルできるようにしました。
JspCompiler.javaの中でjspの中身をwrite.out()へ書き込む簡単なjavaのソースへ変換します。
その後MiniContainer.jsaddJspメソッドの中でclassファイルへ変換し、インスタンスを生成しています。

動作

image.png

コンテナの実装

MiniContainer.js
import java.io.*;
import java.net.*;
import java.util.*;
import java.lang.reflect.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.tools.*;
import java.net.URLClassLoader;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Element;

public class MiniContainer {
    private static final Map<String, MyServlet> routes = new HashMap<>();

    public static void main(String[] args) throws Exception {
    
        // JSPを登録
        addJsp("/hello.jsp", "hello.jsp");
    
        // ユーザが作ったServletクラスをロードして登録
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.parse(new File("web.xml"));

        NodeList list = doc.getElementsByTagName("servlet");
        for (int i = 0; i < list.getLength(); i++) {
            Element el = (Element) list.item(i);
            String path = el.getAttribute("path");
            String clazz = el.getAttribute("class");
            addServletFromClass(path, clazz);
        }
        //web.xml導入前
        //addServletFromClass("/hello", "HelloServlet");
        //addServletFromClass("/bye", "ByeServlet");

        ServerSocket server = new ServerSocket(8081);
        System.out.println("Running on http://localhost:8081/");

        while (true) {
            Socket client = server.accept();
            handleClient(client);
        }
    }

    // クラス名からユーザのサーブレットをロードして登録
    private static void addServletFromClass(String path, String className) throws Exception {
        Class<?> clazz = Class.forName(className); // クラスをロード
        MyServlet servlet = (MyServlet) clazz.getDeclaredConstructor().newInstance();
        routes.put(path, servlet);
    }

    private static void handleClient(Socket client) throws Exception {
        InputStream in = client.getInputStream();
        OutputStream out = client.getOutputStream();

        // ブラウザから送られてくるHTTPリクエスト1行目を読む
        char[] buf = new char[1024];
        int idx = 0;
        int c;
        while ((c = in.read()) != -1) {
            if (c == '\r') continue; // HTTPではCRLFなのでCRは無視
            if (c == '\n') break; // LFで1行終わり
    	    buf[idx++] = (char)c;
        }
        String requestLine = new String(buf, 0, idx);
        System.out.println("Request: " + requestLine);

        // 例 "GET /hello HTTP/1.1" → ["GET", "/hello", "HTTP/1.1"]
        // "/hello"だけ取り出す
        String path = "/";
        if (!requestLine.isEmpty()) {
            path = requestLine.split(" ")[1];
        }

        MyServlet servlet = routes.get(path);
        if (servlet != null) {
            servlet.service(in, out); // ユーザが作ったサーブレットを呼び出す
        } else {
            String body = "<h1>404 Not Found</h1>";
            String headers = "HTTP/1.1 404 Not Found\r\n" +
                             "Content-Length: " + body.getBytes("UTF-8").length + "\r\n" +
                             "Content-Type: text/html; charset=UTF-8\r\n\r\n";
            out.write(headers.getBytes("UTF-8"));
            out.write(body.getBytes("UTF-8"));
        }

        out.flush();
        client.close();
    }
    
    private static void addJsp(String path, String jspFile) throws Exception {
        String className = jspFile.replace(".jsp", "_jsp");

        // JSP→Java生成
        String javaFile = JspCompiler.compile(jspFile, className);

        // 実行時コンパイル
        JavaCompiler compiler = javax.tools.ToolProvider.getSystemJavaCompiler();
        if (compiler == null) {
            throw new IllegalStateException("JDKで実行してください (JREでは不可)");
        }
        int result = compiler.run(null, null, null, javaFile);
        if (result != 0) throw new RuntimeException("コンパイル失敗: " + javaFile);

        // .classをロード
        URLClassLoader loader = URLClassLoader.newInstance(new URL[]{new File(".").toURI().toURL()});
        Class<?> clazz = Class.forName(className, true, loader);

        // インスタンス化して登録
        MyServlet servlet = (MyServlet) clazz.getDeclaredConstructor().newInstance();
        routes.put(path, servlet);
    }
}

MyServlet.java
import java.io.*;

public interface MyServlet {
    void service(InputStream in, OutputStream out) throws Exception;
}
JspCompiler.java
import java.nio.file.*;
import java.io.*;

public class JspCompiler {
    public static String compile(String jspFile, String className) throws Exception {
        String jsp = Files.readString(Paths.get(jspFile));
        StringBuilder javaSrc = new StringBuilder();

        javaSrc.append("import java.io.*;\n");
        javaSrc.append("public class " + className + " implements MyServlet {\n");
        javaSrc.append("  @Override\n");
        javaSrc.append("  public void service(InputStream in, OutputStream out) throws Exception {\n");
        javaSrc.append("    StringBuilder sb = new StringBuilder();\n");


        //replaceはややこしいが" を \" に置換している。エスケープ
        for (String line : jsp.split("\n")) {
            javaSrc.append("    sb.append(\"" + line.replace("\"", "\\\"") + "\\n\");\n");
        }

        javaSrc.append("    String body = sb.toString();\n");
        javaSrc.append("    String headers = \"HTTP/1.1 200 OK\\r\\n\" +\n");
        javaSrc.append("                     \"Content-Length: \" + body.getBytes(\"UTF-8\").length + \"\\r\\n\" +\n");
        javaSrc.append("                     \"Content-Type: text/html; charset=UTF-8\\r\\n\\r\\n\";\n");
        javaSrc.append("    out.write(headers.getBytes(\"UTF-8\"));\n");
        javaSrc.append("    out.write(body.getBytes(\"UTF-8\"));\n");
        javaSrc.append("  }\n");
        javaSrc.append("}\n");

        // Javaファイル出力
        String fileName = className + ".java";
        Files.writeString(Paths.get(fileName), javaSrc.toString());
        return fileName;
    }
}
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <servlet path="/hello" class="HelloServlet"/>
    <servlet path="/bye" class="ByeServlet"/>
</web-app>

サーブレット

HelloServlet.java
import java.io.*;

public class HelloServlet implements MyServlet {
    @Override
    public void service(InputStream in, OutputStream out) throws Exception {
        String body = "<html><body><h1>Hello from HelloServlet!</h1></body></html>";
        String headers = "HTTP/1.1 200 OK\r\n" +
                         "Content-Length: " + body.getBytes("UTF-8").length + "\r\n" +
                         "Content-Type: text/html; charset=UTF-8\r\n\r\n";
        out.write(headers.getBytes("UTF-8"));
        out.write(body.getBytes("UTF-8"));
    }
}
ByeServlet.java
import java.io.*;

public class ByeServlet implements MyServlet {
    @Override
    public void service(InputStream in, OutputStream out) throws Exception {
        String body = "<html><body><h1>Goodbye!</h1></body></html>";
        String headers = "HTTP/1.1 200 OK\r\n" +
                         "Content-Length: " + body.getBytes("UTF-8").length + "\r\n" +
                         "Content-Type: text/html; charset=UTF-8\r\n\r\n";
        out.write(headers.getBytes("UTF-8"));
        out.write(body.getBytes("UTF-8"));
    }
}

JSP

まだ変数やJavaコードには対応していないため、実質まだHTMLと何も変わらない

hello.jsp
<html>
<body>
    <h1>Hello JSP World!</h1>
    <p>This page is compiled at runtime</p>
</body>
</html>

ByeServlet.java
import java.io.*;

public class ByeServlet implements MyServlet {
    @Override
    public void service(InputStream in, OutputStream out) throws Exception {
        String body = "<html><body><h1>Goodbye!</h1></body></html>";
        String headers = "HTTP/1.1 200 OK\r\n" +
                         "Content-Length: " + body.getBytes("UTF-8").length + "\r\n" +
                         "Content-Type: text/html; charset=UTF-8\r\n\r\n";
        out.write(headers.getBytes("UTF-8"));
        out.write(body.getBytes("UTF-8"));
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?