mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 14:43:42 +08:00
del
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
<audio id="audio" title="31 | Logger组件:Tomcat的日志框架及实战" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/06/7b/06f1a6e8d3084c0929d5d807504b9c7b.mp3"></audio>
|
||||
|
||||
每一个系统都有一些通用的模块,比如日志模块、异常处理模块、工具类等,对于Tomcat来说,比较重要的通用模块有日志、Session管理和集群管理。从今天开始我会分三期来介绍通用模块,今天这一期先来讲日志模块。
|
||||
|
||||
日志模块作为一个通用的功能,在系统里通常会使用第三方的日志框架。Java的日志框架有很多,比如:JUL(Java Util Logging)、Log4j、Logback、Log4j2、Tinylog等。除此之外,还有JCL(Apache Commons Logging)和SLF4J这样的“门面日志”。下面是SLF4J与日志框架Logback、Log4j的关系图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a2/2b/a263790cd53456273d50ab754063a72b.png" alt="">
|
||||
|
||||
我先来解释一下什么是“门面日志”。“门面日志”利用了设计模式中的门面模式思想,对外提供一套通用的日志记录的API,而不提供具体的日志输出服务,如果要实现日志输出,需要集成其他的日志框架,比如Log4j、Logback、Log4j2等。
|
||||
|
||||
这种门面模式的好处在于,记录日志的API和日志输出的服务分离开,代码里面只需要关注记录日志的API,通过SLF4J指定的接口记录日志;而日志输出通过引入JAR包的方式即可指定其他的日志框架。当我们需要改变系统的日志输出服务时,不用修改代码,只需要改变引入日志输出框架JAR包。
|
||||
|
||||
今天我们就来看看Tomcat的日志模块是如何实现的。默认情况下,Tomcat使用自身的JULI作为Tomcat内部的日志处理系统。JULI的日志门面采用了JCL;而JULI的具体实现是构建在Java原生的日志系统`java.util.logging`之上的,所以在看JULI的日志系统之前,我先简单介绍一下Java的日志系统。
|
||||
|
||||
## Java日志系统
|
||||
|
||||
Java的日志包在`java.util.logging`路径下,包含了几个比较重要的组件,我们通过一张图来理解一下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5a/1d/5aae1574eb4e0c33da06484011bb121d.png" alt="">
|
||||
|
||||
从图上我们看到这样几个重要的组件:
|
||||
|
||||
- Logger:用来记录日志的类。
|
||||
- Handler:规定了日志的输出方式,如控制台输出、写入文件。
|
||||
- Level:定义了日志的不同等级。
|
||||
- Formatter:将日志信息格式化,比如纯文本、XML。
|
||||
|
||||
我们可以通过下面的代码来使用这些组件:
|
||||
|
||||
```
|
||||
public static void main(String[] args) {
|
||||
Logger logger = Logger.getLogger("com.mycompany.myapp");
|
||||
logger.setLevel(Level.FINE);
|
||||
logger.setUseParentHandlers(false);
|
||||
Handler hd = new ConsoleHandler();
|
||||
hd.setLevel(Level.FINE);
|
||||
logger.addHandler(hd);
|
||||
logger.info("start log");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## JULI
|
||||
|
||||
JULI对日志的处理方式与Java自带的基本一致,但是Tomcat中可以包含多个应用,而每个应用的日志系统应该相互独立。Java的原生日志系统是每个JVM有一份日志的配置文件,这不符合Tomcat多应用的场景,所以JULI重新实现了一些日志接口。
|
||||
|
||||
**DirectJDKLog**
|
||||
|
||||
Log的基础实现类是DirectJDKLog,这个类相对简单,就包装了一下Java的Logger类。但是它也在原来的基础上进行了一些修改,比如修改默认的格式化方式。
|
||||
|
||||
**LogFactory**
|
||||
|
||||
Log使用了工厂模式来向外提供实例,LogFactory是一个单例,可以通过SeviceLoader为Log提供自定义的实现版本,如果没有配置,就默认使用DirectJDKLog。
|
||||
|
||||
```
|
||||
private LogFactory() {
|
||||
// 通过ServiceLoader尝试加载Log的实现类
|
||||
ServiceLoader<Log> logLoader = ServiceLoader.load(Log.class);
|
||||
Constructor<? extends Log> m=null;
|
||||
|
||||
for (Log log: logLoader) {
|
||||
Class<? extends Log> c=log.getClass();
|
||||
try {
|
||||
m=c.getConstructor(String.class);
|
||||
break;
|
||||
}
|
||||
catch (NoSuchMethodException | SecurityException e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
//如何没有定义Log的实现类,discoveredLogConstructor为null
|
||||
discoveredLogConstructor = m;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
下面的代码是LogFactory的getInstance方法:
|
||||
|
||||
```
|
||||
public Log getInstance(String name) throws LogConfigurationException {
|
||||
//如果discoveredLogConstructor为null,也就没有定义Log类,默认用DirectJDKLog
|
||||
if (discoveredLogConstructor == null) {
|
||||
return DirectJDKLog.getInstance(name);
|
||||
}
|
||||
|
||||
try {
|
||||
return discoveredLogConstructor.newInstance(name);
|
||||
} catch (ReflectiveOperationException | IllegalArgumentException e) {
|
||||
throw new LogConfigurationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**Handler**
|
||||
|
||||
在JULI中就自定义了两个Handler:FileHandler和AsyncFileHandler。FileHandler可以简单地理解为一个在特定位置写文件的工具类,有一些写操作常用的方法,如open、write(publish)、close、flush等,使用了读写锁。其中的日志信息通过Formatter来格式化。
|
||||
|
||||
AsyncFileHandler继承自FileHandler,实现了异步的写操作。其中缓存存储是通过阻塞双端队列LinkedBlockingDeque来实现的。当应用要通过这个Handler来记录一条消息时,消息会先被存储到队列中,而在后台会有一个专门的线程来处理队列中的消息,取出的消息会通过父类的publish方法写入相应文件内。这样就可以在大量日志需要写入的时候起到缓冲作用,防止都阻塞在写日志这个动作上。需要注意的是,我们可以为阻塞双端队列设置不同的模式,在不同模式下,对新进入的消息有不同的处理方式,有些模式下会直接丢弃一些日志:
|
||||
|
||||
```
|
||||
OVERFLOW_DROP_LAST:丢弃栈顶的元素
|
||||
OVERFLOW_DROP_FIRSH:丢弃栈底的元素
|
||||
OVERFLOW_DROP_FLUSH:等待一定时间并重试,不会丢失元素
|
||||
OVERFLOW_DROP_CURRENT:丢弃放入的元素
|
||||
|
||||
```
|
||||
|
||||
**Formatter**
|
||||
|
||||
Formatter通过一个format方法将日志记录LogRecord转化成格式化的字符串,JULI提供了三个新的Formatter。
|
||||
|
||||
- OnlineFormatter:基本与Java自带的SimpleFormatter格式相同,不过把所有内容都写到了一行中。
|
||||
- VerbatimFormatter:只记录了日志信息,没有任何额外的信息。
|
||||
- JdkLoggerFormatter:格式化了一个轻量级的日志信息。
|
||||
|
||||
**日志配置**
|
||||
|
||||
Tomcat的日志配置文件为Tomcat文件夹下`conf/logging.properties`。我来拆解一下这个配置文件,首先可以看到各种Handler的配置:
|
||||
|
||||
```
|
||||
handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3manager.org.apache.juli.AsyncFileHandler, 4host-manager.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
|
||||
|
||||
.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
|
||||
|
||||
```
|
||||
|
||||
以`1catalina.org.apache.juli.AsyncFileHandler`为例,数字是为了区分同一个类的不同实例;catalina、localhost、manager和host-manager是Tomcat用来区分不同系统日志的标志;后面的字符串表示了Handler具体类型,如果要添加Tomcat服务器的自定义Handler,需要在字符串里添加。
|
||||
|
||||
接下来是每个Handler设置日志等级、目录和文件前缀,自定义的Handler也要在这里配置详细信息:
|
||||
|
||||
```
|
||||
1catalina.org.apache.juli.AsyncFileHandler.level = FINE
|
||||
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
|
||||
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
|
||||
1catalina.org.apache.juli.AsyncFileHandler.maxDays = 90
|
||||
1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8
|
||||
|
||||
```
|
||||
|
||||
## Tomcat + SLF4J + Logback
|
||||
|
||||
在今天文章开头我提到,SLF4J和JCL都是日志门面,那它们有什么区别呢?它们的区别主要体现在日志服务类的绑定机制上。JCL采用运行时动态绑定的机制,在运行时动态寻找和加载日志框架实现。
|
||||
|
||||
SLF4J日志输出服务绑定则相对简单很多,在编译时就静态绑定日志框架,只需要提前引入需要的日志框架。另外Logback可以说Log4j的进化版,在性能和可用性方面都有所提升。你可以参考官网上这篇[文章](https://logback.qos.ch/reasonsToSwitch.html)来了解Logback的优势。
|
||||
|
||||
基于此我们来实战一下如何将Tomcat默认的日志框架切换成为“SLF4J + Logback”。具体的步骤是:
|
||||
|
||||
1.根据你的Tomcat版本,从[这里](https://github.com/tomcat-slf4j-logback/tomcat-slf4j-logback/releases)下载所需要文件。解压后你会看到一个类似于Tomcat目录结构的文件夹。<br>
|
||||
2.替换或拷贝下列这些文件到Tomcat的安装目录:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/17/09c024a49b055a86c961ea4a3beb6717.jpg" alt="">
|
||||
|
||||
3.删除`<Tomcat>/conf/logging.properties`<br>
|
||||
4.启动Tomcat
|
||||
|
||||
## 本期精华
|
||||
|
||||
今天我们谈了日志框架与日志门面的区别,以及Tomcat的日志模块是如何实现的。默认情况下,Tomcat的日志模板叫作JULI,JULI的日志门面采用了JCL,而具体实现是基于Java默认的日志框架Java Util Logging,Tomcat在Java Util Logging基础上进行了改造,使得它自身的日志框架不会影响Web应用,并且可以分模板配置日志的输出文件和格式。最后我分享了如何将Tomcat的日志模块切换到时下流行的“SLF4J + Logback”,希望对你有所帮助。
|
||||
|
||||
## 课后思考
|
||||
|
||||
Tomcat独立部署时,各种日志都输出到了相应的日志文件,假如Spring Boot以内嵌式的方式运行Tomcat,这种情况下Tomcat的日志都输出到哪里去了?
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
<audio id="audio" title="32 | Manager组件:Tomcat的Session管理机制解析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/80/7e/80ea367a5d5d2b6f99bac315a3d1147e.mp3"></audio>
|
||||
|
||||
我们可以通过Request对象的getSession方法来获取Session,并通过Session对象来读取和写入属性值。而Session的管理是由Web容器来完成的,主要是对Session的创建和销毁,除此之外Web容器还需要将Session状态的变化通知给监听者。
|
||||
|
||||
当然Session管理还可以交给Spring来做,好处是与特定的Web容器解耦,Spring Session的核心原理是通过Filter拦截Servlet请求,将标准的ServletRequest包装一下,换成Spring的Request对象,这样当我们调用Request对象的getSession方法时,Spring在背后为我们创建和管理Session。
|
||||
|
||||
那么Tomcat的Session管理机制我们还需要了解吗?我觉得还是有必要,因为只有了解这些原理,我们才能更好的理解Spring Session,以及Spring Session为什么设计成这样。今天我们就从Session的创建、Session的清理以及Session的事件通知这几个方面来了解Tomcat的Session管理机制。
|
||||
|
||||
## Session的创建
|
||||
|
||||
Tomcat中主要由每个Context容器内的一个Manager对象来管理Session。默认实现类为StandardManager。下面我们通过它的接口来了解一下StandardManager的功能:
|
||||
|
||||
```
|
||||
public interface Manager {
|
||||
public Context getContext();
|
||||
public void setContext(Context context);
|
||||
public SessionIdGenerator getSessionIdGenerator();
|
||||
public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator);
|
||||
public long getSessionCounter();
|
||||
public void setSessionCounter(long sessionCounter);
|
||||
public int getMaxActive();
|
||||
public void setMaxActive(int maxActive);
|
||||
public int getActiveSessions();
|
||||
public long getExpiredSessions();
|
||||
public void setExpiredSessions(long expiredSessions);
|
||||
public int getRejectedSessions();
|
||||
public int getSessionMaxAliveTime();
|
||||
public void setSessionMaxAliveTime(int sessionMaxAliveTime);
|
||||
public int getSessionAverageAliveTime();
|
||||
public int getSessionCreateRate();
|
||||
public int getSessionExpireRate();
|
||||
public void add(Session session);
|
||||
public void changeSessionId(Session session);
|
||||
public void changeSessionId(Session session, String newId);
|
||||
public Session createEmptySession();
|
||||
public Session createSession(String sessionId);
|
||||
public Session findSession(String id) throws IOException;
|
||||
public Session[] findSessions();
|
||||
public void load() throws ClassNotFoundException, IOException;
|
||||
public void remove(Session session);
|
||||
public void remove(Session session, boolean update);
|
||||
public void addPropertyChangeListener(PropertyChangeListener listener)
|
||||
public void removePropertyChangeListener(PropertyChangeListener listener);
|
||||
public void unload() throws IOException;
|
||||
public void backgroundProcess();
|
||||
public boolean willAttributeDistribute(String name, Object value);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
不出意外我们在接口中看到了添加和删除Session的方法;另外还有load和unload方法,它们的作用是分别是将Session持久化到存储介质和从存储介质加载Session。
|
||||
|
||||
当我们调用`HttpServletRequest.getSession(true)`时,这个参数true的意思是“如果当前请求还没有Session,就创建一个新的”。那Tomcat在背后为我们做了些什么呢?
|
||||
|
||||
HttpServletRequest是一个接口,Tomcat实现了这个接口,具体实现类是:`org.apache.catalina.connector.Request`。
|
||||
|
||||
但这并不是我们拿到的Request,Tomcat为了避免把一些实现细节暴露出来,还有基于安全上的考虑,定义了Request的包装类,叫作RequestFacade,我们可以通过代码来理解一下:
|
||||
|
||||
```
|
||||
public class Request implements HttpServletRequest {}
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
public class RequestFacade implements HttpServletRequest {
|
||||
protected Request request = null;
|
||||
|
||||
public HttpSession getSession(boolean create) {
|
||||
return request.getSession(create);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
因此我们拿到的Request类其实是RequestFacade,RequestFacade的getSession方法调用的是Request类的getSession方法,我们继续来看Session具体是如何创建的:
|
||||
|
||||
```
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Manager manager = context.getManager();
|
||||
if (manager == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
session = manager.createSession(sessionId);
|
||||
session.access();
|
||||
|
||||
```
|
||||
|
||||
从上面的代码可以看出,Request对象中持有Context容器对象,而Context容器持有Session管理器Manager,这样通过Context组件就能拿到Manager组件,最后由Manager组件来创建Session。
|
||||
|
||||
因此最后还是到了StandardManager,StandardManager的父类叫ManagerBase,这个createSession方法定义在ManagerBase中,StandardManager直接重用这个方法。
|
||||
|
||||
接着我们来看ManagerBase的createSession是如何实现的:
|
||||
|
||||
```
|
||||
@Override
|
||||
public Session createSession(String sessionId) {
|
||||
//首先判断Session数量是不是到了最大值,最大Session数可以通过参数设置
|
||||
if ((maxActiveSessions >= 0) &&
|
||||
(getActiveSessions() >= maxActiveSessions)) {
|
||||
rejectedSessions++;
|
||||
throw new TooManyActiveSessionsException(
|
||||
sm.getString("managerBase.createSession.ise"),
|
||||
maxActiveSessions);
|
||||
}
|
||||
|
||||
// 重用或者创建一个新的Session对象,请注意在Tomcat中就是StandardSession
|
||||
// 它是HttpSession的具体实现类,而HttpSession是Servlet规范中定义的接口
|
||||
Session session = createEmptySession();
|
||||
|
||||
|
||||
// 初始化新Session的值
|
||||
session.setNew(true);
|
||||
session.setValid(true);
|
||||
session.setCreationTime(System.currentTimeMillis());
|
||||
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
|
||||
String id = sessionId;
|
||||
if (id == null) {
|
||||
id = generateSessionId();
|
||||
}
|
||||
session.setId(id);// 这里会将Session添加到ConcurrentHashMap中
|
||||
sessionCounter++;
|
||||
|
||||
//将创建时间添加到LinkedList中,并且把最先添加的时间移除
|
||||
//主要还是方便清理过期Session
|
||||
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
|
||||
synchronized (sessionCreationTiming) {
|
||||
sessionCreationTiming.add(timing);
|
||||
sessionCreationTiming.poll();
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
到此我们明白了Session是如何创建出来的,创建出来后Session会被保存到一个ConcurrentHashMap中:
|
||||
|
||||
```
|
||||
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
```
|
||||
|
||||
请注意Session的具体实现类是StandardSession,StandardSession同时实现了`javax.servlet.http.HttpSession`和`org.apache.catalina.Session`接口,并且对程序员暴露的是StandardSessionFacade外观类,保证了StandardSession的安全,避免了程序员调用其内部方法进行不当操作。StandardSession的核心成员变量如下:
|
||||
|
||||
```
|
||||
public class StandardSession implements HttpSession, Session, Serializable {
|
||||
protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();
|
||||
protected long creationTime = 0L;
|
||||
protected transient volatile boolean expiring = false;
|
||||
protected transient StandardSessionFacade facade = null;
|
||||
protected String id = null;
|
||||
protected volatile long lastAccessedTime = creationTime;
|
||||
protected transient ArrayList<SessionListener> listeners = new ArrayList<>();
|
||||
protected transient Manager manager = null;
|
||||
protected volatile int maxInactiveInterval = -1;
|
||||
protected volatile boolean isNew = false;
|
||||
protected volatile boolean isValid = false;
|
||||
protected transient Map<String, Object> notes = new Hashtable<>();
|
||||
protected transient Principal principal = null;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Session的清理
|
||||
|
||||
我们再来看看Tomcat是如何清理过期的Session。在Tomcat[热加载和热部署](https://time.geekbang.org/column/article/104423)的文章里,我讲到容器组件会开启一个ContainerBackgroundProcessor后台线程,调用自己以及子容器的backgroundProcess进行一些后台逻辑的处理,和Lifecycle一样,这个动作也是具有传递性的,也就是说子容器还会把这个动作传递给自己的子容器。你可以参考下图来理解这个过程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/eb/3b2dfa635469c0fe7e3a17e2517c53eb.jpg" alt="">
|
||||
|
||||
其中父容器会遍历所有的子容器并调用其backgroundProcess方法,而StandardContext重写了该方法,它会调用StandardManager的backgroundProcess进而完成Session的清理工作,下面是StandardManager的backgroundProcess方法的代码:
|
||||
|
||||
```
|
||||
public void backgroundProcess() {
|
||||
// processExpiresFrequency 默认值为6,而backgroundProcess默认每隔10s调用一次,也就是说除了任务执行的耗时,每隔 60s 执行一次
|
||||
count = (count + 1) % processExpiresFrequency;
|
||||
if (count == 0) // 默认每隔 60s 执行一次 Session 清理
|
||||
processExpires();
|
||||
}
|
||||
|
||||
/**
|
||||
* 单线程处理,不存在线程安全问题
|
||||
*/
|
||||
public void processExpires() {
|
||||
|
||||
// 获取所有的 Session
|
||||
Session sessions[] = findSessions();
|
||||
int expireHere = 0 ;
|
||||
for (int i = 0; i < sessions.length; i++) {
|
||||
// Session 的过期是在isValid()方法里处理的
|
||||
if (sessions[i]!=null && !sessions[i].isValid()) {
|
||||
expireHere++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
backgroundProcess由Tomcat后台线程调用,默认是每隔10秒调用一次,但是Session的清理动作不能太频繁,因为需要遍历Session列表,会耗费CPU资源,所以在backgroundProcess方法中做了取模处理,backgroundProcess调用6次,才执行一次Session清理,也就是说Session清理每60秒执行一次。
|
||||
|
||||
## Session事件通知
|
||||
|
||||
按照Servlet规范,在Session的生命周期过程中,要将事件通知监听者,Servlet规范定义了Session的监听器接口:
|
||||
|
||||
```
|
||||
public interface HttpSessionListener extends EventListener {
|
||||
//Session创建时调用
|
||||
public default void sessionCreated(HttpSessionEvent se) {
|
||||
}
|
||||
|
||||
//Session销毁时调用
|
||||
public default void sessionDestroyed(HttpSessionEvent se) {
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
注意到这两个方法的参数都是HttpSessionEvent,所以Tomcat需要先创建HttpSessionEvent对象,然后遍历Context内部的LifecycleListener,并且判断是否为HttpSessionListener实例,如果是的话则调用HttpSessionListener的sessionCreated方法进行事件通知。这些事情都是在Session的setId方法中完成的:
|
||||
|
||||
```
|
||||
session.setId(id);
|
||||
|
||||
@Override
|
||||
public void setId(String id, boolean notify) {
|
||||
//如果这个id已经存在,先从Manager中删除
|
||||
if ((this.id != null) && (manager != null))
|
||||
manager.remove(this);
|
||||
|
||||
this.id = id;
|
||||
|
||||
//添加新的Session
|
||||
if (manager != null)
|
||||
manager.add(this);
|
||||
|
||||
//这里面完成了HttpSessionListener事件通知
|
||||
if (notify) {
|
||||
tellNew();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从代码我们看到setId方法调用了tellNew方法,那tellNew又是如何实现的呢?
|
||||
|
||||
```
|
||||
public void tellNew() {
|
||||
|
||||
// 通知org.apache.catalina.SessionListener
|
||||
fireSessionEvent(Session.SESSION_CREATED_EVENT, null);
|
||||
|
||||
// 获取Context内部的LifecycleListener并判断是否为HttpSessionListener
|
||||
Context context = manager.getContext();
|
||||
Object listeners[] = context.getApplicationLifecycleListeners();
|
||||
if (listeners != null && listeners.length > 0) {
|
||||
|
||||
//创建HttpSessionEvent
|
||||
HttpSessionEvent event = new HttpSessionEvent(getSession());
|
||||
for (int i = 0; i < listeners.length; i++) {
|
||||
//判断是否是HttpSessionListener
|
||||
if (!(listeners[i] instanceof HttpSessionListener))
|
||||
continue;
|
||||
|
||||
HttpSessionListener listener = (HttpSessionListener) listeners[i];
|
||||
//注意这是容器内部事件
|
||||
context.fireContainerEvent("beforeSessionCreated", listener);
|
||||
//触发Session Created 事件
|
||||
listener.sessionCreated(event);
|
||||
|
||||
//注意这也是容器内部事件
|
||||
context.fireContainerEvent("afterSessionCreated", listener);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面代码的逻辑是,先通过StandardContext将HttpSessionListener类型的Listener取出,然后依次调用它们的sessionCreated方法。
|
||||
|
||||
## 本期精华
|
||||
|
||||
今天我们从Request谈到了Session的创建、销毁和事件通知,里面涉及不少相关的类,下面我画了一张图帮你理解和消化一下这些类的关系:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/cf/11493762a465c27152dbb4aa4b563ecf.jpg" alt="">
|
||||
|
||||
Servlet规范中定义了HttpServletRequest和HttpSession接口,Tomcat实现了这些接口,但具体实现细节并没有暴露给开发者,因此定义了两个包装类,RequestFacade和StandardSessionFacade。
|
||||
|
||||
Tomcat是通过Manager来管理Session的,默认实现是StandardManager。StandardContext持有StandardManager的实例,并存放了HttpSessionListener集合,Session在创建和销毁时,会通知监听器。
|
||||
|
||||
## 课后思考
|
||||
|
||||
TCP连接的过期时间和Session的过期时间有什么区别?
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
<audio id="audio" title="33 | Cluster组件:Tomcat的集群通信原理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4c/ad/4cc3977b530ed4e0f67c38e639cc38ad.mp3"></audio>
|
||||
|
||||
为了支持水平扩展和高可用,Tomcat提供了集群部署的能力,但与此同时也带来了分布式系统的一个通用问题,那就是如何在集群中的多个节点之间保持数据的一致性,比如会话(Session)信息。
|
||||
|
||||
要实现这一点,基本上有两种方式,一种是把所有Session数据放到一台服务器或者一个数据库中,集群中的所有节点通过访问这台Session服务器来获取数据。另一种方式就是在集群中的节点间进行Session数据的同步拷贝,这里又分为两种策略:第一种是将一个节点的Session拷贝到集群中其他所有节点;第二种是只将一个节点上的Session数据拷贝到另一个备份节点。
|
||||
|
||||
对于Tomcat的Session管理来说,这两种方式都支持。今天我们就来看看第二种方式的实现原理,也就是Tomcat集群通信的原理和配置方法,最后通过官网上的一个例子来了解下Tomcat集群到底是如何工作的。
|
||||
|
||||
## 集群通信原理
|
||||
|
||||
要实现集群通信,首先要知道集群中都有哪些成员。Tomcat是通过**组播**(Multicast)来实现的。那什么是组播呢?为了理解组播,我先来说说什么是“单播”。网络节点之间的通信就好像是人们之间的对话一样,一个人对另外一个人说话,此时信息的接收和传递只在两个节点之间进行,比如你在收发电子邮件、浏览网页时,使用的就是单播,也就是我们熟悉的“点对点通信”。
|
||||
|
||||
如果一台主机需要将同一个消息发送多个主机逐个传输,效率就会比较低,于是就出现组播技术。组播是**一台主机向指定的一组主机发送数据报包**,组播通信的过程是这样的:每一个Tomcat节点在启动时和运行时都会周期性(默认500毫秒)发送组播心跳包,同一个集群内的节点都在相同的**组播地址**和**端口**监听这些信息;在一定的时间内(默认3秒)不发送**组播报文**的节点就会被认为已经崩溃了,会从集群中删去。因此通过组播,集群中每个成员都能维护一个集群成员列表。
|
||||
|
||||
## 集群通信配置
|
||||
|
||||
有了集群成员的列表,集群中的节点就能通过TCP连接向其他节点传输Session数据。Tomcat通过SimpleTcpCluster类来进行会话复制(In-Memory Replication)。要开启集群功能,只需要将`server.xml`里的这一行的注释去掉就行:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/89/45760766a99cad8a1e7001beae6d5589.png" alt="">
|
||||
|
||||
变成这样:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/99/bb/99d7bd2cebfd19dfd4d702351a4450bb.png" alt="">
|
||||
|
||||
虽然只是简单的一行配置,但这一行配置等同于下面这样的配置,也就是说Tomcat给我们设置了很多默认参数,这些参数都跟集群通信有关。
|
||||
|
||||
```
|
||||
<!--
|
||||
SimpleTcpCluster是用来复制Session的组件。复制Session有同步和异步两种方式:
|
||||
同步模式下,向浏览器的发送响应数据前,需要先将Session拷贝到其他节点完;
|
||||
异步模式下,无需等待Session拷贝完成就可响应。异步模式更高效,但是同步模式
|
||||
可靠性更高。
|
||||
同步异步模式由channelSendOptions参数控制,默认值是8,为异步模式;4是同步模式。
|
||||
在异步模式下,可以通过加上"拷贝确认"(Acknowledge)来提高可靠性,此时
|
||||
channelSendOptions设为10
|
||||
-->
|
||||
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
|
||||
channelSendOptions="8">
|
||||
<!--
|
||||
Manager决定如何管理集群的Session信息。
|
||||
Tomcat提供了两种Manager:BackupManager和DeltaManager。
|
||||
BackupManager-集群下的某一节点的Session,将复制到一个备份节点。
|
||||
DeltaManager- 集群下某一节点的Session,将复制到所有其他节点。
|
||||
DeltaManager是Tomcat默认的集群Manager。
|
||||
|
||||
expireSessionsOnShutdown-设置为true时,一个节点关闭时,
|
||||
将导致集群下的所有Session失效
|
||||
notifyListenersOnReplication-集群下节点间的Session复制、
|
||||
删除操作,是否通知session listeners
|
||||
|
||||
maxInactiveInterval-集群下Session的有效时间(单位:s)。
|
||||
maxInactiveInterval内未活动的Session,将被Tomcat回收。
|
||||
默认值为1800(30min)
|
||||
-->
|
||||
<Manager className="org.apache.catalina.ha.session.DeltaManager"
|
||||
expireSessionsOnShutdown="false"
|
||||
notifyListenersOnReplication="true"/>
|
||||
|
||||
<!--
|
||||
Channel是Tomcat节点之间进行通讯的工具。
|
||||
Channel包括5个组件:Membership、Receiver、Sender、
|
||||
Transport、Interceptor
|
||||
-->
|
||||
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
|
||||
<!--
|
||||
Membership维护集群的可用节点列表。它可以检查到新增的节点,
|
||||
也可以检查没有心跳的节点
|
||||
className-指定Membership使用的类
|
||||
address-组播地址
|
||||
port-组播端口
|
||||
frequency-发送心跳(向组播地址发送UDP数据包)的时间间隔(单位:ms)。
|
||||
dropTime-Membership在dropTime(单位:ms)内未收到某一节点的心跳,
|
||||
则将该节点从可用节点列表删除。默认值为3000。
|
||||
-->
|
||||
<Membership className="org.apache.catalina.tribes.membership.
|
||||
McastService"
|
||||
address="228.0.0.4"
|
||||
port="45564"
|
||||
frequency="500"
|
||||
dropTime="3000"/>
|
||||
|
||||
<!--
|
||||
Receiver用于各个节点接收其他节点发送的数据。
|
||||
接收器分为两种:BioReceiver(阻塞式)、NioReceiver(非阻塞式)
|
||||
|
||||
className-指定Receiver使用的类
|
||||
address-接收消息的地址
|
||||
port-接收消息的端口
|
||||
autoBind-端口的变化区间,如果port为4000,autoBind为100,
|
||||
接收器将在4000-4099间取一个端口进行监听。
|
||||
selectorTimeout-NioReceiver内Selector轮询的超时时间
|
||||
maxThreads-线程池的最大线程数
|
||||
-->
|
||||
<Receiver className="org.apache.catalina.tribes.transport.nio.
|
||||
NioReceiver"
|
||||
address="auto"
|
||||
port="4000"
|
||||
autoBind="100"
|
||||
selectorTimeout="5000"
|
||||
maxThreads="6"/>
|
||||
|
||||
<!--
|
||||
Sender用于向其他节点发送数据,Sender内嵌了Transport组件,
|
||||
Transport真正负责发送消息。
|
||||
-->
|
||||
<Sender className="org.apache.catalina.tribes.transport.
|
||||
ReplicationTransmitter">
|
||||
<!--
|
||||
Transport分为两种:bio.PooledMultiSender(阻塞式)
|
||||
和nio.PooledParallelSender(非阻塞式),PooledParallelSender
|
||||
是从tcp连接池中获取连接,可以实现并行发送,即集群中的节点可以
|
||||
同时向其他所有节点发送数据而互不影响。
|
||||
-->
|
||||
<Transport className="org.apache.catalina.tribes.
|
||||
transport.nio.PooledParallelSender"/>
|
||||
</Sender>
|
||||
|
||||
<!--
|
||||
Interceptor : Cluster的拦截器
|
||||
TcpFailureDetector-TcpFailureDetector可以拦截到某个节点关闭
|
||||
的信息,并尝试通过TCP连接到此节点,以确保此节点真正关闭,从而更新集
|
||||
群可用节点列表
|
||||
-->
|
||||
<Interceptor className="org.apache.catalina.tribes.group.
|
||||
interceptors.TcpFailureDetector"/>
|
||||
|
||||
<!--
|
||||
MessageDispatchInterceptor-查看Cluster组件发送消息的
|
||||
方式是否设置为Channel.SEND_OPTIONS_ASYNCHRONOUS,如果是,
|
||||
MessageDispatchInterceptor先将等待发送的消息进行排队,
|
||||
然后将排好队的消息转给Sender。
|
||||
-->
|
||||
<Interceptor className="org.apache.catalina.tribes.group.
|
||||
interceptors.MessageDispatchInterceptor"/>
|
||||
</Channel>
|
||||
|
||||
<!--
|
||||
Valve : Tomcat的拦截器,
|
||||
ReplicationValve-在处理请求前后打日志;过滤不涉及Session变化的请求。
|
||||
-->
|
||||
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
|
||||
filter=""/>
|
||||
<Valve className="org.apache.catalina.ha.session.
|
||||
JvmRouteBinderValve"/>
|
||||
|
||||
<!--
|
||||
Deployer用于集群的farm功能,监控应用中文件的更新,以保证集群中所有节点
|
||||
应用的一致性,如某个用户上传文件到集群中某个节点的应用程序目录下,Deployer
|
||||
会监测到这一操作并把文件拷贝到集群中其他节点相同应用的对应目录下以保持
|
||||
所有应用的一致,这是一个相当强大的功能。
|
||||
-->
|
||||
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
|
||||
tempDir="/tmp/war-temp/"
|
||||
deployDir="/tmp/war-deploy/"
|
||||
watchDir="/tmp/war-listen/"
|
||||
watchEnabled="false"/>
|
||||
|
||||
<!--
|
||||
ClusterListener : 监听器,监听Cluster组件接收的消息
|
||||
使用DeltaManager时,Cluster接收的信息通过ClusterSessionListener
|
||||
传递给DeltaManager,从而更新自己的Session列表。
|
||||
-->
|
||||
<ClusterListener className="org.apache.catalina.ha.session.
|
||||
ClusterSessionListener"/>
|
||||
|
||||
</Cluster>
|
||||
|
||||
```
|
||||
|
||||
从上面的的参数列表可以看到,默认情况下Session管理组件DeltaManager会在节点之间拷贝Session,DeltaManager采用的一种all-to-all的工作方式,即集群中的节点会把Session数据向所有其他节点拷贝,而不管其他节点是否部署了当前应用。当集群节点数比较少时,比如少于4个,这种all-to-all的方式是不错的选择;但是当集群中的节点数量比较多时,数据拷贝的开销成指数级增长,这种情况下可以考虑BackupManager,BackupManager只向一个备份节点拷贝数据。
|
||||
|
||||
在大体了解了Tomcat集群实现模型后,就可以对集群作出更优化的配置了。Tomcat推荐了一套配置,使用了比DeltaManager更高效的BackupManager,并且通过ReplicationValve设置了请求过滤。
|
||||
|
||||
这里还请注意在一台服务器部署多个节点时需要修改Receiver的侦听端口,另外为了在节点间高效地拷贝数据,所有Tomcat节点最好采用相同的配置,具体配置如下:
|
||||
|
||||
```
|
||||
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
|
||||
channelSendOptions="6">
|
||||
|
||||
<Manager className="org.apache.catalina.ha.session.BackupManager"
|
||||
expireSessionsOnShutdown="false"
|
||||
notifyListenersOnReplication="true"
|
||||
mapSendOptions="6"/>
|
||||
|
||||
<Channel className="org.apache.catalina.tribes.group.
|
||||
GroupChannel">
|
||||
|
||||
<Membership className="org.apache.catalina.tribes.membership.
|
||||
McastService"
|
||||
address="228.0.0.4"
|
||||
port="45564"
|
||||
frequency="500"
|
||||
dropTime="3000"/>
|
||||
|
||||
<Receiver className="org.apache.catalina.tribes.transport.nio.
|
||||
NioReceiver"
|
||||
address="auto"
|
||||
port="5000"
|
||||
selectorTimeout="100"
|
||||
maxThreads="6"/>
|
||||
|
||||
<Sender className="org.apache.catalina.tribes.transport.
|
||||
ReplicationTransmitter">
|
||||
<Transport className="org.apache.catalina.tribes.transport.
|
||||
nio.PooledParallelSender"/>
|
||||
</Sender>
|
||||
|
||||
<Interceptor className="org.apache.catalina.tribes.group.
|
||||
interceptors.TcpFailureDetector"/>
|
||||
|
||||
<Interceptor className="org.apache.catalina.tribes.group.
|
||||
interceptors.MessageDispatchInterceptor"/>
|
||||
|
||||
<Interceptor className="org.apache.catalina.tribes.group.
|
||||
interceptors.ThroughputInterceptor"/>
|
||||
</Channel>
|
||||
|
||||
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
|
||||
filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\
|
||||
.htm|.*\.html|.*\.css|.*\.txt"/>
|
||||
|
||||
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
|
||||
tempDir="/tmp/war-temp/"
|
||||
deployDir="/tmp/war-deploy/"
|
||||
watchDir="/tmp/war-listen/"
|
||||
watchEnabled="false"/>
|
||||
|
||||
<ClusterListener className="org.apache.catalina.ha.session.
|
||||
ClusterSessionListener"/>
|
||||
</Cluster>
|
||||
|
||||
```
|
||||
|
||||
## 集群工作过程
|
||||
|
||||
Tomcat的官网给出了一个例子,来说明Tomcat集群模式下是如何工作的,以及Tomcat集群是如何实现高可用的。比如集群由Tomcat A和Tomcat B两个Tomcat实例组成,按照时间先后顺序发生了如下事件:
|
||||
|
||||
**1. Tomcat A启动**
|
||||
|
||||
Tomcat A启动过程中,当Host对象被创建时,一个Cluster组件(默认是SimpleTcpCluster)被关联到这个Host对象。当某个应用在`web.xml`中设置了Distributable时,Tomcat将为此应用的上下文环境创建一个DeltaManager。SimpleTcpCluster启动Membership服务和Replication服务。
|
||||
|
||||
**2. Tomcat B启动(在Tomcat A之后启动)**
|
||||
|
||||
首先Tomcat B会执行和Tomcat A一样的操作,然后SimpleTcpCluster会建立一个由Tomcat A和Tomcat B组成的Membership。接着Tomcat B向集群中的Tomcat A请求Session数据,如果Tomcat A没有响应Tomcat B的拷贝请求,Tomcat B会在60秒后time out。在Session数据拷贝完成之前Tomcat B不会接收浏览器的请求。
|
||||
|
||||
**3. Tomcat A接收HTTP请求,创建Session 1**
|
||||
|
||||
Tomcat A响应客户请求,在把结果发送回客户端之前,ReplicationValve会拦截当前请求(如果Filter中配置了不需拦截的请求类型,这一步就不会进行,默认配置下拦截所有请求),如果发现当前请求更新了Session,就调用Replication服务建立TCP连接将Session拷贝到Membership列表中的其他节点即Tomcat B。在拷贝时,所有保存在当前Session中的可序列化的对象都会被拷贝,而不仅仅是发生更新的部分。
|
||||
|
||||
**4. Tomcat A崩溃**
|
||||
|
||||
当Tomcat A崩溃时,Tomcat B会被告知Tomcat A已从集群中退出,然后Tomcat B就会把Tomcat A从自己的Membership列表中删除。并且Tomcat B的Session更新时不再往Tomcat A拷贝,同时负载均衡器会把后续的HTTP请求全部转发给Tomcat B。在此过程中所有的Session数据不会丢失。
|
||||
|
||||
**5. Tomcat B接收Tomcat A的请求**
|
||||
|
||||
Tomcat B正常响应本应该发往Tomcat A的请求,因为Tomcat B保存了Tomcat A的所有Session数据。
|
||||
|
||||
**6. Tomcat A重新启动**
|
||||
|
||||
Tomcat A按步骤1、2操作启动,加入集群,并从Tomcat B拷贝所有Session数据,拷贝完成后开始接收请求。
|
||||
|
||||
**7. Tomcat A接收请求,Session 1被用户注销**
|
||||
|
||||
Tomcat继续接收发往Tomcat A的请求,Session 1设置为失效。请注意这里的失效并非因为Tomcat A处于非活动状态超过设置的时间,而是应用程序执行了注销的操作(比如用户登出)而引起的Session失效。这时Tomcat A向Tomcat B发送一个Session 1 Expired的消息,Tomcat B收到消息后也会把Session 1设置为失效。
|
||||
|
||||
**8. Tomcat B接收到一个新请求,创建Session 2**
|
||||
|
||||
同理这个新的Session也会被拷贝到Tomcat A。
|
||||
|
||||
**9. Tomcat A上的Session 2过期**
|
||||
|
||||
因超时原因引起的Session失效Tomcat A无需通知Tomcat B,Tomcat B同样知道Session 2已经超时。因此对于Tomcat集群有一点非常重要,**所有节点的操作系统时间必须一致**。不然会出现某个节点Session已过期而在另一节点此Session仍处于活动状态的现象。
|
||||
|
||||
## 本期精华
|
||||
|
||||
今天我谈了Tomcat的集群工作原理和配置方式,还通过官网上的一个例子说明了Tomcat集群的工作过程。Tomcat集群对Session的拷贝支持两种方式:DeltaManager和BackupManager。
|
||||
|
||||
当集群中节点比较少时,可以采用DeltaManager,因为Session数据在集群中各个节点都有备份,任何一个节点崩溃都不会对整体造成影响,可靠性比较高。
|
||||
|
||||
当集群中节点数比较多时,可以采用BackupManager,这是因为一个节点的Session只会拷贝到另一个节点,数据拷贝的开销比较少,同时只要这两个节点不同时崩溃,Session数据就不会丢失。
|
||||
|
||||
## 课后思考
|
||||
|
||||
在Tomcat官方推荐的配置里,ReplicationValve被配置成下面这样:
|
||||
|
||||
```
|
||||
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
|
||||
filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\
|
||||
.htm|.*\.html|.*\.css|.*\.txt"/>
|
||||
|
||||
```
|
||||
|
||||
你是否注意到,filter的值是一些JS文件或者图片等,这是为什么呢?
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<audio id="audio" title="特别放送 | 如何持续保持对学习的兴趣?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e9/ef/e9eba83a465fad6c5b225273c26228ef.mp3"></audio>
|
||||
|
||||
你好,我是李号双。今天我们抛开技术本身的内容,来聊聊专栏或者一门新技术的学习方法,我也分享一下自己是如何啃下Tomcat和Jetty源码的。
|
||||
|
||||
专栏如今已经更新完了五个模块,我们学习了Tomcat和Jetty的整体架构、连接器、容器和通用组件,这些内容可以说是Tomcat和Jetty的设计核心。在日常工作的使用中,我们使用到了Tomcat和Jetty提供的功能,我希望通过学习专栏,还能帮你了解这些功能是如何实现的,以及Tomcat和Jetty在设计时都考虑了哪些地方。
|
||||
|
||||
所以在学习专栏时,你不妨思考这样一个问题,假如让你来设计并实现一个Web容器,你会怎么做呢?如何合理设计顶层模块?如何考虑方方面面的需求,比如最基本的功能需求是加载和运行Web程序,最重要的非功能需求是高性能、高并发。你可以顺着这两条线先思考下你会怎么做,然后再回过头来看看Tomcat和Jetty是如何做到的。这样的学习方法其实就在有意识地训练自己独立设计一个系统的能力,不管是对于学习这个专栏还是其他技术,带着问题再去学习都会有所帮助。
|
||||
|
||||
说完关于专栏的学习方法,下面我必须要鼓励一下坚持学习到现在的你。专栏从第三模块开始,开始讲解连接器、容器和通用组件的设计和原理,有些内容可能比较偏向底层,确实难度比较大,如果对底层源码不熟悉或者不感兴趣,学习起来会有些痛苦。但是,我之所以设计了这部分内容,就是希望能够揭开Tomcat和Jetty的内部细节,因为任何一个优秀的中间件之所以可以让用户使用比较容易,其内部一定都是很复杂的。这也从侧面传递出一个信号:美好的东西都是有代价的,需要也值得我们去付出时间和精力。
|
||||
|
||||
我和你一样我们都身处IT行业,这个行业技术更新迭代非常快,因此我们需要以一个开放的心态持续学习。而学习恰恰又是一个反人性的过程,甚至是比较痛苦的,尤其是有些技术框架本身比较庞大,设计得非常复杂,我们在学习初期很容易遇到“挫折感”,一些技术点怎么想也想不明白,往往也会有放弃的想法。我同样经历过这个过程,**我的经验是找到适合自己的学习方法非常重要,同样关键的是要保持学习的兴趣和动力**。
|
||||
|
||||
举个我学习Spring框架的例子,记得当时我在接触Spring框架的时候,一开始就钻进一个模块开始啃起了源代码。由于Spring框架本身比较庞杂,分很多模块,当时给我最直观的感受就是看不懂,我不明白代码为什么要这么写,为什么设计得这么“绕”。这里面的问题是,首先**我还没弄清楚森林长什么样子,就盯着树叶看**,很可能是盲人摸象,看不到全貌和整体的设计思路。第二个问题是我还没学会用Spring,就开始研究它是如何设计的,结果可想而知,也遇到了挫折。后来我逐渐总结出一些学习新技术的小经验:在学习一门技术的时候,一定要先看清它的全貌,我推荐先看官方文档,看看都有哪些模块、整体上是如何设计的。接着我们先不要直接看源码,而是要动手跑一跑官网上的例子,或者用这个框架实现一个小系统,关键是要学会怎么使用。只有在这个基础上,才能深入到特定模块,去研究设计思路,或者深入到某一模块源码之中。这样在学习的过程中,按照一定的顺序一步一步来,就能够即时获得**成就感**,有了成就感你才会更加专注,才会愿意花更多时间和精力去深入研究。因此要保持学习的兴趣,我觉得有两个方面比较重要:
|
||||
|
||||
第一个是我们需要**带着明确的目标去学习**。比如某些知识点是面试的热点,那学习目标就是彻底理解和掌握它,当被问到相关问题时,你的回答能够使得面试官对你刮目相看,有时候往往凭着某一个亮点就能影响最后的录用结果。再比如你想掌握一门新技术来解决工作上的问题,那你的学习目标应该是不但要掌握清楚原理,还要能真正的将新技术合理运用到实际工作中,解决实际问题,产生实际效果。我们学习了Tomcat和Jetty的责任链模式,是不是在实际项目中的一些场景下就可以用到这种设计呢?再比如学习了调优方法,是不是可以在生产环境里解决性能问题呢?总之技术需要变现才有学习动力。
|
||||
|
||||
第二个是**一定要动手实践**。只有动手实践才会让我们对技术有最直观的感受。有时候我们听别人讲经验和理论,感觉似乎懂了,但是过一段时间便又忘记了。如果我们动手实践了,特别是在这个过程中碰到了一些问题,通过网上查找资料,或者跟同事讨论解决了问题,这便是你积累的宝贵经验,想忘记都难。另外适当的动手实践能够树立起信心,培养起兴趣,这跟玩游戏上瘾有点类似,通过打怪升级,一点点积累起成就感。比如学习了Tomcat的线程池实现,我们就可以自己写一个定制版的线程池;学习了Tomcat的类加载器,我们也可以自己动手写一个类加载器。
|
||||
|
||||
专栏更新到现在,内容最难的部分已经结束,在后面的实战调优模块,我在设计内容时都安排了实战环节。毕竟调优本身就是一个很贴近实际场景的话题,应该基于特定场景,去解决某个性能问题,而不是为了调优而调优。所以这部分内容也更贴近实际工作场景,你可以尝试用我前面讲的方法,带着问题学习后面的专栏。
|
||||
|
||||
调优的过程中需要一些知识储备,比如我们需要掌握操作系统、JVM以及网络通信的原理,这些原理在专栏前面的文章也讲到过。虽然涉及很多原理也很复杂,并不是说要面面俱到,我们也不太容易深入到每个细节,所以最关键的是要弄懂相关参数的含义,比如JVM内存的参数、GC的参数、Linux内核的相关参数等。
|
||||
|
||||
除此之外,调优的过程还需要借助大量的工具,包括性能监控工具、日志分析工具、网络抓包工具和流量压测工具等,熟练使用这些工具也是每一个后端程序员必须掌握的看家本领,因此在实战环节,我也设计了一些场景来带你熟悉这些工具。
|
||||
|
||||
说了那么多,就是希望你保持对学习的热情,树立明确的目标,再加上亲自动手实践。专栏学习到现在这个阶段,是时候开始动手实践了,希望你每天都能积累一点,每天都能有所进步。
|
||||
|
||||
最后欢迎你在留言区分享一下你学习一门新技术的方法和心得,与我和其他同学一起讨论。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user