Tomcat原理 (持续更新_tomcat同一个请求访问 list内容会变吗-程序员宅基地

技术标签: java  tomcat  intellij-idea  

HTTP

​ 总是由客户端通过建立连接并发送http请求

​ 框架源码都好喜欢用三元运算符呀,我发现我用的频率不是很高,以后也会考虑把简单的if语句化简成三元运算符

Socket

host

​ 在Socket的构造方法中,我注意到这个细节,我发现host不仅可以代表ip地址,还能是网址,说明底层可以把网址解析成对应的ip地址

作用

​ 书上描述的非常好,用来向网络发送和接受数据。而具体的实现则是由java强大的io流来实现的。

​ 如果用这个来实现浏览器功能的话,就先获取out流,然后往这个里面写http协议的内容,然后再用获取的输出流得到服务器返回给我们的信息

ServerSocket

​ Socket代表的是客户端的链接,而这个是服务端的链接。服务端必须随时待命,如果发现了一个客户端请求,那么它会创建一个Socket链接,没错,就是和上面那个Socket一样的socket链接。然后你就利用这个通过accept方法得到的socket来完成数据流的读写。

user.dir

​ 工作目录,说的实际一点,就是相对目录,如果我们根据相对目录创建和读取文件夹的时候,就是相对这个目录来的。总之,在idea中相对目录就是这样,和你java文件所在的目录无关。当然,在spring中,我们用的都是类路径:classpath

\r\n 的区别

​ \n单纯的换行,但是指针的水平位置不变。\r单纯的回车,就是回到这行的开头,那我们的Enter实现了什么功能呢,答案是先换行\n,然后再回车\n回到了这个行的开头。

​ 我觉得我对这个有误解的原因就是,在编程中,一个\n就直接实现了回车的效果,可能,对于程序来说,它的指针一直在开头吗?

​ 不过我发现了一个好玩的,如果我们用的是\r,那它就能实现一边清理屏幕,一边输出,比如做那种倒计时的效果。

File.separator

​ 我发现window下的文件分割符号是\但是linux是和网址相同的/,不愧是被称为网络而生的系统,所以我们可以使用这种分隔符号,而且可读性还比较好

Servlet

生命周期

init()

​ 只执行一次,因为是用来初始化的,我感觉这个在一定程度下类似于构造方法,但这种构造方法可以和反射配合的很好。

​ 如果让我实现的话,我肯定会弄过标志位,判断是否被执行过,然后要考虑并发执行,弄个双重锁。

service()

​ 不用说,会执行很多次。

​ 这个实现有个难点,怎么针对每次请求注入一个request和response呢,也许是我用词的不对,可以它就是初始化了两个对象,然后调用了这个方法吧

destroy()

​ 在销毁service之前执行,用于释放资源,和object的销毁方法一样,销毁之后再也不会调用service方法了

其他

​ socket是一次请求对应一个的,处理完一个之后,你就要关闭它close

autoFlush

​ 在PrintWriter构造方法中,有个属性可以设置自动刷新,不过prinln()会刷新输出,而print()不会。这些细节上的问题,如果你对java源码没有过了解是很难知道的

题外话

​ 我希望自己写的工具类也能和现在写service一样,有特别多的判断,保证别人在调用他们的时候不会出错。

实现

​ 猜想一下servlet的实现,应该是启动的时候,扫描某个包下的类,如果这个类有注解,就把它的url存在一个map里面作为键,然后把这个类作为值,等处理请求的时候,就通过map找到类然后通过反射执行对应的方法。

问题解决

​ 如果我们有一个类,他里面有一些方法,我们想让他被自己写的其他类调用,但是不想让他被用户调用,那我们可以把这个类的修饰符改成默认的。这样,只有在同一个包下的类才能调用。

​ 此外还能解决一个问题,就是类的强制类型转换,我们知道,如果我们的一个方法中传入了一个接口,这个接口能实现的方法肯定是少数的,而我们其实知道jvm传递给我们的是一个类,所以我们可以向上转型,然后调用更多的方法,但这种不一定安全,比如转型错误就是。而且我们能调用框架内部的方法,对封装的影响也不好,框架怎么避免呢?框架肯定是不会用我们上面的那种方法

Facade 外观模式

​ 设计模式的一种,如果我们有一个类不想暴露给用户使用,我用另一个类,把你这个类当成private属性注入,这样即使你在函数中对我进行了向上转型,你也访问不了我的方法。这就是设计模式,如果利用面向对象的特性,实现我们想要的某些效果。我在spring项目中确实见到了这个类,但是tomcat项目里面没有见到。

​ 我觉得这种提出问题,解决问题的学习方式才是我爱的,因为至少能让我感受到,这非常的有用。

连接器

思路

​ 有时候我在想,我们想完成一件功能的时候,是不是应该交给某类来完成,然后在具体的实现上,尽量让类自己完成,而不是调用get和set方法。

子类

​ 从使用的效果来看,就是相当于把父类的属性,方法复制到子类中,只不过,子类的重载方法可以调用父类的super方法,所以,你在项目中要学会使用这种方法来化简自己的类,减少自己代码的量

​ 比如通用数据库对象DO,我们可以有一个类,它是抽象的,然后有Id,有create_time,update_time然后我们可以写addCheck(),update(),然后提供一个抽象方法,commonCheck()让子类去实现它。

​ 所以,你不要不敢使用继承,不敢承担它所带来的不确定性,这是面向对象的特性,你要学会去使用它。

getAllDeclaredMethods

​ tomcat的文档说,head请求只是一个不返回结果的get请求。代码文档写的真的很细。建议多看看,能有很多意想不到的收获。

    private Method[] getAllDeclaredMethods(Class<? extends HttpServlet> c) {
    
		//获取所有的方法,包括父类的方法
        Class<?> clazz = c;
        Method[] allMethods = null;

        while (!clazz.equals(HttpServlet.class)) {
    //直到获取到HttpServlet这个类为止
            Method[] thisMethods = clazz.getDeclaredMethods();
            if (allMethods != null && allMethods.length > 0) {
    
                //这个细节处理的相当漂亮,如果你是新的,直接赋值,否则我就要用数组的复制
                //说实话,这段代码让我来实现的话,我肯定用的是ArrayList,然后无脑加,其实也不错(
                Method[] subClassMethods = allMethods;
                allMethods =
                    new Method[thisMethods.length + subClassMethods.length];
                System.arraycopy(thisMethods, 0, allMethods, 0,
                                 thisMethods.length);
                System.arraycopy(subClassMethods, 0, allMethods, thisMethods.length,
                                 subClassMethods.length);
            } else {
    
                allMethods = thisMethods;
            }

            clazz = clazz.getSuperclass();//这个是获取超类的方法,我以前没有使用过
        }

        return ((allMethods != null) ? allMethods : new Method[0]);
    }
	//这个方法是干什么用的?我在下面看到tomcat用这方法判断之类有没有重写固定的方法,如果重写了说明是可以处理这种请求,否则就不行

OPTIONS

​ HEAD, TRACE, OPTIONS,默认情况下,tomcat会放行如下几种方法,然后我们OPTOINS的主要作用就是,判断我是否容许你使用某种HTTP请求方式来请求我。

​ header里面的Allow就是这个方法来的,因为是要用逗号隔开的嘛,但是第一个前面又没有逗号,最后一个又没有逗号,一句话概括,我要逗号分割。我看源码的实现是用sb.length()>0来判断的,但是我觉得设置一个bool变量,写的时候判断一下,如果true就加个逗号,然后把bool设置为true,所以默认是false。为什么要这样做呢,因为我们根本不确实谁是第一个,谁是最后一个。

​ CRLF = “\r\n” 在源码里看到的,原来在SQL报错的时候,CRLF就是我今天看到的\r\n

浏览器缓存

if (method.equals(METHOD_GET)) {
    
    long lastModified = getLastModified(req);
    if (lastModified == -1) {
    
        // servlet doesn't support if-modified-since, no reason
        // to go through further expensive logic
        doGet(req, resp);
    } else {
    
        long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
        if (ifModifiedSince < lastModified) {
    
            // If the servlet mod time is later, call doGet()
            // Round down to the nearest second for a proper compare
            // A ifModifiedSince of -1 will always be less
            maybeSetLastModified(resp, lastModified);
            doGet(req, resp);
        } else {
    
            resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            //直接告诉浏览器,资源没有被改变,直接从你自己的缓存里面读取就可以了
        }
    }
}

ParameterMap

​ 实现了map接口,然后把两个map放在自己内部,并且加了一个不让修改map的判断标识,这个也就是明确了,我们用户自己确实不能修改传递过来的请求参数,我以前还想着,通过AOP修改这个参数,就直接把token转换成userId在controller里面获取。不过现在用的技术是通过AOP放入attr里面,然后通过工具类直接获取,要方便很多!

Collections.unmodifiableMap通过这个来创建一个不可修改的Map,如果我进入锁定状态,返回给你的就是这个map。我在想,为什么不用一个map来实现。比如这样,当我返回的时候,之间返回给你一个不可修改的map不就行了吗?我想了想,可能因为是用这个函数创建副本是有一定代价的,也就是说,每次获取都要重新创建吗?另外,我还注意到了,它没有给unmodifiableMap赋值,就能直接返回,我不禁好奇Collections.unmodifiableMap干了什么?

Map<String, String> map=new HashMap<>();
Map<String, String> unmodifiableMap = Collections.unmodifiableMap(map);
String name = unmodifiableMap.get("name");
System.out.println("name = " + name);//null
map.put("name","常珂洁");
String name1 = unmodifiableMap.get("name");
System.out.println("name1 = " + name1);//常珂洁

Collections.unmodifiableMap

public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
    
    // Not checking for subclasses because of heap pollution and information leakage.
    if (m.getClass() == UnmodifiableMap.class) {
    
        return (Map<K,V>) m;//这个考虑的我确实没有想到,如果我传递给你的本身是不能修改的,我可以直接返回
        //看来是对用户一点也不信任,不过这也就是为什么排查bug的时候,肯本不用怀疑jdk的原因,我觉得这样非常有道理,能够保证系统的稳定性,这才是开源工具该具备的特征,你的算法题什么都可以,就是不能因为特殊情况而报错,因为这对程序的影响是致命的。
    }
    return new UnmodifiableMap<>(m);
}

​ 原理也很简单,就是重新map接口,这种类型的map是一个内部的实现类而且是私有,但为什么可以在外面直接用呢,我也测试了一下。

PrivateInnerTest privateInnerTest = new PrivateInnerTest();
Hello innerClass = privateInnerTest.getInnerClass();
innerClass.hello();
public class PrivateInnerTest {
    

    public MyInnerClass getInnerClass(){
    
        return new MyInnerClass();
    }

    private class MyInnerClass implements Hello{
    //这个地方必须要有一个可以对外直接使用的类或接口
        //因为你的修饰符是private如果你不用接口在外面实现的话,idea就会直接给我报错
        public void hello(){
    
            System.out.println("我被调用了");
        }
    }

}

volatile

​ 这个关键字我以前看到过,但是从来没有了解过它是什么意思,今天来学习一下。volatile

​ 首先,cpu执行指令的速度是远远快于内存的处理速度,而我们又知道,数据和指令是离不开的,所以,CPU里面有了高速缓存,在内存中读取数据到高速缓存,然后运算,运算完成后刷新到内存里面。

​ 但是,多线程下,这样是有问题的。那怎么解决这种并发问题呢?让我来学习一下,这种问题在硬件的层面是怎么解决的。

​ 有一种是把cpu用于交互的总线锁住,但明显,效率不行。

​ 缓存一致性协议,当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

​ 我演绎了一下,如果我读了i=0,然后别的线程读了i,而且没来的及修改,他发现我读了,让我设置成无效的,然后我准备修改的时候,又读出了0,然后发现另一个也读了i,我让他变成无效的,然后重新读取。

​ 原子性:从cpu的角度看,给变量赋值,给低16位设置完成后,被中断了,如果有其他cpu来读,那么就是一个错误的值。

​ 可见性:内存刷新不及时,导致其他cpu获取了为刷新的值。

​ 有序性:JVM指令重排序,根据依赖关系排序,能保证单线程执行不出错,但是多线程环境下,难免,因为指令重排是在保证单线程不出错的情况下,为了执行顺序的优化,而设计的,并没有考虑多线程。

​ 所以说,如果这些cpu层面的并发问题让我们来处理的话,更不触及不到,幸好我们在使用高级语言的时候,不用考虑这些复杂的过程。

JVM为我们解决了哪些问题?

​ 内存模型,所以的对象都在堆中,然后有每个线程都有自己的虚拟机栈。首先,你不能只能修改自己的内存,不能修改堆的内容,而禁止访问其他线程的内存。你看这个线程里面的内存,像不像cpu的高速缓存。

​ 原子性: 对基本数据类型的变量的读取赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。只在最简单的读取和赋值,因为要保证效率,而且,你比如y=x都没有原子性,因为有从内存中读取x,然后赋值给y的过程。

​ 可见性: volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。(我想起来之前好像看过,现在竟然没有印象了。

​ 同时,Java的锁也能保证,因为只有一个线程可以读取,退出的时候,必然会刷新内存,然后别的线程才能访问。此外要说一点,就是普通变量不一定会立即刷新到内存的,我想也是为了效率吧,就像批量写入一样。

​ 有序性: 应该是jvm对指令重排的一种限制吧

但是现在你会使用volatile了吗

​  第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

​ 首先,volatile可以保证我们flag变量会立即刷新,并且让以前的线程读取新的内容,这样,我通过flag来中断就不会有任何问题了。不然以前确实会产生问题的。

​ 然后是关于原子性,因为java对原子性的定义范围很小,实际开发中根本保证不了我们想要的那种原子性。

​ 最后是指令重排,指令重排遇到volatile会让保证它后面的代码不会重排到它的前面。

总结

​ 不能保证原子性,所以要和synchronized等配合,但是效率比synchronized高,常见的应用场景有。

​ 状态标记量,双重检测


回到tomcat

​ 还是对接口打断点快,自己真的很难找到呢。

​ 书在第四章,讲了tomcat的正式链接器,Coyote,所以我刚刚在代码里找到的都是这个,这个据说是性能比较好。所以,我们直接研究这个。

​ 所以,连接器到底是什么?它实现了Connector接口,那我们肯定要看看这个接口有什么方法,这不就是连接器的作用吗?此外它还负责创建Request和Response对象。

HTTP1.1的新特性

默认持久连接

​ 在HTTP 1.1之前,无论浏览器何时连接到Web服务器,当服务器将请求的资源返回后,就会断开与浏览器的连接。但是一个静态资源的请求会有很多。如果页面和它引用的所有资源文件都使用不同的连接进行下载的话,处理过程会很慢。这就是为什么HTTP 1.1中会引入持久连接。使用持久连接后,当下载了页面后,服务器并不会立即关闭连接。相反,它会等待Web客户端请求被该页面所引用的所有资源。这样一来,页面和被页面引用的资源都会使用同一个连接来下载。考虑到建立/关闭HTTP连接是一个系统开销很大的操作,使用同一个连接来下载所有的资源会为Web服务器、客户端和网络节省很多时间和工作量。

​ 在HTTP 1.1中,会默认使用持久连接。当然,也可以显式地使用,方法是浏览器发送如下的请求头信息:

​ connection: keep-alive

​ 我这里补充一点,这个和web socket的区别还是很大的,websocket请求的话,首先是websocket可以主动给客户端发信息,然后浏览器和服务器直接的请求可以间断开。

块编码

​ 这个是关于消息长度的,有的时候你下载文件,发现它不显示文件的大小,但是却可以下载完,原理其实很简单,我就一直读,读到文件结束的标识位。

​ 此外,还有分块发送的,总结一下,就是每块告诉你有多大,如果告诉你下面这块的大小为0,说明就结束了

状态码 100

​ 如果我想发一个较长的请求体,那我要先看看你是不是能接受,别你不能接受那我就白发送了,所以这就相当于一个预请求。

HttpConnector

HttpProcessor对象池

​ 对象池的目的是这样的,首先这肯定是多线程情况下的,单线程的话,这个根本没有什么意义。然后是对象的创建和回收的开销比较大。

​ 默认会创建min个对象,如果超出了就开始创建新的对象,但是不会超出max,如果超出了就不会处理你的http请求了,和限流一个道理。那如果我就是想处理呢,简单,把max设置为-1,很多程序中,无穷大就是用-1表示的,不过这肯定要在代码层面做。

​ 其实我觉得这个弹性保证在系统能够承受的范围之内越大越好。就像现在的serverLess架构一样。

异步调用

​ 我们在保证在处理一个请求的时候,能够同时处理其他请求。所以我们的请求要用异步方法调用,而我们的多线程就可以完成这种效果,让我们的请求在多线程里面调用。


​ 换了一本书《tomcat架构解析》,我不喜欢那种自底层开始学习的方法,我一般是想对一个东西有一个全局的脉络理解,然后在好奇的驱动下,我会深入的了解它的作用。而且真的太老了,我更喜欢自己去看源码学习。

总体架构

​ 作为一款知名的轻量级应用服务器,Tomcat的架构设计(如生命周期管理可扩展的容器组件设计、类加载方式)可以为我们的服务器中间件设计,甚至是应用系统组件设计提供非常好的借鉴意义。

​ 总结一下,tomcat的架构非常好,我们要学习。

灵活性

​ 怎么说,每次解决问题的时候,都加了一个中间层,就好像是一层层积木一样,为了积木可以替换,我们又设计了接口,因为同层之间又比较相近,我们有了抽象类。(加层

​ 生命周期的自我管理,生命周期我用的好少,但是在vue和spring的设计中,生命周期的管理都是交给每个组件自己管理的,灵活性没得说。

​ 那每个组件有生命周期,那把它们都抽象出一个接口,然后用一个抽象类实现统一的管理,然后我们具体的类负责生命周期的细节。而且tomcat中这个生命周期的管理真的很细,比如启动start的过程就有启动前,启动中,已启动这样的状态

​ java在接口中也能抛出异常。

扩展性

​ 除了留给用户必要的接口以外,让我们程序扩展性增强,最好的办法就是使用责任链模式。责任链模式能够让用户更加方便的修改系统架构的功能。


​ 具体组件

Connector

​ 总结一下,就是这个接口需要监听端口读取请求,然后根据请求的协议针对不同的协议进行不同的解析,并且根据地址匹配对应的容器,然后返回客户端,所以说连接器处理了我们所编写的servlet容器业务逻辑之外的其他流程。

​ 接口还有一个好处就是当我换了某个类的内容之后,根本不需要去修改具体的代码,就需要用一个类负责实现这样的接口,然后把对应的类替换掉就可以。就像我们在定义list的时候,可以有arrayList和灵linkedList,所以我们后面的内容根本不需要替换。

Mapper

​ tomcat的mapper在这里,就相当于通过网址去查询对应的servlet中的容器,我猜这应该是用map实现的,但如果是模糊匹配,怎么实现呢?是重新定义equals方法吗?

MapperLisener

​ 这种监听类的模式是怎么实现的?是使用java自带的观察者模式吗?

Executor

​ tomcat处理多线程并发的解决方案,和线程池有关

Bootstrap和Catalina

​ 前者通过反射来构造后者并且管理后者的生命周期,实现了入口类和核心类库的解耦,可以通过jar包直接启动

org.apache.catalina.startup.Tomcat也是一个启动类,但是这个启动类可以融合于应用服务器,也就是说spring boot能够自动嵌套tomcat的原理就是这个。

​ 所以tomcat有很多实现方式,值得我们去学习。

类加载器

​ 当我们把应用程序打包成war包之后。把他们放到同一个tomcat的里面,他们可以独立的运行,并且可以保证不同的应用版本之间互不干扰,甚至我使用不同的spring版本都可以,就是因为它把每个应用程序的类加载都隔离开来。

​ 这个是jvm里面的内容,不得不说,要求复杂了之后,其实很多东西都是非常有用的,如果让你来实现tomcat这种同时可以加载不同类的应用服务器,而且还互不干扰,至少我是没有任何思路的。

​ 也就是说tomcat通过自己实现的默认的类加载实现了。让那些服务器共同的类能够共享同一个类,而隔离了不同服务器应用,让他们有自己各自的实现,甚至有版本冲突类都没有问题。有点像web运行的jvm。

java默认的类加载器是委派模式

​ 首先我会从缓存中加载,如果没有的话。我会从父类的加载器中加载之后。才在自己的类加载,如果自己没有的话就会抛出异常。

web应用类加载器

  1. 从缓存中加载
  2. 如果没有,则从JVM的Bootstrap类加载器加载
  3. 如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序)
  4. 如果没有,则从父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序为System、Common、Shared。

target

​ 我们在运行web服务器的时候。会生成这个文件,如果你点进去看的话会发现他的classes文件夹里面就是在lombok影响下的具体实现。可以看到我们的get和set方法,和我们的构造方法,如果你对构造方法不确定,或者对继承后会产生什么问题,可以在这里看。

​ 然后是jar包,里面有spring的核心类,然后有依赖的lib和我们自己的代码。这个是可以直接解压得到源文件的,当然肯定没有那么多具体的。

Catalina

​ 是servlet容器的一种实现。

容器的创建

Digester

作用

​ 解析XML

对象栈

​ 在解析上xml开始的部分就让这个对象入栈,然后维护这个栈中的对象,当节点创建结束的时候让对象出栈。

​ 我一开始在想,为什么要用这样的结构。因为我们要创建父对象,父对象里面有子对象,而我们需要建立起父对象对子对象的引用,所以我们创建了一半的父对象开始创建子对象,当子对象创建完成的时候,将引用指向父对象,让父对象创建完成,再让父对象出栈。

如果我们以后在解析嵌套对象的时候,是不是可以使用这个思路

​ 如果我们指定了ID,比如mybatis,那么我们以ID作为键,以对象作为值储存在map里。我们可以通过这个ID快速的查找到这个对象,因为对象都是指向堆内存中的,所以说我们操作的是同一个对象,我们把饮用赋给这个对象之后,用流转换成集合,就可以完成嵌套对象生成。

​ 就是我自己有想法,不知道里面具体是怎么实现的。

匹配模式

​ 它会自动遍历我们的xml文档,表达式没太明白,我觉得通过反射更好一些,这样我们只需要创建对象。当然肯定不能忘记校验,处理额外的事务,我觉得反射完全可以做到,比如我在反射中指定必须实现某个接口的类,然后当它传递这个类的时候,我就可以使用反射执行这个接口中的方法。

​ 这个使用方法就是对反射的调用,但是具体实现起来的效果很差,因为很多东西都是默认的,不需要重新去指定,完全可以通过反射来分析。

创建Server

​ 在类上注解这个类是干什么用的真的是一个非常好的习惯。

生命周期监听器

​ 我选了比较熟悉的日志监听,另外我发现第三方包里面的main方法竟然是可以运行的。以前从来第三方包没见过main方法。

    @Override
    public void lifecycleEvent(LifecycleEvent event) {
    
        if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) {
    
            log();
        }
    }

Catalina

​ 我发现了这个类,因为作者写作的时候并没有告诉我这是哪个类来实现的,所以说我通过点击方法,从而知道是哪个类。所以说能点击的方法就不要点击类

    protected boolean arguments(String args[]) {
    

        boolean isConfig = false;
        boolean isGenerateCode = false;

        if (args.length < 1) {
    
            usage();
            return false;
        }

        for (String arg : args) {
    
            if (isConfig) {
    
                configFile = arg;
                isConfig = false;
            } else if (arg.equals("-config")) {
    
                isConfig = true;
            } else if (arg.equals("-generateCode")) {
    
                setGenerateCode(true);
                isGenerateCode = true;
            } else if (arg.equals("-useGeneratedCode")) {
    
                setUseGeneratedCode(true);
                isGenerateCode = false;
            } else if (arg.equals("-nonaming")) {
    
                setUseNaming(false);
                isGenerateCode = false;
            } else if (arg.equals("-help")) {
    
                usage();
                return false;
            } else if (arg.equals("start")) {
    
                isGenerateCode = false;
                // NOOP
            } else if (arg.equals("configtest")) {
    
                isGenerateCode = false;
                // NOOP
            } else if (arg.equals("stop")) {
    
                isGenerateCode = false;
                // NOOP
            } else if (isGenerateCode) {
    
                generatedCodeLocationParameter = arg;
                isGenerateCode = false;
            } else {
    
                usage();
                return false;
            }
        }

        return true;
    }

​ 命令行参数怎么处理?我们处理命令行,然后每次都通过switch或者if-else语句来确定这个命令所执行的内容,因此我们命令行前后的顺序是可以不一致的。

createStartDigester

​ 这是上面那个类用于解析xml的具体实现,我们可以通过它直接找到到底是由哪个具体类实现了指定的接口,当然也可以debug。

​ Catalina共享Exector的级别为Service。 Catalina默认情况下未配置Executor,即不共享。

​ 什么是Exector,线程池。

​ Server解析->Engine解析->Host解析->Context解析

​ 后面我感觉没什么意思,就是读配置文件,我觉得这点,spring boot做的更加超前。

web应用加载

​ 首先是两种方式,一种是通过xml指定,这种方法灵活,可以自由定义路径,而且可以指定一些额外的内容。另一种是扫描目录,就是我最常用的,它会自动解开war包。这些是由StandardHost来完成的。

​ Deploy:部署

ExceptionUtils

​ 如果我们的类会出现加载失败的异常,我们可以给这个类设置一个静态方法,先执行这个静态方法,这样我们的类就会被加载了。

Thread.join()

​ 如果子线程执行的时间长,而且主线程需要知道子线程的结果,怎么解决这个问题呢?我能想到的是主线程用while循环去判断一个变量,但是jdk为我们提供了join方法,一旦看到了这个方法,主线程阻塞,等待子线程执行。但是你要注意,要在start后面调用它。不然拿什么去join。

Runable和Callable的区别

​ Runable没有返回值而且无法处理异常,Callable可以有返回值也可以处理异常,但是它不能再Thread中使用。不过,两者都可以在ExectorService里面使用,我们也经常通过线程池来使用。

ExectorService

​ 线程池,有execute方法,传递的是Runnable,明显没有返回值。但是submit就可以有返回值,而且返回值就是通过Future来接受的。

Future

​ get方法可以阻塞线程,直到得到我们的返回结果,我们也可以设定一个值,当等待时间太长的时候,直接放弃。当然我们可以通过判断来看看它到底有没有执行完,或者给它取消了。

Csrf

​ 利用浏览器会自动发送cookie,因为cookie有个人信息,只要我们的第三方网站让浏览器认为这是真实网站的域名,那么就会把cookie帮我们发送过去,我们只需要设置好对应的参数,而浏览器就把用户的cookie直接帮助我们带上了。所以跨域的第一个问题,就是解决什么是跨域,明显,协议,ip和端口不同,都可能是跨域。

雪花算法

​ 首先是时间戳,然后是机器id,因为机器id是我们自己指定的,所以不同机器产生的id就不需要考虑冲突的问题,然后是解决时间戳的问题,如果1ms内有好多id生产呢?那就用并发限制,1ms是真接受2^11,也就是4000多个。

​ 那么我有几个问题,首先是我觉得时长戳留的位数太长,然后是机器,我们的机器也许真的比1024要多,而且最最关键的是,我们不同表的id产生了重复根本没有任何问题,我觉得至少他要把表区别开。还有一个问题,就是1ms4096个,我觉得这个压力MySQL有点大,而且,如果产生不了的话,会对位数产生很大的浪费。

​ 算法实现,不同机器直接不一样,这个不需要我们考虑,然后是获取时间戳,并且判断和前一毫秒获取的是否相同,如果相同就要+1,但是不能超过4096。然后通过位运算合并在一起。

​ 反正我觉得不是很好。

HostConfig.deployWARs()

   protected void deployWARs(File appBase, String[] files) {
    //部署war包

        if (files == null) {
    
            return;
        }

        ExecutorService es = host.getStartStopExecutor();
        List<Future<?>> results = new ArrayList<>();//用来保存多线程调用的结果

        for (String file : files) {
    
            //这两种会被跳过,不能起这样的名字
            if (file.equalsIgnoreCase("META-INF")) {
    
                continue;
            }
            if (file.equalsIgnoreCase("WEB-INF")) {
    
                continue;
            }

            File war = new File(appBase, file);//利用指定的根目录构成文件夹
            if (file.toLowerCase(Locale.ENGLISH).endsWith(".war") && war.isFile() && !invalidWars.contains(file)) {
    //我记得在文档里,java说明了字符串可以按照区域方言来处理,比如汉字按照拼音排序
                ContextName cn = new ContextName(file, true);
                if (tryAddServiced(cn.getName())) {
    //添加成功
                    //将服务应用程序添加到列表中并指示该应用程序是否已存在于列表中
                    try {
    
                        if (deploymentExists(cn.getName())) {
    //如果部署上去了
                            DeployedApplication app = deployed.get(cn.getName());//获得这个应用
                            //没想到tomcat把我们的应用抽象成了一个对象
                            boolean unpackWAR = unpackWARs;
                            //这操作就很迷,直接复制,除非是担心多线程修改导致不统一吧
                            if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
    
                                unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
                            }
                            if (!unpackWAR && app != null) {
    //说实话,!unpackWAR的可读性很差
                                // Need to check for a directory that should not be
                                // there
                                File dir = new File(appBase, cn.getBaseName());
                                if (dir.exists()) {
    
                                    if (!app.loggedDirWarning) {
    
                                        log.warn(sm.getString("hostConfig.deployWar.hiddenDir",
                                                dir.getAbsoluteFile(), war.getAbsoluteFile()));
                                        app.loggedDirWarning = true;
                                    }
                                } else {
    
                                    app.loggedDirWarning = false;
                                }
                            }
                            removeServiced(cn.getName());//从集合中移除
                            continue;
                        }

                        // Check for WARs with /../ /./ or similar sequences in the name
                        if (!validateContextPath(appBase, cn.getBaseName())) {
    
                            log.error(sm.getString("hostConfig.illegalWarName", file));
                            invalidWars.add(file);
                            removeServiced(cn.getName());
                            continue;
                        }

                        // DeployWAR will call removeServiced
                        results.add(es.submit(new DeployWar(this, cn, war)));//实际的操作语句
                        //很明显,DeployWar实现run接口
                    } catch (Throwable t) {
    
                        ExceptionUtils.handleThrowable(t);
                        removeServiced(cn.getName());
                        throw t;
                    }
                }
            }
        }

        for (Future<?> result : results) {
    
            try {
    
                result.get();
            } catch (Exception e) {
    
                log.error(sm.getString("hostConfig.deployWar.threaded.error"), e);
            }
        }
    }

HostConfig.deployWAR()

​ 你把鼠标轻轻放在一个变量或者方法上面,idea会给你很多想要的提示!

    protected void deployWAR(ContextName cn, File war) {
    

        File xml = new File(host.getAppBaseFile(), cn.getBaseName() + "/" + Constants.ApplicationContextXml);

        File warTracker = new File(host.getAppBaseFile(), cn.getBaseName() + Constants.WarTracker);

        boolean xmlInWar = false;
        try (JarFile jar = new JarFile(war)) {
    
            JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
            if (entry != null) {
    
                xmlInWar = true;
            }
        } catch (IOException e) {
    
            /* Ignore */
        }

        boolean useXml = false;
        if (xml.exists() && unpackWARs && (!warTracker.exists() || warTracker.lastModified() == war.lastModified())) {
    //lastModified获取最后修改时间,我们可以用这个功能来实现动态刷新,利用之前的缓存
            useXml = true;
        }

        Context context = null;
        boolean deployThisXML = isDeployThisXML(war, cn);

        try {
    
            if (deployThisXML && useXml && !copyXML) {
    
                synchronized (digesterLock) {
    
                    //这个你注意一下,是在类里面的一个私有变量,是一个普通的对象,但是tomcat用它来作为锁
                    try {
    
                        context = (Context) digester.parse(xml);
                    } catch (Exception e) {
    
                        log.error(sm.getString("hostConfig.deployDescriptor.error", war.getAbsolutePath()), e);
                    } finally {
    
                        digester.reset();
                        if (context == null) {
    
                            context = new FailedContext();
                        }
                    }
                }
                context.setConfigFile(xml.toURI().toURL());
            } else if (deployThisXML && xmlInWar) {
    
                //后面是针对不同情况的加载,我删除了
            }
        } catch (Throwable t) {
    
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t);
        } finally {
    
            if (context == null) {
    
                context = new FailedContext();
            }
        }

        boolean copyThisXml = false;
        if (deployThisXML) {
    //移动xml,书上都有说,不过我们要学习的东西至少是有用的
            if (host instanceof StandardHost) {
    
                copyThisXml = ((StandardHost) host).isCopyXML();
            }

            // If Host is using default value Context can override it.
            if (!copyThisXml && context instanceof StandardContext) {
    
                copyThisXml = ((StandardContext) context).getCopyXML();
            }

            if (xmlInWar && copyThisXml) {
    
                // Change location of XML file to config base
                xml = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");
                try (JarFile jar = new JarFile(war)) {
    
                    JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
                    try (InputStream istream = jar.getInputStream(entry);
                            OutputStream ostream = new FileOutputStream(xml)) {
    
                        IOTools.flow(istream, ostream);//比如像,这个,我们学习它是怎么复制文件的
                    }
                } catch (IOException e) {
    
                    /* Ignore */
                }
            }
        }

        DeployedApplication deployedApp = new DeployedApplication(
                cn.getName(), xml.exists() && deployThisXML && copyThisXml);

        long startTime = 0;
        // Deploy the application in this WAR file
        if(log.isInfoEnabled()) {
    
            startTime = System.currentTimeMillis();
            log.info(sm.getString("hostConfig.deployWar", war.getAbsolutePath()));
        }

        try {
    
            // Populate redeploy resources with the WAR file
            deployedApp.redeployResources.put(war.getAbsolutePath(), Long.valueOf(war.lastModified()));

            if (deployThisXML && xml.exists() && copyThisXml) {
    
                deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified()));
            } else {
    
                // In case an XML file is added to the config base later
                deployedApp.redeployResources.put(
                        (new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml")).getAbsolutePath(),
                        Long.valueOf(0));
            }

            Class<?> clazz = Class.forName(host.getConfigClass());
            LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
            context.addLifecycleListener(listener);

            context.setName(cn.getName());
            context.setPath(cn.getPath());
            context.setWebappVersion(cn.getVersion());
            context.setDocBase(cn.getBaseName() + ".war");
            host.addChild(context);
        } catch (Throwable t) {
    
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t);
        } finally {
    
            // If we're unpacking WARs, the docBase will be mutated after
            // starting the context
            boolean unpackWAR = unpackWARs;
            if (unpackWAR && context instanceof StandardContext) {
    
                unpackWAR = ((StandardContext) context).getUnpackWAR();
            }
            if (unpackWAR && context.getDocBase() != null) {
    
                File docBase = new File(host.getAppBaseFile(), cn.getBaseName());
                deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified()));
                addWatchedResources(deployedApp, docBase.getAbsolutePath(), context);
                if (deployThisXML && !copyThisXml && (xmlInWar || xml.exists())) {
    
                    deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified()));
                }
            } else {
    
                // Passing null for docBase means that no resources will be
                // watched. This will be logged at debug level.
                addWatchedResources(deployedApp, null, context);
            }
            // Add the global redeploy resources (which are never deleted) at
            // the end so they don't interfere with the deletion process
            addGlobalRedeployResources(deployedApp);
        }

        deployed.put(cn.getName(), deployedApp);//前面看到的使用map去判断是否存在,就是在这个时候放进去的

        if (log.isInfoEnabled()) {
    
            log.info(sm.getString("hostConfig.deployWar.finished",
                    war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
        }
    }

​ 这个时候,war部署完成了,实际我们更多是要学会怎么实现,这种程度源码学习,我感觉只是蜻蜓点水,我只是看懂它的流程,具体的实现还是很迷

热部署

​ 怎么实现定期扫描,然后更新。ContainerBase.backgroundProcess()就是这样的周期执行的函数,它处理了热部署。还处理了集群和安全。

​ 至少我知道了一点,就是它会自动刷新部署。

StandardContext

​ 具体web应用的启动

​ {@link org.apache.catalina.util.LifecycleBase#startInternal()}
上面这种方法可以在java文档里指定的跳转到某个类的具体方法

​ 我发现书中已经把这个方法实现了什么都说明了,不过很多名词我不是太懂,所以我感觉应该先十分的熟悉一个东西的使用,然后才去学习它的源码,不然是毫无意义的。

​ 我先跳过web应用的加载,看看我最感兴趣的web请求的处理。

org.apache.catalina.mapper.Mapper

​ 维护了映射关系,我在它里面发现了addWrapper方法,它对/*等特殊的映射规则做了特殊的处理。那计算有了这个方法,什么时候调用呢?

MapperListener

Container[] conHosts = engine.findChildren();
        for (Container conHost : conHosts) {
    
            Host host = (Host) conHost;
            if (!LifecycleState.NEW.equals(host.getState())) {
    
                // Registering the host will register the context and wrappers
                registerHost(host);
            }
        }

​ 比如我监听了Host,那host肯定不止一个。然后我肯定要有一个集合,可以获取所有的Host就像是spring的bean管理。然后我会遍历这些host,获取host的state,这个state肯定是host自己维护的,然后通过对这个状态的判断做出相应的处理。

​ Wrapper:包装器。Mapper:映射器。注意一下两者的区别。

​ 最好的学习方法就是自己想怎么去实现,然后看看人家是怎么实现的,对比一下,这样才能更好的进步,然后想想,能不能有更好的实现,因为就目前来看,更好的都是再不断产出的

生命周期

​ 生命周期的一般实现是使用标志属性,一般是枚举类,然后随着生命周期的不同,改变这个属性的值,在方法执行的时候,判断并且处理就可以了。这个地方我学习到了两点,一个是生命周期它本质上是被触发执行的,并不是主动执行的,这个反转的思想真的很有用,可以化简很多问题,第二个是需要在一个集合中维护这些组件,方便我们把他们找出来遍历

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_52008536/article/details/122717241

智能推荐

Python程序员周边(一)----键盘选择+修改按键位置_82键少了哪些键位-程序员宅基地

文章浏览阅读1.5k次。Python程序员来说,啥键盘最合适?来看看Python几个有代表性的语法:1、没有分号 " ; "2、缩进代表段落3、简介,使得一行可以凑更多的代码,行数少就能完成任务4、对于新手来说,感受最深的就是各种乱七八糟的符号最麻烦,尤其是各种括号里面需要写东西的,更麻烦,比如在Pycharm中:for i in range(1,66,2): print('this is : %s' ..._82键少了哪些键位

html++相对地址什么意思,详解相对路径和绝对路径的区别-程序员宅基地

文章浏览阅读377次。HTML初学者会经常遇到这样一个问题,如何正确引用一个文件。比如,怎样在一个HTML网页中引用另外一个HTML网页作为超链接(hyperlink)?怎样在一个网页中插入一张图片如果你在引用文件时(如加入超链接,或者插入图片等),使用了错误的文件路径,就会导致引用失效(无法浏览链HTML初学者会经常遇到这样一个问题,如何正确引用一个文件。比如,怎样在一个HTML网页中引用另外一个HTML网页作为超链..._html网址的相对地址

TreeTable的应用-程序员宅基地

文章浏览阅读1.3w次。这个Jquery插件确实很实用_treetable

华为如何显示我的电脑连接到服务器地址,我的电脑如何访问服务器地址-程序员宅基地

文章浏览阅读295次。我的电脑如何访问服务器地址 内容精选换一换“包年/包月”计费模式属于预付费,即您已提前支付资源的费用,例如购买包年/包月的裸金属服务器。包年/包月资源到期前,若您未主动续费或者虽然开通了自动续费但自动续费失败,则到期后资源会被冻结,并且进入保留期。保留期到期前,若您主动续费,则资源会被解冻。保留期到期前,若您未主动续费,则保留期结束后资源会被释放。保留期的时长会根据客户的等级有所SSH密钥是一种加..._华为笔记本的服务器地址在哪里

failed to compute cache key: “/src“ not found: not found_failed to compute cache key: "/src" not found: not-程序员宅基地

文章浏览阅读8.5k次。问题情况docker执行docker build命令是出现的问题,Dockerfile如下FROM node:lts-slimLABEL maintainer knight <[email protected]>ENV NODE_ENV=productionWORKDIR /opt/k8s-mongo-sidecarCOPY package.json package-lock.json /opt/k8s-mongo-sidecar/RUN npm installCOP_failed to compute cache key: "/src" not found: not found

vSphere环境虚机如何添加共享磁盘?三步骤就对了!_只有厚置备置零磁盘可以进行磁盘共享-程序员宅基地

文章浏览阅读5.7k次,点赞3次,收藏5次。本期问题:vSphere环境虚机如何添加共享磁盘?点击查看更多技术故障解决方案前提条件:给虚拟机添加共享磁盘只能在虚拟机关机时进行,否则SCSI控制器模式无法更改。1. 选择第一台虚机,编辑设置添加新的SCSI控制器,选择虚拟或者物理模式(具体取决于需求)。添加新的硬盘,选择新添加的SCSI控制器,磁盘置备为厚置备置零(需要到存储策略中检查确认)。“虚拟设备节点”切记选择独立-持久..._只有厚置备置零磁盘可以进行磁盘共享

随便推点

将token添加到请求头中进行网络请求_nsmutableurlrequest设置token-程序员宅基地

文章浏览阅读1.7w次。//网络请求字符串NSString *urlString = @"";//UTF-8转码NSString *urlStr = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];//创建请求对象NSMutableURLRequest *request = [[AFHTTPRequestSeri_nsmutableurlrequest设置token

Android LinearLayout布局_linearlayout按比例分配-程序员宅基地

文章浏览阅读2.8k次,点赞4次,收藏6次。1. LinearLayout类线性布局(LinearLayout)将子视图元素以线性方式显示。主要属性orientation,horizontal代表水平方向,vertical代表垂直方向weight,按一定比例分配。计算完剩余视图后,按比例分配。最外层LinearLayout为垂直布局,而第一个子控件时水平布局,最后三个子控件按1:2:3分配高度&amp;amp;amp;amp;amp;amp;lt;LinearLay..._linearlayout按比例分配

java1.8安装步骤,java jdk1.8.0_221 安装步骤-程序员宅基地

文章浏览阅读717次。一、下载jdkOracle JDK下载官网https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html下载jdk1.8.0_221. 需要注册账号登陆才能下载。下载完成,双击jdk-8u221-windows-x64.exe,进行安装。二、安装jdk安装前准备工作,D盘新建文件夹JA..._jdk1.8.0_221下载官网

java高级面试题(易错题)_用throw定义了方法可能抛出的异常,那么调用此方法时一定会抛出此异常-程序员宅基地

文章浏览阅读4.6k次,点赞3次,收藏18次。5.// point Xpublic class Foo { public static void main(String[] args) throws Exception { PrintWriter out = new PrintWriter(new java.io.OutputStreamWriter(System.out), true); System.out.println("Hello"); }}选项中哪一个代码插入到point X能够使代_用throw定义了方法可能抛出的异常,那么调用此方法时一定会抛出此异常

十五个实用的mysql语句分享_分享6个有用的MySQL语句_mysql-程序员宅基地

文章浏览阅读102次。今天给大家介绍六条比较有用的mysql的SQL语句,可能很多人都通过php来实现这些功能。1. 计算年数你想通过生日来计算这个人有几岁了。SELECT DATE_FORMAT(FROM_DAYS(TO_DAYS(now()) - TO_DAYS(@dateofbirth)), '%Y') + 0;2. 两个时间的差取得两个 datetime 值的差。假设 dt1 和 dt2 是 datetime..._mysql中有什么好玩的语句

STM32 CubeMX 使用实例教程_cubemx例程-程序员宅基地

文章浏览阅读2.8k次,点赞2次,收藏8次。CubeMX实例教程本文所建立的工程,是https://blog.csdn.net/Star19180325/article/details/103267945此文的工程文件,通过此文的讲解,大家可以大致了解下CubeMx从建立工程到Creat Code过程1.首先,我们安装好此版本的CubeMX2.打开界面后本次我们以STM32F103C8类型的muc为例子在SEARCH框中输入..._cubemx例程

推荐文章

热门文章

相关标签