session与cookie
session是服务器端开辟的一块内存空间,利用session可以存储一些信息,一段时间内再次请求就不用重新生成了。一个客户端对应一个session,在tomcat中通过调用request.getSession()方法获得session。
getSession()的整体逻辑很简单,从客户端拿到sessionId,看标准会话管理器StandardManager的sessions变量(key是sessionId,value是Session类的Map集合)中是否有该sessionId,有就返回,没有看是否创建再返回,在创建的过程中会加入到sessions,同时请求头增加Set-Cookie属性,这样请求时客户端也会在请求头中添加cookie = JSESSIONID=XXX的属性。它的核心方法是Request->doGetSession()。
StandardManager是标准会话管理器,它负责管理所有会话。除了记录sessionId外,它还判断会话是否过期,主要逻辑在backgroundProcess()方法中。Tomcat容器中有一条线程专门用于执行后台处理,这个线程也是无限循环的。
ContainerBase->ContainerBackgroundProcessor->processChildren():
// 执行后台任务
container.backgroundProcess();
// 子容器调用processChildren方法
Container[] children = container.findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i].getBackgroundProcessorDelay() <= 0) {
processChildren(children[i]);
}
}
最开始是Engine,它的子容器是Host,Host子容器是Context,Context的子容器是Wrapper,标准会话管理器判断会话是否过期就是在Context的backgroundProcess()方法执行的。
当Tomcat停止时,管理器会将属于此Web应用的所有会话持久化到磁盘中(stopInternal()方法,注意是优雅地关闭服务,而不是直接停止或重新启动),文件名为SESSIONS.ser。当Tomcat启动时,又会加载此文件,体现就是sessions变量,加载完成后,会将文件删除,所以每次启动成功后就不会看到该文件的存在。
jsp与servlet
前面说过Tomcat根据不同的url匹配wrapper,后缀名.jsp匹配的是jsp的wrapper,对应的Servlet类是JspServlet,所有servlet都是从它的service(ServletRequest req, ServletResponse res)方法开始的。
jsp的重新编译机制
JSP每次都会读取被引用的JSP文件的最后修改时间,与生成的Class或者Java类的最后修改时间相比,如果不相同,会将生成的文件删除重新编译,这一段代码可见JspCompilationContext->compile():jspCompiler.isOutDated()
jsp编译文件
当获取到jsp文件资源后,jsp会根据相应的模板编译成Java文件,这一段代码可见Compiler->compile():Map<String,SmapStratum> smaps = generateJava(),然后再编译成JVM可执行的Class文件,这一段代码见Compiler->compile():generateClass(smaps)。生成Class的过程中,会将它的的最后修改时间设成Java的最后修改时间,同时有行号的对应。
我们知jsp是在html里写Java代码,Servlet是在Java里写html写代码,那么假如JSP文件长这样,编译后Java类以及主要方法如下图所示
继承的HttpJspBase是HttpServlet的子类,可见它是一个符合Servlet规范的Java类,html语句以out.write的形式被写入到请求体中。
安全策略与安全管理
源代码中经常看到类似System.getSecurityManager()、security.checkPermission(permission)、AccessController.doPrivileged()这样的代码,这与安全管理有关。安全管理指的是开发时,赋予应用一定的权限。在执行某些操作前,应当先检查权限,如果没有权限则拒绝执行。
权限检查在SpringBoot中默认不开启,开启的方式有两种,一种是在代码中添加System.setSecurityManager(new SecurityManager()),另一种是VM添加参数-Djava.security.manager,检查权限最终调用java.security.AccessController.checkPermission(Permission perm)静态方法,如果没有权限,那么会抛出AccessControlException异常。
安全策略是一个文件,指定应用应当获得什么权限,根据实际情况分配。如果不指定安全策略,则使用jre的默认安全策略,这个文件位于lib/security目录下。
指定安全策略也有两种,一种是在代码中添加System.setProperty("java.security.policy", "具体文件"),另一种是VM添加参数-Djava.security.policy=具体文件。
假设安全策略为有运行时以及系统配置的读写权限。
grant {
permission java.util.PropertyPermission "*", "read";
permission java.util.PropertyPermission "*", "write";
permission java.lang.RuntimePermission "*";
};
用下面这段代码测试
public static void main(String[] args) throws Exception {
System.setProperty("java.security.policy", "src/main/resources/policy.txt");
System.setSecurityManager(new SecurityManager());
dir();
File fs = new File(dir() + "/temp.txt");
fs.createNewFile();
}
public static String dir() {
return System.getProperty("user.dir");
}
这段代码会在执行fs.createNewFile()这一行报错。
调用不同的模块时(在这里的体现是不同的jar包),假设core模块调用common模块,那么需要两个模块都有权限才可以。Jdk提供了一种特权的机制,使用AccessController.doPrivileged()可以使调用者免除权限检查。
将安全策略文件的权限改为common模块有运行时及系统配置的权限。
grant codebase "file:demo-common/target/classes"{
permission java.util.PropertyPermission "*", "read";
permission java.util.PropertyPermission "*", "write";
permission java.lang.RuntimePermission "*";
};
common模块的代码如下
public static String getDir() throws Exception {
AccessController.doPrivileged((PrivilegedAction<String>) CommonDTO::dir);
return dir();
}
private static String dir() {
return System.getProperty("user.dir");
}
位于core模块的main函数改为:
public static void main(String[] args) throws Exception {
System.setProperty("java.security.policy", "src/main/resources/policy.txt");
System.setSecurityManager(new SecurityManager());
CommonDTO.getDir();
}
这段代码会在执行return dir()这一行报错。