├── .gitignore ├── pom.xml └── src ├── main └── java │ └── kis │ └── di │ ├── Context.java │ ├── LocalCache.java │ ├── annotation │ ├── InvokeLog.java │ ├── Path.java │ └── RequestScoped.java │ ├── mvc │ ├── RequestInfo.java │ └── Server.java │ └── sample │ ├── Bar.java │ ├── Foo.java │ ├── Main.java │ ├── Now.java │ └── mvc │ ├── IndexController.java │ └── RequestInfoController.java └── test └── java └── kis └── di └── mvc └── ServerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | /licenseheader.txt 14 | /nb-configuration.xml 15 | /nbproject/ 16 | /target/ 17 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | kis.di 5 | TinyDI 6 | 1.0-SNAPSHOT 7 | jar 8 | 9 | 10 | javax.inject 11 | javax.inject 12 | 1 13 | 14 | 15 | org.javassist 16 | javassist 17 | 3.20.0-GA 18 | 19 | 20 | junit 21 | junit 22 | 4.12 23 | test 24 | 25 | 26 | org.hamcrest 27 | hamcrest-core 28 | 1.3 29 | test 30 | 31 | 32 | org.projectlombok 33 | lombok 34 | 1.16.8 35 | provided 36 | 37 | 38 | 39 | UTF-8 40 | 1.8 41 | 1.8 42 | 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-compiler-plugin 48 | 3.5.1 49 | 50 | 1.8 51 | 1.8 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/main/java/kis/di/Context.java: -------------------------------------------------------------------------------- 1 | package kis.di; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.UncheckedIOException; 6 | import java.lang.reflect.Field; 7 | import java.net.URISyntaxException; 8 | import java.net.URL; 9 | import java.nio.file.Files; 10 | import java.nio.file.Path; 11 | import java.util.Collection; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.Objects; 15 | import java.util.Set; 16 | import java.util.stream.Collectors; 17 | import java.util.stream.Stream; 18 | 19 | import javax.inject.Inject; 20 | import javax.inject.Named; 21 | 22 | import javassist.CannotCompileException; 23 | import javassist.ClassPool; 24 | import javassist.CtClass; 25 | import javassist.CtField; 26 | import javassist.CtMethod; 27 | import javassist.Modifier; 28 | import javassist.NotFoundException; 29 | import kis.di.annotation.InvokeLog; 30 | import kis.di.annotation.RequestScoped; 31 | 32 | /** 33 | * @author naoki 34 | */ 35 | public class Context { 36 | 37 | static Map types = new HashMap<>(); 38 | static Map beans = new HashMap<>(); 39 | static ThreadLocal> requestBeans = new InheritableThreadLocal<>(); 40 | 41 | public static void autoRegister() { 42 | try { 43 | URL res = Context.class.getResource( 44 | "/" + Context.class.getName().replace('.', '/') + ".class"); 45 | Path classPath = new File(res.toURI()).toPath().getParent().getParent().getParent(); 46 | try(Stream stream = Files.walk(classPath)){ 47 | stream.filter(p -> !Files.isDirectory(p)) 48 | .filter(p -> p.toString().endsWith(".class")) 49 | .map(p -> classPath.relativize(p)) 50 | .map(p -> p.toString().replace(File.separatorChar, '.')) 51 | .map(n -> n.substring(0, n.length() - 6)) 52 | .map(n -> { 53 | try { 54 | return Class.forName(n); 55 | } catch (ClassNotFoundException ex) { 56 | throw new RuntimeException(ex); 57 | } 58 | }) 59 | .filter(c -> c.isAnnotationPresent(Named.class)) 60 | .forEach(c -> { 61 | String simpleName = c.getSimpleName(); 62 | register(simpleName.substring(0, 1).toLowerCase() + simpleName.substring(1), c); 63 | }); 64 | } 65 | } catch (IOException ex) { 66 | throw new UncheckedIOException(ex); 67 | } catch (URISyntaxException ex) { 68 | throw new RuntimeException(ex); 69 | } 70 | } 71 | 72 | public static Collection> registeredClasses() { 73 | return types.entrySet(); 74 | } 75 | 76 | public static void register(String name, Class type) { 77 | types.put(name, type); 78 | } 79 | 80 | public static Object getBean(String name) { 81 | Class type = types.get(name); 82 | Objects.requireNonNull(type, name + " not found."); 83 | 84 | Map scope; 85 | if (type.isAnnotationPresent(RequestScoped.class)) { 86 | scope = requestBeans.get(); 87 | if (scope == null) { 88 | scope = new HashMap<>(); 89 | requestBeans.set(scope); 90 | } 91 | } else { 92 | scope = beans; 93 | } 94 | return scope.computeIfAbsent(name, key -> { 95 | try { 96 | return createObject(type); 97 | } catch (InstantiationException | IllegalAccessException ex) { 98 | throw new RuntimeException(name + " can not instanciate", ex); 99 | } 100 | }); 101 | } 102 | 103 | private static T createObject(Class type) throws InstantiationException, IllegalAccessException { 104 | T object; 105 | if (Stream.of(type.getDeclaredMethods()).anyMatch(m -> m.isAnnotationPresent(InvokeLog.class))) { 106 | object = wrap(type).newInstance(); 107 | } else { 108 | object = type.newInstance(); 109 | } 110 | inject(type, object); 111 | return object; 112 | } 113 | 114 | private static Class wrap(Class type) { 115 | try { 116 | ClassPool pool = ClassPool.getDefault(); 117 | CtClass orgCls = pool.get(type.getName()); 118 | 119 | CtClass cls = pool.makeClass(type.getName() + "$$"); 120 | cls.setSuperclass(orgCls); 121 | 122 | for(CtMethod method : orgCls.getDeclaredMethods()) { 123 | if (!method.hasAnnotation(InvokeLog.class)) { 124 | continue; 125 | } 126 | CtMethod newMethod = new CtMethod( 127 | method.getReturnType(), method.getName(), method.getParameterTypes(), cls); 128 | newMethod.setExceptionTypes(method.getExceptionTypes()); 129 | newMethod.setBody( 130 | "{" 131 | + " System.out.println(java.time.LocalDateTime.now() + " 132 | + "\":" + method.getName() + " invoked.\"); " 133 | + " return super." + method.getName() + "($$);" 134 | + "}"); 135 | cls.addMethod(newMethod); 136 | } 137 | return cls.toClass(); 138 | } catch (NotFoundException | CannotCompileException ex) { 139 | throw new RuntimeException(ex); 140 | } 141 | } 142 | 143 | private static void inject(Class type, T object) throws IllegalArgumentException, IllegalAccessException { 144 | for (Field field : type.getDeclaredFields()) { 145 | if (!field.isAnnotationPresent(Inject.class)) { 146 | continue; 147 | } 148 | field.setAccessible(true); 149 | Object bean; 150 | if (!type.isAnnotationPresent(RequestScoped.class) && field.getType().isAnnotationPresent(RequestScoped.class)) { 151 | bean = scopeWrapper(field.getType(), field.getName()); 152 | } else { 153 | bean = getBean(field.getName()); 154 | } 155 | field.set(object, bean); 156 | } 157 | } 158 | private static Set cannotOverrides = Stream.of("finalize", "clone").collect(Collectors.toSet()); 159 | private static T scopeWrapper(Class type, String name) { 160 | try { 161 | ClassPool pool = ClassPool.getDefault(); 162 | CtClass cls = pool.getOrNull(type.getName() + "$$_"); 163 | if (cls == null) { 164 | CtClass orgCls = pool.get(type.getName()); 165 | cls = pool.makeClass(type.getName() + "$$_"); 166 | cls.setSuperclass(orgCls); 167 | 168 | CtClass tl = pool.get(LocalCache.class.getName()); 169 | CtField org = new CtField(tl, "org", cls); 170 | cls.addField(org, "new " + LocalCache.class.getName() + "(\"" + name + "\");"); 171 | 172 | for (CtMethod method : orgCls.getMethods()) { 173 | if (Modifier.isFinal(method.getModifiers()) | cannotOverrides.contains(method.getName())) { 174 | continue; 175 | } 176 | CtMethod override = new CtMethod(method.getReturnType(), method.getName(), method.getParameterTypes(), cls); 177 | override.setExceptionTypes(method.getExceptionTypes()); 178 | override.setBody( 179 | "{" 180 | + " return ((" + type.getName() + ")org.get())." + method.getName() + "($$);" 181 | + "}"); 182 | cls.addMethod(override); 183 | } 184 | } 185 | return (T) cls.toClass().newInstance(); 186 | } catch (NotFoundException |IllegalAccessException | CannotCompileException | InstantiationException ex) { 187 | throw new RuntimeException(ex); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/kis/di/LocalCache.java: -------------------------------------------------------------------------------- 1 | package kis.di; 2 | 3 | /** 4 | * 5 | * @author naoki 6 | */ 7 | public class LocalCache { 8 | private ThreadLocal local = new InheritableThreadLocal<>(); 9 | private String name; 10 | 11 | public LocalCache(String name) { 12 | this.name = name; 13 | } 14 | 15 | public T get() { 16 | T obj = local.get(); 17 | if (obj == null) { 18 | obj = (T) Context.getBean(name); 19 | local.set(obj); 20 | } 21 | return obj; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/kis/di/annotation/InvokeLog.java: -------------------------------------------------------------------------------- 1 | package kis.di.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 10 | * @author naoki 11 | */ 12 | @Target(ElementType.METHOD) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface InvokeLog { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/kis/di/annotation/Path.java: -------------------------------------------------------------------------------- 1 | package kis.di.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * @author naoki 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target({ElementType.METHOD, ElementType.TYPE}) 13 | public @interface Path { 14 | String value(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/kis/di/annotation/RequestScoped.java: -------------------------------------------------------------------------------- 1 | package kis.di.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 10 | * @author naoki 11 | */ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target(ElementType.TYPE) 14 | public @interface RequestScoped { 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/kis/di/mvc/RequestInfo.java: -------------------------------------------------------------------------------- 1 | package kis.di.mvc; 2 | 3 | import java.net.InetAddress; 4 | 5 | import javax.inject.Named; 6 | 7 | import kis.di.annotation.RequestScoped; 8 | import lombok.Data; 9 | 10 | /** 11 | * 12 | * @author naoki 13 | */ 14 | @Named 15 | @RequestScoped 16 | @Data 17 | public class RequestInfo { 18 | private String path; 19 | private InetAddress localAddress; 20 | private String userAgent; 21 | private String sessionId; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/kis/di/mvc/Server.java: -------------------------------------------------------------------------------- 1 | package kis.di.mvc; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStream; 8 | import java.io.PrintWriter; 9 | import java.lang.reflect.Method; 10 | import java.net.ServerSocket; 11 | import java.net.Socket; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.atomic.AtomicLong; 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | import java.util.stream.Stream; 20 | 21 | import kis.di.Context; 22 | import kis.di.annotation.Path; 23 | 24 | /** 25 | * 26 | * @author naoki 27 | */ 28 | public class Server { 29 | static class ProcessorMethod { 30 | public ProcessorMethod(String name, Method method) { 31 | this.name = name; 32 | this.method = method; 33 | } 34 | String name; 35 | Method method; 36 | } 37 | 38 | static String trimSlash(String str) { 39 | return str.replaceFirst("^/", "").replaceFirst("/$", ""); 40 | } 41 | 42 | public static void main(String[] args) throws IOException { 43 | Context.autoRegister(); 44 | Map methods = new HashMap<>(); 45 | Context.registeredClasses().forEach(entry -> { 46 | Class cls = entry.getValue(); 47 | Path rootAno = (Path) cls.getAnnotation(Path.class); 48 | if (rootAno == null) { 49 | return; // continue 50 | } 51 | String root = trimSlash(rootAno.value()); 52 | if (!root.isEmpty()) { 53 | root = "/" + root; 54 | } 55 | for (Method m : cls.getMethods()) { 56 | Path pathAno = m.getAnnotation(Path.class); 57 | if (pathAno == null) { 58 | continue; 59 | } 60 | if (!m.getReturnType().equals(String.class)) { 61 | continue; 62 | } 63 | if (m.getParameterCount() > 0) { 64 | continue; 65 | } 66 | String path = root + "/" + pathAno.value(); 67 | if (path.endsWith("/index")) { 68 | path = path.replaceFirst("index$", ""); 69 | } 70 | methods.put(path, new ProcessorMethod(entry.getKey(), m)); 71 | } 72 | }); 73 | 74 | Pattern pattern = Pattern.compile("([A-Z]+) ([^ ]+) (.+)"); 75 | Pattern patternHeader = Pattern.compile("([A-Za-z-]+): (.+)"); 76 | AtomicLong lastSessionId = new AtomicLong(); 77 | ServerSocket serverSoc = new ServerSocket(8993); 78 | ExecutorService executors = Executors.newFixedThreadPool(10); 79 | for (;;) { 80 | Socket s = serverSoc.accept(); 81 | executors.execute(() -> { 82 | try (InputStream is = s.getInputStream(); 83 | BufferedReader bur = new BufferedReader(new InputStreamReader(is))) 84 | { 85 | String first = bur.readLine(); 86 | if (first == null) { 87 | System.out.println("null request"); 88 | return; 89 | } 90 | Matcher mat = pattern.matcher(first); 91 | mat.find(); 92 | String httpMethod = mat.group(1); 93 | String path = mat.group(2); 94 | String protocol = mat.group(3); 95 | 96 | RequestInfo info = (RequestInfo) Context.getBean("requestInfo"); 97 | info.setLocalAddress(s.getLocalAddress()); 98 | info.setPath(path); 99 | Map cookies = new HashMap<>(); 100 | for (String line; (line = bur.readLine()) != null && !line.isEmpty();) { 101 | Matcher matHeader = patternHeader.matcher(line); 102 | if (matHeader.find()) { 103 | String value = matHeader.group(2); 104 | switch (matHeader.group(1)) { 105 | case "User-Agent": 106 | info.setUserAgent(value); 107 | break; 108 | case "Cookie": 109 | Stream.of(value.split(";")) 110 | .map(exp -> exp.trim().split("=")) 111 | .filter(kv -> kv.length == 2) 112 | .forEach(kv -> cookies.put(kv[0], kv[1])); 113 | } 114 | } 115 | } 116 | String sessionId = cookies.getOrDefault("jsessionid", 117 | Long.toString(lastSessionId.incrementAndGet())); 118 | info.setSessionId(sessionId); 119 | try (OutputStream os = s.getOutputStream(); 120 | PrintWriter pw = new PrintWriter(os)) 121 | { 122 | ProcessorMethod method = methods.get(path.replaceFirst("/index$", "/")); 123 | if (method == null) { 124 | pw.println("HTTP/1.0 404 Not Found"); 125 | pw.println("Content-Type: text/html"); 126 | pw.println(); 127 | pw.println("

404 Not Found

"); 128 | pw.println(path + " Not Found"); 129 | return; 130 | } 131 | try{ 132 | Object bean = Context.getBean(method.name); 133 | Object output = method.method.invoke(bean); 134 | pw.println("HTTP/1.0 200 OK"); 135 | pw.println("Content-Type: text/html"); 136 | pw.println("Set-Cookie: jsessionid=" + sessionId + "; path=/"); 137 | pw.println(); 138 | pw.println(output); 139 | } catch (Exception ex) { 140 | pw.println("HTTP/1.0 200 OK"); 141 | pw.println("Content-Type: text/html"); 142 | pw.println(); 143 | pw.println("

500 Internal Server Error

"); 144 | pw.println(ex); 145 | } 146 | } 147 | } catch (IOException ex) { 148 | System.out.println(ex); 149 | } 150 | }); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/kis/di/sample/Bar.java: -------------------------------------------------------------------------------- 1 | package kis.di.sample; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import javax.inject.Inject; 6 | import javax.inject.Named; 7 | 8 | import kis.di.annotation.InvokeLog; 9 | 10 | /** 11 | * @author naoki 12 | */ 13 | @Named 14 | public class Bar { 15 | 16 | @Inject 17 | Foo foo; 18 | 19 | @Inject 20 | Now now; 21 | 22 | @InvokeLog 23 | void showMessage() { 24 | System.out.println(foo.getName() + " " + foo.getMessage()); 25 | } 26 | 27 | void longProcess() { 28 | now.setTime(LocalDateTime.now()); 29 | System.out.println("start:" + now.getTime()); 30 | try { 31 | Thread.sleep(5000); 32 | } catch (InterruptedException ex) { 33 | } 34 | System.out.println("end :" + now.getTime()); 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/kis/di/sample/Foo.java: -------------------------------------------------------------------------------- 1 | package kis.di.sample; 2 | 3 | import javax.inject.Named; 4 | 5 | import kis.di.annotation.InvokeLog; 6 | 7 | /** 8 | * @author naoki 9 | */ 10 | @Named 11 | public class Foo { 12 | 13 | String getMessage() { 14 | return "Hello!"; 15 | } 16 | 17 | @InvokeLog 18 | String getName() { 19 | return "foo!"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/kis/di/sample/Main.java: -------------------------------------------------------------------------------- 1 | package kis.di.sample; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | 6 | import kis.di.Context; 7 | 8 | /** 9 | * @author naoki 10 | */ 11 | public class Main { 12 | 13 | public static void main(String[] args) { 14 | Context.autoRegister(); 15 | Bar bar = (Bar) Context.getBean("bar"); 16 | bar.showMessage(); 17 | 18 | ExecutorService es = Executors.newFixedThreadPool(2); 19 | for (int i = 0; i < 2; ++i) { 20 | es.execute(() -> { 21 | bar.longProcess(); 22 | }); 23 | try { 24 | Thread.sleep(2000); 25 | } catch (InterruptedException ex) { 26 | } 27 | } 28 | es.shutdown(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/kis/di/sample/Now.java: -------------------------------------------------------------------------------- 1 | package kis.di.sample; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import javax.inject.Named; 6 | 7 | import kis.di.annotation.RequestScoped; 8 | 9 | /** 10 | * 11 | * @author naoki 12 | */ 13 | @RequestScoped 14 | @Named 15 | public class Now { 16 | private LocalDateTime time; 17 | 18 | public LocalDateTime getTime() { 19 | return time; 20 | } 21 | 22 | public void setTime(LocalDateTime time) { 23 | this.time = time; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/kis/di/sample/mvc/IndexController.java: -------------------------------------------------------------------------------- 1 | package kis.di.sample.mvc; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | import javax.inject.Named; 6 | 7 | import kis.di.annotation.Path; 8 | 9 | /** 10 | * 11 | * @author naoki 12 | */ 13 | @Named 14 | @Path("") 15 | public class IndexController { 16 | @Path("") 17 | public String index() { 18 | return "

Hello

" + LocalDateTime.now(); 19 | } 20 | 21 | @Path("message") 22 | public String mes() { 23 | return "

Message

Nice to meet you!"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/kis/di/sample/mvc/RequestInfoController.java: -------------------------------------------------------------------------------- 1 | package kis.di.sample.mvc; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Named; 5 | 6 | import kis.di.annotation.Path; 7 | import kis.di.mvc.RequestInfo; 8 | 9 | /** 10 | * 11 | * @author naoki 12 | */ 13 | @Named 14 | @Path("info") 15 | public class RequestInfoController { 16 | @Inject 17 | RequestInfo requestInfo; 18 | 19 | @Path("index") 20 | public String index() { 21 | return String.format("

Info

" 22 | + "Host:%s
" 23 | + "Path:%s
" 24 | + "UserAgent:%s
" 25 | + "SessionId:%s
", 26 | requestInfo.getLocalAddress(), 27 | requestInfo.getPath(), 28 | requestInfo.getUserAgent(), 29 | requestInfo.getSessionId()); 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/kis/di/mvc/ServerTest.java: -------------------------------------------------------------------------------- 1 | package kis.di.mvc; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import static org.hamcrest.CoreMatchers.is; 6 | import org.junit.Test; 7 | import static org.junit.Assert.assertThat; 8 | 9 | /** 10 | * 11 | * @author naoki 12 | */ 13 | public class ServerTest { 14 | 15 | @Test 16 | public void testTrimSlash() { 17 | assertThat(Server.trimSlash("/test"), is("test")); 18 | assertThat(Server.trimSlash("/test/"), is("test")); 19 | assertThat(Server.trimSlash("test/"), is("test")); 20 | assertThat(Server.trimSlash("test/test"), is("test/test")); 21 | } 22 | 23 | } 24 | --------------------------------------------------------------------------------