Repository: all4you/redant Branch: master Commit: 38f32e1bd310 Files: 118 Total size: 254.7 KB Directory structure: gitextract_2l6xrivj/ ├── .gitignore ├── LICENSE ├── README.md ├── md/ │ ├── interceptor.md │ ├── overview.md │ └── processor.md ├── pom.xml ├── redant-cluster/ │ ├── pom.xml │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── redant/ │ └── cluster/ │ ├── bootstrap/ │ │ ├── MasterServerBootstrap.java │ │ ├── SlaveServerBootstrap.java │ │ └── ZkBootstrap.java │ ├── master/ │ │ ├── MasterServer.java │ │ ├── MasterServerBackendHandler.java │ │ └── MasterServerHandler.java │ ├── node/ │ │ └── Node.java │ ├── service/ │ │ ├── discover/ │ │ │ ├── ServiceDiscover.java │ │ │ └── ZkServiceDiscover.java │ │ └── register/ │ │ ├── ServiceRegister.java │ │ └── ZkServiceRegister.java │ ├── slave/ │ │ └── SlaveServer.java │ └── zk/ │ ├── ZkClient.java │ ├── ZkConfig.java │ ├── ZkNode.java │ └── ZkServer.java ├── redant-core/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── redant/ │ │ │ └── core/ │ │ │ ├── ServerBootstrap.java │ │ │ ├── anno/ │ │ │ │ └── Order.java │ │ │ ├── aware/ │ │ │ │ ├── Aware.java │ │ │ │ └── BeanContextAware.java │ │ │ ├── bean/ │ │ │ │ ├── BaseBean.java │ │ │ │ ├── annotation/ │ │ │ │ │ ├── Autowired.java │ │ │ │ │ └── Bean.java │ │ │ │ └── context/ │ │ │ │ ├── BeanContext.java │ │ │ │ └── DefaultBeanContext.java │ │ │ ├── common/ │ │ │ │ ├── constants/ │ │ │ │ │ └── CommonConstants.java │ │ │ │ ├── enums/ │ │ │ │ │ ├── ContentType.java │ │ │ │ │ └── RequestMethod.java │ │ │ │ ├── exception/ │ │ │ │ │ ├── InvalidSessionException.java │ │ │ │ │ ├── InvocationException.java │ │ │ │ │ └── ValidationException.java │ │ │ │ ├── html/ │ │ │ │ │ ├── DefaultHtmlMaker.java │ │ │ │ │ ├── HtmlMaker.java │ │ │ │ │ ├── HtmlMakerEnum.java │ │ │ │ │ └── HtmlMakerFactory.java │ │ │ │ ├── util/ │ │ │ │ │ ├── GenericsUtil.java │ │ │ │ │ ├── HtmlContentUtil.java │ │ │ │ │ ├── HttpRenderUtil.java │ │ │ │ │ ├── HttpRequestUtil.java │ │ │ │ │ ├── PropertiesUtil.java │ │ │ │ │ ├── TagUtil.java │ │ │ │ │ └── ThreadUtil.java │ │ │ │ └── view/ │ │ │ │ ├── HtmlKeyHolder.java │ │ │ │ ├── Page404.java │ │ │ │ ├── Page500.java │ │ │ │ ├── PageError.java │ │ │ │ └── PageIndex.java │ │ │ ├── context/ │ │ │ │ └── RedantContext.java │ │ │ ├── controller/ │ │ │ │ ├── ControllerProxy.java │ │ │ │ ├── ProxyInvocation.java │ │ │ │ ├── annotation/ │ │ │ │ │ ├── Controller.java │ │ │ │ │ ├── Mapping.java │ │ │ │ │ └── Param.java │ │ │ │ └── context/ │ │ │ │ ├── ControllerContext.java │ │ │ │ └── DefaultControllerContext.java │ │ │ ├── converter/ │ │ │ │ ├── AbstractConverter.java │ │ │ │ ├── Converter.java │ │ │ │ ├── PrimitiveConverter.java │ │ │ │ └── PrimitiveTypeUtil.java │ │ │ ├── cookie/ │ │ │ │ ├── CookieManager.java │ │ │ │ └── DefaultCookieManager.java │ │ │ ├── executor/ │ │ │ │ ├── AbstractExecutor.java │ │ │ │ ├── Executor.java │ │ │ │ └── HttpResponseExecutor.java │ │ │ ├── handler/ │ │ │ │ ├── ControllerDispatcher.java │ │ │ │ └── ssl/ │ │ │ │ └── SslContextHelper.java │ │ │ ├── init/ │ │ │ │ ├── InitExecutor.java │ │ │ │ ├── InitFunc.java │ │ │ │ └── InitOrder.java │ │ │ ├── interceptor/ │ │ │ │ ├── Interceptor.java │ │ │ │ ├── InterceptorBuilder.java │ │ │ │ ├── InterceptorHandler.java │ │ │ │ └── InterceptorProvider.java │ │ │ ├── render/ │ │ │ │ └── RenderType.java │ │ │ ├── router/ │ │ │ │ ├── BadClientSilencer.java │ │ │ │ ├── MethodlessRouter.java │ │ │ │ ├── OrderlessRouter.java │ │ │ │ ├── PathPattern.java │ │ │ │ ├── RouteResult.java │ │ │ │ ├── Router.java │ │ │ │ └── context/ │ │ │ │ ├── DefaultRouterContext.java │ │ │ │ └── RouterContext.java │ │ │ ├── server/ │ │ │ │ ├── NettyHttpServer.java │ │ │ │ ├── NettyHttpServerInitializer.java │ │ │ │ └── Server.java │ │ │ └── session/ │ │ │ ├── HttpSession.java │ │ │ ├── SessionConfig.java │ │ │ ├── SessionHelper.java │ │ │ └── SessionManager.java │ │ └── resources/ │ │ ├── logback.xml │ │ ├── redant.properties │ │ └── zk.cfg │ └── test/ │ └── java/ │ └── com/ │ └── redant/ │ └── core/ │ └── context/ │ └── RedantContextTest.java └── redant-example/ ├── pom.xml └── src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── redant/ │ │ └── example/ │ │ ├── bootstrap/ │ │ │ ├── cluster/ │ │ │ │ ├── MasterServerBootstrap.java │ │ │ │ ├── SlaveServerBootstrap.java │ │ │ │ └── ZkBootstrap.java │ │ │ └── standalone/ │ │ │ └── ServerBootstrap.java │ │ ├── controller/ │ │ │ ├── BaseController.java │ │ │ ├── CookieController.java │ │ │ └── UserController.java │ │ ├── interceptor/ │ │ │ ├── BlockInterceptor.java │ │ │ ├── CustomInterceptorBuilder.java │ │ │ └── PerformanceInterceptor.java │ │ └── service/ │ │ ├── UserBean.java │ │ ├── UserService.java │ │ └── UserServiceImpl.java │ └── resources/ │ └── logback.xml └── test/ └── java/ └── com/ └── lememo/ └── core/ └── interceptor/ └── InterceptorProviderTest.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Compiled source # ################### *.com *.class *.dll *.exe *.o *.so # Packages # ############ # it's better to unpack these files and commit the raw source # git has its own built in compression methods *.7z *.dmg *.gz *.iso *.jar *.rar *.tar *.zip # Logs and databases # ###################### *.log # OS generated files # ###################### .DS_Store* ehthumbs.db Icon? Thumbs.db # Editor Files # ################ *~ *.swp # Gradle Files # ################ .gradle .m2 # Build output directies target/ build/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans # IntelliJ specific files/directories out .idea *.ipr *.iws *.iml atlassian-ide-plugin.xml # Eclipse specific files/directories .classpath .project .settings .metadata # NetBeans specific files/directories .nbattrs nbproject/private/ build/ nbbuild/ dist/ nbdist/ ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017 all4you Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # RedAnt 项目 **RedAnt** 是一个基于 Netty 的轻量级 Web 容器 **特性:** - [x] **IOC容器** : 通过 @Bean 注解可以管理所有对象,通过 @Autowired 注解进行对象注入 - [x] **自定义路由** : 通过 @Controller @Mapping @Param 注解可以自定义路由 - [x] **自动参数转换** : 通过 TypeConverter 接口,http 参数会被转成对象(支持基本类型,Map,List,JavaBean) - [x] **结果渲染** : 支持对结果进行渲染,支持 html, xml, plain, json 格式 - [x] **Cookie管理** : 内置一个 Cookie 管理器 - [x] **前置后置拦截器** :支持前置拦截器与后置拦截器 - [x] **单机模式** : 支持单机模式 - [x] **集群模式** : 支持集群模式 - [x] **服务注册与发现** :实现了一个基于 Zk 的服务注册与发现,来支持多节点模式 - [ ] **Session管理** : 因为涉及到多节点模式,分布式 session 暂未实现 ## 快速启动 ### 1.单机模式 Redant 是一个基于 Netty 的 Web 容器,类似 Tomcat 和 WebLogic 等容器 只需要启动一个 Server,默认的实现类是 NettyHttpServer 就能快速启动一个 web 容器了,如下所示: ``` java public final class ServerBootstrap { public static void main(String[] args) { Server nettyServer = new NettyHttpServer(); // 各种初始化工作 nettyServer.preStart(); // 启动服务器 nettyServer.start(); } } ``` ### 2.集群模式 到目前为止,我描述的都是单节点模式,如果哪一天单节点的性能无法满足了,那就需要使用集群了,所以我也实现了集群模式。 集群模式是由一个主节点和若干个从节点构成的。主节点接收到请求后,将请求转发给从节点来处理,从节点把处理好的结果返回给主节点,由主节点把结果响应给请求。 要想实现集群模式需要有一个服务注册和发现的功能,目前是借助于 Zk 来做的服务注册与发现。 #### 准备一个 Zk 服务端 因为主节点需要把请求转发给从节点,所以主节点需要知道目前有哪些从节点,我通过 ZooKeeper 来实现服务注册与发现。 如果你没有可用的 Zk 服务端的话,那你可以通过运行下面的 Main 方法来启动一个 ZooKeeper 服务端: ``` java public final class ZkBootstrap { private static final Logger LOGGER = LoggerFactory.getLogger(ZkBootstrap.class); public static void main(String[] args) { try { ZkServer zkServer = new ZkServer(); zkServer.startStandalone(ZkConfig.DEFAULT); }catch (Exception e){ LOGGER.error("ZkBootstrap start failed,cause:",e); System.exit(1); } } } ``` 这样你就可以在后面启动主从节点的时候使用这个 Zk 了。但是这并不是必须的,如果你已经有一个正在运行的 Zk 的服务端,那么你可以在启动主从节点的时候直接使用它,通过在 main 方法的参数中指定 Zk 的地址即可。 #### 启动主节点 只需要运行下面的代码,就可以启动一个主节点了: ``` java public class MasterServerBootstrap { public static void main(String[] args) { String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT); // 启动MasterServer Server masterServer = new MasterServer(zkAddress); masterServer.preStart(); masterServer.start(); } } ``` 如果在 main 方法的参数中指定了 Zk 的地址,就通过该地址去进行服务发现,否则会使用默认的 Zk 地址 #### 启动从节点 只需要运行下面的代码,就可以启动一个从节点了: ``` java public class SlaveServerBootstrap { public static void main(String[] args) { String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT); Node node = Node.getNodeWithArgs(args); // 启动SlaveServer Server slaveServer = new SlaveServer(zkAddress,node); slaveServer.preStart(); slaveServer.start(); } } ``` 如果在 main 方法的参数中指定了 Zk 的地址,就通过该地址去进行服务注册,否则会使用默认的 Zk 地址 ## 例子 你可以运行 redant-example 模块中提供的例子来体验一下,example 模块中内置了两个 Controller 。 启动完之后,你可以在浏览器中访问 http://127.0.0.1:8888 来查看具体的效果 (默认的端口可以在 redant.properties 中修改) 如果你看到了这样的消息:"Welcome to redant!" 这就意味着你已经启动成功了。 在 redant-example 模块中,内置了以下几个默认的路由: | 方法类型 | URL | 响应类型 | | ----------------- | ---------------------------- | ----------------------------- | | GET | / | HTML | | \* | \* | HTML | | GET | /user/count | JSON | | GET | /user/list | JSON | | GET | /user/info | JSON | ## Bean 管理器 跟 Spring 一样,你可以通过 @Bean 注解来管理所有的对象,通过 @Autowired 来自动注入 **Tips:** 更多信息请查看wiki: [Bean][1] ## 自定义路由 跟 Spring 一样,你可以通过 @Controller 来自定义一个 Controller. @Mapping 注解用在方法级别,@Controller + @Mapping 唯一定义一个 http 请求。 @Param 注解用在方法的参数上。通过该注解可以自动将基本类型转成 POJO 对象。 **Tips:** 更多信息请查看wiki: [Router][2] ## Cookie 管理器 Cookie 管理器可以管理用户自定义的 Cookie。 **Tips:** 更多信息请查看wiki: [Cookie][4] ## 联系我 > wh_all4you#hotmail.com ![contact-me](./logo.jpg) [1]: https://github.com/all4you/redant/wiki/1:Bean [2]: https://github.com/all4you/redant/wiki/2:Router [3]: https://github.com/all4you/redant/wiki/3:Session [4]: https://github.com/all4you/redant/wiki/4:Cookie ================================================ FILE: md/interceptor.md ================================================ # 用责任链模式设计拦截器 我在 Redant(https://github.com/all4you/redant) 中通过继承 ChannelHandler 实现了拦截器的功能,并且 pipeline 就是一种责任链模式的应用。但是我后面对原本的拦截器进行了重新设计,为什么这样做呢,因为原本的方式是在 ChannelHandler 的基础上操作的,而我们知道 Netty 的数据处理都是基于 ByteBuf 的,这就涉及到引用计数释放的问题,前面的 ChannelHandler 在处理时可以不关心引用计数的问题,而交给最后一个 ChannelHandler 去释放。 但是拦截器的一大特性就是当某个条件不满足时需要中断后面的操作直接返回,所以这就造成了在 pipeline 中某个节点需要释放引用计数,另外一个方面就是原先的设计使用了很多自定义的 ChannelHandler,有的只做了一些简单的工作,所以完全可以对他们进行合并,使代码变得更加精简紧凑。 合并多个 ChannelHandler 是比较简单的,重新设计拦截器相对就复杂一些了。 ## 重新设计拦截器 首先我把原本的前置拦截器和后置拦截器统一成一个拦截器,然后抽象出两个方法,分别表示:前置处理,后置处理,如下图所示: ![interceptor](interceptor/interceptor.png) 默认前置处理的方法返回 true,用户可以根据他们的业务进行覆盖。 这里是定义了一个抽象类,也可以用接口,java 8 开始接口中可以有默认方法实现。 拦截器定义好之后,现在就可以在 ChannelHandler 中加入拦截器的方法调用了,如下图所示: ![interceptor-handle](interceptor/interceptor-handle.png) 当前置方法返回 false 时,直接返回,中断后面的业务逻辑处理,最终会到 finally 中将结果写入 response 中返回给前端。 现在只要实现 InterceptorHandler 中的两个方法就可以了,其实这也很简单,只要获取到所有的 Interceptor 的实现类,然后依次调用这些实现类的前置方法和后置方法就好了,如下图所示: ![interceptor-call](interceptor/interceptor-call.png) ## 获取拦截器 现在的重点就是怎样获取到所有的拦截器,首先可以想到的是通过扫描的方法,找到所有 Interceptor 的实现类,然后将这些实现类加入到一个 List 中即可。 那怎么保证拦截器的执行顺序呢,很简单,只要在加入 List 之前对他们进行排序就可以了。再定义一个 @Order 注解来表示排序的顺序,然后用一个 Wrapper 包装类将 Interceptor 和 Order 包装起来,排序到包装类的 List 中,最后再从包装类的 List 中依次取出所有的 Interceptor 就完成了 Interceptor 的排序了。 知道了大致的原理之后,实现起来就很简单了,如下图所示: ![get-interceptor](interceptor/get-interceptor.png) 但是我们不能每次都通过调用 scanInterceptors() 方法来获取所有的拦截器,如果这样每次都扫描一次的话性能会有影响,所以我们只需要第一次调用一下该方法,然后把结果保存在一个私有的变量中,获取的时候直接读取该变量的值即可,如下图所示: ![interceptor-provider](interceptor/interceptor-provider.png) ## 自定义拦截器实现类 下面让我们来自定义两个拦截器实现类,来验证下具体的效果。 第一个拦截器,在前置方法中对请求参数进行判断,如果请求参数中有 block=true 的参数,则进行拦截,如下图所示: ![block-interceptor](interceptor/block-interceptor.png) 第二个拦截器,在后置方法中打印出每次请求的耗时,如下图所示: ![performance-interceptor](interceptor/performance-interceptor.png) 通过 @Order 注解来指定执行的顺序,先执行 BlockInterceptor 再执行 PerformanceInterceptor。 ## 查看效果 现在我们请求 /user/info 这个接口,查看下效果。 首先我们只提交正常的参数,如下图所示: ![common-request](interceptor/common-request.png) 打印的结果如下图所示: ![common-request-effect](interceptor/common-request-effect.png) 从打印的结果中可以看到依次执行了: - BlockInterceptor 的 preHandle 方法 - PerformanceInterceptor 的 preHandle方法 - BlockInterceptor 的 postHandle 方法 - PerformanceInterceptor 的 postHandle方法 这说明拦截器是按照 @Order 注解进行了排序,然后依次执行的。 然后我们再提交一个 block=true 的参数,再次请求该接口,如下图所示: ![block-request](interceptor/block-request.png) 可以看到该请求已经被拦截器的前置方法给拦截了,再看下打印的日志,如下图所示: ![block-request-effect](interceptor/block-request-effect.png) 只打印了 BlockInterceptor 的 preHandler 方法中的部分日志,后面的方法都没有执行,因为被拦截了直接返回了。 ## 存在的问题 到这里已经对拦截器完成了改造,并且也验证了效果,看上去效果还可以。但是有没有什么问题呢? 还真有一个问题:所有的 Interceptor 实现类只要被扫描到了,就会被加入到 List 中去,如果不想应用某一个拦截器这时就做不到了,因为无法对 list 中的值进行动态的更改。 如果我们可以动态的获取一个保存了 Interceptor 的 list ,如果该 list 中没有获取到值,再通过扫描的方式去拿到所有的 Interceptor 这样就完美了。 动态获取 Interceptor 的 list 的方法,可以由用户自定义实现,根据某些规则来确定要不要将某个 Interceptor 加入到 list 中去,这样就把 Interceptor 的实现和使用进行了解耦了。用户可以实现任意多的 Interceptor,但是只根据规则去使用其中的某些 Interceptor。 理清楚了原理之后,就很好实现了,首先定义一个接口,用来构造 Interceptor 的 List,如下图所示: ![interceptor-builder](interceptor/interceptor-builder.png) 有了 InterceptorBuilder 之后,在获取 Interceptor 的时候,就可以先根据 InterceptorBuilder 来获取了,如下图所示: ![interceptor-provider-with-builder](interceptor/interceptor-provider-with-builder.png) 以下是一个示例的 InterceptorBuilder,具体的可以用户自行扩展设计,如下图所示: ![custom-interceptor-builder](interceptor/custom-interceptor-builder.png) 这样用户只要实现一个 InterceptorBuilder 接口,即可按照自己的意图去组装所有的拦截器。 ================================================ FILE: md/overview.md ================================================ **RedAnt** 是一个基于 Netty 的轻量级 Web 容器,创建这个项目的目的主要是学习使用 Netty,俗话说不要轻易的造轮子,但是通过造轮子我们可以学到很多优秀开源框架的设计思路,编写优美的代码,更好的提升自己。 ![features](./overview/features.png) ## 快速启动 Redant 是一个基于 Netty 的 Web 容器,类似 Tomcat 和 WebLogic 等容器 只需要启动一个 Server,默认的实现类是 NettyHttpServer 就能快速启动一个 web 容器了,如下所示: ```java public final class ServerBootstrap { public static void main(String[] args) { Server nettyServer = new NettyHttpServer(); // 各种初始化工作 nettyServer.preStart(); // 启动服务器 nettyServer.start(); } } ``` 我们可以直接启动 redant-example 模块中的 ServerBootstrap 类,因为 redant-example 中有很多示例的 Controller,我们直接运行 example 中的 ServerBootstrap,启动后你会看到如下的日志信息: ![start-up](./overview/start-up.png) 在 redant-example 模块中,内置了以下几个默认的路由: | 方法类型 | URL | 响应类型 | | -------- | ----------- | -------- | | GET | / | HTML | | \* | \* | HTML | | GET | /user/count | JSON | | GET | /user/list | JSON | | GET | /user/info | JSON | 启动成功后,可以访问 http://127.0.0.1:8888/ 查看效果,如下图所示: ![welcome-to-redant](./overview/welcome-to-redant.png) 如果你可以看到 "Welcome to redant!" 这样的消息,那就说明你启动成功了。 ## 自定义路由 框架实现了自定义路由,通过 @Controller @Mapping 注解就可以唯一确定一个自定义路由。如下列的 UserController 所示: ![user-controller](./overview/user-controller.png) 和 Spring 的使用方式一样,访问 /user/list 来看下效果,如下图所示: ![user-list](./overview/user-list.png) ## 结果渲染 目前支持 json、html、xml、text 等类型的结果渲染,用户只需要在 方法的 @Mapping 注解上通过 renderType 来指定具体的渲染类型即可,如果不指定的话,默认以 json 类型范围。 如下图所示,首页就是通过指定 renderType 为 html 来返回一个 html 页面的: ![base-controller](./overview/base-controller.png) ## IOC容器 从 UserController 的代码中,我们看到 userServerce 对象是通过 @Autowired 注解自动注入的,这个功能是任何一个 IOC 容器基本的能力,下面我们来看看如何实现一个简单的 IOC 容器。 首先定义一个 BeanContext 接口,如下所示: ``` java public interface BeanContext { /** * 获得Bean * @param name Bean的名称 * @return Bean */ Object getBean(String name); /** * 获得Bean * @param name Bean的名称 * @param clazz Bean的类 * @param 泛型 * @return Bean */ T getBean(String name,Class clazz); } ``` 然后我们需要在系统启动的时候,扫描出所有被 @Bean 注解修饰的类,然后对这些类进行实例化,然后把实例化后的对象保存在一个 Map 中即可,如下图所示: ![init-bean](./overview/init-bean.png) 代码很简单,通过在指定路径下扫描出所有的类之后,把实例对象加入map中,但是对于已经加入的 bean 不能继续加入了,加入之后要获取一个 Bean 也很简单了,直接通过 name 到 map 中去获取就可以了。 现在我们已经把所有 @Bean 的对象管理起来了,那对于依赖到的其他的 bean 该如何注入呢,换句话说就是将我们实例化好的对象赋值给 @Autowired 注解修饰的变量。 简单点的做法就是遍历 beanMap,然后对每个 bean 进行检查,看这个 bean 里面的每个 setter 方法和属性,如果有 @Autowired 注解,那就找到具体的 bean 实例之后将值塞进去。 ### setter注入 ![setter-inject](./overview/setter-inject.png) ### field注入 ![field-inject](./overview/field-inject.png) ### 通过Aware获取BeanContext BeanContext 已经实现了,那怎么获取 BeanContext 的实例呢?想到 Spring 中有很多的 Aware 接口,每种接口负责一种实例的回调,比如我们想要获取一个 BeanFactory 那只要将我们的类实现 BeanFactoryAware 接口就可以了,接口中的 setBeanFactory(BeanFactory factory) 方法参数中的 BeanFactory 实例就是我们所需要的,我们只要实现该方法,然后将参数中的实例保存在我们的类中,后续就可以直接使用了。 那现在我就来实现这样的功能,首先定义一个 Aware 接口,所有其他需要回调塞值的接口都继承自该接口,如下所示: ``` java public interface Aware { } public interface BeanContextAware extends Aware{ /** * 设置BeanContext * @param beanContext BeanContext对象 */ void setBeanContext(BeanContext beanContext); } ``` 接下来需要将 BeanContext 的实例注入到所有 BeanContextAware 的实现类中去。BeanContext 的实例很好得到,BeanContext 的实现类本身就是一个 BeanContext 的实例,并且可以将该实例设置为单例,这样的话所有需要获取 BeanContext 的地方都可以获取到同一个实例。 拿到 BeanContext 的实例后,我们就需要扫描出所有实现了 BeanContextAware 接口的类,并实例化这些类,然后调用这些类的 setBeanContext 方法,参数就传我们拿到的 BeanContext 实例。 逻辑理清楚之后,实现起来就很简单了,如下图所示: ![bean-context-aware](./overview/bean-context-aware.png) ## Cookie管理 基本上所有的 web 容器都会有 cookie 管理的能力,那我们的 redant 也不能落后。首先定义一个 CookieManager 的接口,核心的操作 cookie 的方法如下: ``` java public interface CookieManager { Set getCookies(); Cookie getCookie(String name); void addCookie(String name,String value); void setCookie(Cookie cookie); boolean deleteCookie(String name); } ``` 其中我只列举了几个核心的方法,另外有一些不同参数的重载方法,这里就不详细介绍了。最关键的是两个方法,一个是读 Cookie 一个是写 Cookie 。 ### 读 Cookie Netty 中是通过 HttpRequest 的 Header 来保存请求中所携带的 Cookie的,所以要读取 Cookie 的话,最关键的是获取到 HttpRequest。而 HttpRequest 可以在 ChannelHandler 中拿到,通过 HttpServerCodec 编解码器,Netty 已经帮我们把请求的数据转换成 HttpRequest 了。但是这个 HttpRequest 只在 ChannelHandler 中才能访问到,而处理 Cookie 通常是用户自定义的操作,并且对用户来说他是不关心 HttpRequest 的,他只需要通过 CookieManager 去获取一个 Cookie 就行了。 这种情况下,最适合的就是将 HttpRequest 对象保存在一个 ThreadLocal 中,在 CookieManager 中需要获取的时候,直接到 ThreadLocal 中去取出来就可以了,如下列代码所示: ``` java @Override public Set getCookies() { HttpRequest request = TemporaryDataHolder.loadHttpRequest(); Set cookies = new HashSet<>(); if(request != null) { String value = request.headers().get(HttpHeaderNames.COOKIE); if (value != null) { cookies = ServerCookieDecoder.STRICT.decode(value); } } return cookies; } ``` TemporaryDataHolder 就是那个通过 ThreadLocal 保存了 HttpRequest 的类。 ### 写 Cookie 写 Cookie 和读 Cookie 面临着一样的问题,就是写的时候需要借助于 HttpResponse,将 Cookie 写入 HttpResponse 的 Header 中去,但是用户执行写 Cookie 操作的时候,根本就不关心 HttpResponse,甚至他在写的时候,还没有 HttpResponse。 这时的做法也是将需要写到 HttpResponse 中的 Cookie 保存在 ThreadLocal 中,然后在最后通过 channel 写响应之前,将 Cookie 拿出来塞到 HttpResponse 中去即可,如下列代码所示: ``` java @Override public void setCookie(Cookie cookie) { TemporaryDataHolder.storeCookie(cookie); } /** * 响应消息 */ private void writeResponse(){ boolean close = isClose(); response.headers().add(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(response.content().readableBytes())); // 从ThreadLocal中取出待写入的cookie Set cookies = TemporaryDataHolder.loadCookies(); if(!CollectionUtil.isEmpty(cookies)){ for(Cookie cookie : cookies){ // 将cookie写入response中 response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie)); } } ChannelFuture future = channel.write(response); if(close){ future.addListener(ChannelFutureListener.CLOSE); } } ``` ## 拦截器 拦截器是一个框架很重要的功能,通过拦截器可以实现一些通用的工作,比如登录鉴权,事务处理等等。记得在 Servlet 的年代,拦截器是非常重要的一个功能,基本上每个系统都会在 web.xml 中配置很多的拦截器。 拦截器的基本思想是,通过一连串的类去执行某个拦截的操作,一旦某个类中的拦截操作返回了 false,那就终止后面的所有流程,直接返回。 这种场景非常适合用责任链模式去实现,而 Netty 的 pipeline 本身就是一个责任链模式的应用,所以我们就可以通过 pipeline 来实现我们的拦截器。这里我定义了两种类型的拦截器:前置拦截器和后置拦截器。 前置拦截器是在处理用户的业务逻辑之前的一个拦截操作,如果该操作返回了 false 则直接 return,不会继续执行用户的业务逻辑。 后置拦截器就有点不同了,后置拦截器主要就是处理一些后续的操作,因为后置拦截器再跟前置拦截器一样,当操作返回了 false 直接 return 的话,已经没有意义了,因为业务逻辑已经执行完了。 理解清楚了具体的逻辑之后,实现起来就很简单了,如下列代码所示: ### 前置拦截器 ![pre-interceptor](./overview/pre-interceptor.png) ### 后置拦截器 ![after-interceptor](./overview/after-interceptor.png) 有了实现之后,我们需要把他们加到 pipeline 中合适的位置,让他们在整个责任链中生效,如下图所示: ![init-channel-handler](./overview/init-channel-handler.png) ### 指定拦截器的执行顺序 目前拦截器还没有实现指定顺序执行的功能,其实也很简单,可以定义一个 @InterceptorOrder 的注解应用在所有的拦截器的实现类上,扫描到拦截器的结果之后,根据该注解进行排序,然后把拍完序之后的结果添加到 pipeline 中即可。 ## 集群模式 到目前为止,我描述的都是单节点模式,如果哪一天单节点的性能无法满足了,那就需要使用集群了,所以我也实现了集群模式。 集群模式是由一个主节点和若干个从节点构成的。主节点接收到请求后,将请求转发给从节点来处理,从节点把处理好的结果返回给主节点,由主节点把结果响应给请求。 要想实现集群模式需要有一个服务注册和发现的功能,目前是借助于 Zk 来做的服务注册与发现。 ### 准备一个 Zk 服务端 因为主节点需要把请求转发给从节点,所以主节点需要知道目前有哪些从节点,我通过 ZooKeeper 来实现服务注册与发现。 如果你没有可用的 Zk 服务端的话,那你可以通过运行下面的 Main 方法来启动一个 ZooKeeper 服务端: ```java public final class ZkBootstrap { private static final Logger LOGGER = LoggerFactory.getLogger(ZkBootstrap.class); public static void main(String[] args) { try { ZkServer zkServer = new ZkServer(); zkServer.startStandalone(ZkConfig.DEFAULT); }catch (Exception e){ LOGGER.error("ZkBootstrap start failed,cause:",e); System.exit(1); } } } ``` 这样你就可以在后面启动主从节点的时候使用这个 Zk 了。但是这并不是必须的,如果你已经有一个正在运行的 Zk 的服务端,那么你可以在启动主从节点的时候直接使用它,通过在 main 方法的参数中指定 Zk 的地址即可。 ### 启动主节点 只需要运行下面的代码,就可以启动一个主节点了: ```java public class MasterServerBootstrap { public static void main(String[] args) { String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT); // 启动MasterServer Server masterServer = new MasterServer(zkAddress); masterServer.preStart(); masterServer.start(); } } ``` 如果在 main 方法的参数中指定了 Zk 的地址,就通过该地址去进行服务发现,否则会使用默认的 Zk 地址。 ### 启动从节点 只需要运行下面的代码,就可以启动一个从节点了: ```java public class SlaveServerBootstrap { public static void main(String[] args) { String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT); Node node = Node.getNodeWithArgs(args); // 启动SlaveServer Server slaveServer = new SlaveServer(zkAddress,node); slaveServer.preStart(); slaveServer.start(); } } ``` 如果在 main 方法的参数中指定了 Zk 的地址,就通过该地址去进行服务注册,否则会使用默认的 Zk 地址。 实际上多节点模式具体的处理逻辑还是复用了单节点模式的核心功能,只是把原本一台实例扩展到多台实例而已。 ## 总结 本文通过介绍一个基于 Netty 的 web 容器,让我们了解了一个 http 服务端的大概的构成,当然实现中可能有更加好的方法。但是主要的还是要了解内在的思想,包括 Netty 的一些基本的使用方法。 我会继续优化该项目,加入更多的特性,例如服务发现与注册当前是通过 Zk 来实现的,未来可能会引入其他的组件去实现服务注册与发现。 除此之外,Session 的管理还未完全实现,后续也需要对这一块进行完善。 ================================================ FILE: md/processor.md ================================================ # 责任链 我在 Redant 中实现的拦截器所使用的责任链,其实是通过了一个 List 来保存了所有的 Interceptor,那我们通常所说的责任链除了使用 List 来实现外,还可以通过真正的链表结构来实现,Netty 和 Sentinel 中都有这样的实现,下面我来实现一个简单的链式结构的责任链。 责任链的应用已经有很多了,这里不再赘述,假设我们需要对前端提交的请求做以下操作:鉴权,登录,日志记录,通过责任链来做这些处理是非常合适的。 首先定义一个处理接口,如下图所示: ![processor](processor/processor.png) 通过 List 方式的实现很简单,只需要把每个 Processor 的实现类添加到一个 List 中即可,处理的时候遍历该 List 依次处理,这里不再做具体的描述,感兴趣的可以自行实现。 ## 定义节点 如果是通过链表的形式来实现的话,首先我们需要有一个类表示链表中的某个节点,并且该节点需要有一个同类型的私有变量表示该节点的下个节点,这样就可以实现一个链表了,如下图所示: ![abstract-linked-processor](processor/abstract-linked-processor.png) ## 定义容器 接着我们需要定义一个容器,在容器中有头,尾两个节点,头结点作为一个空节点,真正的节点将添加到头结点的 next 节点上去,尾节点作为一个指针,用来指向当前添加的节点,下一次添加新节点时,将从尾节点处添加。有了具体的处理逻辑之后,实现起来就很简单了,这个容器的实现如下图所示: ![linked-processor-chain](processor/linked-processor-chain.png) ## 定义实现类 下面我们可以实现具体的 Processor 来处理业务逻辑了,只要继承 AbstractLinkedProcessor 即可,如下图所示: ![auth-processor](processor/auth-processor.png) 其他两个实现类: LoginProcessor ,LogProcessor 类似,这里就不贴出来了。 然后就可以根据规则来组装所需要的 Processor 了,假设我们的规则是需要对请求依次进行:鉴权,登录,日志记录,那组装的代码如下图所示: ![linked-processor-chain-test](processor/linked-processor-chain-test.png) 执行该代码,结果如下图所示: ![linked-processor-chain-test-result](processor/linked-processor-chain-test-result.png) ## 存在的问题 看的仔细的同学可以发现了,在 AuthProcessor 的业务逻辑实现中,除了执行了具体的逻辑代码之外,还调用了一行 super.process(content) 代码,这行代码的作用是调用链表中的下一个节点的 process 方法。但是如果有一天我们在写自己的 Processor 实现类时,忘记调用这行代码的话,会是怎样的结果呢? 结果就是当前节点后面的节点不会被调用,整个链表就像断掉一样,那怎样来避免这种问题的发生呢?其实我们在 AbstractProcessor 中已经实现了 process 方法,该方法就是调用下个节点的 process 方法的。那我们在这个方法触发调用下个节点之前,再抽象出一个用以具体的业务逻辑处理的方法 doProcess ,先执行 doProcess 方法,执行完之后再触发下个节点的 process ,这样就不会出现链表断掉的情况了,具体的实现如下图所示: ![fixed-abstract-linked-processor](processor/fixed-abstract-linked-processor.png) 相应的 LinkedProcessorChain 和具体的实现类也要做响应的调整,如下图所示: ![fixed-linked-processor-chain](processor/fixed-linked-processor-chain.png) ![fixed-auth-processor](processor/fixed-auth-processor.png) 重新执行刚刚的测试类,发现结果和之前的一样,至此一个简单的链式责任链完成了。 ================================================ FILE: pom.xml ================================================ 4.0.0 com.redant redant 1.0.0-SNAPSHOT pom redant 1.8 UTF-8 redant 1.0.0-SNAPSHOT 1.9.2 1.7.7 1.1.7 1.2.54 4.1.9.Final 3.2.4 4.2.1 3.4.13 4.0.1 redant-core redant-cluster redant-example commons-beanutils commons-beanutils ${common-beans.version} org.slf4j slf4j-api ${slf4j.version} org.slf4j jcl-over-slf4j ${slf4j.version} ch.qos.logback logback-classic ${logback.version} com.alibaba fastjson ${fastjson.version} io.netty netty-all ${netty.version} cglib cglib ${cglib.version} cn.hutool hutool-all ${hutool-all.version} org.apache.zookeeper zookeeper ${zookeeper.version} org.apache.curator curator-recipes ${curator.version} com.redant redant-core ${project-version} com.redant redant-cluster ${project-version} com.redant redant-example ${project-version} ${project.artifactId} org.apache.maven.plugins maven-compiler-plugin 3.0 ${java.version} ${java.version} ${java.encoding} src/main/java **/*.vm ================================================ FILE: redant-cluster/pom.xml ================================================ redant com.redant 1.0.0-SNAPSHOT 4.0.0 redant-cluster jar redant-cluster 1.0.0-SNAPSHOT com.redant redant-core 1.0.0-SNAPSHOT org.apache.zookeeper zookeeper org.apache.curator curator-recipes org.apache.zookeeper zookeeper ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/bootstrap/MasterServerBootstrap.java ================================================ package com.redant.cluster.bootstrap; import com.redant.cluster.master.MasterServer; import com.redant.cluster.zk.ZkConfig; import com.redant.cluster.zk.ZkServer; import com.redant.core.server.Server; /** * MasterServerBootstrap * @author houyi.wh * @date 2017/11/20 **/ public class MasterServerBootstrap { public static void main(String[] args) { String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT); // 启动MasterServer Server masterServer = new MasterServer(zkAddress); masterServer.preStart(); masterServer.start(); } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/bootstrap/SlaveServerBootstrap.java ================================================ package com.redant.cluster.bootstrap; import com.redant.cluster.node.Node; import com.redant.cluster.slave.SlaveServer; import com.redant.cluster.zk.ZkConfig; import com.redant.cluster.zk.ZkServer; import com.redant.core.server.Server; /** * SlaveServerBootstrap * @author houyi.wh * @date 2017/11/20 **/ public class SlaveServerBootstrap { public static void main(String[] args) { String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT); Node node = Node.getNodeWithArgs(args); // 启动SlaveServer Server slaveServer = new SlaveServer(zkAddress,node); slaveServer.preStart(); slaveServer.start(); } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/bootstrap/ZkBootstrap.java ================================================ package com.redant.cluster.bootstrap; import com.redant.cluster.zk.ZkConfig; import com.redant.cluster.zk.ZkServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * ZK启动入口 * @author houyi.wh * @date 2017/11/21 **/ public class ZkBootstrap { private static final Logger LOGGER = LoggerFactory.getLogger(ZkBootstrap.class); public static void main(String[] args) { try { ZkServer zkServer = new ZkServer(); zkServer.startStandalone(ZkConfig.DEFAULT); }catch (Exception e){ LOGGER.error("ZkBootstrap start failed,cause:",e); System.exit(1); } } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/master/MasterServer.java ================================================ package com.redant.cluster.master; import com.redant.cluster.service.discover.ZkServiceDiscover; import com.redant.core.common.constants.CommonConstants; import com.redant.core.server.Server; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpContentCompressor; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.stream.ChunkedWriteHandler; import io.netty.util.concurrent.DefaultThreadFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * MasterServer * @author houyi.wh * @date 2017/11/20 */ public final class MasterServer implements Server { private static final Logger LOGGER = LoggerFactory.getLogger(MasterServer.class); private String zkAddress; public MasterServer(String zkAddress){ this.zkAddress = zkAddress; } @Override public void preStart() { // 监听SlaveNode的变化 ZkServiceDiscover.getInstance(zkAddress).watch(); } @Override public void start() { EventLoopGroup bossGroup = new NioEventLoopGroup(CommonConstants.BOSS_GROUP_SIZE, new DefaultThreadFactory("boss", true)); EventLoopGroup workerGroup = new NioEventLoopGroup(CommonConstants.WORKER_GROUP_SIZE, new DefaultThreadFactory("worker", true)); try { long start = System.currentTimeMillis(); ServerBootstrap b = new ServerBootstrap(); b.option(ChannelOption.SO_BACKLOG, 1024); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new MasterServerInitializer(zkAddress)); ChannelFuture future = b.bind(CommonConstants.SERVER_PORT).sync(); long cost = System.currentTimeMillis()-start; LOGGER.info("[MasterServer] Startup at port:{} cost:{}[ms]",CommonConstants.SERVER_PORT,cost); // 等待服务端Socket关闭 future.channel().closeFuture().sync(); } catch (InterruptedException e) { LOGGER.error("InterruptedException:",e); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } private static class MasterServerInitializer extends ChannelInitializer { private String zkAddress; MasterServerInitializer(String zkAddress){ this.zkAddress = zkAddress; } @Override public void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); addAdvanced(pipeline); pipeline.addLast(new ChunkedWriteHandler()); pipeline.addLast(new MasterServerHandler(zkAddress)); } /** * 可以在 HttpServerCodec 之后添加这些 ChannelHandler 进行开启高级特性 */ private void addAdvanced(ChannelPipeline pipeline){ if(CommonConstants.USE_COMPRESS) { // 对 http 响应结果开启 gizp 压缩 pipeline.addLast(new HttpContentCompressor()); } if(CommonConstants.USE_AGGREGATOR) { // 将多个HttpRequest组合成一个FullHttpRequest pipeline.addLast(new HttpObjectAggregator(CommonConstants.MAX_CONTENT_LENGTH)); } } } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/master/MasterServerBackendHandler.java ================================================ package com.redant.cluster.master; import io.netty.channel.*; /** * @author houyi.wh * @date 2018/1/18 **/ public class MasterServerBackendHandler extends ChannelInboundHandlerAdapter { private final Channel inboundChannel; public MasterServerBackendHandler(Channel inboundChannel){ this.inboundChannel = inboundChannel; } @Override public void channelActive(ChannelHandlerContext ctx) { ctx.read(); } @Override public void channelRead(final ChannelHandlerContext ctx, Object msg) { inboundChannel.writeAndFlush(msg).addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { if (future.isSuccess()) { ctx.channel().read(); } else { future.channel().close(); } } }); } @Override public void channelInactive(ChannelHandlerContext ctx) { MasterServerHandler.closeOnFlush(inboundChannel); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); MasterServerHandler.closeOnFlush(ctx.channel()); } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/master/MasterServerHandler.java ================================================ package com.redant.cluster.master; import com.redant.cluster.service.discover.ServiceDiscover; import com.redant.cluster.service.discover.ZkServiceDiscover; import com.redant.cluster.node.Node; import com.redant.core.common.constants.CommonConstants; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * MasterServerHandler is a http master which * will transfer http request to node server * @author houyi.wh * @date 2017/11/20 */ public class MasterServerHandler extends ChannelInboundHandlerAdapter { private final static Logger LOGGER = LoggerFactory.getLogger(MasterServerHandler.class); private Node node; /** * Client--->Master Channel */ private Channel inboundChannel; /** * Master--->Slave Channel */ private Channel outboundChannel; private ChannelFuture outboundConnectFuture; private ServiceDiscover serviceDiscover; public MasterServerHandler(String zkAddress){ this.serviceDiscover = ZkServiceDiscover.getInstance(zkAddress); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { node = serviceDiscover.discover(); inboundChannel = ctx.channel(); // Start the connection attempt. Bootstrap bootstrap = new Bootstrap(); bootstrap.group(inboundChannel.eventLoop()) .channel(ctx.channel().getClass()) // use master inboundChannel to write back the response get from remote server .handler(new ChannelInitializer() { @Override public void initChannel(SocketChannel channel) throws Exception { ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast(new HttpClientCodec()); pipeline.addLast(new HttpObjectAggregator(CommonConstants.MAX_CONTENT_LENGTH)); pipeline.addLast(new MasterServerBackendHandler(inboundChannel)); } }); bootstrap.option(ChannelOption.AUTO_READ, false); // connect to node outboundConnectFuture = bootstrap.connect(node.getHost(), node.getPort()); // get outboundChannel to remote server outboundChannel = outboundConnectFuture.channel(); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { outboundConnectFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { if (future.isSuccess()) { // connection complete start to read first data inboundChannel.read(); if(outboundChannel.isActive()) { if(msg instanceof HttpRequest){ HttpRequest request = (HttpRequest)msg; if(request.uri().equals(CommonConstants.FAVICON_ICO)){ return; } outboundChannel.writeAndFlush(request).addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { if (future.isSuccess()) { // was able to flush out data, start to read the next chunk ctx.channel().read(); } else { LOGGER.error("write to backend {}:{} error,cause:{}", node.getHost(), node.getPort(),future.cause()); future.channel().close(); } } }); }else{ closeOnFlush(ctx.channel()); } } } else { LOGGER.error("connect to backend {}:{} error,cause:{}", node.getHost(), node.getPort(),future.cause()); // Close the connection if the connection attempt has failed. inboundChannel.close(); } } }); } @Override public void channelInactive(ChannelHandlerContext ctx) { if (outboundChannel != null) { closeOnFlush(outboundChannel); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); closeOnFlush(ctx.channel()); } /** * Closes the specified channel after all queued write requests are flushed. */ static void closeOnFlush(Channel channel) { if (channel.isActive()) { channel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/node/Node.java ================================================ package com.redant.cluster.node; import cn.hutool.core.util.NumberUtil; import cn.hutool.crypto.SecureUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.redant.core.common.util.GenericsUtil; /** * Node * @author houyi.wh * @date 2017/11/20 **/ public class Node { private static final String DEFAULT_HOST = GenericsUtil.getLocalIpV4(); private static final int DEFAULT_PORT = 8088; public static final Node DEFAULT_PORT_NODE = new Node(DEFAULT_HOST,DEFAULT_PORT); private String id; private String host; private int port; public Node(int port){ this(DEFAULT_HOST,port); } public Node(String host, int port){ this(SecureUtil.md5(host+"&"+port),host,port); } public Node(String id, String host, int port){ this.id = id; this.host = host; this.port = port; } public static Node getNodeWithArgs(String[] args){ Node node = Node.DEFAULT_PORT_NODE; if(args.length>1 && NumberUtil.isInteger(args[1])){ node = new Node(Integer.parseInt(args[1])); } return node; } /** * 从JsonObject中解析出SlaveNode * @param object 对象 * @return 节点 */ public static Node parse(JSONObject object){ if(object==null){ return null; } String host = object.getString("host"); int port = object.getIntValue("port"); String id = object.getString("id"); return new Node(id,host,port); } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public String getId(){ return id; } @Override public String toString() { return JSON.toJSONString(this); } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/service/discover/ServiceDiscover.java ================================================ package com.redant.cluster.service.discover; import com.redant.cluster.node.Node; /** * 服务发现-应用级别 * @author houyi.wh * @date 2017/11/20 **/ public interface ServiceDiscover { /** * 监听Slave节点 */ void watch(); /** * 发现可用的Slave节点 * @return 可用节点 */ Node discover(); } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/service/discover/ZkServiceDiscover.java ================================================ package com.redant.cluster.service.discover; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.redant.cluster.node.Node; import com.redant.cluster.zk.ZkClient; import com.redant.cluster.zk.ZkNode; import io.netty.util.CharsetUtil; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.recipes.cache.ChildData; import org.apache.curator.framework.recipes.cache.PathChildrenCache; import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 服务发现 * @author houyi.wh * @date 2017/11/20 **/ public class ZkServiceDiscover implements ServiceDiscover { private static final Logger LOGGER = LoggerFactory.getLogger(ZkServiceDiscover.class); private CuratorFramework client; private Map nodeMap; private Lock lock; private int slaveIndex = 0; private static ServiceDiscover discover; private ZkServiceDiscover(){ } private ZkServiceDiscover(String zkAddress){ client = ZkClient.getClient(zkAddress); nodeMap = new HashMap<>(); lock = new ReentrantLock(); } public static ServiceDiscover getInstance(String zkAddress){ if(discover==null) { synchronized (ZkServiceDiscover.class) { if(discover==null) { discover = new ZkServiceDiscover(zkAddress); } } } return discover; } @Override public void watch() { if(client==null){ throw new IllegalArgumentException("param illegal with client=null"); } initNodeOnFirst(); doWatch(); } @Override public Node discover() { if(client==null){ throw new IllegalArgumentException("param illegal with client=null"); } lock.lock(); try { if (nodeMap.size() == 0) { LOGGER.error("No available Node!"); return null; } Node[] nodes = new Node[]{}; nodes = nodeMap.values().toArray(nodes); // 通过CAS循环获取下一个可用服务 if (slaveIndex>=nodes.length) { slaveIndex = 0; } return nodes[slaveIndex++]; }finally { lock.unlock(); } } private void initNodeOnFirst(){ try { if(client.checkExists().forPath(ZkNode.ROOT_NODE_PATH)!=null){ List children = client.getChildren().forPath(ZkNode.ROOT_NODE_PATH); for(String child : children){ String childPath = ZkNode.ROOT_NODE_PATH+"/"+child; byte[] data = client.getData().forPath(childPath); Node node = Node.parse(JSON.parseObject(data,JSONObject.class)); if(node !=null){ LOGGER.info("add slave={} to nodeMap when init", node); nodeMap.put(node.getId(), node); } } } } catch (Exception e) { LOGGER.error("initNodeOnFirst error cause:",e); } } private void doWatch(){ try { PathChildrenCache watcher = new PathChildrenCache( client, ZkNode.ROOT_NODE_PATH, true ); watcher.getListenable().addListener(new SlaveNodeWatcher()); watcher.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE); }catch(Exception e){ LOGGER.error("doWatch error cause:",e); } } private class SlaveNodeWatcher implements PathChildrenCacheListener { @Override public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception { ChildData data = event.getData(); if(data==null || data.getData()==null){ return; } Node node = Node.parse(JSON.parseObject(data.getData(),JSONObject.class)); if(node ==null){ LOGGER.error("get a null slave with eventType={},path={},data={}",event.getType(),data.getPath(),data.getData()); }else { switch (event.getType()) { case CHILD_ADDED: nodeMap.put(node.getId(), node); LOGGER.info("CHILD_ADDED with path={},data={},current slave size={}", data.getPath(), new String(data.getData(),CharsetUtil.UTF_8), nodeMap.size()); break; case CHILD_REMOVED: nodeMap.remove(node.getId()); LOGGER.info("CHILD_REMOVED with path={},data={},current slave size={}", data.getPath(), new String(data.getData(),CharsetUtil.UTF_8), nodeMap.size()); break; case CHILD_UPDATED: nodeMap.replace(node.getId(), node); LOGGER.info("CHILD_UPDATED with path={},data={},current slave size={}", data.getPath(), new String(data.getData(),CharsetUtil.UTF_8), nodeMap.size()); break; default: break; } } } } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/service/register/ServiceRegister.java ================================================ package com.redant.cluster.service.register; import com.redant.cluster.node.Node; /** * 服务注册-应用级别 * @author houyi.wh * @date 2017/11/21 **/ public interface ServiceRegister { /** * 注册节点 * @param node 节点 */ void register(Node node); } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/service/register/ZkServiceRegister.java ================================================ package com.redant.cluster.service.register; import cn.hutool.core.util.StrUtil; import com.redant.cluster.node.Node; import com.redant.cluster.zk.ZkClient; import com.redant.cluster.zk.ZkNode; import org.apache.curator.framework.CuratorFramework; import org.apache.zookeeper.CreateMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author houyi.wh * @date 2017/11/21 **/ public class ZkServiceRegister implements ServiceRegister { private static final Logger LOGGER = LoggerFactory.getLogger(ZkServiceRegister.class); private CuratorFramework client; private static ZkServiceRegister register; private ZkServiceRegister(){ } private ZkServiceRegister(String zkAddress){ client = ZkClient.getClient(zkAddress); } public static ServiceRegister getInstance(String zkAddress){ if(register==null) { synchronized (ZkServiceRegister.class) { if(register==null) { register = new ZkServiceRegister(zkAddress); } } } return register; } @Override public void register(Node node) { if(client==null || node ==null){ throw new IllegalArgumentException(String.format("param illegal with client={%s},slave={%s}",client==null?null:client.toString(), node ==null?null: node.toString())); } try { if(client.checkExists().forPath(ZkNode.SLAVE_NODE_PATH)==null) { // 创建临时节点 client.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) .forPath(ZkNode.SLAVE_NODE_PATH, StrUtil.utf8Bytes(node.toString())); } } catch (Exception e) { LOGGER.error("register slave error with slave={},cause:", node,e); } } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/slave/SlaveServer.java ================================================ package com.redant.cluster.slave; import com.redant.cluster.node.Node; import com.redant.cluster.service.register.ZkServiceRegister; import com.redant.core.common.constants.CommonConstants; import com.redant.core.init.InitExecutor; import com.redant.core.server.NettyHttpServerInitializer; import com.redant.core.server.Server; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.util.concurrent.DefaultThreadFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * SlaveServer * @author houyi.wh * @date 2017/11/20 */ public final class SlaveServer implements Server { private static final Logger LOGGER = LoggerFactory.getLogger(SlaveServer.class); private String zkAddress; private Node node; public SlaveServer(String zkAddress, Node node){ this.zkAddress = zkAddress; this.node = node; } @Override public void preStart() { InitExecutor.init(); // 注册Slave到ZK ZkServiceRegister.getInstance(zkAddress).register(node); } @Override public void start() { if(node ==null){ throw new IllegalArgumentException("slave is null"); } EventLoopGroup bossGroup = new NioEventLoopGroup(CommonConstants.BOSS_GROUP_SIZE, new DefaultThreadFactory("boss", true)); EventLoopGroup workerGroup = new NioEventLoopGroup(CommonConstants.WORKER_GROUP_SIZE, new DefaultThreadFactory("worker", true)); try { long start = System.currentTimeMillis(); ServerBootstrap b = new ServerBootstrap(); b.option(ChannelOption.SO_BACKLOG, 1024); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new NettyHttpServerInitializer()); ChannelFuture future = b.bind(node.getPort()).sync(); long cost = System.currentTimeMillis()-start; LOGGER.info("SlaveServer Startup at port:{} cost:{}[ms]", node.getPort(),cost); // 等待服务端Socket关闭 future.channel().closeFuture().sync(); } catch (InterruptedException e) { LOGGER.error("InterruptedException:",e); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/zk/ZkClient.java ================================================ package com.redant.cluster.zk; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.RetryNTimes; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 操作ZK的客户端 * @author houyi.wh * @date 2017/11/21 **/ public class ZkClient { /** * 节点的session超时时间,当Slave服务停掉后, * curator客户端需要等待该节点超时后才会触发CHILD_REMOVED事件 */ private static final int DEFAULT_SESSION_TIMEOUT_MS = 5000; private static final int DEFAULT_CONNECTION_TIMEOUT_MS = 15000; /** * 操作ZK的客户端 */ private static Map clients; private static Lock lock; static{ clients = new ConcurrentHashMap<>(); lock = new ReentrantLock(); } /** * 获取ZK客户端 * @param zkAddress zk服务端地址 * @return zk客户端 */ public static CuratorFramework getClient(String zkAddress){ if(zkAddress == null || zkAddress.trim().length() == 0){ return null; } CuratorFramework client = clients.get(zkAddress); if(client==null){ lock.lock(); try { if(!clients.containsKey(zkAddress)) { client = CuratorFrameworkFactory.newClient( zkAddress, DEFAULT_SESSION_TIMEOUT_MS, DEFAULT_CONNECTION_TIMEOUT_MS, new RetryNTimes(10, 5000) ); client.start(); clients.putIfAbsent(zkAddress,client); }else{ client = clients.get(zkAddress); } }finally { lock.unlock(); } } return client; } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/zk/ZkConfig.java ================================================ package com.redant.cluster.zk; import com.redant.core.common.util.GenericsUtil; import java.util.Properties; /** * 启动zk所需要的配置信息 * @author houyi.wh * @date 2017/11/21 */ public class ZkConfig { private interface ZkConstant { int CLIENT_PORT = 2181; String DATA_DIR = "/Users/houyi/zookeeper/data"; String DATA_LOG_DIR = "/Users/houyi/zookeeper/log"; } /** * 客户端连接的端口 */ private int clientPort; private int tickTime = 2000; private int initLimit = 10; private int syncLimit = 5; /** * 数据存储目录,格式为: * /home/zookeeper/1/data */ private String dataDir; /** * 日志存储目录,格式为: * /home/zookeeper/1/log */ private String dataLogDir; /** * 客户端连接数上限 */ private int maxClientCnxns = 60; public ZkConfig(int clientPort,String dataDir,String dataLogDir){ this.clientPort = clientPort; this.dataDir = dataDir; this.dataLogDir = dataLogDir; } public static ZkConfig DEFAULT = new ZkConfig(ZkConstant.CLIENT_PORT,ZkConstant.DATA_DIR,ZkConstant.DATA_LOG_DIR); public String generateZkAddress(){ return GenericsUtil.getLocalIpV4()+":"+this.clientPort; } public Properties toProp(){ Properties properties = new Properties(); properties.put("clientPort",this.clientPort); properties.put("clientPortAddress",GenericsUtil.getLocalIpV4()); properties.put("tickTime",this.tickTime); properties.put("initLimit",this.initLimit); properties.put("syncLimit",this.syncLimit); properties.put("dataDir",this.dataDir); properties.put("dataLogDir",this.dataLogDir); properties.put("maxClientCnxns",this.maxClientCnxns); return properties; } public int getClientPort() { return clientPort; } public void setClientPort(int clientPort) { this.clientPort = clientPort; } public int getTickTime() { return tickTime; } public void setTickTime(int tickTime) { this.tickTime = tickTime; } public int getInitLimit() { return initLimit; } public void setInitLimit(int initLimit) { this.initLimit = initLimit; } public int getSyncLimit() { return syncLimit; } public void setSyncLimit(int syncLimit) { this.syncLimit = syncLimit; } public String getDataDir() { return dataDir; } public void setDataDir(String dataDir) { this.dataDir = dataDir; } public String getDataLogDir() { return dataLogDir; } public void setDataLogDir(String dataLogDir) { this.dataLogDir = dataLogDir; } public int getMaxClientCnxns() { return maxClientCnxns; } public void setMaxClientCnxns(int maxClientCnxns) { this.maxClientCnxns = maxClientCnxns; } } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/zk/ZkNode.java ================================================ package com.redant.cluster.zk; /** * @author houyi.wh * @date 2017/11/21 **/ public class ZkNode { /** * 根节点 */ public static final String ROOT_NODE_PATH = "/redant"; /** * SlaveNode注册的节点 */ public static final String SLAVE_NODE_PATH = ROOT_NODE_PATH+"/slave"; } ================================================ FILE: redant-cluster/src/main/java/com/redant/cluster/zk/ZkServer.java ================================================ package com.redant.cluster.zk; import cn.hutool.core.util.StrUtil; import org.apache.zookeeper.server.ServerConfig; import org.apache.zookeeper.server.ZooKeeperServerMain; import org.apache.zookeeper.server.quorum.QuorumPeerConfig; import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException; import org.apache.zookeeper.server.quorum.QuorumPeerMain; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Properties; /** * ZooKeeper服务端 * @author houyi.wh * @date 2017/11/21 * */ public class ZkServer { private static final Logger LOGGER = LoggerFactory.getLogger(ZkServer.class); public static String getZkAddressArgs(String[] args, ZkConfig zkConfig){ String zkAddress = ZkServer.getZkAddress(zkConfig); if(args.length>0 && StrUtil.isNotBlank(args[0])){ LOGGER.info("zkAddress is read from args"); zkAddress = args[0]; } if(StrUtil.isBlank(zkAddress)){ System.exit(1); } return zkAddress; } public static String getZkAddress(ZkConfig zkConfig){ return zkConfig!=null ? zkConfig.generateZkAddress() : null; } /** * 通过官方的ZooKeeperServerMain启动类启动单机模式 * @param zkConfig 配置对象 * @throws ConfigException 配置异常 * @throws IOException IO异常 */ public void startStandalone(ZkConfig zkConfig) throws ConfigException, IOException { Properties zkProp = zkConfig.toProp(); QuorumPeerConfig config = new QuorumPeerConfig(); config.parseProperties(zkProp); ServerConfig serverConfig = new ServerConfig(); serverConfig.readFrom(config); ZooKeeperServerMain zkServer = new ZooKeeperServerMain(); zkServer.runFromConfig(serverConfig); } /** * 通过官方的QuorumPeerMain启动类启动真集群模式 * 会执行quorumPeer.join(); * 需要在不同的服务器上执行 * @param zkConfig 配置对象 * @throws ConfigException 配置异常 * @throws IOException IO异常 */ public void startCluster(ZkConfig zkConfig) throws ConfigException, IOException { Properties zkProp = zkConfig.toProp(); QuorumPeerConfig config = new QuorumPeerConfig(); config.parseProperties(zkProp); QuorumPeerMain main = new QuorumPeerMain(); main.runFromConfig(config); } } ================================================ FILE: redant-core/pom.xml ================================================ redant com.redant 1.0.0-SNAPSHOT 4.0.0 redant-core jar redant-core 1.0.0-SNAPSHOT commons-beanutils commons-beanutils org.slf4j slf4j-api org.slf4j jcl-over-slf4j ch.qos.logback logback-classic com.alibaba fastjson io.netty netty-all cglib cglib cn.hutool hutool-all ================================================ FILE: redant-core/src/main/java/com/redant/core/ServerBootstrap.java ================================================ package com.redant.core; import com.redant.core.server.NettyHttpServer; import com.redant.core.server.Server; /** * 服务端启动入口 * @author houyi.wh * @date 2017-10-20 */ public final class ServerBootstrap { public static void main(String[] args) { Server nettyServer = new NettyHttpServer(); // 各种初始化工作 nettyServer.preStart(); // 启动服务器 nettyServer.start(); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/anno/Order.java ================================================ package com.redant.core.anno; import java.lang.annotation.*; /** * 排序规则,升序排序 * @author houyi.wh * @date 2019-01-14 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented public @interface Order { /** * 最低优先级 */ int LOWEST_PRECEDENCE = Integer.MAX_VALUE; /** * 最高优先级 */ int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; /** * The order value. Lowest precedence by default. * * @return the order value */ int value() default LOWEST_PRECEDENCE; } ================================================ FILE: redant-core/src/main/java/com/redant/core/aware/Aware.java ================================================ package com.redant.core.aware; /** * @author houyi.wh * @date 2019-01-14 */ public interface Aware { } ================================================ FILE: redant-core/src/main/java/com/redant/core/aware/BeanContextAware.java ================================================ package com.redant.core.aware; import com.redant.core.bean.context.BeanContext; /** * @author houyi.wh * @date 2019-01-14 */ public interface BeanContextAware extends Aware{ /** * 设置BeanContext * @param beanContext BeanContext对象 */ void setBeanContext(BeanContext beanContext); } ================================================ FILE: redant-core/src/main/java/com/redant/core/bean/BaseBean.java ================================================ package com.redant.core.bean; import com.alibaba.fastjson.JSON; import java.io.Serializable; /** * BaseBean 所有bean都继承该类 * @author houyi.wh * @date 2017-10-20 */ public class BaseBean implements Serializable { private static final long serialVersionUID = -4976516540408695147L; public BaseBean() { } @Override public String toString() { return JSON.toJSONString(this); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/bean/annotation/Autowired.java ================================================ package com.redant.core.bean.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Autowired { String name() default ""; } ================================================ FILE: redant-core/src/main/java/com/redant/core/bean/annotation/Bean.java ================================================ package com.redant.core.bean.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Bean { String name() default ""; } ================================================ FILE: redant-core/src/main/java/com/redant/core/bean/context/BeanContext.java ================================================ package com.redant.core.bean.context; /** * Bean上下文 * @author houyi.wh * @date 2017-10-20 */ public interface BeanContext { /** * 获得Bean * @param name Bean的名称 * @return Bean */ Object getBean(String name); /** * 获得Bean * @param name Bean的名称 * @param clazz Bean的类 * @param 泛型 * @return Bean */ T getBean(String name,Class clazz); } ================================================ FILE: redant-core/src/main/java/com/redant/core/bean/context/DefaultBeanContext.java ================================================ package com.redant.core.bean.context; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.ClassScaner; import cn.hutool.core.util.StrUtil; import com.redant.core.aware.BeanContextAware; import com.redant.core.bean.annotation.Autowired; import com.redant.core.bean.annotation.Bean; import com.redant.core.common.constants.CommonConstants; import com.redant.core.init.InitFunc; import com.redant.core.init.InitOrder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; /** * DefaultBeanContext * * @author houyi.wh * @date 2017-10-20 */ @InitOrder(1) public class DefaultBeanContext implements BeanContext, InitFunc { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultBeanContext.class); /** * 保存所有的bean,初始化过一次后不会改变 */ private static Map beanMap; /** * bean加载完毕的标志 */ private static volatile boolean inited; private static final class DefaultBeanContextHolder { private static DefaultBeanContext context = new DefaultBeanContext(); } private DefaultBeanContext() { } public static BeanContext getInstance() { return DefaultBeanContextHolder.context; } @Override public void init() { // 初始化 doInit(); } /** * 根据bean的name获取具体的bean对象 */ @Override public Object getBean(String name) { return inited() ? beanMap.get(name) : null; } /** * 根据bean的name获取具体的bean对象 */ @Override public T getBean(String name, Class clazz) { Object bean = getBean(name); return bean == null ? null : (T) bean; } /** * bean是否加载完毕 */ private boolean inited() { while (!inited) { doInit(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } return inited; } /** * 执行初始化工作 */ private void doInit() { // 初始化时需要同步 synchronized (DefaultBeanContext.class) { if (!inited) { LOGGER.info("[DefaultBeanContext] doInit"); // 初始化bean initBean(); // 对象注入 injectAnnotation(); // 注入BeanContext到BeanContextAware processBeanContextAware(); inited = true; LOGGER.info("[DefaultBeanContext] doInit success!"); } } } /** * 初始化Bean */ private void initBean() { LOGGER.info("[DefaultBeanContext] start initBean"); try { /* * 扫描指定package下指定的类,并返回set */ Set> classSet = ClassScaner.scanPackageByAnnotation(CommonConstants.BEAN_SCAN_PACKAGE, Bean.class); beanMap = new LinkedHashMap<>(classSet.size() + 1); if (CollectionUtil.isNotEmpty(classSet)) { /* * 遍历所有类,找出有beanClass注解的类,并封装到linkedHashMap里 */ for (Class cls : classSet) { Bean bean = cls.getAnnotation(Bean.class); if (bean != null) { String beanName = StrUtil.isNotBlank(bean.name()) ? bean.name() : cls.getName(); if (beanMap.containsKey(beanName)) { LOGGER.warn("[DefaultBeanContext] duplicate bean with name={}", beanName); continue; } beanMap.put(beanName, cls.newInstance()); } } LOGGER.info("[DefaultBeanContext] initBean success!"); } else { LOGGER.warn("[DefaultBeanContext] no bean classes scanned!"); } } catch (Exception e) { LOGGER.error("[DefaultBeanContext] initBean error,cause:{}", e.getMessage(), e); } } /** * 注解处理器 * 如果注解Autowired配置了name属性,则根据name所指定的名称获取要注入的实例引用, * 否则根据属性所属类型来扫描配置文件获取要注入的实例引用 */ private void injectAnnotation() { LOGGER.info("[DefaultBeanContext] start injectAnnotation"); for (Map.Entry entry : beanMap.entrySet()) { Object bean = entry.getValue(); if (bean != null) { propertyAnnotation(bean); fieldAnnotation(bean); } } LOGGER.info("[DefaultBeanContext] injectAnnotation success!"); } /** * 处理BeanContextAware * 让那些实现了BeanContextAware接口的类能注入BeanContext */ private void processBeanContextAware() { LOGGER.info("[DefaultBeanContext] start processBeanContextAware"); try { /* * 扫描指定package下指定的类,并返回set */ Set> classSet = ClassScaner.scanPackageBySuper(CommonConstants.BEAN_SCAN_PACKAGE, BeanContextAware.class); if (CollectionUtil.isNotEmpty(classSet)) { for (Class cls : classSet) { // 如果cls是BeanContextAware的实现类 if (!cls.isInterface() && BeanContextAware.class.isAssignableFrom(cls)) { Constructor constructor = cls.getDeclaredConstructor(); constructor.setAccessible(true); BeanContextAware aware = (BeanContextAware) constructor.newInstance(); aware.setBeanContext(getInstance()); } } } LOGGER.info("[DefaultBeanContext] processBeanContextAware success!"); } catch (Exception e) { LOGGER.error("[DefaultBeanContext] processBeanContextAware error,cause:{}", e.getMessage(), e); } } /** * 处理在set方法加入的注解 * * @param bean 处理的bean */ private void propertyAnnotation(Object bean) { LOGGER.info("[DefaultBeanContext] start propertyAnnotation"); try { // 获取其属性的描述 PropertyDescriptor[] descriptors = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors(); for (PropertyDescriptor descriptor : descriptors) { // 获取所有set方法 Method setter = descriptor.getWriteMethod(); // 判断set方法是否定义了注解 if (setter != null && setter.isAnnotationPresent(Autowired.class)) { // 获取当前注解,并判断name属性是否为空 Autowired resource = setter.getAnnotation(Autowired.class); String name; Object value = null; if (StrUtil.isNotBlank(resource.name())) { // 获取注解的name属性的内容 name = resource.name(); value = beanMap.get(name); } else { // 如果当前注解没有指定name属性,则根据类型进行匹配 for (Map.Entry entry : beanMap.entrySet()) { // 判断当前属性所属的类型是否在beanHolderMap中存在 if (descriptor.getPropertyType().isAssignableFrom(entry.getValue().getClass())) { // 获取类型匹配的实例对象 value = entry.getValue(); break; } } } // 允许访问private方法 setter.setAccessible(true); // 把引用对象注入属性 setter.invoke(bean, value); } } LOGGER.info("[DefaultBeanContext] propertyAnnotation success!"); } catch (Exception e) { LOGGER.info("[DefaultBeanContext] propertyAnnotation error,cause:{}", e.getMessage(), e); } } /** * 处理在字段上的注解 * * @param bean 处理的bean */ private void fieldAnnotation(Object bean) { LOGGER.info("[DefaultBeanContext] start fieldAnnotation"); try { // 获取其全部的字段描述 Field[] fields = bean.getClass().getDeclaredFields(); for (Field field : fields) { if (field != null && field.isAnnotationPresent(Autowired.class)) { Autowired resource = field.getAnnotation(Autowired.class); String name; Object value = null; if (StrUtil.isNotBlank(resource.name())) { name = resource.name(); value = beanMap.get(name); } else { for (Map.Entry entry : beanMap.entrySet()) { // 判断当前属性所属的类型是否在配置文件中存在 if (field.getType().isAssignableFrom(entry.getValue().getClass())) { // 获取类型匹配的实例对象 value = entry.getValue(); break; } } } // 允许访问private字段 field.setAccessible(true); // 把引用对象注入属性 field.set(bean, value); } } LOGGER.info("[DefaultBeanContext] fieldAnnotation success!"); } catch (Exception e) { LOGGER.info("[DefaultBeanContext] fieldAnnotation error,cause:{}", e.getMessage(), e); } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/constants/CommonConstants.java ================================================ package com.redant.core.common.constants; import com.redant.core.common.util.PropertiesUtil; /** * 公共常量 * @author houyi.wh * @date 2017-10-20 */ public class CommonConstants { private static final String REDANT_PROPERTIES_PATH = "/redant.properties"; private static PropertiesUtil propertiesUtil = PropertiesUtil.getInstance(REDANT_PROPERTIES_PATH); /** * 服务端口号 */ public static final int SERVER_PORT = propertiesUtil.getInt("netty.server.port",8888); /** * BossGroup Size * 先从启动参数中获取:-Dnetty.server.bossGroup.size=2 * 如果获取不到从配置文件中获取 * 如果再获取不到则取默认值 */ public static final int BOSS_GROUP_SIZE = null!=Integer.getInteger("netty.server.bossGroup.size")?Integer.getInteger("netty.server.bossGroup.size"):propertiesUtil.getInt("netty.server.bossGroup.size",2); /** * WorkerGroup Size * 先从启动参数中获取:-Dnetty.server.workerGroup.size=4 * 如果获取不到从配置文件中获取 * 如果再获取不到则取默认值 */ public static final int WORKER_GROUP_SIZE = null!=Integer.getInteger("netty.server.workerGroup.size")?Integer.getInteger("netty.server.workerGroup.size"):propertiesUtil.getInt("netty.server.workerGroup.size",4); /** * 能处理的最大数据的字节数 */ public static final int MAX_CONTENT_LENGTH = propertiesUtil.getInt("netty.maxContentLength",10485760); /** * 是否开启ssl */ public static final boolean USE_SSL = propertiesUtil.getBoolean("netty.server.use.ssl"); /** * 是否开启压缩 */ public static final boolean USE_COMPRESS = propertiesUtil.getBoolean("netty.server.use.compress"); /** * 是否开启http对象聚合 */ public static final boolean USE_AGGREGATOR = propertiesUtil.getBoolean("netty.server.use.aggregator"); /** * KeyStore path */ public static final String KEY_STORE_PATH = propertiesUtil.getString("ssl.keyStore.path"); /** * KeyStore password */ public static final String KEY_STORE_PASSWORD = propertiesUtil.getString("ssl.keyStore.password"); /** * 扫描bean的包路径 */ public static final String BEAN_SCAN_PACKAGE = propertiesUtil.getString("bean.scan.package"); /** * 扫描interceptor的包路径 */ public static final String INTERCEPTOR_SCAN_PACKAGE = propertiesUtil.getString("interceptor.scan.package"); /** * 服务端出错时的错误描述 */ public static final String SERVER_INTERNAL_ERROR_DESC = propertiesUtil.getString("server.internal.error.desc"); public static final String FAVICON_ICO = "/favicon.ico"; public static final String CONNECTION_KEEP_ALIVE = "keep-alive"; public static final String CONNECTION_CLOSE = "close"; /** * 是否异步处理业务逻辑 */ public static final boolean ASYNC_EXECUTE_EVENT = propertiesUtil.getBoolean("async.execute.event"); /** * 业务线程池核心线程数 */ public static final int EVENT_EXECUTOR_POOL_CORE_SIZE = propertiesUtil.getInt("async.executor.pool.core.size",10); /** * 业务线程池最大线程数 */ public static final int EVENT_EXECUTOR_POOL_MAX_SIZE = propertiesUtil.getInt("async.executor.pool.max.size",20); /** * 业务线程池临时线程存活时间,单位:s */ public static final int EVENT_EXECUTOR_POOL_KEEP_ALIVE_SECONDS = propertiesUtil.getInt("async.executor.pool.keep.alive.seconds",10); /** * 业务线程池阻塞队列大学 */ public static final int EVENT_EXECUTOR_POOL_BLOCKING_QUEUE_SIZE = propertiesUtil.getInt("async.executor.pool.blocking.queue.size",10); } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/enums/ContentType.java ================================================ package com.redant.core.common.enums; public enum ContentType { APPLICATION_ATOM_XML("application/atom+xml"), APPLICATION_FORM_URLENCODED("application/x-www-form-urlencoded"), APPLICATION_JSON("application/json"), APPLICATION_OCTET_STREAM("application/octet-stream"), APPLICATION_SVG_XML("application/svg+xml"), APPLICATION_XHTML_XML("application/xhtml+xml"), APPLICATION_XML("application/xml") ; private String content; ContentType(String content){ this.content = content; } @Override public String toString() { return content; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/enums/RequestMethod.java ================================================ package com.redant.core.common.enums; import io.netty.handler.codec.http.HttpMethod; /** * @author houyi.wh * @date 2017/12/1 **/ public enum RequestMethod { /** * GET */ GET(HttpMethod.GET), /** * HEAD */ HEAD(HttpMethod.HEAD), /** * POST */ POST(HttpMethod.POST), /** * PUT */ PUT(HttpMethod.PUT), /** * PATCH */ PATCH(HttpMethod.PATCH), /** * DELETE */ DELETE(HttpMethod.DELETE), /** * OPTIONS */ OPTIONS(HttpMethod.OPTIONS), /** * TRACE */ TRACE(HttpMethod.TRACE); HttpMethod httpMethod; RequestMethod(HttpMethod httpMethod) { this.httpMethod = httpMethod; } public static HttpMethod getHttpMethod(RequestMethod requestMethod){ for(RequestMethod method : values()){ if(requestMethod==method){ return method.httpMethod; } } return null; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/exception/InvalidSessionException.java ================================================ package com.redant.core.common.exception; /** * 非法session * @author houyi.wh * @date 2017-10-20 */ public class InvalidSessionException extends RuntimeException{ private static final long serialVersionUID = 1L; public InvalidSessionException(String s) { super(s); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/exception/InvocationException.java ================================================ package com.redant.core.common.exception; public class InvocationException extends Exception{ private static final long serialVersionUID = 1L; public InvocationException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/exception/ValidationException.java ================================================ package com.redant.core.common.exception; /** * ValidationException * @author houyi.wh * @date 2017-10-20 */ public class ValidationException extends RuntimeException{ private static final long serialVersionUID = 1L; public ValidationException(String s) { super(s); } public ValidationException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/html/DefaultHtmlMaker.java ================================================ package com.redant.core.common.html; import cn.hutool.core.collection.CollectionUtil; import com.redant.core.common.view.HtmlKeyHolder; import java.util.Map; /** * 默认的HtmlMaker,只处理字符串 * @author houyi.wh * @date 2017/12/1 **/ public class DefaultHtmlMaker implements HtmlMaker { @Override public String make(String htmlTemplate, Map contentMap) { String html = htmlTemplate; if(CollectionUtil.isNotEmpty(contentMap)){ for(Map.Entry entry : contentMap.entrySet()){ String key = entry.getKey(); Object val = entry.getValue(); if(val instanceof String){ html = html.replaceAll(HtmlKeyHolder.START_ESCAPE+key+HtmlKeyHolder.END,val.toString()); } } } return html; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/html/HtmlMaker.java ================================================ package com.redant.core.common.html; import java.util.Map; /** * html生成器 * @author houyi.wh * @date 2017/12/1 **/ public interface HtmlMaker { /** * 根据html模板生成html内容 * @param htmlTemplate html模板 * @param contentMap 参数 * @return html内容 */ String make(String htmlTemplate,Map contentMap); } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/html/HtmlMakerEnum.java ================================================ package com.redant.core.common.html; /** * @author houyi.wh * @date 2017/12/1 **/ public enum HtmlMakerEnum { /** * 字符串 */ STRING } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/html/HtmlMakerFactory.java ================================================ package com.redant.core.common.html; import cn.hutool.core.util.ReflectUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author houyi.wh * @date 2017/12/1 **/ public class HtmlMakerFactory { private volatile static HtmlMakerFactory factory; private Map htmlMakerMap; private final Lock lock; private HtmlMakerFactory(){ htmlMakerMap = new ConcurrentHashMap<>(); lock = new ReentrantLock(); } /** * 获取工厂实例 */ public static HtmlMakerFactory instance(){ if(factory==null){ synchronized (HtmlMakerFactory.class) { if (factory==null) { factory = new HtmlMakerFactory(); } } } return factory; } /** * 创建HtmlMaker实例 */ public HtmlMaker build(HtmlMakerEnum type,Class clazz){ if(type==null){ return null; }else{ HtmlMaker htmlMaker = htmlMakerMap.get(type); if(htmlMaker==null){ lock.lock(); try { if(!htmlMakerMap.containsKey(type)) { htmlMaker = ReflectUtil.newInstance(clazz); htmlMakerMap.putIfAbsent(type,htmlMaker); }else{ htmlMaker = htmlMakerMap.get(type); } }finally { lock.unlock(); } } return htmlMaker; } } public static void main(String[] args) { int loopTimes = 200; class Runner implements Runnable{ private Logger logger = LoggerFactory.getLogger(Runner.class); @Override public void run() { HtmlMakerFactory factory = HtmlMakerFactory.instance(); logger.info("factory={},currentThread={}",(factory!=null?factory.getClass().getName():"null"),Thread.currentThread().getName()); } } for(int i=0;i maps, List names){} * @param method 方法 * @param index 第几个输入参数 * @return 输入参数的泛型参数的实际类型集合, 如果没有实现ParameterizedType接口,即不支持泛型,所以直接返回空集合 */ @SuppressWarnings("rawtypes") public static List getMethodGenericParameterTypes(Method method, int index) { List results = new ArrayList(); Type[] genericParameterTypes = method.getGenericParameterTypes(); if (index >= genericParameterTypes.length || index < 0) { throw new RuntimeException("你输入的索引" + (index < 0 ? "不能小于0" : "超出了参数的总数")); } Type genericParameterType = genericParameterTypes[index]; if (genericParameterType instanceof ParameterizedType) { ParameterizedType aType = (ParameterizedType) genericParameterType; Type[] parameterArgTypes = aType.getActualTypeArguments(); for (Type parameterArgType : parameterArgTypes) { Class parameterArgClass = (Class) parameterArgType; results.add(parameterArgClass); } return results; } return results; } /** * 断言非空 * @param dataName 参数 * @param values 值 */ public static void checkNull(String dataName, Object... values){ if(values == null){ throw new ValidationException("["+ dataName + "] cannot be null"); } for (Object value : values) { if (value == null) { throw new ValidationException("[" + dataName + "] cannot be null"); } } } /** * 断言非空 * @param dataName 参数 * @param values 值 */ public static void checkBlank(String dataName, Object... values){ if(values == null){ throw new ValidationException("["+ dataName + "] cannot be null"); } for (Object value : values) { if (value == null || StrUtil.isBlank(value.toString())) { throw new ValidationException("[" + dataName + "] cannot be blank"); } } } /** * 获取ipV4 * @return ipV4 */ public static String getLocalIpV4(){ LinkedHashSet ipV4Set = NetUtil.localIpv4s(); return ipV4Set.isEmpty()?"":ipV4Set.toArray()[0].toString(); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/util/HtmlContentUtil.java ================================================ package com.redant.core.common.util; import com.redant.core.common.html.HtmlMaker; import com.redant.core.common.constants.CommonConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; /** * @author houyi.wh * @date 2017/12/1 **/ public class HtmlContentUtil { private final static Logger logger = LoggerFactory.getLogger(HtmlContentUtil.class); private HtmlContentUtil(){ } /** * 获取页面内容 * @param htmlMaker htmlMaker * @param htmlTemplate html模板 * @param contentMap 参数 * @return 页面内容 */ public static String getPageContent(HtmlMaker htmlMaker, String htmlTemplate, Map contentMap){ try { return htmlMaker.make(htmlTemplate,contentMap); } catch (Exception e) { logger.error("getPageContent Error,cause:",e); } return CommonConstants.SERVER_INTERNAL_ERROR_DESC; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/util/HttpRenderUtil.java ================================================ package com.redant.core.common.util; import com.alibaba.fastjson.JSONObject; import com.redant.core.common.html.DefaultHtmlMaker; import com.redant.core.common.html.HtmlMaker; import com.redant.core.common.html.HtmlMakerEnum; import com.redant.core.common.html.HtmlMakerFactory; import com.redant.core.common.view.Page404; import com.redant.core.render.RenderType; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; /** * HttpRenderUtil * * @author houyi.wh * @date 2017-10-20 */ public class HttpRenderUtil { public static final String EMPTY_CONTENT = ""; private HttpRenderUtil() { } /** * response输出 * * @param content 内容 * @param renderType 返回类型 * @return 响应对象 */ public static FullHttpResponse render(Object content, RenderType renderType) { byte[] bytes = HttpRenderUtil.getBytes(content); ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf); RenderType type = renderType != null ? renderType : RenderType.JSON; response.headers().add(HttpHeaderNames.CONTENT_TYPE, type.getContentType()); response.headers().add(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(byteBuf.readableBytes())); return response; } /** * 404NotFoundResponse * * @return 响应对象 */ public static FullHttpResponse getNotFoundResponse() { HtmlMaker htmlMaker = HtmlMakerFactory.instance().build(HtmlMakerEnum.STRING, DefaultHtmlMaker.class); String htmlTpl = Page404.HTML; String content = HtmlContentUtil.getPageContent(htmlMaker, htmlTpl, null); return render(content, RenderType.HTML); } /** * ServerErrorResponse * * @return 响应对象 */ public static FullHttpResponse getServerErrorResponse() { JSONObject object = new JSONObject(); object.put("code", 500); object.put("message", "Server Internal Error!"); return render(object, RenderType.JSON); } /** * ErrorResponse * * @param errorMessage 错误信息 * @return 响应对象 */ public static FullHttpResponse getErrorResponse(String errorMessage) { JSONObject object = new JSONObject(); object.put("code", 300); object.put("message", errorMessage); return render(object, RenderType.JSON); } /** * BlockedResponse * * @return 响应对象 */ public static FullHttpResponse getBlockedResponse() { JSONObject object = new JSONObject(); object.put("code", 1000); object.put("message", "Blocked by user defined interceptor"); return render(object, RenderType.JSON); } /** * 转换byte * * @param content 内容 * @return 响应对象 */ private static byte[] getBytes(Object content) { if (content == null) { return EMPTY_CONTENT.getBytes(CharsetUtil.UTF_8); } String data = content.toString(); data = (data == null || data.trim().length() == 0) ? EMPTY_CONTENT : data; return data.getBytes(CharsetUtil.UTF_8); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/util/HttpRequestUtil.java ================================================ package com.redant.core.common.util; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.redant.core.common.enums.ContentType; import com.redant.core.converter.PrimitiveTypeUtil; import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author houyi.wh * @date 2017/11/16 **/ public class HttpRequestUtil { /** * 获取请求参数的Map * @param request http请求 * @return 参数map */ public static Map> getParameterMap(HttpRequest request){ Map> paramMap = new HashMap<>(); HttpMethod method = request.method(); if(HttpMethod.GET.equals(method)){ String uri = request.uri(); QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, CharsetUtil.UTF_8); paramMap = queryDecoder.parameters(); }else if(HttpMethod.POST.equals(method)){ FullHttpRequest fullRequest = (FullHttpRequest) request; paramMap = getPostParamMap(fullRequest); } return paramMap; } /** * 获取post请求的参数map * 目前支持最常用的 application/json 、application/x-www-form-urlencoded 几种 POST Content-type,可自行扩展!!! */ @SuppressWarnings("unchecked") private static Map> getPostParamMap(FullHttpRequest fullRequest) { Map> paramMap = new HashMap<>(); HttpHeaders headers = fullRequest.headers(); String contentType = getContentType(headers); if(ContentType.APPLICATION_JSON.toString().equals(contentType)){ String jsonStr = fullRequest.content().toString(CharsetUtil.UTF_8); JSONObject obj = JSON.parseObject(jsonStr); for(Map.Entry item : obj.entrySet()){ String key = item.getKey(); Object value = item.getValue(); Class valueType = value.getClass(); List valueList; if(paramMap.containsKey(key)){ valueList = paramMap.get(key); }else{ valueList = new ArrayList(); } if(PrimitiveTypeUtil.isPriType(valueType)){ valueList.add(value.toString()); paramMap.put(key, valueList); }else if(valueType.isArray()){ int length = Array.getLength(value); for(int i=0; i) value; } paramMap.put(key, valueList); }else if(Map.class.isAssignableFrom(valueType)){ Map tempMap = (Map) value; for(Map.Entry entry : tempMap.entrySet()){ List tempList = new ArrayList(); tempList.add(entry.getValue()); paramMap.put(entry.getKey(), tempList); } } } }else if(ContentType.APPLICATION_FORM_URLENCODED.toString().equals(contentType)){ String jsonStr = fullRequest.content().toString(CharsetUtil.UTF_8); QueryStringDecoder queryDecoder = new QueryStringDecoder(jsonStr, false); paramMap = queryDecoder.parameters(); } return paramMap; } /** * 获取contentType * @param headers http请求头 * @return 内容类型 */ private static String getContentType(HttpHeaders headers){ String contentType = headers.get(HttpHeaderNames.CONTENT_TYPE); String[] list = contentType.split(";"); return list[0]; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/util/PropertiesUtil.java ================================================ package com.redant.core.common.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.CountDownLatch; /** * 使用单例模式,获取配置文件中的信息 * @author hwang * @date 2015-04-15 */ public class PropertiesUtil { private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesUtil.class); private static Map propertiesUtilsHolder = null; private static Map propertiesMap = null; private volatile boolean propertiesLoaded; private PropertiesUtil(){ } static{ propertiesUtilsHolder = new HashMap<>(); propertiesMap = new HashMap<>(); } /** * 是否加载完毕 */ private boolean propertiesLoaded(){ int retryTime = 0; int retryTimeout = 1000; int sleep = 500; while(!propertiesLoaded && retryTime tags = Collections.synchronizedMap(new HashMap<>()); } /** * 新增标签点 * @param tag 标签名称 */ public static void addTag(String tag){ if(tag==null || tag.trim().length()==0){ throw new RuntimeException("标签名称不可以为空"); } GHandle.tags.put(tag, System.currentTimeMillis()); } /** * 计算开始标签和结束标签之间的耗时 * @param startTag 开始标签名称 * @param endTag 结束标签名称,如果为空,以当前调用代码所在行设置默认标签名称并计算耗时 */ public static void showCost(String startTag,String endTag){ if(startTag==null || startTag.trim().length()==0){ throw new RuntimeException("开始标签名称不可以为空"); } if(endTag==null || endTag.trim().length()==0){ String tempTag= "cur_"+System.currentTimeMillis(); addTag(tempTag); endTag=tempTag; }else if(!GHandle.tags.containsKey(endTag)){ addTag(endTag); } Long start= GHandle.tags.get(startTag); Long end= GHandle.tags.get(endTag); if(start==null){ throw new RuntimeException("获取标签["+startTag+"]信息失败!"); } if(end==null){ throw new RuntimeException("获取标签["+endTag+"]信息失败!"); } long cost = end-start; LOGGER.info("from ["+startTag+"] to ["+endTag+"] cost ["+cost+"ms]"); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/util/ThreadUtil.java ================================================ package com.redant.core.common.util; /** * 线程工具类 * @author houyi.wh * @date 2017-10-20 */ public class ThreadUtil { /** * 获取当前线程名称 * @return 线程名称 */ public static String currentThreadName(){ return Thread.currentThread().getName(); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/view/HtmlKeyHolder.java ================================================ package com.redant.core.common.view; /** * @author houyi.wh * @date 2017/12/1 **/ public interface HtmlKeyHolder { /** * 未转义 */ String START_NO_ESCAPE = "#["; /** * 对[转义 */ String START_ESCAPE = "#\\["; String END = "]"; } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/view/Page404.java ================================================ package com.redant.core.common.view; import cn.hutool.core.util.StrUtil; /** * @author houyi.wh * @date 2017/12/1 **/ public final class Page404 { private Page404(){ } public static final String HTML; static{ StringBuffer sb = new StringBuffer(); sb.append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("404-Resource Not Found").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("
").append(StrUtil.CRLF) .append(StrUtil.TAB).append(StrUtil.TAB).append("Resource Not Found!").append(StrUtil.CRLF) .append(StrUtil.TAB).append("
").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(""); HTML = sb.toString(); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/view/Page500.java ================================================ package com.redant.core.common.view; import cn.hutool.core.util.StrUtil; /** * @author houyi.wh * @date 2017/12/1 **/ public final class Page500 { private Page500(){ } public static final String HTML; static{ StringBuffer sb = new StringBuffer(); sb.append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("500-Server Internal Error").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("
").append(StrUtil.CRLF) .append(StrUtil.TAB).append(StrUtil.TAB).append("Server Internal Error!").append(StrUtil.CRLF) .append(StrUtil.TAB).append("
").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(""); HTML = sb.toString(); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/view/PageError.java ================================================ package com.redant.core.common.view; import cn.hutool.core.util.StrUtil; /** * @author houyi.wh * @date 2017/12/1 **/ public final class PageError { private PageError(){ } public static final String HTML; static{ StringBuffer sb = new StringBuffer(); sb.append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("Error Occur").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("
").append(StrUtil.CRLF) .append(StrUtil.TAB).append(StrUtil.TAB).append("

").append("Error Occur,Cause:").append("

").append(StrUtil.CRLF) .append(StrUtil.TAB).append(StrUtil.TAB).append("

").append(HtmlKeyHolder.START_NO_ESCAPE+"errorMessage"+HtmlKeyHolder.END).append("

").append(StrUtil.CRLF) .append(StrUtil.TAB).append("
").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(""); HTML = sb.toString(); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/common/view/PageIndex.java ================================================ package com.redant.core.common.view; import cn.hutool.core.util.StrUtil; /** * @author houyi.wh * @date 2017/12/1 **/ public final class PageIndex { private PageIndex(){ } public static final String HTML; static{ StringBuffer sb = new StringBuffer(); sb.append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("redant").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(StrUtil.TAB).append("
").append(StrUtil.CRLF) .append(StrUtil.TAB).append(StrUtil.TAB).append("Welcome to redant!").append(StrUtil.CRLF) .append(StrUtil.TAB).append("
").append(StrUtil.CRLF) .append("").append(StrUtil.CRLF) .append(""); HTML = sb.toString(); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/context/RedantContext.java ================================================ package com.redant.core.context; import cn.hutool.core.collection.CollectionUtil; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.util.concurrent.FastThreadLocal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashSet; import java.util.Set; /** * @author houyi **/ public class RedantContext { private static final Logger LOGGER = LoggerFactory.getLogger(RedantContext.class); /** * 使用FastThreadLocal替代JDK自带的ThreadLocal以提升并发性能 */ private static final FastThreadLocal CONTEXT_HOLDER = new FastThreadLocal<>(); private HttpRequest request; private ChannelHandlerContext context; private HttpResponse response; private Set cookies; private RedantContext(){ } public RedantContext setRequest(HttpRequest request){ this.request = request; return this; } public RedantContext setContext(ChannelHandlerContext context){ this.context = context; return this; } public RedantContext setResponse(HttpResponse response){ this.response = response; return this; } public RedantContext addCookie(Cookie cookie){ if(cookie!=null){ if(CollectionUtil.isEmpty(cookies)){ cookies = new HashSet<>(); } cookies.add(cookie); } return this; } public RedantContext addCookies(Set cookieSet){ if(CollectionUtil.isNotEmpty(cookieSet)){ if(CollectionUtil.isEmpty(cookies)){ cookies = new HashSet<>(); } cookies.addAll(cookieSet); } return this; } public HttpRequest getRequest() { return request; } public ChannelHandlerContext getContext() { return context; } public HttpResponse getResponse() { return response; } public Set getCookies() { return cookies; } public static RedantContext currentContext(){ RedantContext context = CONTEXT_HOLDER.get(); if(context==null){ context = new RedantContext(); CONTEXT_HOLDER.set(context); } return context; } public static void clear(){ CONTEXT_HOLDER.remove(); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/controller/ControllerProxy.java ================================================ package com.redant.core.controller; import com.redant.core.common.enums.RequestMethod; import com.redant.core.render.RenderType; import java.lang.reflect.Method; /** * 路由请求代理,用以根据路由调用具体的controller类 * @author houyi.wh * @date 2017-10-20 */ public class ControllerProxy { private RenderType renderType; private RequestMethod requestMethod; private Object controller; private Method method; private String methodName; public RenderType getRenderType() { return renderType; } public void setRenderType(RenderType renderType) { this.renderType = renderType; } public RequestMethod getRequestMethod() { return requestMethod; } public void setRequestMethod(RequestMethod requestMethod) { this.requestMethod = requestMethod; } public Object getController() { return controller; } public void setController(Object controller) { this.controller = controller; } public Method getMethod() { return method; } public void setMethod(Method method) { this.method = method; } public String getMethodName() { return methodName; } public void setMethodName(String methodName) { this.methodName = methodName; } @Override public String toString() { return "{requestMethod:"+requestMethod+",controller:"+controller.getClass().getName()+",methodName:"+methodName+"}"; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/controller/ProxyInvocation.java ================================================ package com.redant.core.controller; import com.redant.core.common.exception.InvocationException; import com.redant.core.common.exception.ValidationException; import com.redant.core.common.util.GenericsUtil; import com.redant.core.common.util.HttpRequestUtil; import com.redant.core.context.RedantContext; import com.redant.core.converter.PrimitiveConverter; import com.redant.core.converter.PrimitiveTypeUtil; import com.redant.core.controller.annotation.Param; import net.sf.cglib.reflect.FastClass; import net.sf.cglib.reflect.FastMethod; import org.apache.commons.beanutils.BeanUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; /** * 封装了ControllerProxy的调用过程 * @author houyi.wh * @date 2017-10-20 */ public class ProxyInvocation { private static Invocation invocation = new Invocation(); public static Object invoke(ControllerProxy proxy) throws Exception{ Object controller = proxy.getController(); Method method = proxy.getMethod(); String methodName = proxy.getMethodName(); return invocation.invoke(controller,method,methodName); } private static class Invocation { private static Logger logger = LoggerFactory.getLogger(Invocation.class); private final int KEY_VALUE_SIZE = 2; public Invocation(){ } /** * 获得方法调用的参数 * @param method 方法 * @param parameterTypes 方法参数类型 * @return 参数 * @throws Exception 参数异常 */ private Object[] getParameters(Method method,Class[] parameterTypes) throws Exception { //用于存放调用参数的对象数组 Object[] parameters = new Object[parameterTypes.length]; //获得所调用方法的参数的Annotation数组 Annotation[][] annotationArray = method.getParameterAnnotations(); //获取参数列表 Map> paramMap = HttpRequestUtil.getParameterMap(RedantContext.currentContext().getRequest()); //构造调用所需要的参数数组 for (int i = 0; i < parameterTypes.length; i++) { Object parameter; Class type = parameterTypes[i]; Annotation[] annotation = annotationArray[i]; // 如果该参数没有 Param 注解 if (annotation == null || annotation.length == 0) { // 如果该参数类型是基础类型,则需要加 Param 注解 if(PrimitiveTypeUtil.isPriType(type)){ logger.warn("Must specify a @Param annotation for primitive type parameter in method={}", method.getName()); continue; } // 封装对象类型的parameter parameter = type.newInstance(); BeanUtils.populate(parameter,paramMap); parameters[i] = parameter; }else{ Param param = (Param) annotation[0]; try{ // 生成当前的调用参数 parameter = parseParameter(paramMap, type, param, method, i); if(param.notNull()){ GenericsUtil.checkNull(param.key(), parameter); } if(param.notBlank()){ GenericsUtil.checkBlank(param.key(), parameter); } parameters[i] = parameter; }catch(Exception e){ logger.error("param ["+param.key()+"] is invalid,cause:"+e.getMessage()); throw new IllegalArgumentException("参数 "+param.key()+" 不合法:"+e.getMessage()); } } } return parameters; } /** * GET 参数解析 */ @SuppressWarnings({ "rawtypes", "unchecked" }) private Object parseParameter(Map> paramMap, Class type, Param param, Method method, int index) throws InstantiationException, IllegalAccessException{ Object value = null; String key = param.key(); String defaultValue= param.defaultValue(); if(key.length() > 0){ // 如果参数是map类型 if(Map.class.isAssignableFrom(type)){ if(index > 0){ throw new ValidationException("Must have only one Map type parameter"); } List types = GenericsUtil.getMethodGenericParameterTypes(method, index); if(types.size() == KEY_VALUE_SIZE && (types.get(0) != String.class || types.get(1) != String.class)){ throw new ValidationException("Map type parameter must both be String, Occurring Point: " + method.toGenericString()); } Map valueMap = new HashMap(paramMap.size()); for(Map.Entry> entry : paramMap.entrySet()){ List valueList = entry.getValue(); valueMap.put(entry.getKey(), valueList.get(0)); } value = valueMap; }else{ List params = paramMap.get(key); if(params != null){ // 基础类型 if(PrimitiveTypeUtil.isPriType(type)){ value = PrimitiveConverter.getInstance().convert(params.get(0), type); // 数组 }else if(type.isArray()){ String[] strArray = params.toArray(new String[]{}); value = PrimitiveConverter.getInstance().convert(strArray, type); // List }else if(List.class.isAssignableFrom(type)){ List list; List types = GenericsUtil.getMethodGenericParameterTypes(method, index); Class listType = types.size() == 1?types.get(0):String.class; if(List.class == type){ list = new ArrayList(); }else{ list = (List) type.newInstance(); } for (String param1 : params) { if (param1.length() > 0) { list.add(PrimitiveConverter.getInstance().convert(param1, listType)); } } value = list; } }else{ if(PrimitiveTypeUtil.isPriType(type)){ value = PrimitiveConverter.getInstance().convert(defaultValue, type); } } } } return value; } /** * 返回调用异常 * @param msg 消息 * @param cause 异常 * @return 调用异常 */ private InvocationException getInvokeException(String msg, Throwable cause){ return new InvocationException(msg,cause); } //================================== /** * 执行方法的调用 * @param controller 控制器 * @param method 方法 * @param methodName 方法名 * @return 渲染结果 * @throws Exception 异常 */ Object invoke(Object controller, Method method, String methodName) throws Exception { if (method == null) { throw new NoSuchMethodException("Can not find specified method: " + methodName); } Class clazz = controller.getClass(); Class[] parameterTypes = method.getParameterTypes(); Object[] parameters = null; Object result; try { parameters = getParameters(method,parameterTypes); // 使用 CGLib 执行反射调用 FastClass fastClass = FastClass.create(clazz); FastMethod fastMethod = fastClass.getMethod(methodName, parameterTypes); // 调用,并得到调用结果 result = fastMethod.invoke(controller, parameters); } catch(InvocationTargetException e){ String msg = "调用出错,请求类["+controller.getClass().getName()+"],方法名[" + method.getName() + "],参数[" + Arrays.toString(parameters)+"]"; throw getInvokeException(msg, e); } return result; } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/controller/annotation/Controller.java ================================================ package com.redant.core.controller.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Controller { /** * 请求uri * @return url */ String path() default ""; } ================================================ FILE: redant-core/src/main/java/com/redant/core/controller/annotation/Mapping.java ================================================ package com.redant.core.controller.annotation; import com.redant.core.common.enums.RequestMethod; import com.redant.core.render.RenderType; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Mapping { /** * 请求方法类型 * @return 方法类型 */ RequestMethod requestMethod() default RequestMethod.GET; /** * 请求的uri * @return url */ String path() default ""; /** * 返回类型 * @return 返回类型 */ RenderType renderType() default RenderType.JSON; } ================================================ FILE: redant-core/src/main/java/com/redant/core/controller/annotation/Param.java ================================================ package com.redant.core.controller.annotation; import java.lang.annotation.*; @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(value = RetentionPolicy.RUNTIME) public @interface Param { /** * 将使用什么样的键值读取对象, * 对于field,就是他名字 * 对于method的parameter,需要指明 * @return 参数的key */ String key() default ""; /** * 提供设置缺省值 * @return 提供设置缺省值 */ String defaultValue() default ""; /** * 是否校验参数为空 * @return true:校验参数 false:不校验参数 */ boolean notNull() default false; /** * 是否校验参数为空 * @return true:校验参数 false:不校验参数 */ boolean notBlank() default false; } ================================================ FILE: redant-core/src/main/java/com/redant/core/controller/context/ControllerContext.java ================================================ package com.redant.core.controller.context; import com.redant.core.controller.ControllerProxy; import io.netty.handler.codec.http.HttpMethod; /** * @author houyi.wh * @date 2019-01-15 */ public interface ControllerContext { /** * 添加Controller代理 * @param path 请求路径 * @param proxy 代理 */ void addProxy(String path,ControllerProxy proxy); /** * 获取Controller代理 * @param method 请求方法类型 * @param uri 请求url * @return 代理 */ ControllerProxy getProxy(HttpMethod method, String uri); } ================================================ FILE: redant-core/src/main/java/com/redant/core/controller/context/DefaultControllerContext.java ================================================ package com.redant.core.controller.context; import com.redant.core.controller.ControllerProxy; import com.redant.core.render.RenderType; import com.redant.core.router.RouteResult; import com.redant.core.router.context.DefaultRouterContext; import com.redant.core.router.context.RouterContext; import io.netty.handler.codec.http.HttpMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author houyi.wh * @date 2019-01-15 */ public class DefaultControllerContext implements ControllerContext { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultControllerContext.class); /** * 保存所有的RouterController的代理类 */ private static Map proxyMap; /** * 路由上下文 */ private static RouterContext routerContext; private static final class DefaultControllerContextHolder { private static DefaultControllerContext context = new DefaultControllerContext(); } private DefaultControllerContext() { routerContext = DefaultRouterContext.getInstance(); proxyMap = new ConcurrentHashMap<>(); } public static ControllerContext getInstance() { return DefaultControllerContextHolder.context; } @Override public void addProxy(String path, ControllerProxy proxy) { proxyMap.putIfAbsent(path, proxy); } @Override public ControllerProxy getProxy(HttpMethod method, String uri) { RouteResult routeResult = routerContext.getRouteResult(method, uri); if (routeResult == null) { return null; } // 获取代理 ControllerProxy controllerProxy = proxyMap.get(routeResult.decodedPath()); LOGGER.debug("\n========================= getControllerProxy =========================" + "\nmethod={}, uri={}" + "\ncontrollerProxy={}" + "\n========================= getControllerProxy =========================", method, uri, controllerProxy); return controllerProxy; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/converter/AbstractConverter.java ================================================ package com.redant.core.converter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Method; /** * 转换器的抽象实现类 * @author houyi.wh * @date 2017-10-20 */ public abstract class AbstractConverter implements Converter { protected final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 实现了TypeConverter中的相同方法 */ @Override public Object convert(Object source, Class toType, Object... parmas) { /** * 如果对象本身已经是所指定的类型则不进行转换直接返回 * 如果对象能够被复制,则返回复制后的对象 */ if (source != null && toType.isInstance(source)) { if (source instanceof Cloneable) { if(source.getClass().isArray() && source.getClass().getComponentType() == String.class){ // 字符串数组虽然是Cloneable的子类,但并没有clone方法 return source; } try { Method m = source.getClass().getDeclaredMethod("clone", new Class[0]); m.setAccessible(true); return m.invoke(source, new Object[0]); } catch (Exception e) { logger.debug("Can not clone object " + source, e); } } return source; } /** * 如果需要转换,且value为String类型并且长度为0,则按照null值进行处理 */ if (source != null && source instanceof String && ((String)source).length() == 0) { source = null; } /** * 不对Annotation, Interface, * Enummeration类型进行转换。 */ if (toType == null || (source == null && !toType.isPrimitive()) || toType.isInterface() || toType.isAnnotation() || toType.isEnum()) { return null; } return doConvertValue(source, toType); } /** * 需要被子类所实现的转换方法 * @param source 需要进行类型转换的对象 * @param toType 需要被转换成的类型 * @param params 转值时需要提供的可选参数 * @return 转换后所生成的对象,如果不能够进行转换则返回null */ protected abstract Object doConvertValue(Object source, Class toType, Object... params); } ================================================ FILE: redant-core/src/main/java/com/redant/core/converter/Converter.java ================================================ package com.redant.core.converter; /** * 类型转换器所需要实现的总接口。TypeConverter中有唯一的一个方法,实现类请特别注意方法所需要返回的值。 * @author houyi.wh * @date 2017-10-20 */ public interface Converter { /** * 类型转换 * @param source 需要被转换的值 * @param toType 需要被转换成的类型 * @param params 转值时需要提供的可选参数 * @return 经转换过的类型,如果实现类没有能力进行所指定的类型转换,应返回null */ Object convert(Object source, Class toType, Object... params); } ================================================ FILE: redant-core/src/main/java/com/redant/core/converter/PrimitiveConverter.java ================================================ package com.redant.core.converter; import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; /** * 基本类型和基本类型数组转换器 * 能够转换的基本类型仅限于com.sitechasia.webx.core.utils.populator.PrimitiveType中所定义的类型 * * @see # com.sitechasia.webx.core.utils.populator.PrimitiveTypeUtil * @author houyi.wh * @date 2017-10-20 */ public final class PrimitiveConverter extends AbstractConverter { private static final PrimitiveConverter CONVERTER; static { CONVERTER = new PrimitiveConverter(); } private static final char[] DIGITAL_CHAR = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; /** * 对基本类型进行类型转换 */ @Override @SuppressWarnings("unused") protected Object doConvertValue(Object source, Class toType, Object... params) { /** * 如果是基础类型,则直接返回 */ if (source != null && (!PrimitiveTypeUtil.isPriType(source.getClass()) || !PrimitiveTypeUtil.isPriType(toType))) { return null; } /** * 如果都是数组类型,则构造数组 */ if (source != null && source.getClass().isArray() && toType.isArray()) { Object result; Class componentType = toType.getComponentType(); result = Array.newInstance(componentType, Array.getLength(source)); for (int i = 0; i < Array.getLength(source); i++) { Array.set(result, i, convert(Array.get(source, i),componentType, params)); } return result; } return doConvert(source, toType); } private boolean isNumberString(String stringValue) { if (stringValue == null || stringValue.length() == 0){ return false; } OUTER: for (char charInString : stringValue.toCharArray()) { for (char digit : DIGITAL_CHAR) { if (charInString == digit){ continue OUTER; } } return false; } return true; } private boolean booleanValue(Object source) { if (source == null){ return false; } Class c = source.getClass(); if (c == Boolean.class){ return (Boolean) source; } if (c == String.class) { String stringValue = (String) source; return !(stringValue.length() == 0 || stringValue.equals("0") || stringValue.equalsIgnoreCase("false") || stringValue.equalsIgnoreCase("no") || stringValue.equalsIgnoreCase("f") || stringValue .equalsIgnoreCase("n")); } if (c == Character.class){ return ((Character) source).charValue() != 0; } if (source instanceof Number){ return ((Number) source).doubleValue() != 0; } return true; } private long longValue(Object source) throws NumberFormatException { if (source == null){ return 0L; } Class c = source.getClass(); if (c.getSuperclass() == Number.class){ return ((Number) source).longValue(); } if (c == Boolean.class){ return ((Boolean) source).booleanValue() ? 1 : 0; } if (c == Character.class){ return ((Character) source).charValue(); } String s = stringValue(source, true); return (s.length() == 0) ? 0L : Long.parseLong(s); } private double doubleValue(Object source) throws NumberFormatException { if (source == null){ return 0.0; } Class c = source.getClass(); if (c.getSuperclass() == Number.class){ return ((Number) source).doubleValue(); } if (c == Boolean.class){ return ((Boolean) source).booleanValue() ? 1 : 0; } if (c == Character.class){ return ((Character) source).charValue(); } String s = stringValue(source, true); return (s.length() == 0) ? 0.0 : Double.parseDouble(s); } private BigInteger bigIntValue(Object source) throws NumberFormatException { if (source == null){ return BigInteger.valueOf(0L); } Class c = source.getClass(); if (c == BigInteger.class){ return (BigInteger) source; } if (c == BigDecimal.class){ return ((BigDecimal) source).toBigInteger(); } if (c.getSuperclass() == Number.class){ return BigInteger.valueOf(((Number) source).longValue()); } if (c == Boolean.class){ return BigInteger.valueOf(((Boolean) source).booleanValue() ? 1 : 0); } if (c == Character.class){ return BigInteger.valueOf(((Character) source).charValue()); } String s = stringValue(source, true); return (s.length() == 0) ? BigInteger.valueOf(0L) : new BigInteger(s); } private BigDecimal bigDecValue(Object source) throws NumberFormatException { if (source == null){ return BigDecimal.valueOf(0L); } Class c = source.getClass(); if (c == BigDecimal.class){ return (BigDecimal) source; } if (c == BigInteger.class){ return new BigDecimal((BigInteger) source); } if (c.getSuperclass() == Number.class){ return new BigDecimal(((Number) source).doubleValue()); } if (c == Boolean.class){ return BigDecimal.valueOf(((Boolean) source).booleanValue() ? 1 : 0); } if (c == Character.class){ return BigDecimal.valueOf(((Character) source).charValue()); } String s = stringValue(source, true); return (s.length() == 0) ? BigDecimal.valueOf(0L) : new BigDecimal(s); } private String stringValue(Object source, boolean trim) { String result; if (source == null) { result = null; } else { result = source.toString(); if (trim) { result = result.trim(); } } return result; } private char charValue(Object source) { char result; if (source.getClass() == String.class && ((String) source).length() > 0 && !isNumberString((String) source)){ result = ((String) source).charAt(0); }else{ result = (char) longValue(source); } return result; } private String stringValue(Object source) { return stringValue(source, false); } private Object doConvert(Object source, Class toType) { Object result = null; if (source != null) { if ((toType == Integer.class) || (toType == Integer.TYPE)) { result = (int) longValue(source); }else if ((toType == Double.class) || (toType == Double.TYPE)){ result = doubleValue(source); }else if ((toType == Boolean.class) || (toType == Boolean.TYPE)){ result = booleanValue(source); }else if ((toType == Byte.class) || (toType == Byte.TYPE)){ result = (byte) longValue(source); }else if ((toType == Character.class) || (toType == Character.TYPE)){ result = charValue(source); }else if ((toType == Short.class) || (toType == Short.TYPE)){ result = (short) longValue(source); }else if ((toType == Long.class) || (toType == Long.TYPE)){ result = longValue(source); }else if ((toType == Float.class) || (toType == Float.TYPE)){ result = (float) doubleValue(source); }else if (toType == BigInteger.class){ result = bigIntValue(source); }else if (toType == BigDecimal.class){ result = bigDecValue(source); }else if (toType == String.class){ result = stringValue(source); } } else { if (toType.isPrimitive()) { result = PrimitiveTypeUtil.getPriDefaultValue(toType); } } return result; } public static PrimitiveConverter getInstance(){ return CONVERTER; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/converter/PrimitiveTypeUtil.java ================================================ package com.redant.core.converter; import java.math.BigDecimal; import java.math.BigInteger; import java.util.HashMap; import java.util.Map; /** * 定义了基本类型的工具类, * 可以方便的判断一个Class对象是否属于基本类型或基本类型的数组。 * 本工具类所包含的基本类型判断包括如下一些内容: * * String * boolean * byte * short * int * long * float * double * char * Boolean * Byte * Short * Integer * Long * Float * Double * Character * BigInteger * BigDecimal * * @author yunfeng.cheng * @date 2016-08-12 */ public class PrimitiveTypeUtil { /** * 私有的构造函数防止用户进行实例化。 */ private PrimitiveTypeUtil() {} /** 基本类型 **/ private static final Class[] PRI_TYPE = { String.class, boolean.class, byte.class, short.class, int.class, long.class, float.class, double.class, char.class, Boolean.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Character.class, BigInteger.class, BigDecimal.class }; /** 基本数组类型 **/ private static final Class[] PRI_ARRAY_TYPE = { String[].class, boolean[].class, byte[].class, short[].class, int[].class, long[].class, float[].class, double[].class, char[].class, Boolean[].class, Byte[].class, Short[].class, Integer[].class, Long[].class, Float[].class, Double[].class, Character[].class, BigInteger[].class, BigDecimal[].class }; /** * 基本类型默认值 */ private static final Map, Object> primitiveDefaults = new HashMap, Object>(9); static { primitiveDefaults.put(boolean.class, false); primitiveDefaults.put(byte.class, (byte)0); primitiveDefaults.put(short.class, (short)0); primitiveDefaults.put(char.class, (char)0); primitiveDefaults.put(int.class, 0); primitiveDefaults.put(long.class, 0L); primitiveDefaults.put(float.class, 0.0f); primitiveDefaults.put(double.class, 0.0); } /** * 判断是否为基本类型 * @param cls 需要进行判断的Class对象 * @return 是否为基本类型 */ public static boolean isPriType(Class cls) { for (Class priType : PRI_TYPE) { if (cls == priType){ return true; } } return false; } /** * 判断是否为基本类型数组 * @param cls 需要进行判断的Class对象 * @return 是否为基本类型数组 */ public static boolean isPriArrayType(Class cls) { for (Class priType : PRI_ARRAY_TYPE) { if (cls == priType){ return true; } } return false; } /** * 获得基本类型的默认值 * @param type 基本类型的Class * @return 基本类型的默认值 */ public static Object getPriDefaultValue(Class type) { return primitiveDefaults.get(type); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/cookie/CookieManager.java ================================================ package com.redant.core.cookie; import io.netty.handler.codec.http.cookie.Cookie; import java.util.Map; import java.util.Set; /** * cookie管理 * @author houyi.wh * @date 2019-01-16 */ public interface CookieManager { /** * 获取所有的cookie * @return cookie集合 */ Set getCookies(); /** * 获取所有的cookie,并返回一个Map * @return cookie的map */ Map getCookieMap(); /** * 根据名称获取cookie * @param name 名称 * @return cookie */ Cookie getCookie(String name); /** * 根据名称获取cookie的值 * @param name 名称 * @return cookie的值 */ String getCookieValue(String name); /** * 设置cookie到响应结果中 * @param cookie cookie */ void setCookie(Cookie cookie); /** * 获取所有的cookie后,全部设置到响应结果中 */ void setCookies(); /** * 添加一个cookie * @param name cookie的名称 * @param value cookie的值 */ void addCookie(String name,String value); /** * 添加一个cookie * @param name cookie的名称 * @param value cookie的值 * @param domain cookie的作用域 */ void addCookie(String name,String value,String domain); /** * 添加一个cookie * @param name cookie的名称 * @param value cookie的值 * @param maxAge cookie的有效期 */ void addCookie(String name,String value,long maxAge); /** * 添加一个cookie * @param name cookie的名称 * @param value cookie的值 * @param domain cookie的作用域 * @param maxAge cookie的有效期 */ void addCookie(String name,String value,String domain,long maxAge); /** * 删除一个cookie * @param name cookie的名称 * @return 操作结果 */ boolean deleteCookie(String name); } ================================================ FILE: redant-core/src/main/java/com/redant/core/cookie/DefaultCookieManager.java ================================================ package com.redant.core.cookie; import cn.hutool.core.util.StrUtil; import com.redant.core.context.RedantContext; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.DefaultCookie; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Cookie管理器 * * @author houyi.wh * @date 2019-01-16 */ public class DefaultCookieManager implements CookieManager { private static final class DefaultCookieManagerHolder { private static DefaultCookieManager cookieManager = new DefaultCookieManager(); } private DefaultCookieManager() { } public static CookieManager getInstance() { return DefaultCookieManagerHolder.cookieManager; } @Override public Set getCookies() { HttpRequest request = RedantContext.currentContext().getRequest(); Set cookies = new HashSet<>(); if (request != null) { String value = request.headers().get(HttpHeaderNames.COOKIE); if (value != null) { cookies = ServerCookieDecoder.STRICT.decode(value); } } return cookies; } @Override public Map getCookieMap() { Map cookieMap = new HashMap<>(); Set cookies = getCookies(); if (null != cookies && !cookies.isEmpty()) { for (Cookie cookie : cookies) { cookieMap.put(cookie.name(), cookie); } } return cookieMap; } @Override public Cookie getCookie(String name) { Map cookieMap = getCookieMap(); return cookieMap.getOrDefault(name, null); } @Override public String getCookieValue(String name) { Cookie cookie = getCookie(name); return cookie == null ? null : cookie.value(); } @Override public void setCookie(Cookie cookie) { RedantContext.currentContext().addCookie(cookie); } @Override public void setCookies() { Set cookies = getCookies(); if (!cookies.isEmpty()) { for (Cookie cookie : cookies) { setCookie(cookie); } } } @Override public void addCookie(String name, String value) { addCookie(name, value, null); } @Override public void addCookie(String name, String value, String domain) { addCookie(name, value, domain, 0); } @Override public void addCookie(String name, String value, long maxAge) { addCookie(name, value, null, maxAge); } @Override public void addCookie(String name, String value, String domain, long maxAge) { if (StrUtil.isNotBlank(name) && StrUtil.isNotBlank(value)) { Cookie cookie = new DefaultCookie(name, value); cookie.setPath("/"); if (domain != null && domain.trim().length() > 0) { cookie.setDomain(domain); } if (maxAge > 0) { cookie.setMaxAge(maxAge); } setCookie(cookie); } } @Override public boolean deleteCookie(String name) { Cookie cookie = getCookie(name); if (cookie != null) { cookie.setMaxAge(0); cookie.setPath("/"); setCookie(cookie); return true; } return false; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/executor/AbstractExecutor.java ================================================ package com.redant.core.executor; import com.redant.core.common.constants.CommonConstants; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @author houyi */ public abstract class AbstractExecutor implements Executor { private final static Logger LOGGER = LoggerFactory.getLogger(AbstractExecutor.class); private java.util.concurrent.Executor eventExecutor; public AbstractExecutor() { this(null); } public AbstractExecutor(java.util.concurrent.Executor eventExecutor) { this.eventExecutor = eventExecutor == null ? EventExecutorHolder.eventExecutor : eventExecutor; } @Override public T execute(Object... request) { return doExecute(request); } @Override public Future asyncExecute(Promise promise, Object... request) { if (promise == null) { throw new IllegalArgumentException("promise should not be null"); } // 异步执行 eventExecutor.execute(new Runnable() { @Override public void run() { try { T response = doExecute(request); promise.setSuccess(response); } catch (Exception e) { promise.setFailure(e); } } }); // 返回promise return promise; } /** * 执行具体的方法 * * @param request 请求对象 * @return 返回结果 */ public abstract T doExecute(Object... request); private static final class EventExecutorHolder { private static java.util.concurrent.Executor eventExecutor = new ThreadPoolExecutor( CommonConstants.EVENT_EXECUTOR_POOL_CORE_SIZE, CommonConstants.EVENT_EXECUTOR_POOL_MAX_SIZE, CommonConstants.EVENT_EXECUTOR_POOL_KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, new ArrayBlockingQueue<>(CommonConstants.EVENT_EXECUTOR_POOL_BLOCKING_QUEUE_SIZE)); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/executor/Executor.java ================================================ package com.redant.core.executor; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; /** * @author houyi */ public interface Executor { /** * 同步执行任务获得结果 * @param request 请求对象 * @return 结果 */ T execute(Object... request); /** * 异步执行任务获得 Future 结果 * @param promise 异步结果包装类 * @param request 请求对象 * @return 异步结果 */ Future asyncExecute(Promise promise, Object... request); } ================================================ FILE: redant-core/src/main/java/com/redant/core/executor/HttpResponseExecutor.java ================================================ package com.redant.core.executor; import cn.hutool.core.collection.CollectionUtil; import com.redant.core.common.exception.InvocationException; import com.redant.core.common.util.HttpRenderUtil; import com.redant.core.common.util.HttpRequestUtil; import com.redant.core.context.RedantContext; import com.redant.core.controller.ControllerProxy; import com.redant.core.controller.ProxyInvocation; import com.redant.core.controller.context.ControllerContext; import com.redant.core.controller.context.DefaultControllerContext; import com.redant.core.interceptor.InterceptorHandler; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.ServerCookieEncoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Map; import java.util.Set; /** * @author houyi */ public class HttpResponseExecutor extends AbstractExecutor { private final static Logger LOGGER = LoggerFactory.getLogger(HttpResponseExecutor.class); private static ControllerContext controllerContext = DefaultControllerContext.getInstance(); public static HttpResponseExecutor getInstance() { return HttpResponseExecutorHolder.executor; } private HttpResponseExecutor() { } @Override public HttpResponse doExecute(Object... request) { HttpRequest httpRequest = (HttpRequest) request[0]; // 暂存请求对象 // 将request存储到ThreadLocal中去,便于后期在其他地方获取并使用 RedantContext.currentContext().setRequest(httpRequest); HttpResponse response = null; try { // 获取参数列表 Map> paramMap = HttpRequestUtil.getParameterMap(httpRequest); // 处理拦截器的前置方法 if (!InterceptorHandler.preHandle(paramMap)) { // 先从RedantContext中获取response,检查用户是否设置了response response = RedantContext.currentContext().getResponse(); // 若用户没有设置就返回一个默认的 if (response == null) { response = HttpRenderUtil.getBlockedResponse(); } } else { // 处理业务逻辑 response = invoke(httpRequest); // 处理拦截器的后置方法 InterceptorHandler.postHandle(paramMap); } } catch (Exception e) { LOGGER.error("Server Internal Error,cause:", e); response = getErrorResponse(e); } finally { // 构造响应头 buildHeaders(response, RedantContext.currentContext()); // 释放ThreadLocal对象 RedantContext.clear(); } return response; } private HttpResponse invoke(HttpRequest request) throws Exception { // 根据路由获得具体的ControllerProxy ControllerProxy controllerProxy = controllerContext.getProxy(request.method(), request.uri()); if (controllerProxy == null) { return HttpRenderUtil.getNotFoundResponse(); } // 调用用户自定义的Controller,获得结果 Object result = ProxyInvocation.invoke(controllerProxy); return HttpRenderUtil.render(result, controllerProxy.getRenderType()); } private HttpResponse getErrorResponse(Exception e) { HttpResponse response; if (e instanceof IllegalArgumentException || e instanceof InvocationException) { response = HttpRenderUtil.getErrorResponse(e.getMessage()); } else { response = HttpRenderUtil.getServerErrorResponse(); } return response; } private void buildHeaders(HttpResponse response, RedantContext redantContext) { if (response == null) { return; } FullHttpResponse fullHttpResponse = (FullHttpResponse) response; fullHttpResponse.headers().add(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(fullHttpResponse.content().readableBytes())); // 写cookie Set cookies = redantContext.getCookies(); if (CollectionUtil.isNotEmpty(cookies)) { for (Cookie cookie : cookies) { fullHttpResponse.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie)); } } } private static final class HttpResponseExecutorHolder { private static HttpResponseExecutor executor = new HttpResponseExecutor(); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/handler/ControllerDispatcher.java ================================================ package com.redant.core.handler; import com.redant.core.common.constants.CommonConstants; import com.redant.core.executor.Executor; import com.redant.core.executor.HttpResponseExecutor; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.util.concurrent.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 请求分发控制器 * * @author houyi.wh * @date 2017-10-20 */ public class ControllerDispatcher extends SimpleChannelInboundHandler { private final static Logger LOGGER = LoggerFactory.getLogger(ControllerDispatcher.class); private static Executor executor = HttpResponseExecutor.getInstance(); @Override public void channelRead0(ChannelHandlerContext ctx, HttpRequest request) { if (CommonConstants.ASYNC_EXECUTE_EVENT) { // 当前通道所持有的线程池 EventExecutor channelExecutor = ctx.executor(); // 创建一个异步结果,并指定该promise Promise promise = new DefaultPromise<>(channelExecutor); // 在自定义线程池中执行业务逻辑,并返回一个异步结果 Future future = executor.asyncExecute(promise, request); future.addListener(new GenericFutureListener>() { @Override public void operationComplete(Future f) throws Exception { // 异步结果执行成功后,取出结果 HttpResponse response = f.get(); // 通过IO线程写响应结果 ctx.channel().writeAndFlush(response); } }); } else { // 同步执行 HttpResponse response = executor.execute(request); ctx.channel().writeAndFlush(response); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ctx.close(); LOGGER.error("ctx close,cause:", cause); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/handler/ssl/SslContextHelper.java ================================================ package com.redant.core.handler.ssl; import cn.hutool.crypto.SecureUtil; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLException; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author houyi.wh * @date 2017/11/19 **/ public class SslContextHelper { private static final String KEY_STORE_JKS = "JKS"; private static final String ALGORITHM = "SunX509"; private static Map contents = new ConcurrentHashMap(); private static String getKey(String keyPath,String keyPassword){ if(keyPath==null || keyPath.trim().length()==0 || keyPassword==null || keyPassword.trim().length()==0){ return null; } String keyStr = keyPath+"&"+keyPassword; return SecureUtil.md5(keyStr); } /** * 获取SslContext * @param keyPath * @param keyPassword * @return */ public static SslContext getSslContext(String keyPath,String keyPassword){ if(keyPath==null || keyPath.trim().length()==0 || keyPassword==null || keyPassword.trim().length()==0){ return null; } SslContext sslContext = null; InputStream is = null; try { String key = getKey(keyPath,keyPassword); sslContext = contents.get(key); if(sslContext!=null){ return sslContext; } KeyStore keyStore = KeyStore.getInstance(KEY_STORE_JKS); is = new FileInputStream(keyPath); keyStore.load(is, keyPassword.toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(ALGORITHM); keyManagerFactory.init(keyStore,keyPassword.toCharArray()); sslContext = SslContextBuilder.forServer(keyManagerFactory).build(); if(sslContext!=null){ contents.put(key,sslContext); } } catch (KeyStoreException | UnrecoverableKeyException | NoSuchAlgorithmException | CertificateException | IOException e) { e.printStackTrace(); } finally { if(is!=null){ try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } return sslContext; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/init/InitExecutor.java ================================================ package com.redant.core.init; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.ClassScaner; import com.redant.core.common.constants.CommonConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * @author houyi.wh * @date 2019-01-14 */ public final class InitExecutor { private static final Logger LOGGER = LoggerFactory.getLogger(InitExecutor.class); private static AtomicBoolean initialized = new AtomicBoolean(false); public static void init() { if (!initialized.compareAndSet(false, true)) { return; } try { Set> classSet = ClassScaner.scanPackageBySuper(CommonConstants.BEAN_SCAN_PACKAGE,InitFunc.class); if (CollectionUtil.isNotEmpty(classSet)) { List initList = new ArrayList<>(); for (Class cls : classSet) { // 如果cls是InitFunc的实现类 if(!cls.isInterface() && InitFunc.class.isAssignableFrom(cls)){ Constructor constructor = cls.getDeclaredConstructor(); constructor.setAccessible(true); InitFunc initFunc = (InitFunc)constructor.newInstance(); LOGGER.info("[InitExecutor] found InitFunc: " + initFunc.getClass().getCanonicalName()); insertSorted(initList, initFunc); } } for (OrderWrapper w : initList) { w.func.init(); LOGGER.info("[InitExecutor] initialized: {} with order={}", w.func.getClass().getCanonicalName(), w.order); } } } catch (Exception ex) { LOGGER.warn("[InitExecutor] init failed", ex); ex.printStackTrace(); } catch (Error error) { LOGGER.warn("[InitExecutor] init failed with fatal error", error); error.printStackTrace(); throw error; } } private static void insertSorted(List list, InitFunc func) { int order = resolveOrder(func); int idx = 0; for (; idx < list.size(); idx++) { // 将func插入到order值比他大的第一个func前面 if (list.get(idx).getOrder() > order) { break; } } list.add(idx, new OrderWrapper(order, func)); } private static int resolveOrder(InitFunc func) { if (!func.getClass().isAnnotationPresent(InitOrder.class)) { return InitOrder.LOWEST_PRECEDENCE; } else { return func.getClass().getAnnotation(InitOrder.class).value(); } } private InitExecutor() {} private static class OrderWrapper { private final int order; private final InitFunc func; OrderWrapper(int order, InitFunc func) { this.order = order; this.func = func; } int getOrder() { return order; } InitFunc getFunc() { return func; } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/init/InitFunc.java ================================================ package com.redant.core.init; /** * 初始化接口 * @author houyi.wh * @date 2019-01-14 */ public interface InitFunc { /** * 初始化方法 */ void init(); } ================================================ FILE: redant-core/src/main/java/com/redant/core/init/InitOrder.java ================================================ package com.redant.core.init; import java.lang.annotation.*; /** * 初始化器的排序规则,升序排序 * @author houyi.wh * @date 2019-01-14 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented public @interface InitOrder { /** * 最低优先级 */ int LOWEST_PRECEDENCE = Integer.MAX_VALUE; /** * 最高优先级 */ int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; /** * The order value. Lowest precedence by default. * * @return the order value */ int value() default LOWEST_PRECEDENCE; } ================================================ FILE: redant-core/src/main/java/com/redant/core/interceptor/Interceptor.java ================================================ package com.redant.core.interceptor; import java.util.List; import java.util.Map; /** * @author houyi **/ public abstract class Interceptor { /** * 拦截器的前置处理方法 */ public boolean preHandle(Map> paramMap){ return true; } /** * 拦截器的后置处理方法 */ public abstract void postHandle(Map> paramMap); } ================================================ FILE: redant-core/src/main/java/com/redant/core/interceptor/InterceptorBuilder.java ================================================ package com.redant.core.interceptor; import java.util.List; /** * 用户可以通过该接口自行定义需要生效哪些拦截器 * @author houyi **/ public interface InterceptorBuilder { /** * 构造拦截器列表 * @return 列表 */ List build(); } ================================================ FILE: redant-core/src/main/java/com/redant/core/interceptor/InterceptorHandler.java ================================================ package com.redant.core.interceptor; import cn.hutool.core.collection.CollectionUtil; import java.util.*; /** * @author houyi.wh * @date 2017/11/15 **/ public class InterceptorHandler { public static boolean preHandle(Map> paramMap){ List interceptors = InterceptorProvider.getInterceptors(); if(CollectionUtil.isEmpty(interceptors)){ return true; } for(Interceptor interceptor : interceptors){ if(!interceptor.preHandle(paramMap)){ return false; } } return true; } public static void postHandle(Map> paramMap){ List interceptors = InterceptorProvider.getInterceptors(); if(CollectionUtil.isEmpty(interceptors)){ return; } for(Interceptor interceptor : interceptors){ interceptor.postHandle(paramMap); } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/interceptor/InterceptorProvider.java ================================================ package com.redant.core.interceptor; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.ClassScaner; import com.redant.core.anno.Order; import com.redant.core.common.constants.CommonConstants; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * @author houyi **/ public class InterceptorProvider { private static volatile boolean loaded = false; private static volatile InterceptorBuilder builder = null; public static List getInterceptors(){ // 优先获取用户自定义的 InterceptorBuilder 构造的 Interceptor if(!loaded){ synchronized (InterceptorProvider.class) { if(!loaded) { Set> builders = ClassScaner.scanPackageBySuper(CommonConstants.INTERCEPTOR_SCAN_PACKAGE, InterceptorBuilder.class); if (CollectionUtil.isNotEmpty(builders)) { try { for (Class cls : builders) { builder = (InterceptorBuilder) cls.newInstance(); break; } } catch (IllegalAccessException | InstantiationException e) { e.printStackTrace(); } } loaded = true; } } } if(builder!=null){ return builder.build(); } // 获取不到时,再扫描所有指定目录下的 Interceptor return InterceptorsHolder.interceptors; } static class InterceptorsHolder { static List interceptors; static { interceptors = scanInterceptors(); } private static List scanInterceptors() { Set> classSet = ClassScaner.scanPackageBySuper(CommonConstants.INTERCEPTOR_SCAN_PACKAGE,Interceptor.class); if(CollectionUtil.isEmpty(classSet)){ return Collections.emptyList(); } List wrappers = new ArrayList<>(classSet.size()); try { for (Class cls : classSet) { Interceptor interceptor =(Interceptor)cls.newInstance(); insertSorted(wrappers,interceptor); } }catch (IllegalAccessException | InstantiationException e) { e.printStackTrace(); } return wrappers.stream() .map(InterceptorWrapper::getInterceptor) .collect(Collectors.toList()); } private static void insertSorted(List list, Interceptor interceptor) { int order = resolveOrder(interceptor); int idx = 0; for (; idx < list.size(); idx++) { // 将当前interceptor插入到order值比他大的第一个interceptor前面 if (list.get(idx).getOrder() > order) { break; } } list.add(idx, new InterceptorWrapper(order, interceptor)); } private static int resolveOrder(Interceptor interceptor) { if (!interceptor.getClass().isAnnotationPresent(Order.class)) { return Order.LOWEST_PRECEDENCE; } else { return interceptor.getClass().getAnnotation(Order.class).value(); } } private static class InterceptorWrapper { private final int order; private final Interceptor interceptor; InterceptorWrapper(int order, Interceptor interceptor) { this.order = order; this.interceptor = interceptor; } int getOrder() { return order; } Interceptor getInterceptor() { return interceptor; } } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/render/RenderType.java ================================================ package com.redant.core.render; /** * 返回的响应类型 * @author houyi.wh * @date 2017-10-20 */ public enum RenderType { /** * JSON */ JSON("application/json;charset=UTF-8"), /** * XML */ XML("text/xml;charset=UTF-8"), /** * TEXT */ TEXT("text/plain;charset=UTF-8"), /** * HTML */ HTML("text/html;charset=UTF-8"); private String contentType; RenderType(String contentType){ this.contentType = contentType; } public String getContentType() { return contentType; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/router/BadClientSilencer.java ================================================ /* * Copyright 2015 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.redant.core.router; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; /** * This utility handler should be put at the last position of the inbound pipeline to * catch all exceptions caused by bad client (closed connection, malformed request etc.) * and server processing, then close the connection. * * By default exceptions are logged to Netty internal LOGGER. You may need to override * {@link #onUnknownMessage(Object)}, {@link #onBadClient(Throwable)}, and * {@link #onBadServer(Throwable)} to log to more suitable places. */ @Sharable public class BadClientSilencer extends SimpleChannelInboundHandler { private static final InternalLogger log = InternalLoggerFactory.getInstance(BadClientSilencer.class); /** Logs to Netty internal LOGGER. Override this method to log to other places if you want. */ protected void onUnknownMessage(Object msg) { log.warn("Unknown msg: " + msg); } /** Logs to Netty internal LOGGER. Override this method to log to other places if you want. */ protected void onBadClient(Throwable e) { log.warn("Caught exception (maybe client is bad)", e); } /** Logs to Netty internal LOGGER. Override this method to log to other places if you want. */ protected void onBadServer(Throwable e) { log.warn("Caught exception (maybe server is bad)", e); } //---------------------------------------------------------------------------- @Override public void channelRead0(ChannelHandlerContext ctx, Object msg) { // This handler is the last inbound handler. // This means msg has not been handled by any previous handler. ctx.close(); if (msg != LastHttpContent.EMPTY_LAST_CONTENT) { onUnknownMessage(msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) { ctx.close(); // To clarify where exceptions are from, imports are not used if (e instanceof java.io.IOException || // Connection reset by peer, Broken pipe e instanceof java.nio.channels.ClosedChannelException || e instanceof io.netty.handler.codec.DecoderException || e instanceof io.netty.handler.codec.CorruptedFrameException || // Bad WebSocket frame e instanceof IllegalArgumentException || // Use https://... to connect to HTTP server e instanceof javax.net.ssl.SSLException || // Use http://... to connect to HTTPS server e instanceof io.netty.handler.ssl.NotSslRecordException) { onBadClient(e); // Maybe client is bad } else { onBadServer(e); // Maybe server is bad } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/router/MethodlessRouter.java ================================================ /* * Copyright 2015 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.redant.core.router; /** * Router that contains information about route matching orders, but doesn't * contain information about HTTP request methods. * *

Routes are devided into 3 sections: "first", "last", and "other". * Routes in "first" are matched first, then in "other", then in "last". */ final class MethodlessRouter { private final OrderlessRouter first = new OrderlessRouter(); private final OrderlessRouter other = new OrderlessRouter(); private final OrderlessRouter last = new OrderlessRouter(); //-------------------------------------------------------------------------- /** * Returns the "first" router; routes in this router will be matched first. */ public OrderlessRouter first() { return first; } /** * Returns the "other" router; routes in this router will be matched after * those in the "first" router, but before those in the "last" router. */ public OrderlessRouter other() { return other; } /** * Returns the "last" router; routes in this router will be matched last. */ public OrderlessRouter last() { return last; } /** * Returns the number of routes in this router. */ public int size() { return first.routes().size() + other.routes().size() + last.routes().size(); } //-------------------------------------------------------------------------- /** * Adds route to the "first" section. * *

A path pattern can only point to one target. This method does nothing if the pattern * has already been added. */ public MethodlessRouter addRouteFirst(String pathPattern, T target) { first.addRoute(pathPattern, target); return this; } /** * Adds route to the "other" section. * *

A path pattern can only point to one target. This method does nothing if the pattern * has already been added. */ public MethodlessRouter addRoute(String pathPattern, T target) { other.addRoute(pathPattern, target); return this; } /** * Adds route to the "last" section. * *

A path pattern can only point to one target. This method does nothing if the pattern * has already been added. */ public MethodlessRouter addRouteLast(String pathPattern, T target) { last.addRoute(pathPattern, target); return this; } //-------------------------------------------------------------------------- /** * Removes the route specified by the path pattern. */ public void removePathPattern(String pathPattern) { first.removePathPattern(pathPattern); other.removePathPattern(pathPattern); last.removePathPattern(pathPattern); } /** * Removes all routes leading to the target. */ public void removeTarget(T target) { first.removeTarget(target); other.removeTarget(target); last.removeTarget(target); } //-------------------------------------------------------------------------- /** * @return {@code null} if no match */ public RouteResult route(String uri, String decodedPath, String[] pathTokens) { RouteResult ret = first.route(uri, decodedPath, pathTokens); if (ret != null) { return ret; } ret = other.route(uri, decodedPath, pathTokens); if (ret != null) { return ret; } ret = last.route(uri, decodedPath, pathTokens); if (ret != null) { return ret; } return null; } /** * Checks if there's any matching route. */ public boolean anyMatched(String[] requestPathTokens) { return first.anyMatched(requestPathTokens) || other.anyMatched(requestPathTokens) || last.anyMatched(requestPathTokens); } /** * Given a target and params, this method tries to do the reverse routing * and returns the URI. * *

Placeholders in the path pattern will be filled with the params. * The params can be a map of {@code placeholder name -> value} * or ordered values. * *

If a param doesn't have a corresponding placeholder, it will be put * to the query part of the result URI. * * @return {@code null} if there's no match */ public String uri(T target, Object... params) { String ret = first.uri(target, params); if (ret != null) { return ret; } ret = other.uri(target, params); if (ret != null) { return ret; } return last.uri(target, params); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/router/OrderlessRouter.java ================================================ /* * Copyright 2015 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.redant.core.router; import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Router that doesn't contain information about HTTP request methods and route * matching orders. */ final class OrderlessRouter { private static final InternalLogger log = InternalLoggerFactory.getInstance(OrderlessRouter.class); /** * A path pattern can only point to one target */ private final Map routes = new HashMap(); /** * Reverse index to create reverse routes fast (a target can have multiple path patterns) */ private final Map> reverseRoutes = new HashMap>(); //-------------------------------------------------------------------------- /** * Returns all routes in this router, an unmodifiable map of {@code PathPattern -> Target}. */ public Map routes() { return Collections.unmodifiableMap(routes); } /** * This method does nothing if the path pattern has already been added. * A path pattern can only point to one target. */ public OrderlessRouter addRoute(String pathPattern, T target) { PathPattern p = new PathPattern(pathPattern); if (routes.containsKey(p)) { return this; } routes.put(p, target); addReverseRoute(target, p); return this; } private void addReverseRoute(T target, PathPattern pathPattern) { Set patterns = reverseRoutes.get(target); if (patterns == null) { patterns = new HashSet(); patterns.add(pathPattern); reverseRoutes.put(target, patterns); } else { patterns.add(pathPattern); } } //-------------------------------------------------------------------------- /** * Removes the route specified by the path pattern. */ public void removePathPattern(String pathPattern) { PathPattern p = new PathPattern(pathPattern); T target = routes.remove(p); if (target == null) { return; } Set paths = reverseRoutes.remove(target); paths.remove(p); } /** * Removes all routes leading to the target. */ public void removeTarget(T target) { Set patterns = reverseRoutes.remove(ObjectUtil.checkNotNull(target, "target")); if (patterns == null) { return; } // A pattern can only point to one target. // A target can have multiple patterns. // Remove all patterns leading to this target. for (PathPattern pattern : patterns) { routes.remove(pattern); } } //-------------------------------------------------------------------------- /** * @return {@code null} if no match */ public RouteResult route(String uri, String decodedPath, String[] pathTokens) { // Optimize: reuse requestPathTokens and pathParams in the loop Map pathParams = new HashMap(); for (Map.Entry entry : routes.entrySet()) { PathPattern pattern = entry.getKey(); if (pattern.match(pathTokens, pathParams)) { T target = entry.getValue(); return new RouteResult(uri, decodedPath, pathParams, Collections.>emptyMap(), target); } // Reset for the next try pathParams.clear(); } return null; } /** * Checks if there's any matching route. */ public boolean anyMatched(String[] requestPathTokens) { Map pathParams = new HashMap(); for (PathPattern pattern : routes.keySet()) { if (pattern.match(requestPathTokens, pathParams)) { return true; } // Reset for the next loop pathParams.clear(); } return false; } //-------------------------------------------------------------------------- /** * Given a target and params, this method tries to do the reverse routing * and returns the URI. * *

Placeholders in the path pattern will be filled with the params. * The params can be a map of {@code placeholder name -> value} * or ordered values. * *

If a param doesn't have a corresponding placeholder, it will be put * to the query part of the result URI. * * @return {@code null} if there's no match */ @SuppressWarnings("unchecked") public String uri(T target, Object... params) { if (params.length == 0) { return uri(target, Collections.emptyMap()); } if (params.length == 1 && params[0] instanceof Map) { return pathMap(target, (Map) params[0]); } if (params.length % 2 == 1) { throw new IllegalArgumentException("Missing value for param: " + params[params.length - 1]); } Map map = new HashMap(params.length / 2); for (int i = 0; i < params.length; i += 2) { String key = params[i].toString(); String value = params[i + 1].toString(); map.put(key, value); } return pathMap(target, map); } /** * @return {@code null} if there's no match, or the params can't be UTF-8 encoded */ private String pathMap(T target, Map params) { Set patterns = reverseRoutes.get(target); if (patterns == null) { return null; } try { // The best one is the one with minimum number of params in the query String bestCandidate = null; int minQueryParams = Integer.MAX_VALUE; boolean matched = true; Set usedKeys = new HashSet(); for (PathPattern pattern : patterns) { matched = true; usedKeys.clear(); // "+ 16": Just in case the part befor that is 0 int initialCapacity = pattern.pattern().length() + 20 * params.size() + 16; StringBuilder b = new StringBuilder(initialCapacity); for (String token : pattern.tokens()) { b.append('/'); if (token.length() > 0 && token.charAt(0) == ':') { String key = token.substring(1); Object value = params.get(key); if (value == null) { matched = false; break; } usedKeys.add(key); b.append(value.toString()); } else { b.append(token); } } if (matched) { int numQueryParams = params.size() - usedKeys.size(); if (numQueryParams < minQueryParams) { if (numQueryParams > 0) { boolean firstQueryParam = true; for (Map.Entry entry : params.entrySet()) { String key = entry.getKey().toString(); if (!usedKeys.contains(key)) { if (firstQueryParam) { b.append('?'); firstQueryParam = false; } else { b.append('&'); } String value = entry.getValue().toString(); // May throw UnsupportedEncodingException b.append(URLEncoder.encode(key, "UTF-8")); b.append('='); // May throw UnsupportedEncodingException b.append(URLEncoder.encode(value, "UTF-8")); } } } bestCandidate = b.toString(); minQueryParams = numQueryParams; } } } return bestCandidate; } catch (UnsupportedEncodingException e) { log.warn("Params can't be UTF-8 encoded: " + params); return null; } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/router/PathPattern.java ================================================ /* * Copyright 2015 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.redant.core.router; import io.netty.util.internal.ObjectUtil; import java.util.Map; /** * The pattern can contain constants or placeholders, example: * {@code constant1/:placeholder1/constant2/:*}. * *

{@code :*} is a special placeholder to catch the rest of the path * (may include slashes). If exists, it must appear at the end of the path. * *

The pattern must not contain query, example: * {@code constant1/constant2?foo=bar}. * *

The pattern will be broken to tokens, example: * {@code ["constant1", ":variable", "constant2", ":*"]} */ final class PathPattern { public static String removeSlashesAtBothEnds(String path) { ObjectUtil.checkNotNull(path, "path"); if (path.isEmpty()) { return path; } int beginIndex = 0; while (beginIndex < path.length() && path.charAt(beginIndex) == '/') { beginIndex++; } if (beginIndex == path.length()) { return ""; } int endIndex = path.length() - 1; while (endIndex > beginIndex && path.charAt(endIndex) == '/') { endIndex--; } return path.substring(beginIndex, endIndex + 1); } //-------------------------------------------------------------------------- private final String pattern; private final String[] tokens; /** * The pattern must not contain query, example: * {@code constant1/constant2?foo=bar}. * *

The pattern will be stored without slashes at both ends. */ public PathPattern(String pattern) { if (pattern.contains("?")) { throw new IllegalArgumentException("Path pattern must not contain query"); } this.pattern = removeSlashesAtBothEnds(ObjectUtil.checkNotNull(pattern, "pattern")); this.tokens = this.pattern.split("/"); } /** * Returns the pattern given at the constructor, without slashes at both ends. */ public String pattern() { return pattern; } /** * Returns the pattern given at the constructor, without slashes at both ends, * and split by {@code '/'}. */ public String[] tokens() { return tokens; } //-------------------------------------------------------------------------- // Instances of this class can be conveniently used as Map keys. @Override public int hashCode() { return pattern.hashCode(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof PathPattern)) { return false; } PathPattern otherPathPattern = (PathPattern) o; return pattern.equals(otherPathPattern.pattern); } //-------------------------------------------------------------------------- /** * {@code params} will be updated with params embedded in the request path. * *

This method signature is designed so that {@code requestPathTokens} and {@code params} * can be created only once then reused, to optimize for performance when a * large number of path patterns need to be matched. * * @return {@code false} if not matched; in this case params should be reset */ public boolean match(String[] requestPathTokens, Map params) { if (tokens.length == requestPathTokens.length) { for (int i = 0; i < tokens.length; i++) { String key = tokens[i]; String value = requestPathTokens[i]; if (key.length() > 0 && key.charAt(0) == ':') { // This is a placeholder params.put(key.substring(1), value); } else if (!key.equals(value)) { // This is a constant return false; } } return true; } if (tokens.length > 0 && tokens[tokens.length - 1].equals(":*") && tokens.length <= requestPathTokens.length) { // The first part for (int i = 0; i < tokens.length - 2; i++) { String key = tokens[i]; String value = requestPathTokens[i]; if (key.length() > 0 && key.charAt(0) == ':') { // This is a placeholder params.put(key.substring(1), value); } else if (!key.equals(value)) { // This is a constant return false; } } // The last :* part StringBuilder b = new StringBuilder(requestPathTokens[tokens.length - 1]); for (int i = tokens.length; i < requestPathTokens.length; i++) { b.append('/'); b.append(requestPathTokens[i]); } params.put("*", b.toString()); return true; } return false; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/router/RouteResult.java ================================================ /* * Copyright 2015 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.redant.core.router; import io.netty.handler.codec.http.HttpMethod; import io.netty.util.internal.ObjectUtil; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; /** * Result of calling {@link Router#route(HttpMethod, String)}. */ public class RouteResult { private final String uri; private final String decodedPath; private final Map pathParams; private final Map> queryParams; private final T target; /** * The maps will be wrapped in Collections.unmodifiableMap. */ public RouteResult( String uri, String decodedPath, Map pathParams, Map> queryParams, T target ) { this.uri = ObjectUtil.checkNotNull(uri, "uri"); this.decodedPath = ObjectUtil.checkNotNull(decodedPath, "decodedPath"); this.pathParams = Collections.unmodifiableMap(ObjectUtil.checkNotNull(pathParams, "pathParams")); this.queryParams = Collections.unmodifiableMap(ObjectUtil.checkNotNull(queryParams, "queryParams")); this.target = ObjectUtil.checkNotNull(target, "target"); } /** * Returns the original request URI. */ public String uri() { return uri; } /** * Returns the decoded request path. */ public String decodedPath() { return decodedPath; } /** * Returns all params embedded in the request path. */ public Map pathParams() { return pathParams; } /** * Returns all params in the query part of the request URI. */ public Map> queryParams() { return queryParams; } public T target() { return target; } //---------------------------------------------------------------------------- // Utilities to get params. /** * Extracts the first matching param in {@code queryParams}. * * @return {@code null} if there's no match */ public String queryParam(String name) { List values = queryParams.get(name); return (values == null) ? null : values.get(0); } /** * Extracts the param in {@code pathParams} first, then falls back to the first matching * param in {@code queryParams}. * * @return {@code null} if there's no match */ public String param(String name) { String pathValue = pathParams.get(name); return (pathValue == null) ? queryParam(name) : pathValue; } /** * Extracts all params in {@code pathParams} and {@code queryParams} matching the name. * * @return Unmodifiable list; the list is empty if there's no match */ public List params(String name) { List values = queryParams.get(name); String value = pathParams.get(name); if (values == null) { return (value == null) ? Collections.emptyList() : Collections.singletonList(value); } if (value == null) { return Collections.unmodifiableList(values); } else { List aggregated = new ArrayList(values.size() + 1); aggregated.addAll(values); aggregated.add(value); return Collections.unmodifiableList(aggregated); } } @Override public String toString() { return "{target:"+target+",decodedPath:"+decodedPath+",queryParams:"+queryParams+"}"; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/router/Router.java ================================================ /* * Copyright 2015 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.redant.core.router; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.QueryStringDecoder; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; /** * Router that contains information about both route matching orders and * HTTP request methods. * *

Routes are devided into 3 sections: "first", "last", and "other". * Routes in "first" are matched first, then in "other", then in "last". * *

Create router

* *

Route targets can be any type. In the below example, targets are classes: * *

 * {@code
 * Router router = new Router()
 *   .GET      ("/articles",     IndexHandler.class)
 *   .GET      ("/articles/:id", ShowHandler.class)
 *   .POST     ("/articles",     CreateHandler.class)
 *   .GET      ("/download/:*",  DownloadHandler.class)  // ":*" must be the last token
 *   .GET_FIRST("/articles/new", NewHandler.class);      // This will be matched first
 * }
 * 
* *

Slashes at both ends are ignored. These are the same: * *

 * {@code
 * router.GET("articles",   IndexHandler.class);
 * router.GET("/articles",  IndexHandler.class);
 * router.GET("/articles/", IndexHandler.class);
 * }
 * 
* *

You can remove routes by target or by path pattern: * *

 * {@code
 * router.removeTarget(IndexHandler.class);
 * router.removePathPattern("/articles");
 * }
 * 
* *

Match with request method and URI

* *

Use {@link #route(HttpMethod, String)}. * *

From the {@link RouteResult} you can extract params embedded in * the path and from the query part of the request URI. * *

404 Not Found target

* *

Use {@link #notFound(Object)}. It will be used as the target * when there's no match. * *

 * {@code
 * router.notFound(My404Handler.class);
 * }
 * 
* *

Create reverse route

* *

Use {@code #uri}: * *

 * {@code
 * router.uri(HttpMethod.GET, IndexHandler.class);
 * // Returns "/articles"
 * }
 * 
* *

You can skip HTTP method if there's no confusion: * *

 * {@code
 * router.uri(CreateHandler.class);
 * // Also returns "/articles"
 * }
 * 
* *

You can specify params as map: * *

 * {@code
 * // Things in params will be converted to String
 * Map params = new HashMap();
 * params.put("id", 123);
 * router.uri(ShowHandler.class, params);
 * // Returns "/articles/123"
 * }
 * 
* *

Convenient way to specify params: * *

 * {@code
 * router.uri(ShowHandler.class, "id", 123);
 * // Returns "/articles/123"
 * }
 * 
* *

Allowed methods

* *

If you want to implement * OPTIONS * or * CORS, * you can use {@link #allowedMethods(String)}. * *

For {@code OPTIONS *}, use {@link #allAllowedMethods()}. */ public class Router { private final Map> routers = new HashMap>(); private final MethodlessRouter anyMethodRouter = new MethodlessRouter(); private T notFound; //-------------------------------------------------------------------------- // Design decision: // We do not allow access to routers and anyMethodRouter, because we don't // want to expose MethodlessRouter, OrderlessRouter, and PathPattern. // Exposing those will complicate the use of this package. /** * Returns the fallback target for use when there's no match at * {@link #route(HttpMethod, String)}. */ public T notFound() { return notFound; } /** * Returns the number of routes in this router. */ public int size() { int ret = anyMethodRouter.size(); for (MethodlessRouter router : routers.values()) { ret += router.size(); } return ret; } //-------------------------------------------------------------------------- /** * Add route to the "first" section. * *

A path pattern can only point to one target. This method does nothing if the pattern * has already been added. */ public Router addRouteFirst(HttpMethod method, String pathPattern, T target) { getMethodlessRouter(method).addRouteFirst(pathPattern, target); return this; } /** * Add route to the "other" section. * *

A path pattern can only point to one target. This method does nothing if the pattern * has already been added. */ public Router addRoute(HttpMethod method, String pathPattern, T target) { getMethodlessRouter(method).addRoute(pathPattern, target); return this; } /** * Add route to the "last" section. * *

A path pattern can only point to one target. This method does nothing if the pattern * has already been added. */ public Router addRouteLast(HttpMethod method, String pathPattern, T target) { getMethodlessRouter(method).addRouteLast(pathPattern, target); return this; } /** * Sets the fallback target for use when there's no match at * {@link #route(HttpMethod, String)}. */ public Router notFound(T target) { this.notFound = target; return this; } private MethodlessRouter getMethodlessRouter(HttpMethod method) { if (method == null) { return anyMethodRouter; } MethodlessRouter r = routers.get(method); if (r == null) { r = new MethodlessRouter(); routers.put(method, r); } return r; } //-------------------------------------------------------------------------- /** * Removes the route specified by the path pattern. */ public void removePathPattern(String pathPattern) { for (MethodlessRouter r : routers.values()) { r.removePathPattern(pathPattern); } anyMethodRouter.removePathPattern(pathPattern); } /** * Removes all routes leading to the target. */ public void removeTarget(T target) { for (MethodlessRouter r : routers.values()) { r.removeTarget(target); } anyMethodRouter.removeTarget(target); } //-------------------------------------------------------------------------- /** * If there's no match, returns the result with {@link #notFound(Object) notFound} * as the target if it is set, otherwise returns {@code null}. */ public RouteResult route(HttpMethod method, String uri) { MethodlessRouter router = routers.get(method); if (router == null) { router = anyMethodRouter; } QueryStringDecoder decoder = new QueryStringDecoder(uri); String[] tokens = decodePathTokens(uri); RouteResult ret = router.route(uri, decoder.path(), tokens); if (ret != null) { return new RouteResult(uri, decoder.path(), ret.pathParams(), decoder.parameters(), ret.target()); } if (router != anyMethodRouter) { ret = anyMethodRouter.route(uri, decoder.path(), tokens); if (ret != null) { return new RouteResult(uri, decoder.path(), ret.pathParams(), decoder.parameters(), ret.target()); } } if (notFound != null) { return new RouteResult(uri, decoder.path(), Collections.emptyMap(), decoder.parameters(), notFound); } return null; } private String[] decodePathTokens(String uri) { // Need to split the original URI (instead of QueryStringDecoder#path) then decode the tokens (components), // otherwise /test1/123%2F456 will not match /test1/:p1 int qPos = uri.indexOf("?"); String encodedPath = (qPos >= 0) ? uri.substring(0, qPos) : uri; String[] encodedTokens = PathPattern.removeSlashesAtBothEnds(encodedPath).split("/"); String[] decodedTokens = new String[encodedTokens.length]; for (int i = 0; i < encodedTokens.length; i++) { String encodedToken = encodedTokens[i]; decodedTokens[i] = QueryStringDecoder.decodeComponent(encodedToken); } return decodedTokens; } //-------------------------------------------------------------------------- // For implementing OPTIONS and CORS. /** * Returns allowed methods for a specific URI. *

* For {@code OPTIONS *}, use {@link #allAllowedMethods()} instead of this method. */ public Set allowedMethods(String uri) { QueryStringDecoder decoder = new QueryStringDecoder(uri); String[] tokens = PathPattern.removeSlashesAtBothEnds(decoder.path()).split("/"); if (anyMethodRouter.anyMatched(tokens)) { return allAllowedMethods(); } Set ret = new HashSet(routers.size()); for (Entry> entry : routers.entrySet()) { MethodlessRouter router = entry.getValue(); if (router.anyMatched(tokens)) { HttpMethod method = entry.getKey(); ret.add(method); } } return ret; } /** * Returns all methods that this router handles. For {@code OPTIONS *}. */ public Set allAllowedMethods() { if (anyMethodRouter.size() > 0) { Set ret = new HashSet(9); ret.add(HttpMethod.CONNECT); ret.add(HttpMethod.DELETE); ret.add(HttpMethod.GET); ret.add(HttpMethod.HEAD); ret.add(HttpMethod.OPTIONS); ret.add(HttpMethod.PATCH); ret.add(HttpMethod.POST); ret.add(HttpMethod.PUT); ret.add(HttpMethod.TRACE); return ret; } else { return new HashSet(routers.keySet()); } } //-------------------------------------------------------------------------- // Reverse routing. /** * Given a target and params, this method tries to do the reverse routing * and returns the URI. * *

Placeholders in the path pattern will be filled with the params. * The params can be a map of {@code placeholder name -> value} * or ordered values. * *

If a param doesn't have a corresponding placeholder, it will be put * to the query part of the result URI. * * @return {@code null} if there's no match */ public String uri(HttpMethod method, T target, Object... params) { MethodlessRouter router = (method == null) ? anyMethodRouter : routers.get(method); // Fallback to anyMethodRouter if no router is found for the method if (router == null) { router = anyMethodRouter; } String ret = router.uri(target, params); if (ret != null) { return ret; } // Fallback to anyMethodRouter if the router was not anyMethodRouter and no path is found return (router != anyMethodRouter) ? anyMethodRouter.uri(target, params) : null; } /** * Given a target and params, this method tries to do the reverse routing * and returns the URI. * *

Placeholders in the path pattern will be filled with the params. * The params can be a map of {@code placeholder name -> value} * or ordered values. * *

If a param doesn't have a corresponding placeholder, it will be put * to the query part of the result URI. * * @return {@code null} if there's no match */ public String uri(T target, Object... params) { Collection> rs = routers.values(); for (MethodlessRouter r : rs) { String ret = r.uri(target, params); if (ret != null) { return ret; } } return anyMethodRouter.uri(target, params); } //-------------------------------------------------------------------------- /** * Returns visualized routing rules. */ @Override public String toString() { // Step 1/2: Dump routers and anyMethodRouter in order int numRoutes = size(); List methods = new ArrayList(numRoutes); List patterns = new ArrayList(numRoutes); List targets = new ArrayList(numRoutes); // For router for (Entry> e : routers.entrySet()) { HttpMethod method = e.getKey(); MethodlessRouter router = e.getValue(); aggregateRoutes(method.toString(), router.first().routes(), methods, patterns, targets); aggregateRoutes(method.toString(), router.other().routes(), methods, patterns, targets); aggregateRoutes(method.toString(), router.last().routes(), methods, patterns, targets); } // For anyMethodRouter aggregateRoutes("*", anyMethodRouter.first().routes(), methods, patterns, targets); aggregateRoutes("*", anyMethodRouter.other().routes(), methods, patterns, targets); aggregateRoutes("*", anyMethodRouter.last().routes(), methods, patterns, targets); // For notFound if (notFound != null) { methods.add("*"); patterns.add("*"); targets.add(targetToString(notFound)); } // Step 2/2: Format the List into aligned columns: int maxLengthMethod = maxLength(methods); int maxLengthPattern = maxLength(patterns); String format = "%-" + maxLengthMethod + "s %-" + maxLengthPattern + "s %s\n"; int initialCapacity = (maxLengthMethod + 1 + maxLengthPattern + 1 + 20) * methods.size(); StringBuilder b = new StringBuilder(initialCapacity); for (int i = 0; i < methods.size(); i++) { String method = methods.get(i); String pattern = patterns.get(i); String target = targets.get(i); b.append(String.format(format, method, pattern, target)); } return b.toString(); } /** * Helper for toString. */ private static void aggregateRoutes( String method, Map routes, List accMethods, List accPatterns, List accTargets) { for (Entry entry : routes.entrySet()) { accMethods.add(method); accPatterns.add("/" + entry.getKey().pattern()); accTargets.add(targetToString(entry.getValue())); } } /** * Helper for toString. */ private static int maxLength(List coll) { int max = 0; for (String e : coll) { int length = e.length(); if (length > max) { max = length; } } return max; } /** * Helper for toString. * *

For example, returns * "io.server.example.http.router.HttpRouterServerHandler" instead of * "class io.server.example.http.router.HttpRouterServerHandler" */ private static String targetToString(Object target) { if (target instanceof Class) { return ((Class) target).getName(); } else { return target.toString(); } } //-------------------------------------------------------------------------- public Router CONNECT(String path, T target) { return addRoute(HttpMethod.CONNECT, path, target); } public Router DELETE(String path, T target) { return addRoute(HttpMethod.DELETE, path, target); } public Router GET(String path, T target) { return addRoute(HttpMethod.GET, path, target); } public Router HEAD(String path, T target) { return addRoute(HttpMethod.HEAD, path, target); } public Router OPTIONS(String path, T target) { return addRoute(HttpMethod.OPTIONS, path, target); } public Router PATCH(String path, T target) { return addRoute(HttpMethod.PATCH, path, target); } public Router POST(String path, T target) { return addRoute(HttpMethod.POST, path, target); } public Router PUT(String path, T target) { return addRoute(HttpMethod.PUT, path, target); } public Router TRACE(String path, T target) { return addRoute(HttpMethod.TRACE, path, target); } public Router ANY(String path, T target) { return addRoute(null, path, target); } //-------------------------------------------------------------------------- public Router CONNECT_FIRST(String path, T target) { return addRouteFirst(HttpMethod.CONNECT, path, target); } public Router DELETE_FIRST(String path, T target) { return addRouteFirst(HttpMethod.DELETE, path, target); } public Router GET_FIRST(String path, T target) { return addRouteFirst(HttpMethod.GET, path, target); } public Router HEAD_FIRST(String path, T target) { return addRouteFirst(HttpMethod.HEAD, path, target); } public Router OPTIONS_FIRST(String path, T target) { return addRouteFirst(HttpMethod.OPTIONS, path, target); } public Router PATCH_FIRST(String path, T target) { return addRouteFirst(HttpMethod.PATCH, path, target); } public Router POST_FIRST(String path, T target) { return addRouteFirst(HttpMethod.POST, path, target); } public Router PUT_FIRST(String path, T target) { return addRouteFirst(HttpMethod.PUT, path, target); } public Router TRACE_FIRST(String path, T target) { return addRouteFirst(HttpMethod.TRACE, path, target); } public Router ANY_FIRST(String path, T target) { return addRouteFirst(null, path, target); } //-------------------------------------------------------------------------- public Router CONNECT_LAST(String path, T target) { return addRouteLast(HttpMethod.CONNECT, path, target); } public Router DELETE_LAST(String path, T target) { return addRouteLast(HttpMethod.DELETE, path, target); } public Router GET_LAST(String path, T target) { return addRouteLast(HttpMethod.GET, path, target); } public Router HEAD_LAST(String path, T target) { return addRouteLast(HttpMethod.HEAD, path, target); } public Router OPTIONS_LAST(String path, T target) { return addRouteLast(HttpMethod.OPTIONS, path, target); } public Router PATCH_LAST(String path, T target) { return addRouteLast(HttpMethod.PATCH, path, target); } public Router POST_LAST(String path, T target) { return addRouteLast(HttpMethod.POST, path, target); } public Router PUT_LAST(String path, T target) { return addRouteLast(HttpMethod.PUT, path, target); } public Router TRACE_LAST(String path, T target) { return addRouteLast(HttpMethod.TRACE, path, target); } public Router ANY_LAST(String path, T target) { return addRouteLast(null, path, target); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/router/context/DefaultRouterContext.java ================================================ package com.redant.core.router.context; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.ClassScaner; import cn.hutool.core.util.StrUtil; import com.redant.core.bean.annotation.Bean; import com.redant.core.bean.context.BeanContext; import com.redant.core.bean.context.DefaultBeanContext; import com.redant.core.common.constants.CommonConstants; import com.redant.core.common.enums.RequestMethod; import com.redant.core.controller.ControllerProxy; import com.redant.core.controller.annotation.Controller; import com.redant.core.controller.annotation.Mapping; import com.redant.core.controller.context.ControllerContext; import com.redant.core.controller.context.DefaultControllerContext; import com.redant.core.init.InitFunc; import com.redant.core.init.InitOrder; import com.redant.core.render.RenderType; import com.redant.core.router.RouteResult; import com.redant.core.router.Router; import io.netty.handler.codec.http.HttpMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Method; import java.util.Set; /** * 默认的RouterContext * * @author houyi.wh * @date 2017-10-20 */ @InitOrder(2) public class DefaultRouterContext implements RouterContext, InitFunc { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultRouterContext.class); /** * 所有的路由信息 */ private static Router router; /** * 初始化完毕的标志 */ private static volatile boolean inited; /** * Bean上下文 */ private static BeanContext beanContext; /** * Controller上下文 */ private static ControllerContext controllerContext; private static final class DefaultRouterContextHolder { private static DefaultRouterContext context = new DefaultRouterContext(); } private DefaultRouterContext() { } public static RouterContext getInstance() { return DefaultRouterContextHolder.context; } @Override public void init() { doInit(); } /** * 获取路由结果 */ @Override public RouteResult getRouteResult(HttpMethod method, String uri) { if (inited()) { RouteResult routeResult = router.route(method, uri); LOGGER.debug("getRouteResult with method={}, uri={}, routeResult={}", method, uri, routeResult); return routeResult; } return null; } /** * Router是否加载完毕 */ private boolean inited() { while (!inited) { doInit(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } return inited; } /** * 执行初始化工作 */ private void doInit() { // 初始化时需要同步 synchronized (DefaultRouterContext.class) { if (!inited) { LOGGER.info("[DefaultRouterContext] doInit"); beanContext = DefaultBeanContext.getInstance(); controllerContext = DefaultControllerContext.getInstance(); initRouter(); // 加载完毕 inited = true; LOGGER.info("[DefaultRouterContext] doInit success!"); } } } private void initRouter() { try { LOGGER.info("[DefaultRouterContext] initRouter"); // 获取所有RouterController Set> classSet = ClassScaner.scanPackageByAnnotation(CommonConstants.BEAN_SCAN_PACKAGE, Controller.class); router = new Router<>(); if (CollectionUtil.isNotEmpty(classSet)) { for (Class cls : classSet) { Controller controller = cls.getAnnotation(Controller.class); // 获取Controller中所有的方法 Method[] methods = cls.getMethods(); for (Method method : methods) { Mapping mapping = method.getAnnotation(Mapping.class); if (mapping != null) { addRoute(controller, mapping); // 添加控制器 addProxy(cls, method, controller, mapping); } } } router.notFound(RenderType.HTML); LOGGER.info("[DefaultRouterContext] initRouter success! routers are listed blow:" + "\n*************************************" + "\n{}" + "*************************************\n", router); } else { LOGGER.warn("[DefaultRouterContext] No Controller Scanned!"); } } catch (RuntimeException e) { throw e; } catch (Exception e) { LOGGER.error("[DefaultRouterContext] Init controller error,cause:", e); } } private void addRoute(Controller controller, Mapping mapping) { // Controller+Mapping 唯一确定一个控制器的方法 String path = controller.path() + mapping.path(); HttpMethod method = RequestMethod.getHttpMethod(mapping.requestMethod()); // 添加路由 router.addRoute(method, path, mapping.renderType()); } private void addProxy(Class cls, Method method, Controller controller, Mapping mapping) { try { // Controller+Mapping 唯一确定一个控制器的方法 String path = controller.path() + mapping.path(); ControllerProxy proxy = new ControllerProxy(); proxy.setRenderType(mapping.renderType()); proxy.setRequestMethod(mapping.requestMethod()); Object object; // 如果该controller也使用了Bean注解,则从BeanContext中获取该controller的实现类 Bean bean = cls.getAnnotation(Bean.class); if (bean != null) { // 如果该controller设置了bean的名字则以该名称从BeanHolder中获取bean,否则以 String beanName = StrUtil.isNotBlank(bean.name()) ? bean.name() : cls.getName(); object = beanContext.getBean(beanName); } else { object = cls.newInstance(); } proxy.setController(object); proxy.setMethod(method); proxy.setMethodName(method.getName()); controllerContext.addProxy(path, proxy); LOGGER.info("[DefaultRouterContext] addProxy path={} to proxy={}", path, proxy); } catch (Exception e) { LOGGER.error("[DefaultRouterContext] addProxy error,cause:", e.getMessage(), e); } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/router/context/RouterContext.java ================================================ package com.redant.core.router.context; import com.redant.core.render.RenderType; import com.redant.core.router.RouteResult; import io.netty.handler.codec.http.HttpMethod; /** * 路由上下文 * @author houyi.wh * @date 2017-10-20 */ public interface RouterContext { /** * 获取路由结果 * @param method 请求类型 * @param uri url * @return 路由结果 */ RouteResult getRouteResult(HttpMethod method, String uri); } ================================================ FILE: redant-core/src/main/java/com/redant/core/server/NettyHttpServer.java ================================================ package com.redant.core.server; import com.redant.core.common.constants.CommonConstants; import com.redant.core.init.InitExecutor; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.util.concurrent.DefaultThreadFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * NettyHttpServer * @author houyi.wh * @date 2017-10-20 */ public final class NettyHttpServer implements Server { private static final Logger LOGGER = LoggerFactory.getLogger(NettyHttpServer.class); @Override public void preStart() { InitExecutor.init(); } @Override public void start() { EventLoopGroup bossGroup = new NioEventLoopGroup(CommonConstants.BOSS_GROUP_SIZE, new DefaultThreadFactory("boss", true)); EventLoopGroup workerGroup = new NioEventLoopGroup(CommonConstants.WORKER_GROUP_SIZE, new DefaultThreadFactory("worker", true)); try { long start = System.currentTimeMillis(); ServerBootstrap b = new ServerBootstrap(); b.option(ChannelOption.SO_BACKLOG, 1024); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new NettyHttpServerInitializer()); ChannelFuture future = b.bind(CommonConstants.SERVER_PORT).sync(); long cost = System.currentTimeMillis()-start; LOGGER.info("[NettyHttpServer] Startup at port:{} cost:{}[ms]",CommonConstants.SERVER_PORT,cost); // 等待服务端Socket关闭 future.channel().closeFuture().sync(); } catch (InterruptedException e) { LOGGER.error("[NettyHttpServer] InterruptedException:",e); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/server/NettyHttpServerInitializer.java ================================================ package com.redant.core.server; import com.redant.core.common.constants.CommonConstants; import com.redant.core.handler.ControllerDispatcher; import com.redant.core.handler.ssl.SslContextHelper; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpContentCompressor; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.netty.handler.stream.ChunkedWriteHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLEngine; /** * @author houyi.wh * @date 2019-01-17 */ public class NettyHttpServerInitializer extends ChannelInitializer { public static final Logger LOGGER = LoggerFactory.getLogger(NettyHttpServerInitializer.class); @Override public void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); // HttpServerCodec is a combination of HttpRequestDecoder and HttpResponseEncoder // 使用HttpServerCodec将ByteBuf编解码为httpRequest/httpResponse pipeline.addLast(new HttpServerCodec()); addAdvanced(pipeline); pipeline.addLast(new ChunkedWriteHandler()); // 路由分发器 pipeline.addLast(new ControllerDispatcher()); } private void initSsl(SocketChannel ch){ ChannelPipeline pipeline = ch.pipeline(); if(CommonConstants.USE_SSL){ SslContext context = SslContextHelper.getSslContext(CommonConstants.KEY_STORE_PATH,CommonConstants.KEY_STORE_PASSWORD); if(context!=null) { SSLEngine engine = context.newEngine(ch.alloc()); engine.setUseClientMode(false); pipeline.addLast(new SslHandler(engine)); }else{ LOGGER.warn("SslContext is null with keyPath={}",CommonConstants.KEY_STORE_PATH); } } } /** * 可以在 HttpServerCodec 之后添加这些 ChannelHandler 进行开启高级特性 */ private void addAdvanced(ChannelPipeline pipeline){ if(CommonConstants.USE_COMPRESS) { // 对 http 响应结果开启 gizp 压缩 pipeline.addLast(new HttpContentCompressor()); } if(CommonConstants.USE_AGGREGATOR) { // 将多个HttpRequest组合成一个FullHttpRequest pipeline.addLast(new HttpObjectAggregator(CommonConstants.MAX_CONTENT_LENGTH)); } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/server/Server.java ================================================ package com.redant.core.server; /** * @author houyi.wh * @date 2019-01-10 */ public interface Server { /** * 启动服务器之前的事件处理 */ void preStart(); /** * 启动服务器 */ void start(); } ================================================ FILE: redant-core/src/main/java/com/redant/core/session/HttpSession.java ================================================ package com.redant.core.session; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelId; import java.util.HashMap; import java.util.Map; /** * HttpSession * @author houyi.wh * @date 2017/11/6 */ public class HttpSession { /** * 会话id */ private ChannelId id; /** * 会话保存的ChannelHandlerContext */ private ChannelHandlerContext context; /** * 创建时间 */ private Long createTime; /** * 过期时间 * 每次请求时都更新过期时间 */ private Long expireTime; /** * Session中存储的数据 */ private Map sessionMap; private void assertSessionMapNotNull(){ if(sessionMap ==null){ sessionMap = new HashMap(); } } private HttpSession(){ } //===================================== public HttpSession(ChannelHandlerContext context){ this(context.channel().id(),context); } public HttpSession(ChannelId id,ChannelHandlerContext context){ this(id,context,System.currentTimeMillis()); } public HttpSession(ChannelId id,ChannelHandlerContext context,Long createTime){ this(id,context,createTime,createTime + SessionConfig.instance().sessionTimeOut()); } public HttpSession(ChannelId id,ChannelHandlerContext context,Long createTime,Long expireTime){ this.id = id; this.context = context; this.createTime = createTime; this.expireTime = expireTime; assertSessionMapNotNull(); } public ChannelId getId() { return id; } public void setId(ChannelId id) { this.id = id; } public ChannelHandlerContext getContext() { return context; } public void setContext(ChannelHandlerContext context) { this.context = context; } public Long getCreateTime() { return createTime; } public void setCreateTime(Long createTime) { this.createTime = createTime; } public Long getExpireTime() { return expireTime; } public void setExpireTime(Long expireTime) { this.expireTime = expireTime; } /** * 是否过期 * @return */ public boolean isExpire(){ return this.expireTime>=System.currentTimeMillis(); } /** * 设置attribute * @param key * @param val */ public void setAttribute(String key,Object val){ sessionMap.put(key,val); } /** * 获取key的值 * @param key */ public Object getAttribute(String key){ return sessionMap.get(key); } /** * 是否存在key * @param key */ public boolean containsAttribute(String key){ return sessionMap.containsKey(key); } } ================================================ FILE: redant-core/src/main/java/com/redant/core/session/SessionConfig.java ================================================ package com.redant.core.session; /** * 针对HttpSession的全局配置项 * @author houyi.wh * @date 2017/11/6 */ public class SessionConfig { /** * 默认超时时间 */ private static final Long DEFAULT_SESSION_TIME_OUT = 60*60*1000L; private SessionConfig(){ } /** * session超时时间 */ private Long sessionTimeOut = DEFAULT_SESSION_TIME_OUT; /** * 单例 */ private static SessionConfig config; static{ if(config==null){ config = new SessionConfig(); } } //====================================== /** * 获取实例 * @return */ public static SessionConfig instance(){ return config; } public SessionConfig sessionTimeOut(Long sessionTimeOut){ config.sessionTimeOut = sessionTimeOut; return config; } public Long sessionTimeOut(){ return config.sessionTimeOut; } @Override public String toString() { return "["+super.toString()+"]:{sessionTimeOut:"+config.sessionTimeOut+"}"; } } ================================================ FILE: redant-core/src/main/java/com/redant/core/session/SessionHelper.java ================================================ package com.redant.core.session; import com.redant.core.common.exception.InvalidSessionException; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelId; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Session辅助器 * @author houyi.wh * @date 2017/11/6 */ public class SessionHelper { /** * 保存session对象的map */ private Map sessionMap; private static SessionHelper manager; private SessionHelper(){ } //====================================== /** * 获取单例 * @return */ public static SessionHelper instange(){ synchronized (SessionHelper.class) { if (manager == null) { manager = new SessionHelper(); if (manager.sessionMap == null) { // 需要线程安全的Map manager.sessionMap = new ConcurrentHashMap(); } } } return manager; } /** * 判断session是否存在 * @param context * @return */ public boolean containsSession(ChannelHandlerContext context){ return context!=null && context.channel()!=null && context.channel().id()!=null && manager.sessionMap.get(context.channel().id())!=null; } /** * 添加一个session * @param context * @param session */ public void addSession(ChannelHandlerContext context,HttpSession session){ if(context==null || context.channel()==null || context.channel().id()==null || session==null){ throw new InvalidSessionException("context or session is null"); } manager.sessionMap.put(context.channel().id(),session); } /** * 获取一个session * @param context * @return */ public HttpSession getSession(ChannelHandlerContext context){ if(context==null || context.channel()==null || context.channel().id()==null){ throw new InvalidSessionException("context is null"); } return manager.sessionMap.get(context.channel().id()); } /** * 获取一个session,获取不到时自动创建一个 * @param context * @param createIfNull * @return */ public HttpSession getSession(ChannelHandlerContext context,boolean createIfNull){ HttpSession session = getSession(context); if(session==null && createIfNull){ session = new HttpSession(context); manager.sessionMap.put(context.channel().id(),session); } return session; } /** * 清除过期的session * 需要在定时器中执行该方法 */ public void clearExpireSession(){ Iterator> iterator = manager.sessionMap.entrySet().iterator(); while(iterator.hasNext()){ Map.Entry sessionEntry = iterator.next(); if(sessionEntry.getValue()==null || sessionEntry.getValue().isExpire()){ iterator.remove(); } } } } ================================================ FILE: redant-core/src/main/java/com/redant/core/session/SessionManager.java ================================================ package com.redant.core.session; /** * Session管理器 * @author houyi.wh * @date 2017/11/6 */ public interface SessionManager { /** * 判断session是否存在 */ boolean sessionExists(); /** * 添加一个session * @param session session对象 */ void addSession(HttpSession session); /** * 获取一个session * @return session对象 */ HttpSession getSession(); /** * 获取一个session,获取不到时自动创建一个 * @param createIfNull true:不存在时创建一个,false:不存在时也不创建 * @return session对象 */ HttpSession getSession(boolean createIfNull); /** * 清除过期的session * 需要在定时器中执行该方法 */ void clearExpireSession(); } ================================================ FILE: redant-core/src/main/resources/logback.xml ================================================ %d{yyyy-MM-dd HH:mm:ss,SSS} [%thread] %-5level %logger:%line - %msg%n WARN ${baseLogDir}/logs/%d{yyyy-MM-dd}/warn-log.log ${maxHistory} ${maxFileSize} %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger:%line - %msg%n ${baseLogDir}/logs/%d{yyyy-MM-dd}/debug-log.log ${maxHistory} ${maxFileSize} %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger:%line - %msg%n ${baseLogDir}/logs/%d{yyyy-MM-dd}/routerMsg.log ${maxHistory} ${maxFileSize} %-1relative - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{0}:%line - %msg%n ================================================ FILE: redant-core/src/main/resources/redant.properties ================================================ # server port netty.server.port=8888 # the boss thread size which set to EventLoopGroup netty.server.bossGroup.size=1 # the worker thread size which set to EventLoopGroup netty.server.workerGroup.size=8 # the maxContentLength which set to HttpObjectAggregator netty.maxContentLength=10485760 # whether use ssl or not netty.server.use.ssl=false # KeyStore path ssl.keyStore.path= # KeyStore password ssl.keyStore.password= # the package that beans stored in bean.scan.package=com.redant # the package that netty interceptors stored in interceptor.scan.package=com.redant netty.server.use.compress=true netty.server.use.aggregator=true # the description sent to front end when server internal error occurred server.internal.error.desc=Server Internal Error # whether execute the business event in async mode async.execute.event=false # async executor thread pool async.executor.pool.core.size=10 async.executor.pool.max.size=20 async.executor.pool.keep.alive.seconds=10 async.executor.pool.blocking.queue.size=100 ================================================ FILE: redant-core/src/main/resources/zk.cfg ================================================ { "model" : "cluster", "configs" : [ { "clientPort": 2181, "tickTime": 2000, "initLimit": 10, "syncLimit": 5, "dataDir": "/zookeeper/1/data", "dataLogDir": "/zookeeper/1/log", "maxClientCnxns": 60, "myid": "/zookeeper/1/data/myid:1", "servers": [ "127.0.0.1:2887:3887", "127.0.0.1:2888:3888", "127.0.0.1:2889:3889" ] }, { "clientPort": 2182, "tickTime": 2000, "initLimit": 10, "syncLimit": 5, "dataDir": "/zookeeper/2/data", "dataLogDir": "/zookeeper/2/log", "maxClientCnxns": 60, "myid": "/zookeeper/2/data/myid:2", "servers": [ "127.0.0.1:2887:3887", "127.0.0.1:2888:3888", "127.0.0.1:2889:3889" ] }, { "clientPort": 2183, "tickTime": 2000, "initLimit": 10, "syncLimit": 5, "dataDir": "/zookeeper/3/data", "dataLogDir": "/zookeeper/3/log", "maxClientCnxns": 60, "myid": "/zookeeper/3/data/myid:3", "servers": [ "127.0.0.1:2887:3887", "127.0.0.1:2888:3888", "127.0.0.1:2889:3889" ] } ] } ================================================ FILE: redant-core/src/test/java/com/redant/core/context/RedantContextTest.java ================================================ package com.redant.core.context; /** * @author houyi **/ public class RedantContextTest { public static void main(String[] args) { for(int i=0;i<10;i++){ new Thread(new ContextRunner()).start(); } } private static class ContextRunner implements Runnable { @Override public void run() { RedantContext context = RedantContext.currentContext(); System.out.println(context); } } } ================================================ FILE: redant-example/pom.xml ================================================ redant com.redant 1.0.0-SNAPSHOT 4.0.0 redant-example jar redant-example 1.0.0-SNAPSHOT com.redant redant-core 1.0.0-SNAPSHOT com.redant redant-cluster 1.0.0-SNAPSHOT ================================================ FILE: redant-example/src/main/java/com/redant/example/bootstrap/cluster/MasterServerBootstrap.java ================================================ package com.redant.example.bootstrap.cluster; /** * MasterServerBootstrap * @author houyi.wh * @date 2017/11/20 **/ public class MasterServerBootstrap { public static void main(String[] args) { com.redant.cluster.bootstrap.MasterServerBootstrap.main(args); } } ================================================ FILE: redant-example/src/main/java/com/redant/example/bootstrap/cluster/SlaveServerBootstrap.java ================================================ package com.redant.example.bootstrap.cluster; /** * SlaveServerBootstrap * @author houyi.wh * @date 2017/11/20 **/ public class SlaveServerBootstrap { public static void main(String[] args) { com.redant.cluster.bootstrap.SlaveServerBootstrap.main(args); } } ================================================ FILE: redant-example/src/main/java/com/redant/example/bootstrap/cluster/ZkBootstrap.java ================================================ package com.redant.example.bootstrap.cluster; /** * ZkBootstrap * @author houyi.wh * @date 2017/11/20 **/ public class ZkBootstrap { public static void main(String[] args) { com.redant.cluster.bootstrap.ZkBootstrap.main(args); } } ================================================ FILE: redant-example/src/main/java/com/redant/example/bootstrap/standalone/ServerBootstrap.java ================================================ package com.redant.example.bootstrap.standalone; /** * 服务端启动入口 * @author houyi.wh * @date 2017-10-20 */ public final class ServerBootstrap { public static void main(String[] args) { com.redant.core.ServerBootstrap.main(args); } } ================================================ FILE: redant-example/src/main/java/com/redant/example/controller/BaseController.java ================================================ package com.redant.example.controller; import com.redant.core.common.enums.RequestMethod; import com.redant.core.common.html.DefaultHtmlMaker; import com.redant.core.common.html.HtmlMaker; import com.redant.core.common.html.HtmlMakerEnum; import com.redant.core.common.html.HtmlMakerFactory; import com.redant.core.common.util.HtmlContentUtil; import com.redant.core.common.view.PageIndex; import com.redant.core.controller.annotation.Controller; import com.redant.core.render.RenderType; import com.redant.core.controller.annotation.Mapping; /** * BaseController * @author houyi.wh * @date 2017-10-20 */ @Controller(path="/") public class BaseController { @Mapping(requestMethod=RequestMethod.GET,renderType=RenderType.HTML) public String index(){ HtmlMaker htmlMaker = HtmlMakerFactory.instance().build(HtmlMakerEnum.STRING,DefaultHtmlMaker.class); String htmlTpl = PageIndex.HTML; return HtmlContentUtil.getPageContent(htmlMaker, htmlTpl,null); } } ================================================ FILE: redant-example/src/main/java/com/redant/example/controller/CookieController.java ================================================ package com.redant.example.controller; import com.alibaba.fastjson.JSONObject; import com.redant.core.bean.annotation.Bean; import com.redant.core.common.enums.RequestMethod; import com.redant.core.controller.annotation.Controller; import com.redant.core.controller.annotation.Mapping; import com.redant.core.controller.annotation.Param; import com.redant.core.cookie.CookieManager; import com.redant.core.cookie.DefaultCookieManager; import com.redant.core.render.RenderType; /** * @author houyi.wh * @date 2017/12/1 **/ @Bean @Controller(path="/cookie") public class CookieController { private CookieManager cookieManager = DefaultCookieManager.getInstance(); @Mapping(path="/add",requestMethod=RequestMethod.GET,renderType=RenderType.JSON) public JSONObject add(@Param(key="name", notBlank=true) String name, @Param(key="value", notBlank=true) String value){ JSONObject object = new JSONObject(); object.put("tip","请在响应头 Response Headers 中查看 set-cookie 的值"); object.put("cookieName",name); object.put("cookieValue",value); cookieManager.addCookie(name,value); return object; } @Mapping(path="/delete",requestMethod=RequestMethod.GET,renderType=RenderType.JSON) public JSONObject delete(@Param(key="name", notBlank=true) String name){ JSONObject object = new JSONObject(); object.put("tip","请在响应头 Response Headers 中查看 set-cookie 的值"); object.put("cookieName",name); cookieManager.deleteCookie(name); return object; } } ================================================ FILE: redant-example/src/main/java/com/redant/example/controller/UserController.java ================================================ package com.redant.example.controller; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.redant.core.bean.annotation.Autowired; import com.redant.core.bean.annotation.Bean; import com.redant.core.common.enums.RequestMethod; import com.redant.core.controller.annotation.Controller; import com.redant.core.controller.annotation.Mapping; import com.redant.core.controller.annotation.Param; import com.redant.core.render.RenderType; import com.redant.example.service.UserBean; import com.redant.example.service.UserService; import java.util.concurrent.TimeUnit; /** * @author houyi.wh * @date 2017/12/1 **/ @Bean @Controller(path = "/user") public class UserController { /** * 如果需要使用Autowired,则该类自身需要使用Bean注解标注 */ @Autowired(name = "userService") private UserService userService; @Mapping(path = "/info", requestMethod = RequestMethod.GET, renderType = RenderType.JSON) public UserBean info(@Param(key = "id", notNull = true) Integer id) { try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } return userService.selectUserInfo(id); } @Mapping(path = "/list", requestMethod = RequestMethod.GET, renderType = RenderType.JSON) public JSONArray list() { JSONArray array = new JSONArray(); JSONObject object = new JSONObject(); UserBean user = new UserBean(); user.setId(23); user.setUserName("逅弈逐码"); object.put("user", user); array.add(object); return array; } @Mapping(path = "/count", requestMethod = RequestMethod.GET, renderType = RenderType.JSON) public JSONObject count() { JSONObject object = new JSONObject(); int count = userService.selectCount(); object.put("count", count); return object; } } ================================================ FILE: redant-example/src/main/java/com/redant/example/interceptor/BlockInterceptor.java ================================================ package com.redant.example.interceptor; import cn.hutool.core.collection.CollectionUtil; import com.alibaba.fastjson.JSONObject; import com.redant.core.anno.Order; import com.redant.core.common.util.HttpRenderUtil; import com.redant.core.context.RedantContext; import com.redant.core.interceptor.Interceptor; import com.redant.core.render.RenderType; import io.netty.handler.codec.http.FullHttpResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Map; /** * 当请求的参数中有 block=true 时,就会被该拦截器拦截 * @author houyi **/ @Order(value = 1) public class BlockInterceptor extends Interceptor { private static final Logger LOGGER = LoggerFactory.getLogger(BlockInterceptor.class); @Override public boolean preHandle(Map> paramMap) { if(CollectionUtil.isNotEmpty(paramMap)) { String blockKey = "block"; String blockVal = "true"; List values = paramMap.get(blockKey); if(CollectionUtil.isNotEmpty(values)){ String val = values.get(0); if(blockVal.equals(val)){ JSONObject content = new JSONObject(); content.put("status","你被前置方法拦截了"); content.put("reason","请求参数中有 block=true"); FullHttpResponse response = HttpRenderUtil.render(content, RenderType.JSON); RedantContext.currentContext().setResponse(response); LOGGER.info("[BlockInterceptor] blocked preHandle"); return false; } } } return true; } @Override public void postHandle(Map> paramMap) { // do nothing } } ================================================ FILE: redant-example/src/main/java/com/redant/example/interceptor/CustomInterceptorBuilder.java ================================================ package com.redant.example.interceptor; import com.redant.core.interceptor.Interceptor; import com.redant.core.interceptor.InterceptorBuilder; import java.util.ArrayList; import java.util.List; /** * @author houyi **/ public class CustomInterceptorBuilder implements InterceptorBuilder { private volatile boolean loaded = false; private List interceptors = null; @Override public List build() { if(!loaded){ synchronized (CustomInterceptorBuilder.class) { if(!loaded){ interceptors = new ArrayList<>(); if (activeBlock()) { interceptors.add(new BlockInterceptor()); } if (activePerf()) { interceptors.add(new PerformanceInterceptor()); } loaded = true; } } } return interceptors; } private boolean activeBlock(){ return false; } private boolean activePerf(){ return true; } } ================================================ FILE: redant-example/src/main/java/com/redant/example/interceptor/PerformanceInterceptor.java ================================================ package com.redant.example.interceptor; import com.redant.core.anno.Order; import com.redant.core.context.RedantContext; import com.redant.core.interceptor.Interceptor; import io.netty.handler.codec.http.HttpRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; import java.util.Map; /** * 该拦截器可以计算出用户自定义 Controller 方法的执行时间 * @author houyi **/ @Order(value = 2) public class PerformanceInterceptor extends Interceptor { private static final Logger LOGGER = LoggerFactory.getLogger(PerformanceInterceptor.class); private ThreadLocal start = new ThreadLocal<>(); @Override public boolean preHandle(Map> paramMap) { start.set(System.currentTimeMillis()); return true; } @Override public void postHandle(Map> paramMap) { try { long end = System.currentTimeMillis(); long cost = end - start.get(); HttpRequest request = RedantContext.currentContext().getRequest(); String uri = request.uri(); LOGGER.info("uri={}, cost:{}[ms]", uri, cost); }finally { start.remove(); } } } ================================================ FILE: redant-example/src/main/java/com/redant/example/service/UserBean.java ================================================ package com.redant.example.service; import com.alibaba.fastjson.JSON; import java.io.Serializable; /** * UserBean * @author houyi.wh * @date 2017-10-20 */ public class UserBean implements Serializable { private Integer id; private String userName; private String password; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return JSON.toJSONString(this); } } ================================================ FILE: redant-example/src/main/java/com/redant/example/service/UserService.java ================================================ package com.redant.example.service; /** * @author houyi.wh * @date 2017/12/1 **/ public interface UserService { /** * 获取用户信息 * @param id 用户id * @return 用户信息 */ UserBean selectUserInfo(Integer id); /** * 获取用户个数 * @return 用户个数 */ int selectCount(); } ================================================ FILE: redant-example/src/main/java/com/redant/example/service/UserServiceImpl.java ================================================ package com.redant.example.service; import com.redant.core.bean.annotation.Bean; /** * @author houyi.wh * @date 2017/12/1 **/ @Bean(name="userService") public class UserServiceImpl implements UserService { @Override public UserBean selectUserInfo(Integer id) { UserBean user = new UserBean(); user.setId(id); user.setUserName("逅弈逐码"); return user; } @Override public int selectCount() { return 10; } } ================================================ FILE: redant-example/src/main/resources/logback.xml ================================================ %d{yyyy-MM-dd HH:mm:ss,SSS} [%thread] %-5level %logger:%line - %msg%n WARN ${baseLogDir}/logs/%d{yyyy-MM-dd}/warn-log.log ${maxHistory} ${maxFileSize} %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger:%line - %msg%n ${baseLogDir}/logs/%d{yyyy-MM-dd}/debug-log.log ${maxHistory} ${maxFileSize} %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger:%line - %msg%n ${baseLogDir}/logs/%d{yyyy-MM-dd}/routerMsg.log ${maxHistory} ${maxFileSize} %-1relative - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{0}:%line - %msg%n ================================================ FILE: redant-example/src/test/java/com/lememo/core/interceptor/InterceptorProviderTest.java ================================================ package com.lememo.core.interceptor; import com.redant.core.interceptor.Interceptor; import com.redant.core.interceptor.InterceptorProvider; import java.util.List; /** * @author houyi **/ public class InterceptorProviderTest { public static void main(String[] args) { for(int i=0;i<10;i++){ new Thread(new Run()).start(); } } static class Run implements Runnable { @Override public void run() { List interceptors = InterceptorProvider.getInterceptors(); System.out.println("Thread=[" + Thread.currentThread().getName() + "] interceptors size="+interceptors.size()); } } }