构建自己的web框架(二)
一,核心类:该类封装了ServletContext,是一个单例,其主要功能是提供配置文件的加载和解析,url的解析,work组件的查找和调用,资源的获取,释放,提供事件回调接口,处理注解,注册work组件,提供缓冲区,管理web对象等一系列功能,代码很简单,如下:
1 /** 2 * 3 */ 4 package com.jjh.jee.hawk; 5 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.lang.reflect.Method; 9 import java.net.MalformedURLException; 10 import java.net.URI; 11 import java.net.URISyntaxException; 12 import java.net.URL; 13 import java.nio.file.Path; 14 import java.nio.file.Paths; 15 import java.util.ArrayList; 16 import java.util.Collections; 17 import java.util.HashMap; 18 import java.util.List; 19 import java.util.Map; 20 import java.util.Objects; 21 import java.util.concurrent.ConcurrentLinkedQueue; 22 import java.util.concurrent.ForkJoinPool; 23 import java.util.concurrent.TimeUnit; 24 25 import javax.servlet.AsyncContext; 26 import javax.servlet.AsyncEvent; 27 import javax.servlet.AsyncListener; 28 import javax.servlet.ServletContext; 29 import com.jjh.common.Files; 30 import com.jjh.common.JAXB; 31 import com.jjh.common.StringX; 32 33 /** 34 * @author Administrator 35 * 36 */ 37 public final class HawkContext implements AsyncListener{ 38 /** 39 * @author Administrator 40 * 41 */ 42 private static class AnnotationInfo { 43 /** 44 * @author Administrator 45 * 46 */ 47 private static class AnnotationMethod { 48 private final Method method; 49 private final boolean isMain; 50 private final boolean isUI; 51 private final int async; 52 /** 53 * @param method 54 * @param isMain 55 * @param isUI 56 * @param async 57 */ 58 AnnotationMethod(Method method, boolean isMain, boolean isUI, int async) { 59 super(); 60 this.method = method; 61 this.isMain = isMain; 62 this.isUI = isUI; 63 this.async = async; 64 } 65 66 } 67 68 private final Class<?> claze; 69 private final Map<String, AnnotationMethod> methods; 70 71 /** 72 * @param claze 73 * @param methods 74 */ 75 AnnotationInfo(Class<?> claze, Map<String, AnnotationMethod> methods) { 76 super(); 77 this.claze = claze; 78 this.methods = methods; 79 } 80 81 private AnnotationMethod find(String methodUri) 82 { 83 return methods.get(methodUri); 84 } 85 86 } 87 public static final int SYNC=Integer.MIN_VALUE; 88 public static final int TIMEOUT=520; 89 private static HawkContext context = null; 90 private final ConcurrentLinkedQueue<AsyncContext> clients =new ConcurrentLinkedQueue<>(); 91 private final Map<String, Object> objectMap = Collections.synchronizedMap(new HashMap<>()); 92 private final Map<String, AnnotationInfo> serviceMap = Collections.synchronizedMap(new HashMap<>()); 93 private final ServletContext application; 94 private final ForkJoinPool threadPool; 95 private HawkListener listener; 96 97 /** 98 * @param application 99 * @throws Exception 100 */ 101 HawkContext(ServletContext application) throws Exception { 102 super(); 103 this.application = application; 104 // get config:hawk.xml 105 HawkType hawk = JAXB.loadXml(HawkType.class, getResourceAsStream("WEB-INF/hawk.xml"), 106 HawkType.class.getResourceAsStream("hawk.xsd")); 107 if (StringX.nonEmpty(hawk.listenerClassName)) { 108 Class<?> c = Class.forName(hawk.listenerClassName); 109 if (!HawkListener.class.isAssignableFrom(c)) 110 throw new Exception("'" + c.getName() + "',not a interface HawkListener implement class."); 111 listener = (HawkListener) c.newInstance(); 112 } 113 // scan service package 114 for (ScanPackageType spt : hawk.scanPackageList) { 115 for (String className : Files.scanPackage(spt.packageName)) { 116 Class<?> claze = Class.forName(className); 117 if (claze.isAnnotationPresent(Service.class)) { 118 Service service = claze.getAnnotation(Service.class); 119 String value = service.value(); 120 // com.jjh.j2ee.abc.com.jjh.j2ee.Test 121 if (value.isEmpty()) { 122 value = claze.getName().replaceFirst(hawk.basePackage,"").replaceAll("\\.", "/"); 123 } 124 if(serviceMap.containsKey(value)) 125 throw new RuntimeException("'" + claze.getName() + "',@Service(value='"+value+"'),value is already exist."); 126 // handle @Service and @Sign 127 Method[] methods = claze.getMethods(); 128 if (Objects.nonNull(methods)) { 129 // key:method url 130 Map<String,AnnotationInfo.AnnotationMethod> map = new HashMap<>(); 131 //not exist main method. 132 boolean main=false; 133 for (Method m : methods) { 134 if (m.isAnnotationPresent(Work.class)) { 135 Work sign = m.getAnnotation(Work.class); 136 //main already exist 137 if(main&&sign.main()) 138 throw new Exception("'" + m.getName() + "',duplicate main entrance point."); 139 main=sign.main(); 140 if(main&&!sign.UI()) 141 throw new Exception("'" + m.getName() + "', main entrance point must is a user interface."); 142 String val = sign.value(); 143 if (val.isEmpty()) 144 val=sign.mapping(); 145 if (val.isEmpty()) 146 val = m.getName(); 147 map.put(val, new AnnotationInfo.AnnotationMethod(m,sign.main(),sign.UI(),m.isAnnotationPresent(Async.class)?m.getAnnotation(Async.class).value():SYNC)); 148 } 149 } 150 if (map.isEmpty()) 151 throw new Exception("'" + className + "',at least lack a use @Work annotated method."); 152 serviceMap.put(value, new AnnotationInfo(claze, map)); 153 if (Objects.nonNull(listener)) 154 { 155 List<Method> list=new ArrayList<>(map.size()); 156 for(AnnotationInfo.AnnotationMethod aa:map.values()) 157 { 158 list.add(aa.method); 159 } 160 listener.onScanning(claze, list.toArray(new Method[0])); 161 } 162 163 } 164 165 } 166 167 } 168 } 169 threadPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors() * hawk.threadPoolSize); 170 if (Objects.nonNull(listener)) 171 listener.onInit(this); 172 } 173 174 static HawkContext create(ServletContext application) throws Exception { 175 return context = new HawkContext(application); 176 } 177 178 // like:requestUri="/a/b/c" 179 static String[] parse(String requestUri) { 180 Path path = Paths.get(requestUri); 181 Path classUrl = path.getParent(); 182 Path methodUrl = path.getFileName(); 183 return new String[] { Objects.nonNull(classUrl)?classUrl.toString().replace("\\", "/"):"", Objects.nonNull(methodUrl)?methodUrl.toString():"" }; 184 185 } 186 187 // like:serviceUrl="/a/b",methodUri="c" 188 static int isAsync(String serviceUrl,String methodUri)throws Exception 189 { 190 AnnotationInfo info = context.serviceMap.get(serviceUrl); 191 AnnotationInfo.AnnotationMethod aa = info.find(methodUri); 192 if(Objects.isNull(aa)) 193 throw new Exception("'"+info.claze.getName()+":"+methodUri+"' not exist."); 194 return aa.async; 195 } 196 // like:serviceUrl="/a/b",methodUrl="c" 197 static boolean valid(String serviceUrl,String methodUri) { 198 if(Objects.nonNull(serviceUrl) && context.serviceMap.containsKey(serviceUrl)) 199 { 200 AnnotationInfo.AnnotationMethod aa=context.serviceMap.get(serviceUrl).find(methodUri); 201 return Objects.nonNull(aa)&&aa.isUI; 202 } 203 return false; 204 } 205 206 // like:serviceUrl="/a/b" 207 static boolean valid(String serviceUrl) { 208 return Objects.nonNull(serviceUrl) && context.serviceMap.containsKey(serviceUrl); 209 } 210 211 // like:serviceFullUrl="service:///a/b/c" 212 public static String work(String serviceFullUrl, Bundle bundle) throws Exception{ 213 URI uri = new URI(serviceFullUrl); 214 String path = uri.getPath(); 215 String[] array = parse(path); 216 if (valid(array[0])) { 217 String ret =null; 218 AnnotationInfo info = context.serviceMap.get(array[0]); 219 Class<?> claze = info.claze; 220 Object obj = context.objectMap.get(array[0]); 221 if (Objects.isNull(obj)) { 222 obj = claze.newInstance(); 223 if (claze.isAnnotationPresent(Cache.class)) 224 context.objectMap.put(array[0], obj); 225 } 226 AnnotationInfo.AnnotationMethod aa = info.methods.get(array[1]); 227 if (Objects.nonNull(aa)) { 228 ret = Objects.nonNull(context.listener) ? context.listener.onBeforeWork(claze,aa.method, bundle) : null; 229 if (Objects.isNull(ret)) 230 { 231 try 232 { 233 ret = (String) aa.method.invoke(obj, bundle); 234 } 235 catch(Exception e) 236 { 237 if(Objects.nonNull(context.listener)) 238 return context.listener.onException(bundle, e); 239 240 } 241 if(Objects.nonNull(context.listener)&&Objects.isNull(ret)) 242 ret=context.listener.onAfterWork(claze,aa.method, bundle); 243 if(aa.isMain) 244 { 245 for(Map.Entry<String, AnnotationInfo.AnnotationMethod> entry:info.methods.entrySet()) 246 { 247 AnnotationInfo.AnnotationMethod am=entry.getValue(); 248 if(am.isUI&&!am.isMain) 249 { 250 String url=bundle.getRequest().getContextPath()+bundle.getRequest().getServletPath()+array[0]+"/"+entry.getKey(); 251 if(Objects.nonNull(bundle.getRequest().getAttribute(am.method.getName()))) 252 throw new IllegalArgumentException("'attribute:"+am.method.getName()+"'already exist in request!"); 253 bundle.getRequest().setAttribute(am.method.getName(), bundle.getResponse().encodeURL(url)); 254 } 255 256 } 257 } 258 } 259 260 } 261 return ret; 262 } 263 return null; 264 } 265 // like:serviceFullUrl="service:///a/b/c" 266 public static String workStream(String serviceFullUrl, Bundle bundle) throws Exception 267 { 268 for(;;) 269 { 270 serviceFullUrl=work(serviceFullUrl,bundle); 271 if(Objects.isNull(serviceFullUrl)) 272 return null; 273 URI u=null; 274 try { 275 u = new URI(serviceFullUrl); 276 } catch (URISyntaxException e) { 277 // TODO Auto-generated catch block 278 return serviceFullUrl; 279 } 280 if(StringX.isEmpty(u.getScheme())) 281 return serviceFullUrl; 282 if(Scheme.valueOf(u.getScheme().toLowerCase().trim())!=Scheme.work) 283 return serviceFullUrl; 284 } 285 } 286 287 public void cache(String key,Object value) 288 { 289 application.setAttribute(key, value); 290 } 291 292 public void remove(String key) 293 { 294 application.removeAttribute(key); 295 } 296 297 @SuppressWarnings("unchecked") 298 public <T>T getService(String key) 299 { 300 return (T)objectMap.get(key); 301 } 302 303 @SuppressWarnings("unchecked") 304 public <T>T get(String key) 305 { 306 return (T)application.getAttribute(key); 307 } 308 309 /** 310 * @return the application 311 */ 312 public ServletContext getServletContext() { 313 return application; 314 } 315 316 void destroy() { 317 threadPool.shutdownNow(); 318 try { 319 threadPool.awaitTermination(3, TimeUnit.SECONDS); 320 } catch (InterruptedException e) { 321 // TODO Auto-generated catch block 322 //e.printStackTrace(); 323 } 324 clients.clear(); 325 objectMap.clear(); 326 serviceMap.clear(); 327 context = null; 328 if (Objects.nonNull(listener)) 329 listener.onDestroy(this); 330 } 331 332 333 void addClient(AsyncContext client) 334 { 335 clients.add(client); 336 } 337 338 339 /** 340 * @return the threadPool 341 */ 342 ForkJoinPool getThreadPool() { 343 return threadPool; 344 } 345 346 /** 347 * @return the context 348 */ 349 public static HawkContext getContext() { 350 return context; 351 } 352 353 InputStream getResourceAsStream(String path) { 354 return application.getResourceAsStream(path); 355 } 356 357 URL getResource(String path) throws MalformedURLException { 358 return application.getResource(path); 359 } 360 361 public String getContextPath() 362 { 363 return context.application.getRealPath("/"); 364 } 365 366 @Override 367 public void onComplete(AsyncEvent arg0) throws IOException { 368 // TODO Auto-generated method stub 369 clients.remove(arg0.getAsyncContext()); 370 } 371 372 @Override 373 public void onError(AsyncEvent arg0) throws IOException { 374 // TODO Auto-generated method stub 375 arg0.getAsyncContext().complete(); 376 clients.remove(arg0.getAsyncContext()); 377 arg0.getThrowable().printStackTrace(); 378 } 379 380 @Override 381 public void onStartAsync(AsyncEvent arg0) throws IOException { 382 // TODO Auto-generated method stub 383 384 } 385 386 @Override 387 public void onTimeout(AsyncEvent arg0) throws IOException { 388 // TODO Auto-generated method stub 389 arg0.getAsyncContext().complete(); 390 clients.remove(arg0.getAsyncContext()); 391 } 392 393 }
二,全局控制器,一个servlet,类似于springmvc的DispatcherServlet,负责处理work返回的结果,向用户发送响应。代码如下:
1 package com.jjh.jee.hawk; 2 3 import java.io.IOException; 4 import java.io.OutputStream; 5 import java.net.URI; 6 import java.util.Objects; 7 import java.util.concurrent.RecursiveAction; 8 9 import javax.servlet.AsyncContext; 10 import javax.servlet.ServletConfig; 11 import javax.servlet.ServletException; 12 import javax.servlet.annotation.MultipartConfig; 13 import javax.servlet.annotation.WebServlet; 14 import javax.servlet.http.HttpServlet; 15 import javax.servlet.http.HttpServletRequest; 16 import javax.servlet.http.HttpServletResponse; 17 18 import com.jjh.common.StringX; 19 20 /** 21 * Servlet implementation class GlobalController 22 */ 23 @WebServlet(asyncSupported = true, loadOnStartup = 1, urlPatterns = GlobalController.URL_PREFIX 24 + "/*", name = "GlobalController") 25 @MultipartConfig 26 public class GlobalController extends HttpServlet { 27 private static final long serialVersionUID = 1L; 28 public static final String URL_PREFIX = "/hawk"; 29 30 /** 31 * @see HttpServlet#HttpServlet() 32 */ 33 public GlobalController() { 34 super(); 35 // TODO Auto-generated constructor stub 36 37 } 38 39 /** 40 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse 41 * response) 42 */ 43 @Override 44 protected void doGet(HttpServletRequest request, HttpServletResponse response) 45 throws ServletException, IOException { 46 // TODO Auto-generated method stub 47 request.setCharacterEncoding("UTF-8"); 48 response.setCharacterEncoding("UTF-8"); 49 String uri = request.getRequestURI().replaceFirst(request.getContextPath() + URL_PREFIX, ""); 50 int index = uri.indexOf(';'); 51 String[] urls = HawkContext.parse(uri = uri.substring(0, index < 0 ? uri.length() : index)); 52 String serviceUrl = urls[0]; 53 String methodUrl = urls[1]; 54 if (HawkContext.valid(serviceUrl, methodUrl)) { 55 Bundle bundle = new Bundle(); 56 int timeout; 57 try { 58 timeout = HawkContext.isAsync(serviceUrl, methodUrl); 59 } catch (Exception e) { 60 response.sendError(404, e.getMessage()); 61 return; 62 } 63 // sync request 64 if (timeout == HawkContext.SYNC) { 65 bundle.setResponse(response); 66 bundle.setRequest(request); 67 handleService(uri, bundle, request, response); 68 bundle.clear(); 69 } 70 // async request 71 else if (timeout > 0) { 72 if (request.isAsyncStarted()) { 73 request.getAsyncContext().complete(); 74 } else if (request.isAsyncSupported()) { 75 AsyncContext actx = request.startAsync(); 76 actx.addListener(HawkContext.getContext()); 77 actx.setTimeout(timeout); 78 HawkContext.getContext().addClient(actx); 79 HttpServletRequest req = (HttpServletRequest) actx.getRequest(); 80 HttpServletResponse resp = (HttpServletResponse) actx.getResponse(); 81 bundle.setRequest(req); 82 bundle.setResponse(resp); 83 String local = uri; 84 HawkContext.getContext().getThreadPool().invoke(new RecursiveAction() { 85 /** 86 * 87 */ 88 private static final long serialVersionUID = 1L; 89 90 @Override 91 protected void compute() { 92 // TODO Auto-generated method stub 93 try { 94 handleService(local, bundle, req, resp); 95 bundle.clear(); 96 } catch (Exception e) { 97 // TODO Auto-generated catch block 98 e.printStackTrace(); 99 } 100 } 101 102 }); 103 actx.complete(); 104 105 } else { 106 new Exception("Async Not Supported").printStackTrace(); 107 response.sendError(400, "Async is not supported."); 108 } 109 } 110 } else 111 response.sendError(404, uri); 112 113 } 114 115 private void handleService(String requestUri, Bundle bundle, HttpServletRequest request, 116 HttpServletResponse response) throws IOException, ServletException { 117 // TODO Auto-generated method stub 118 requestUri = "work://" + requestUri; 119 String nextUrl = null; 120 URI u = null; 121 try { 122 nextUrl = HawkContext.workStream(requestUri, bundle); 123 if (Objects.isNull(nextUrl)) 124 return; 125 //JSON Literal 126 if (nextUrl.startsWith("{") && nextUrl.endsWith("}") || nextUrl.startsWith("[") && nextUrl.endsWith("]")) { 127 response.setContentType(Scheme.json.getMime() + ";charset=utf-8"); 128 response.getWriter().append(nextUrl).flush(); 129 return; 130 } 131 u = new URI(nextUrl); 132 //String Literal 133 if(StringX.isEmpty(u.getScheme())) { 134 response.setContentType(Scheme.text.getMime() + ";charset=utf-8"); 135 response.getWriter().append(nextUrl).flush(); 136 return; 137 } 138 }catch (Exception e) { 139 // TODO Auto-generated catch block 140 e.printStackTrace(); 141 throw new ServletException(e); 142 } 143 String path = u.toString().replaceFirst(u.getScheme() + "://", ""); 144 switch (Scheme.valueOf(u.getScheme().toLowerCase().trim())) { 145 case xml: 146 response.setContentType(Scheme.xml.getMime() + ";charset=utf-8"); 147 Object xml = bundle.getXml(); 148 if (Objects.nonNull(xml)) { 149 OutputStream out = response.getOutputStream(); 150 javax.xml.bind.JAXB.marshal(xml, out); 151 out.flush(); 152 } else if (Objects.nonNull(path)) 153 request.getRequestDispatcher(path).forward(request, response); 154 break; 155 case json: 156 response.setContentType(Scheme.json.getMime() + ";charset=utf-8"); 157 Object json = bundle.getJson(); 158 if (Objects.nonNull(json)) { 159 response.getWriter().append(json.toString()).flush(); 160 } else if (Objects.nonNull(path)) 161 request.getRequestDispatcher(path).forward(request, response); 162 break; 163 case sse: 164 response.setContentType(Scheme.sse.getMime() + ";charset=utf-8"); 165 String text = bundle.getText(); 166 if (StringX.isEmpty(text)) 167 text = u.getQuery(); 168 if (StringX.nonEmpty(text)) { 169 response.getWriter().append("data:" + text + "\r\n").flush(); 170 } 171 break; 172 case text: 173 response.setContentType(Scheme.text.getMime() + ";charset=utf-8"); 174 text = bundle.getText(); 175 if (StringX.nonEmpty(text)) { 176 response.getWriter().append(text).flush(); 177 } else if (StringX.nonEmpty(path)) 178 request.getRequestDispatcher(path).forward(request, response); 179 break; 180 case binary: 181 response.setContentType(Scheme.binary.getMime() + ";charset=utf-8"); 182 byte[] bytes = bundle.getBytes(); 183 if (Objects.nonNull(bytes)) { 184 OutputStream out = response.getOutputStream(); 185 out.write(bytes); 186 out.flush(); 187 } 188 break; 189 case script: 190 response.setContentType(Scheme.script.getMime() + ";charset=utf-8"); 191 text = bundle.getText(); 192 if (StringX.nonEmpty(text)) { 193 response.getWriter().append(text).flush(); 194 } else if (StringX.nonEmpty(path)) 195 request.getRequestDispatcher(path).forward(request, response); 196 break; 197 case jsp: 198 case html: 199 response.setContentType(Scheme.html.getMime() + ";charset=utf-8"); 200 text = bundle.getText(); 201 if (StringX.nonEmpty(text)) { 202 response.getWriter().append(text).flush(); 203 } else if (StringX.nonEmpty(path)) 204 request.getRequestDispatcher(path).forward(request, response); 205 break; 206 case file: 207 request.getRequestDispatcher(path).forward(request, response); 208 break; 209 case http: 210 case https: 211 response.sendRedirect(nextUrl); 212 break; 213 case redirect: 214 response.sendRedirect(path); 215 break; 216 default: 217 break; 218 } 219 220 } 221 222 /** 223 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse 224 * response) 225 */ 226 @Override 227 protected void doPost(HttpServletRequest request, HttpServletResponse response) 228 throws ServletException, IOException { 229 // TODO Auto-generated method stub 230 doGet(request, response); 231 } 232 233 /* 234 * (non-Javadoc) 235 * 236 * @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig) 237 */ 238 @Override 239 public void init(ServletConfig config) throws ServletException { 240 // TODO Auto-generated method stub 241 super.init(config); 242 System.out.println("GlobalController init."); 243 } 244 }