作って理解するWebフレームワーク

前回、簡単なDIコンテナを作ってみたので、次はこれを使ってWebフレームワークを作ってみたいと思います。

Webサーバーをつくる


WebHTTP
200 OKHTML
RFC
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSoc = new ServerSocket(8989);
        for (;;) {
            Socket s = serverSoc.accept();
            new Thread(() -> {
                try (InputStream is = s.getInputStream();
                     BufferedReader bur = new BufferedReader(new InputStreamReader(is))) 
                {
                    String firstLine = bur.readLine();
                    for (String line; (line = bur.readLine()) != null && !line.isEmpty(););
                    try (OutputStream os = s.getOutputStream();
                         PrintWriter pw = new PrintWriter(os))
                    {
                        pw.println("HTTP/1.0 200 OK");
                        pw.println("Content-Type: text/html");
                        pw.println();
                        pw.println("<h1>Hello!</h1>");
                        pw.println(LocalDateTime.now());
                    }
                } catch (IOException ex) {
                    System.out.println(ex);
                }
            }).start();
        }
    }
}


First Web Server by kishida · Pull Request #1 · kishida/tinydi

基本的なMVC


MVC
@Named
@Path("")
public class IndexController {
    @Path("index")
    public String index() {
        return "<h1>Hello</h1>" + LocalDateTime.now();
    }
    
    @Path("message")
    public String mes() {
        return "<h1>Message</h1>Nice to meet you!";
    }
}



@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Path {
    String value();
}


Context
    public static Collection<Map.Entry<String, Class>> registeredClasses() {
        return types.entrySet();
    }


URL
    @AllArgsConstractor
    static class ProcessorMethod {
        String name;
        Method method;
    }


@Path@Path
        Context.autoRegister();
        Map<String, ProcessorMethod> methods = new HashMap<>();
        Context.registeredClasses().forEach(entry -> {
            Class cls = entry.getValue();
            Path rootAno = (Path) cls.getAnnotation(Path.class);
            if (rootAno == null) {
                return; // continue
            }
            String root = trimSlash(rootAno.value());
            for (Method m : cls.getMethods()) {
                Path pathAno = m.getAnnotation(Path.class);
                if (pathAno == null) {
                    continue;
                }
                String path = root + "/" + pathAno.value();
                methods.put(path, new ProcessorMethod(entry.getKey(), m));
            }
        });

HTTP1GET /index HTTP/1.1
        Pattern pattern = Pattern.compile("([A-Z]+) ([^ ]+) (.+)");
        String first = bur.readLine();
        Matcher mat = pattern.matcher(first);
        mat.find();
        String httpMethod = mat.group(1);
        String path = mat.group(2);
        String protocol = mat.group(3);


Java
ProcessorMethod method = methods.get(path);





Not Found
    if (method == null) {
        pw.println("HTTP/1.0 404 Not Found");



        Object bean = Context.getBean(method.name);
        Object output = method.method.invoke(bean);



        pw.println("HTTP/1.0 200 OK");
        pw.println("Content-Type: text/html");
        pw.println();
        pw.println(output);


MVC
First MVC by kishida · Pull Request #3 · kishida/tinydi

リクエスト情報のインジェクト




@Named
@RequestScoped
@Data
public class RequestInfo {
    private String path;
    private InetAddress localAddress;
    private String userAgent;
}


@Named
    RequestInfo info = (RequestInfo) Context.getBean("requestInfo");
    info.setLocalAddress(s.getLocalAddress());
    info.setPath(path);


User-Agent
    Pattern patternHeader = Pattern.compile("([A-Za-z-]+): (.+)");
    for (String line; (line = bur.readLine()) != null && !line.isEmpty();) {
        Matcher matHeader = patternHeader.matcher(line);
        if (matHeader.find()) {
            switch (matHeader.group(1)) {
                case "User-Agent":
                    info.setUserAgent(matHeader.group(2));
                    break;
            }
        }
    }



@Named
@Path("info")
public class RequestInfoController {
    @Inject
    RequestInfo requestInfo;
    
    @Path("index")
    public String index() {
        return String.format("<h1>Info</h1>Host:%s<br/>Path:%s<br/>UserAgent:%s<br/>",
                requestInfo.getLocalAddress(), 
                requestInfo.getPath(), 
                requestInfo.getUserAgent());
    }
}


DI
DI


Request Info by kishida · Pull Request #7 · kishida/tinydi

セッションIDを振る


ID
使Set-Cookie: foo=hogeCookie: foo=hoge


AtomicLong
        AtomicLong lastSessionId = new AtomicLong();

HashMap使



    case "Cookie":
        Stream.of(value.split(";"))
              .map(exp -> exp.trim().split("="))
              .filter(kv -> kv.length == 2)
              .forEach(kv -> cookies.put(kv[0], kv[1]));


ID
    String sessionId = cookies.getOrDefault("jsessionid", 
                                            Long.toString(lastSessionId.incrementAndGet()));

ID
ID便


ID
    pw.println("HTTP/1.0 200 OK");
    pw.println("Content-Type: text/html");
    pw.println("Set-Cookie: jsessionid=" + sessionId + "; path=/");


Session ID by kishida · Pull Request #8 · kishida/tinydi

セッション情報をインジェクト



@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SessionScoped {
    
}


Session
public class BeanSession {
    private final ThreadLocal<String> sessionId = new InheritableThreadLocal<>();
    private final Map<String, Map<String, Object>> beans = new HashMap<>();
    
    public void setSessionId(String id) {
        sessionId.set(id);
        if (!beans.containsKey(id)) {
            beans.put(id, new HashMap<>());
        }
    }
    
    public Map<String, Object> getBeans() {
        return beans.get(sessionId.get());
    }

    public boolean isSessionRegistered(String id) {
        return beans.containsKey(id);
    }
    
}


Context
    static BeanSession beanSession;
    
    public static void setBeanSession(BeanSession beanSession) {
        Context.beanSession = beanSession;
    }


@SessionScoped使
    if (type.isAnnotationPresent(SessionScoped.class)) {
        scope = beanSession.getBeans();
    }


寿
    private static int scopeRank(Class type) {
        if (type.isAnnotationPresent(RequestScoped.class)) {
            return 0;
        }
        if (type.isAnnotationPresent(SessionScoped.class)) {
            return 5;
        }
        return 10;
    }


寿寿
            if (scopeRank(type) > scopeRank(field.getType())) {
                bean = scopeWrapper(field.getType(), field.getName());
            } else {
                bean = getBean(field.getName());
            }


Web
ID
    String sessionId = cookies.get("jsessionid");


ID
    if (sessionId != null) {
        if (!beanSession.isSessionRegistered(sessionId)) {
            sessionId = null;
        }
    }


IDID
    if (sessionId == null) {
        sessionId = Long.toString(lastSessionId.incrementAndGet());
    }


ID
    beanSession.setSessionId(sessionId);



@Named
@SessionScoped
@Data
public class LoginSession {
    boolean logined;
    LocalDateTime loginTime;
}



@Named
@Path("login")
public class LoginController {
    
    @Inject
    LoginSession loginSession;
    
    @Path("index")
    public String index() {
        String title = "<h1>Login</h1>";
        if (loginSession.isLogined()) {
            return title + "Login at " + loginSession.getLoginTime();
        } else {
            return title + "Not Login";
        }
    }
    
    @Path("login")
    public String login() {
        loginSession.setLogined(true);
        loginSession.setLoginTime(LocalDateTime.now());
        return "<h1>Login</h1>login";
    }
    @Path("logout")
    public String logout() {
        loginSession.setLogined(false);
        return "<h1>Login</h1>logout";
    }
}


Web


その他やること







HTTP







WebJetty使使使
Spring使


わかったこと

ベースとしてDIがあると、フレームワークを作るときに「汚いこと」をやる必要がなくなってよい。

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)