yb-framework源码分析(一)

前言

  本人非科班出身的java小白一枚,两年多开发经验。由于最近博客看的比较多,听闻某位大牛曾言:种树的时机最好是十年前,其次是现在,写博客也是这样。加上最近喜欢研究现公司的框架源码,发现大体能知道思路但是很难清晰的掌握,而写博客是一个锻炼自己的表达能力、记录学习成果以及有利于帮助自己理清思路的一件事。故开始写自己人生中的第一篇博客,如有不当之处,还请各位大牛指点迷津。

一、框架背景

  yb-framework是上海伊邦医药信息科技有限公司(药房网商城)自主开发的一套轻量级RPC框架,使用java语言实现。除了底层通讯采用了netty之外,未曾使用SSH或SSM三大框架,由于bean的管理,动态代理,dao层的封装等都是自主开发的功能。

  Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架Avro就使用了Netty作为底层通信框架,其他还有业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。采用netty作为底层通讯框架无疑简化了开发难度并且提高了并发的性能。而且netty在现公司替代了传统的tomcat作为服务器容器使用,并且支持http、https、tcp/ip、udp、ssl等多种协议(tomcat只是基于http、https协议的web容器)。

  项目的启动类似spring-boot项目的启动方式,不过是通过ant命令来执行启动类的main函数。系统对每次请求的处理过程中,如果找不到对应的bean的话,会重新扫描本地class文件,从而实现了热部署的功能。

  

二、文件结构分析

  框架整体分为common、core、im三个包。
  1. common里主要是对一些通用功能的封装,主要包括邮件、支付(WX,AliPay)、推送、结算、短信、strindex、tree、程序包更新、util工具包等。
  2. core里主要包含bundle(定义了一些注解以及顶级接口)、business(提供了业务模块需要的上下文)、cache(redis缓存、mongodb缓存、内存缓存)、changelog(版本更新日志)、cmd(请求上下文相关处理,批量执行方法及事务控制)、dao(Mysql、SqlServer、Sqlite、Postgres等dao层方法实现)、dfs(文件系统)、docdb(文档数据库)、io、jsp、loader(定了本地class,jar,dir,annotation,field,method等封装类,实现了对本地文件的加载,以及对bean访问的权限拦截)、log(日志管理)、mq(rabbitMq的相关实现类)、quartz(定时任务)、redis(RedisMap、RedisCache)、serialize、server(防火墙规则,http、https,tcp,tcpdfs,udp等服务器)、session(内存session、mongoSession、redisSession)、start(获取本地配置环境及根路径,系统启动类)、tablemodel、threadpool、xmlMapping
  3. im是系统即时消息以及系统通知功能的实现,主要是基于MobileIMSDK的单机版进行的改装,现实用于分布式系统,支持pc,web,mobile等多客户端登录使用,同时这块也是笔者负责改装实现的。

三、系统运行原理

1. 系统初始化

  整个框架使用netty作为底层通讯框架(充当web服务器的角色,相当于tomcat,tomcat是对http协议进行实现的web服务器,而本公司大多使用tcp作为传输协议),使用ant作为项目管理工具,通过在build.xml里配置target标签,会生成类似maven命令的标签,配置如下:

    <target name="yb-start">
        <java classname="yb.core.start.Main" fork="true" classpathref="yb.lib">
            <arg value="stop" />
        </java>
        <java classname="yb.core.start.Main" fork="true" classpathref="yb.lib">
            <jvmarg value="-Dfile.encoding=UTF-8" />
            <jvmarg value="-Djava.io.tmpdir=../../../tmp" />
            <jvmarg line="-XX:PermSize=128M -XX:MaxPermSize=512m -Xdebug -Xnoagent -Djava.compiler=NONE -Xms128M -Xmx512M" />
            <jvmarg value="-Xrunjdwp:transport=dt_socket,address=9456,server=y,suspend=n" />
            <arg value="start" />
        </java>
    </target>
View Code

  上面的java标签用来执行编译生成的.class文件,classname 表示将执行的类名。fork表示在一个新的虚拟机中运行该类。classpathref指定框架依赖jar包所在目录.jvmarg用来配置jvm参数, arg配置输入参数(main函数),yb.core.start.Main就是框架core.start包下Main类,main类有个main方法,其代码如下:

public static void main(String[] args) {
        try {
            Global.setReady(false);
            String command = "start";
            if(args != null && args.length > 0){
                command = String.valueOf(args[0]).toLowerCase(); } startTime = System.currentTimeMillis(); File f = new File(Main.class.getProtectionDomain().getCodeSource().getLocation().getPath()); if (f.getName().endsWith(".jar")) f = f.getParentFile();        //获取系统根路径 Global.Home_Path = f.getParentFile().getPath(); System.setProperty(Global.Home, Global.Home_Path); System.out.println(Global.Home + " : " + Global.Home_Path); String log4j = Global.Home_Path + File.separator + "start.d" + File.separator + "log4j.properties"; System.out.println("load log4j: " + log4j); PropertyConfigurator.configure(log4j); log = LoggerManager.getLogger(Main.class); String pidPath = Global.Home_Path + File.separator + "start.d" + File.separator + "yb.pid"; System.out.println("PID File Path: " + pidPath); File pidFile = new File(pidPath); if("stop".equals(command) || "restart".equals(command)){ cmdStop(pidFile); } if("start".equals(command) || "restart".equals(command)){ String pid = "0"; if(pidFile.exists()){ pid = FileUtils.readFileToString(pidFile, Charset.defaultCharset()); if(pid == null || pid.isEmpty()) pid = "0"; } if(!"0".equals(pid.trim())){ System.err.println("YB Server already started!"); } else{ cmdStart(pidFile); } } } catch (Exception e) { e.printStackTrace(); } }

  通过代码可以看出,该方法会根据输入参数为start或stop会分别执行cmdStart或cmdStop方法来启动/停止服务器.从配置文件可以看出,每次执行ant yb-start命令会先调用stop,再执行start方法.那么服务器又是如何进行启动初始化的配置呢?接来下看下cmdStart方法的实现:

private static void cmdStart(File pidFile) throws Exception{
     //实例化一个PrintStream用来打印初始化输出日志 PrintStream stdOut
= System.out;
     //初始化日志系统
if (Global.sysout.isInfoEnabled()) { System.setOut(new LogPringStream(System.out, Global.sysout,false)); } System.setErr(new LogPringStream(System.err, Global.syserr, true)); //初始化本地配置文件对象、初始化文档数据库(mongodb)、初始化redis连接、初始化定时任务、初始化rabbitMQ连接以及bean的加载
    init();
    //配置防火墙规则
    FireWallFilter.init(ServerUtils.getMaxBadRequest(), ServerUtils.getBadRequestBlockTime(), ServerUtils.getTrustedIP());
    //初始化服务器
    startServers();
    //生成一个.pid文件,记录程序的进程id,用来执行stop方法的时候杀死进程,程序未运行时的pid为0.
    FileUtils.write(pidFile, String.valueOf(getPid()), Charset.defaultCharset());
    stdOut.println(
"YB Server start @" + (System.currentTimeMillis() - startTime) + " [OK]");
    Global.setReady(
true);
}


  startServers则是根据配置文件初始化各种web服务器,比如HttpCommandServer.Protocol的值为“http-cmd”,同时start.init文件中存在如下配置,则会初始化一个端口号为18080的http协议监听端口服务,同理初始化tcp,tcp上传等监听端口服务,每个服务单独启动一个线程,若是没有配置任何服务,则直接停止.

 [http-cmd] 
 port = 18080

  这样一个轻量级、高吞吐、高性能的后端服务器就启动成功了,并且支持多种类型的客户端,多种传输协议。


2. 请求过程

  服务器支持多种协议类型的请求,通过端口号进行区分,我们暂且以http协议为例,如我们配置的http端口为18080,则可以通过http://127.0.0.1:18080来访问httpServer,由于我们的服务是基于netty框架的,在每个server初始化的时候会实例化一个ServerBootstrap,并且绑定好了配置的端口号注册在netty上(netty框架暂时未研究),并且注册了对应的handle来处理对应请求(HttpCmdRequestHandler extends ChannelInboundHandlerAdapter),HttpCmdRequestHandler里重写了channelRead方法,当有一个新的http请求到来时,httpServer(netty)的线程接收到请求后会调用channelRead方法,channelRead的实现代码如下:

 @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    //ctx是netty提供的上下文,msg是请求数据对象.
        if(!(msg instanceof HttpRequest)){
            //如果msg不是HttpRequest的实例,则不处理
            ctx.close();
            //ReferenceCountUtil.release()其实是ByteBuf.release()方法(从ReferenceCounted接口继承而来)的包装。
            //netty4中的ByteBuf使用了引用计数(netty4实现了一个可选的ByteBuf池),
            //每一个新分配的ByteBuf的引用计数值为1,每对这个ByteBuf对象增加一个引用,
            //需要调用ByteBuf.retain()方法,而每减少一个引用,需要调用ByteBuf.release()方法。
            //当这个ByteBuf对象的引用计数值为0时,表示此对象可回收。
            ReferenceCountUtil.release(msg);
            return;
        }
        //如果是http请求则强转为HttpRequest对象
        HttpRequest req = (HttpRequest) msg;
        if(req.uri().equalsIgnoreCase("/favicon.ico")){
            //如果请求是网站头像,直接返回网站头像处理
            handleFavIconRequest(ctx);
            ReferenceCountUtil.release(msg);
            return;
        }
        //根据请求对象获取CommandRequest,CommandRequest主要包含dbSetId,bundleId,versionId,beanName,
        //methodName,params,sessionId,remoteAddress,alias,requestURI等属性
        List<CommandRequest> cmds = parse(req);
        ReferenceCountUtil.release(msg);
        //从netty提供的上下文中获取当前请求的channel
        Channel cl = ctx.channel();
        InetSocketAddress remoteAddress = null;
        InetSocketAddress localAddress = null;
        if(cl != null){
            //从channel中读取远程地址以及本地地址
            remoteAddress = (InetSocketAddress)cl.remoteAddress();
            localAddress = (InetSocketAddress)cl.localAddress();
        }
        if(cmds == null || cmds.isEmpty()){
            //如果CommandRequest对象集合为空,则返回错误的请求
            handleBadRequest(ctx, remoteAddress, req);
            return;
        }
        
        CommandRequest request = null;
        CommandResponse response = null;
        boolean readable = false;
        if(cmds.size() == 1){  //CommandRequest对象的集合只有一条数据
            //获取当前系统毫秒数
            long now = System.currentTimeMillis();
            request = cmds.get(0);
            readable = ObjectTransfer.booleanValue(request.params.get("__readable"));
            request.setRemoteAddress(remoteAddress);
            request.setLocalAddress(localAddress);
            
            //重点终于来了,处理请求的核心方法,返回对应的response
            response = CommandDispatcher.dispatch(request);
            
            response.setAlias(request.getAlias());
            //获取处理过程所用时间
            long timeout = System.currentTimeMillis() - now;
            //把请求处理相关信息记入日志系统
            ServerUtils.monitor("http", request.getAlias(), request.getRequestURI(), timeout);
        }
        else{ //CommandRequest对象的集合不止一条数据(post请求且uri为空的时候会做特殊处理,可能会产生多条数据,这块不是很明白~~)
            List<HttpCmdTask> tasks = new ArrayList<>();
            //把多条请求依次放入tasks,HttpCmdTask继承了Callable接口,重写的call方法中CommandDispatcher.dispatch(request)
            for(int i = 0; i < cmds.size(); i++){
                request = cmds.get(i);
                if(!readable && request.params.containsKey("__readable")) {
                    readable = ObjectTransfer.booleanValue(request.params.get("__readable"));
                }
                request.setRemoteAddress(remoteAddress);
                request.setLocalAddress(localAddress);
                tasks.add(new HttpCmdTask(request));
            }
            //立即处理所有tasks中的请求
            List<Future<CommandResponse>> futures = DefaultThreadPool.getScheduledExecutorService().invokeAll(tasks);
            List<CommandResponse> resps = new ArrayList<>();
            //将处理过后得到的结果集依次放入resps集合
            for(int i = 0; i < futures.size(); i++){
                resps.add(futures.get(i).get());
            }
            //将resps集合合并成CommandResponse 对象
            response = ServerUtils.mergeResponse(resps);
        }
        
        if (HttpUtil.is100ContinueExpected(req)) {
            ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
        }
        //将CommandResponse封装成FullHttpResponse对象发送给调用方
        FullHttpResponse resp = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(response.getJsonData(readable)));
        resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        resp.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
        resp.headers().set(HttpHeaderNames.CONTENT_LENGTH, resp.content().readableBytes());
        for(String key : response.getCookies().keySet()) {
            String value = response.getCookies().get(key);
            resp.headers().set(HttpHeaderNames.SET_COOKIE, key + "=" + value + "; path=/");
        }
        ctx.write(resp).addListener(ChannelFutureListener.CLOSE);
    }
View Code

  以上只是对请求数据以及响应数据处理的大体过程,还未涉及到bean的获取、类的加载以及业务处理方法的调用等核心功能,下面请看我们是如何处理的:

 public static CommandResponse dispatch(CommandRequest request){ //入参CommandRequest对象,返回CommandResponse对象
        //实例化CommandResponse对象,并根据request设置基础属性
        CommandResponse response = new CommandResponse(request.getAlias());
        response.setClientType(request.client);
        response.setRequest(request);
        response.setCode(-1);
        if(!request.valid || !Global.isReady()){ 
            //如果请求未通过验证或者全局环境未初始化,返回无效的请求
            response.setMsg("invalid request");
            return response;
        }
        //根据dbSetId(数据集id),bundleId(程序id),versionId(版本号)获取对应的程序加载器
        BundleLoader loader = BundleLoader.getLoaderForDBSet(request.dbSetId, request.bundleId, request.versionId);
        if(loader == null) {
            //如果程序加载器为空,直接返回错误信息
            response.setMsg("Application[" + request.dbSetId + "." + request.bundleId + "." + request.versionId + "] Not Exist");
            return response;
        }
        //??
        request.versionId = loader.getVersionId();
        //在程序加载器中根据请求的beanName获取对应的类名,在程序加载器中记录在所有的handle类的别名(相当于requestMapping)和类名,
        //以key-value形式存在,如yz.im.chat:ChatHandle
        String className = loader.getAliasClassName(request.beanName);
        Object bean = null;
        if(className != null && !className.isEmpty()) {
            //根据文件名获取对应的bean,相当于spring的getBean方法,程序加载器在初始化的时候会读取本地程序包下的class文件,
            //依次以key-value的形式保存起来,然后就可以根据className获取到对应的bean,如果没找到bean,会重新扫描本地class,再读取bean
            bean = loader.singletonBean(className);
        }
        if(className == null || bean == null){
            log.error(ObjectTransfer.printf("Bundle[%s] Version[%s] Handle %s not found!", String.valueOf(request.bundleId), String.valueOf(request.versionId),request.beanName));
            response.setMsg("接口[" + request.beanName + "." + request.methodName + "]不存在");
            return response;
        }
        if(bean instanceof IYBHandle){
            //所有的handle类都会继承自YBHandle,且使用了注解@YB @Handle(alias="yz.im.chat")
            //由于bean的获取采用动态代理的方式,在生成代理类之前对标注了@Handle的类都添加了对
            //IYBHandle接口的实现,并且重写了__dispatch方法,所以bean instanceof IYBHandle成立
            IYBHandle handle = (IYBHandle) bean;
            //根据handle获取ClassMetaData对象,ClassMetaData中包含了handle中所有方法名以及对应的hashCode
            //根据方法名能获取到方法的hashCode,故handle.__dispatch(hashCode)方法可以发起对请求方法的调用
            ClassMetaData cm = loader.getClassMetaData(handle);
            Integer hashCode = cm.getHandleHashCode(request.methodName);
            if(hashCode == null){
                log.error(ObjectTransfer.printf("Bundle[%s] Version[%s] Class %s (alias=%s) has no public method %s()\r\n", String.valueOf(request.bundleId), String.valueOf(request.versionId), cm.className, request.beanName, request.methodName));
                response.setMsg("接口[" + request.beanName + "." + request.methodName + "]不存在");
                return response;
            }
            //新建一个请求上下文
            Context ctx = new Context(request, response);
            //将上下文放入ThreadLocal
            Context.putCommandContext(ctx);
            try {
                if(cm.autoStartSession) {
                    if(ctx.request.sessionId == null || ctx.request.sessionId.isEmpty()) {
                        String msg = "SESSIONID[" + request.sessionId + "]不存在";
                        if(SessionManager.isAutoCheckExpired()) {
                            throw new SessionExpiredException(msg);
                        }
                    }
                    ctx.getSession();
                }
                //调用请求方法
                Object ret = handle.__dispatch(hashCode);
                //将请求结果放入response
                if(ret != null){
                    response.setResult(ret);
                }
                response.setCode(1);
            }catch (Exception e) {
                boolean businessException = false;
                String erroMsg=e.getMessage();
                if(erroMsg==null||erroMsg.isEmpty()) {
                    erroMsg="服务器异常,请重试";
                }
                response.setMsg(erroMsg);
                if(e instanceof ProxyException) {
                    ProxyException pe = (ProxyException)e;
                    businessException = pe.isBusinessException();
                    if(!pe.isBusinessException()) {
                        erroMsg = "服务器异常,请重试";
                    }
                    response.setCode(pe.getCode());
                }else if(e instanceof SessionExpiredException) {
                    businessException = true;
                    SessionExpiredException se = (SessionExpiredException)e;
                    response.setCode(se.getCode());
                }    
                else {
                    response.setCode(-1);
                }
                
                //不打印已确定业务异常的stack trace
                if(businessException) {
                    log.debug("Exec " + request.getRequestURI() + " Failed: " + erroMsg);
                }
                else {
                    log.error(e);
                }
                
                Context currentCtx = Context.get();
                if(currentCtx != null){
                    currentCtx.rollback();
                    if(!currentCtx.getId().equals(ctx.getId())){
                        ctx.rollback();
                    }
                }
                
            }
            finally{
                Context currentCtx = Context.get();
                if(currentCtx != null){
                    currentCtx.close();
                    if(!currentCtx.getId().equals(ctx.getId())){
                        ctx.close();
                    }
                    Context.clear();
                }
            }
        }
        else {
            response.setMsg("接口[" + request.beanName + "." + request.methodName + "]非法");
        }
        return response;
    }
View Code

猜你喜欢

转载自www.cnblogs.com/despacito/p/9990016.html
今日推荐