# miniserver **Repository Path**: dajiangnan/miniserver ## Basic Information - **Project Name**: miniserver - **Description**: No description available - **Primary Language**: Java - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 2 - **Created**: 2016-02-14 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README
## MiniServer使用手册 ## ###版本变更记录### - 1.1.3 : 监控页面修改,增加controll方法级aop拦截 - 1.1.4 : 允许controll方法接收byte[]形式的post content(http发送二进制数据的时候) - 1.1.5 : 对于进行了接口缓存的controller方法,response中增加Cache-Control:max-age=xxx的header - 1.1.6 : 基本数据类型作为get参数时候类型不匹配时500Error的bug解决 - 1.1.7 : 带超时配置的controll方法执行时,所调用的callable线程名称置为调用线程的名称,方便日志查询 - 1.1.8 : 增加accessLogInLine配置项 - 1.1.9 : 所依赖的commonUtil升级为1.0.4 - 1.1.10: bug fix: url以/结尾时解析报错 - 1.1.11: 增加了httpRespHeaderDefineFile配置项,用户可以自定义httpRespHeader中的值. 增加.mms的miniType.所依赖的commonUtil升级为1.0.7 - 1.1.12: 去掉commonUtil依赖 ###1. 概述### ####1.1 简介#### - 是一个可内嵌进java应用的httpserver - 是一个基于annotation的极简mvc开发框架 ####1.2 优势#### - 开发迅速,代码量少 - 学习简单,文档详细,demo完备 - 功能完善,多数情况下可替代tomcat+springMMV - 超轻量,易扩展。可对”静态资源cache/接口cache/session cache以及视图渲染方式”进行扩展(使之支持分布式) - 自带runTime页面(基于动态接口访问量/状态码分布/性能参数的统计报表) - http接口的json解析/缓存/业务方法timeout,这些功能由miniserver负责,开发者可以专注业务逻辑 引用jfinal作者詹大侠一句话 > 为开发者节约更多时间去陪家人恋人和朋友 ####1.3 局限性#### - 上传功能/cookie操作没用到过,所以当前版本不支持 - Http Header中的Range功能没有用到过(未使用RandomAccessFile实现文件下载),所以当前版本不能下载大的文件(jvm内存限制)以及断点续传 - 基于工作中最常遇到的功能进行开发,未全面参考RFC规范。不支持”200,302,401,403,404,500,,503,504”之外的状态码,也不支持https/ajp/websocket等比较冷或比较高级的特性 - 无关javax.servlet.*/web.xml ,所以无法整合基于javax.servlet.*的其他开发框架(如cfx等),不支持jsp - 基于bio通讯, 各种反射理论上也会影响性能, 单机处理能力相当于java.net.socket的能力。高并发访问tomcat静态页稳定在5000pv/s的硬件条件下,MiniServer同样并发访问静态页稳定在3500~4000pv/s(开发者可以通过自定义cacher的方式进行分布式支持) ####1.4 如何获取#### [获取源码](https://git.oschina.net/dajiangnan/miniserver.git) [demo源码](https://git.oschina.net/dajiangnan/miniServerDemo.git) Maven坐标如下(version变量为版本变更记录的最新版本号) net.oschina.dajiangnan miniServer $version ###2. 定义Server### ####2.1 HelloWorld#### 或 Server server = new Server(); server.startServer(); //server.shutdownServer(); (后文均基于spring的配置进行说明) - Server可以定义成prototype模式 (不必须单例),进程中可以启动多个Server ####2.2 统计页面#### Server启动后,访问http://{ip}:7070/,即miniServer在该端口的配置信息/统计信息/性能数据页面 ![](http://i.imgur.com/lMVayu9.jpg) ![](http://i.imgur.com/F8himhU.jpg) ####2.3 详细说明#### com.aspire.nm.component.miniServerDemo.server.ControllImpl com.aspire.nm.component.miniServerDemo.server.RsaFilterImpl #####2.3.1 指定配置文件##### 配置项说明如下 ![](http://i.imgur.com/udpgYh9.jpg) #####2.3.2 配置controll##### com.aspire.nm.component.miniServerDemo.server.ControllImpl - Http交互层和业务逻辑层以Controller为界,用户自定义Controller并进行开发业务逻辑(开发细节详见后续章节) - 自定义的Controller必须实现com.aspire.nm.component.miniServer.Controll接口 - 可以定义多个Controller,方便用户对“不同类型业务逻辑/不同Path/不同IP限制”等的规划 #####2.3.3 自定义Filter##### com.aspire.nm.component.miniServerDemo.server.RsaFilterImpl - 请求接收后或应答发送前,用户可能需要做一些全局处理(典型的如权限控制/报文加解密等),基于AOP的思想,用户可以自定义Filter,进行全局过滤 - Filter中可以自定义针对某一个或几个动态请求的Filter处理。开发细节详见后续章节。 - 自定义的Filter必须实现com.aspire.nm.component.miniServer.Filter接口 - 可以定义多个Filter,如定义F1,F2,F3三个过滤器,那么对于请求的过滤顺序是F1->F2->F3 ,对应答的过滤顺序是F3->F2->F1 #####2.3.4 自定义接口缓存##### *关于如何为动态接口增加可缓存特性以及过期时间的配置详见后续章节* - 默认使用jvm内存进行缓存。最大缓存数量10000,饱和后使用LRU算法进行remove。 - 自定义接口缓存类需要实现com.aspire.nm.component.miniServer.plugin.IfCacher #####2.3.5 自定义Session缓存##### - 默认使用jvm内存进行缓存。最大缓存数量10000,饱和后使用LRU算法进行remove - 自定义Session缓存类需要实现com.aspire.nm.component.miniServer.plugin.SessionCacher #####2.3.6 自定义静态资源缓存##### - 默认使用jvm内存进行缓存。最大缓存数量10000,饱和后使用LRU算法进行remove - 自定义接口缓存类需要实现com.aspire.nm.component.miniServer.plugin.MyResourceCacher #####2.3.7 自定义Controller拦截器##### - Filter是针对Http请求/应答的缓存,开发者还可以针对Controll的方法定义全局拦截器,添加全局操作,如日志处理 - 自定义Controll拦截器需要实现com.aspire.nm.component.miniServer.aop.Around 接口定义如下: public interface Around { /** * 每个controll方法执行前执行before方法 * @param controll controll本身 * @param method method方法本身 * @param params 执行参数 */ public void before(Controll controll,Method method,Object[] params); /** * 每个controll方法执行后执行after方法 * @param controll controll本身 * @param method method方法本身 * @param params 执行参数 * @param returnObject 返回值 */ public void after(Controll controll,Method method,Object[] params,Object returnObject); } - 需要注意的是,如果before或after方法抛出异常(Throwable),框架都会catch,即不会影响对Controll方法本身的调用 #####2.3.8 自定义渲染实现##### - miniServer默认使用freemaker渲染视图,开发者可以通过配置文件中配置renderFactoryClassName来自定义渲染实现 - 自定义渲染实现类必须实现com.aspire.nm.component.miniServer.render. RenderProvider接口 ###3. 开发详解### ####3.1 HelloWorld#### /** * http://127.0.0.1:7070/hello */ public class ControllImpl implements Controll { @Controller public String hello(){ return "HelloWord"; } - Controller类须实现com.aspire.nm.component.miniServer.Controll接口 - 标注了@Controller注解的方法才能够匹配Http请求 ####3.2 Controller开发#### #####3.2.1 基本参数获取/接收##### /** * http://127.0.0.1:7070/hello?user=张三&age=15&iswomen=true&c=f */ public class ControllImpl implements Controll { @Controller(desc="基本参数接收") public String hello(@ClientIp String clientIp, @ServerConfig(key="serverPort") String serverPort, @Header(key="User-Agent") String ua, @Req(key="user") String user, @Req(key="age") long age, @Req(key="c") char c, @Req(key="iswomen") boolean iswomen, @Session Map session, @JSessionId String jsessionId, @ServerConfig(key="sessionExpireSec") String sessionExpireSec){ StringBuffer sb = new StringBuffer(); sb.append("客户端IP =" + clientIp+"\r\n"); sb.append("服务端PORT =" + serverPort+"\r\n"); sb.append("header中ua =" + ua+"\r\n"); sb.append("get参数user =" + user+"\r\n"); sb.append("get参数age =" + age+"\r\n"); sb.append("get参数c =" + c+"\r\n"); sb.append("get参数iswomen =" + iswomen+"\r\n"); sb.append("session超时 =" + sessionExpireSec+"\r\n"); sb.append("jsessionId =" + jsessionId+"\r\n"); if(session.get("count") == null){ session.put("count", 1); sb.append("首次访问 " +"\r\n"); }else{ int count = ((Integer)session.get("count")).intValue(); count ++; session.put("count", count); sb.append("访问次数 = "+count +"\r\n"); } return sb.toString(); } } - desc : 方法描述,对应管理页面中的描述 - @Req(key="user"),get参数中key为user对应的value - @Session Map session , 用户Session - @JSessionId String jsessionId , 对于不支持Cookie的客户端,需要使用重写Url的方式获取Session,此时需要Url地址增加JSessionId参数 - @Header(key="User-Agent") String ua,可以获取Http Header中的值 - @ClientIp String clientIp,客户端IP地址 - @ServerConfig(key="serverPort") String serverPort 服务器配置的值,key对应配置文件中的key #####3.2.2 接口超时配置/异常处理##### /** * http://127.0.0.1:7070/sayError */ @Controller(timeOut=2000,desc="500异常") public String sayError(){ throw new RuntimeException(); } - timeOut,单位毫秒,该方法的超时时间,超时后将会返回504状态码 - 当Controller方法有异常抛出后,Http应答的Content为相关异常的stack(与Tomcat的500异常输出相同) #####3.2.3 path路由/传参##### /** * http://127.0.0.1:7070/uu1/1242/1/blog */ @Controller(pathPattern = "(\\w{3,10})/(\\d*)/([1|0])/(blog)",desc="path传参") public String showBlog(@Path String path, @Path(groupIndex=1) String username1, @Path(groupIndex=2) int userId1, @Path(groupIndex=3) boolean ismen1){ return "path = " + path + " username : " + username1 + " userId:" + userId1 +" ismen = " + ismen1; } - http请求路由方法的时候,默认path为方法名称,也可以自定义path(正则表达式),并且根据path获取相关参数 - pathPattern = "(\\w{3,10})/(\\d*)/([1|0])/(blog)",配置http路由进方法的正则表达式 - @Path(groupIndex=1) ,http path 路由进方法后,match时候的groupIndex=1的值 #####3.2.4 Post数据接收##### /** * http://127.0.0.1:7070/postTest.html */ @Controller(pathPattern = "user/post",methodType=MethodType.POST,desc="接受Post数据") public String sendPostDate(@Req String postDate1){ return "数据处理完毕,postDate = " + postDate1; } - methodType=MethodType.POST,定义该方法只接受POST数据 - @Req String postDate1,客户端POST的数据 #####3.2.5 Post对象接收##### /** * http://127.0.0.1:7070/postJson1 */ @Controller(desc="接收json对象Person") public String postJson1(@Req Person person){ if(person == null){ System.out.println("格式错误"); } return ""; } - 当@Req注解的参数为对象时,会自动转换成对象并注入方法参数 - 可以在配置文件中通过配置项jsonFormatErr全局定义Json格式错误的返回,如果格式错误时jsonFormatErr配置项为空,将会把Null值注 #####3.2.6 @接口缓存##### /** * http://127.0.0.1:7070/cacheMethod1 */ @Controller(cacheTime=5000,desc="接口缓存(默认的key)") public String cacheMethod1(){ System.out.println("int cacheTime..."); return "ok "; } - cacheTime=5000,接口缓存5000毫秒。Key值为方法名 #####3.2.7 @接口缓存(指定Key)##### /** * http://127.0.0.1:7070/demo/cacheMethod?cache_a=1&cache_b=2 */ @Controller(cacheTime=5000,desc="接口缓存") public String cacheMethod(@Req(key = "cache_a") String cache_a,@Req(key = "cache_b") String cache_b) { System.out.println("int cacheTime..."); return "ok " + cache_a + " " + cache_b; } public String cacheMethod_OFKEY(@Req(key = "cache_a") String cache_a,@Req(key = "cache_b") String cache_b){ return cache_a + " " + cache_b; } - “methodName+”_OFKEY””方法的返回值作为method方法的缓存的Key - 注意可缓存的方法和定义Key的方法的参数类型/数量必须相同,后者返回String类型(key为String) #####3.2.8 静态资源/Http应答类型##### - 截止到目前为止,我们的Controller方法的返回值均为String类型。返回的Content-Type为text/plain。Controller方法可以返回的类型为: String/对象/File/ViewModel/Redirect | Controller方法返回类型 | 对应Http应答说明 | Content-Type | | -----|:----:| ----:| | String | 返回字符串 | text/plain | | File | 文件下载 | application/octet-stream | | ViewModel | (模板渲染)Html输出 | text/html | | Redirect | http(302)跳转(sendRedirect) | -- | | 对象(String除外) | 对应的Json串 | text/plain |      MiniServer中,xxx/.xxx(含有 . )的path都都视为静态资源,对于静态资源http应答的Content-Type定义如下 .txt=text/plain .html=text/html .htm=text/html .jsp=text/html .xhtml=text/html .tif=image/tiff .jpg=image/jpeg .gif=image/gif .ico=image/x-icon .jpe=image/jpeg .jpeg=image/jpeg .png=application/x-png .bmp=application/x-bmp .img=application/x-img .doc=application/msword .rtf=application/msword .ppt=application/vnd.ms-powerpoint .ppt=application/x-ppt .pdf=application/pdf .xml=text/xml .wml=text/vnd.wap.wml .asp=text/asp .class=java/* .java=java/* .swf=application/x-shockwave-flash .awf=application/vnd.adobe.workflow .mid=audio/mid .mp4=video/mpeg4 .mpeg=video/mpg .rm=application/vnd.rn-realmedia .rmi=audio/mid .avi=video/avi .mpg=video/mpg .rmvb=application/vnd.rn-realmedia-vbr .apk=application/vnd.android.package-archive .sis=application/vnd.symbian.install .exe=application/x-msdownload .torrent=application/x-bittorrent 对于无法获取到Content-Type的静态资源,Content-Type取值为application/octet-stream,视为静态资源下载 访问(下载)静态资源举例如下,注意xx.xxx的文件路径都是相对于配置文件中absoluteWebappDir的值 http://127.0.0.1:7070/aaa.rar http://127.0.0.1:7070/a.txt http://127.0.0.1:7070/b.html http://127.0.0.1:7070/1.zip http://127.0.0.1:7070/xxx.txt #####3.2.9 模板渲染##### /** * http://127.0.0.1:7070/ScoreList */ @Controller(desc="模板渲染") public ViewModel ScoreList(){ List list = new ArrayList(); Student student = new Student(22,"张三"); student.setBirthday(new Birthday("2001年","10月","01日")); student.getScore().put("yw", 68); student.getScore().put("sx", 56); Student student1 = new Student(22,"李四"); student1.setBirthday(new Birthday("2001年","10月","01日")); student1.getScore().put("yw", 61); student1.getScore().put("sx", 78); Student student2 = new Student(22,"李四"); student2.setBirthday(new Birthday("2001年","10月","01日")); student2.getScore().put("yw", 77); student2.getScore().put("sx", 22); list.add(student); list.add(student1); list.add(student2); HashMap> model = new HashMap>(); model.put("studentList", list); return new ViewModel("a_template",model); } - 渲染方式默认使用FreeMaker - ViewModel的第一个参数为模板路径(相对配置文件中absoluteWebappDir的配置),第二个参数为进行渲染的数据 #####3.2.10 Json返回##### /** * http://127.0.0.1:7070/getMap */ @Controller(desc="json返回(Map)") public Map getMap(){ Map map = new HashMap(); map.put("user1", new Student(22,"张三")); map.put("user2", new Student(21,"李四")); return map; } - Controller方法的返回值在不是String,File,ViewModel,Redirect的情况下,都会按照Json输出的方式进行应答 #####3.2.11 文件下载##### /** * http://127.0.0.1:7070/downLoad */ @Controller(desc="文件下载") public File downLoad(){ String path = "G:\\svn_commons\\component\\miniServerDemo\\webapps\\123.doc"; return new File(path); } - Controller方法的返回值为File时即文件下载 #####3.2.12 Redirect重定向##### /** * http://127.0.0.1:7070/redirect?u=张三 */ @Controller(desc="redirect") public Redirect redirect(@Req(key="u") String u){ return new Redirect("redirected?u="+u); } @Controller(desc="redirected") public String redirected(@Req(key="u") String u){ return u; } - 返回值为Redirect时,即重定向(相当于Servlet中response.sendRedirect) #####3.2.13 类的@Controllers注解##### 用户自定义的Controller类上面可以使用可选的Controllers注解标注。举例如下 @Controllers(path="demo/",allowIpsPattern="127\\.0\\.0\\.\\d{1,3}", methodType=MethodType.GET,timeOut=2000) public class ExpensivelImpl implements Controll { - path : 该类的所有业务方法对应的http path都使用此处定义的前缀作为匹配规则。如http://{ip}:{port}/demo/xxxxx才能路由进ExpensiveImpl中的方法 - allowIpsPattern : (全局IP白名单下的)该类的IP白名单(正则表达式) - methodType : 该类所匹配的HTTP METHOD(get或post),会被具体方法上的methodType所覆盖 - timeout: 该类所有业务方法的超时时间,超时后会返回504状态码。会被具体方法上的超时时间定义所覆盖。 #####3.2.14 注解的配置化##### Java语法中,annotation的value不能是变量,即必须在代码中写死。miniServer可以对Controllers注解的值进行可配置化 - 可以对Controllers中的path和allowIpsPattern进行配置 - 注解中的值必须以$开头,后面的字符串对应配置文件中的key 举例: 配置文件中指定annotationValueConfigFile annotationValueConfigFile=config/properties/annotationConfig.miniServer annotationConfig.miniServer中指定注解的值 xxxImpl.path=demo/ xxxImpl.allowIpsPattern=127\\.0\\.0\\.\\d{1,3}|172\\.18\\.7\\.\\d{1,3} 配置文件中指定annotationValueConfigFile @Controllers(path="$xxxImpl.path", allowIpsPattern="$xxxImpl.allowIpsPattern") public class xxxImpl implements Controll { ####3.3 Filter开发#### #####3.3.1 定义##### 参见2.3.3 #####3.3.2 请求过滤 ##### - @FilterRequest注解标注的方法为请求接收后的处理 - @FilterRequest注解标注的方法参数类型必须为com.aspire.nm.component.miniServer.protocol.Request #####3.3.3 应答过滤 ##### - @FilterResponse注解标注的方法为应答发送前的处理 - @FilterResponse注解标注的方法参数类型必须为com.aspire.nm.component.miniServer.protocol.Response #####3.3.4 Filter方法返回值 ##### - 被注解的方法返回null表示继续后续的Filter或Controller方法 - 被注解的方法返回String/对象/File/ViewModel/Redirect 则直接返回与之相对应(参照3.2.8)的Response,不执行后续的Filter或Controller #####3.3.5 报文加解密举例 ##### 报文加解密的Filter方法如下 @FilterRequest(pathPattern = "demo/rsa") public String decrypt(Request request) { //解密 byte[] plainText; try { plainText = new RSAEncrypt().decrypt(privateKey, BytesUtil.hex2bytes(request.getPostdate())); } catch (Exception e) { return e.toString(); } //数据变更为解密后的数据 request.setPostdate(new String(plainText)); return null; } @FilterResponse(pathPattern = "demo/rsa") public String encrypt(Response response){ //加密 byte[] cipher; try { cipher = new RSAEncrypt().encrypt(privateKey, response.getContentBytes()); } catch (Exception e) { return e.toString(); } //数据变更为加密后的数据 response.setContentBytes(BytesUtil.bytes2hex(cipher).getBytes()); return null; } ###4.日志规划### ####4.1 AccessLog日志打印#### Log4j配置文件相关配置如下 log4j.appender.access=org.apache.log4j.DailyRollingFileAppender log4j.appender.access.File=logs/miniserver/access.log log4j.appender.access.DatePattern='.'yyyy-MM-dd log4j.appender.access.layout=org.apache.log4j.PatternLayout log4j.appender.access.layout.ConversionPattern=[%d{yyyy.MM.dd HH:mm:ss}][%t][%c][%M](%L)[%p] - %m%n log4j.logger.Access=info,access log4j.additivity.Access=false ####4.2 按端口规划日志#### miniServer在jvm中可以多例存在(启动不同端口),可以按照端口号配置不同的日志文件路径。方法如下: log4j.appender.access.File=logs/miniserver/#PORT#/access.log 路径可以配置#PORT#的形式(对Log4j进行了扩展),比如进程启动了7070,7071,7072三端口,则相关的AccessLog将会输出到
logs/miniserver/7070/access.log
logs/miniserver/7071/access.log
logs/miniserver/7072/access.log
三个路径下。