Java / Web · 2021年11月20日 0

Java Web基础

前言

开始学Java了,在看《Java代码审计》。

Java EE核心技术

Java EE中有13种核心技术,包括JDBC、JNDI、EJB、RMI、Servlet、JSP、XML、JMS、Java IDL、JTS、JTA、JavaMail、JAF

Servlet

Servlet(Server Applet),是一个在Java WEB容器中运行的小程序,用于实现Java编写的动态网站。狭义的Servlet是Java语言实现的的一个接口,广义的Servlet是指任何实现了Servlet接口的类。JSP文件在运行时也会被转换为Servlet代码。

所有的Servlet类必须直接或间接地实现Java中的Servlet接口,Java中的GenericServlet类实现了Servlet接口,而HttpServlet抽象类通过继承GenericServlet类间接实现了Servlet接口,因此我们可以通过继承HttpServlet类实现一个Servlet:

package com.eastjun.servlets;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
​
@WebServlet("/hello")
public class MyServlet extends HttpServlet {
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       resp.getWriter().write("Hello World");
  }
   @Override
   protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       this.doGet(req,resp);
  }
}

这个例子中我使用了@WebServlet("/hello")注解,在Servlet3.0以前我们需要在web.xml中配置Servlet,在Servlet3.0之后可以使用更便捷的注解方式配置Servlet。(在写上面这一段代码的时候我踩了个坑,tomcat10之后Servlet依赖包名不是javax.servlet,而是jakarta.servlet,当时一直报错,一直没找到原因)

下面是一个web.xml的例子,如果不使用注解或无法使用注解,则需要在web.xml中配置Servlet

<web-app>
 <display-name>Archetype Created Web Application</display-name>
 <servlet>
   <servlet-name>MyServlet</servlet-name>
   <servlet-class>com.eastjun.servlets.MyServlet</servlet-class>
 </servlet>
 <servlet-mapping>
   <servlet-name>MyServlet</servlet-name>
   <url-pattern>/hello</url-pattern>
 </servlet-mapping>
</web-app>

在这个例子中我们在<servlet-name>中指定了Servlet的名称,然后在<servlet-class>中指定了Servlet对应的类的路径。<url-pattern>中指定组件的访问路径,可以使用字符串匹配特定的路径,还可以使用/admin/*匹配一个url模式,还可以使用*.do这样的模式拦截指定的后缀,但/user/*.do这样的操作是非法的。<servlet><servlet-mapping>标签使用标签中相同的<servlet-name>进行关联。

如果使用注解模式可以在@WebServlet中添加参数,web.xml可以配置的属性都可以通过@WebServlet进行配置,常用的属性有下面几个:

名称 参数类型 描述
name String 指定Servlet的name属性,如果没有指定则取包名+类名
value/urlPatterns String[] 两个属性相同,指定Servlet处理的url
loadOnStartup int 标记容器是否在应用启动时就加载这个Servlet
initParams WebInitParam[] 配置初始化参数
displayName String 指定Servlet显示名称
asyncSupported boolean 指定Servlet是否支持异步操作模式

Servlet接口中有5种方法,其中initservicedestroy是Servlet生命周期的方法。还有另外两种方法为getServletConfiggetServletInfo方法

方法 描述
public void init(ServletConfig config) 初始化Servlet,当Servlet对象被服务器创建时调用,仅调用一次
public void service(HttpServletRequest req, HttpServletResponse resp) 为传入的请求提供响应,由Web容器为每个请求调用一次
public void destroy() 销毁Servlet对象时调用
public ServletConfig getServletConfig() 返回ServletConfig对象
public String getServletInfo() 返回Servlet相关信息

Servlet的生命周期是Servlet从创建到销毁的整个过程,有一下几个阶段

  • init()方法

    init()方法在创建Servlet对象时被调用,只被调用一次,init()调用完成之后Servlet才能处理客户端的请求

  • service()方法

    service()方法是Servlet工作的核心方法,大概客户端访问Servlet时Web容器就会调用Servlet的service()方法处理请求

  • destroy()方法

    destroy()方法是Web容器回收Servlet对象之前调用的,只会调用一次。

HTTP协议中8中请求方法,HttpServlet抽象类中帮我们封装了doGetdoPostdoHead等方法分别用于处理HTTP协议中的这几种请求方法,在HttpServlet的源码中可以看到HttpServlet类在service()方法中判断了请求方法,然后根据请求的方法帮我们调用了对应的方法,其中部分源码大概长这样:

if (method.equals("GET")) {
   //somecode
   this.doGet(req, resp);
   //somecode
} else if (method.equals("HEAD")) {
   //somecode
   this.doHead(req, resp);
} else if (method.equals("POST")) {
   this.doPost(req, resp);
}

所以我们可以通过继承HttpServlet抽象类然后重写doGetdoPost等方法实现对GET和POST等请求方法的处理

Filter

filter是过滤器,使用filter可以动态地拦截响应和请求,可用于登录、会话检查、加解密等,它可以在后端处理客户端请求之前对请求进行拦截。我们可以通过实现java中的jakarta.servlet.Filter接口来定义一个Servlet过滤器,也可以通过继承HttpFilter类来实现一个过滤器,下面通过继承HttpFilter实现了一个拦截/admin/*的url的过滤器:

package com.eastjun.controller;
​
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
​
@WebFilter("/admin/*")
public class MyFilter extends HttpFilter {
   @Override
   protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
       HttpSession session = req.getSession();
       if(session.getAttribute("role") == null||!"admin".equals(session.getAttribute("role"))){
           session.setAttribute("role","guest");
           res.getWriter().write("Not Admin\n");
      }else{
           res.getWriter().write("Admin\n");
           chain.doFilter(req,res);//将请求传回过滤链
           res.getWriter().write("Hello");
      }
  }
}

上面的Filter的例子中使用了@WebFilter("/admin")注解,也可以通过web.xml配置过滤器

 <filter>
   <filter-name>admin</filter-name>
   <filter-class>com.eastjun.controller.MyFilter</filter-class>
 </filter>
 <filter-mapping>
   <filter-name>admin</filter-name>
   <url-pattern>/admin/*</url-pattern>
 </filter-mapping>

Filter接口中有3种方法:

方法 描述
public void init(FilterConfig filterConfig) 初始化Filter,当Filter对象被服务器创建时调用,仅调用一次
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 每次请求和相应经过过滤器链时调用一次,
public void destroy() 销毁Filter对象时调用

Filter的执行顺序是在客户端发http请求到WEB服务器,然后WEB服务器根据URL找到对应的过滤器链,如果有多个Filter则会按顺序进行过滤操作,也就是调用Filter的doFilter方法对请求进行拦截,过滤完会调用chain.doFilter方法进行放行,该方法又会调用下一个过滤器的doFilter方法进行过滤,过滤完成之后请求会到达Servlet,然后调用Servlet的service()方法对请求进行处理,处理完再将请求传回给过滤器执行chain.doFilter之后的操作。

例如上面实现的一个Filter会先输出Admin,然后调用chain.doFilter将请求传回过滤链,再经过Servlet对请求进行处理然后执行chain.doFilter后面的代码输出Hello。

如果存在多个过滤器可以在web.xml中配置执行顺序,WEB服务器会根据web.xml中<filter-mapping>定义的顺序执行过滤。

反射

反射是Java语言的特征之一,Java的反射功能是由java.lang.reflect包提供的,利用反射可以获取类内部的属性、方法和构造函数。

获取Class对象

要使用反射需要先获取待操作的类所对应的Class对象,在Java中无论类生成多少个对象都会对应同一个Class对象,通过它可以获悉整个类的结构,获取Class对象有4种方法:

  • 通过Class.forName获取

Class<?> clazz = Class.forName("com.eastjun.MyReflect.Student");
  • 通过类的.class静态属性获取Class对象

Class<?> clazz = Student.class;
  • 通过对象的getClass()方法获取

Class<?> clazz = student.getClass();
  • 通过Classloader获取

ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = classLoader.loadClass("com.eastjun.MyReflect.Student");

Class.forName()Classloader都可以对类进行加载,他们的区别在于Class.forName()将类加载到JVM中后还会执行类中的static块,而Classloader只将类加载到JVM中,只有在类实例化时才会执行static块

创建实例

创建实例主要有2种方式:

  • 使用Class对象的newInstance()方法,用这种方法只能调用类中的无参构造方法

Student student = (Student) clazz.newInstance();
  • 使用Constructor对象的newInstance()方法,使用Constructor对象的newInstance()方法可以调用有参和无参的构造方法创建对象。其中getDeclaredConstructor()方法返回的是public和非public的构造器,而getConstructor()方法只制定参数类型访问权限是public的构造器

Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
Student student = (Student) constructor.newInstance("EastJun",123);

获取类中的方法

获取类中的方法集合有下面这几种方式

  • 利用getDeclaredMethods()方法,利用getDeclaredMethods方法获取的是类中所有的方法,包括public和非public的方法,但不包括继承的方法:

Class<?> clazz = Class.forName("java.lang.Runtime");
for(Method method:clazz.getDeclaredMethods()){
   System.out.println(method.getName());
}
  • 利用getDeclaredMethod()方法,利用getDeclaredMethod()方法可以获取到类中某个特定的方法,其中第一个参数是方法名,第二个参数是方法的参数类型

Class<?> clazz = Class.forName("java.lang.Runtime");
Method method = clazz.getDeclaredMethod("exec", String.class);
System.out.println(method.getName());
  • 利用getMethods()方法,利用getMethods()方法可以获取到类中所有public的方法,其中包括父类的方法

  • 利用getMethod()方法,利用getMethod()方法可以获取到类中某个特定的方法,其中第一个参数是方法名,第二个参数是方法的参数类型

获取到Method对象后可以用invoke()方法对方法进行调用,invoke()方法的第一个参数是需要执行这个方法的对象,后面的参数是执行该方法的参数,如果调用的是静态方法则第一个参数可以置为null

Class<?> clazz = Class.forName("java.lang.Runtime");
Method method  = clazz.getDeclaredMethod("exec", String.class);
method.invoke(Runtime.getRuntime(),"calc");

获取成员变量

和Method类似,有下面4种方法获取类中的成员变量:

  • Class对象的getDeclaredFields()/getFields()方法,这两个方法获取成员变量的集合,其中getDeclaredFields()方法获取所有的成员变量,但不包括继承的成员变量,而getFields()仅获取public的成员变量,包括继承的成员变量

Class<?> clazz = Class.forName("com.eastjun.MyReflect.Student");
for(Field field:clazz.getDeclaredFields()){
   System.out.println(field.getName());
}
  • Class对象的getDeclaredField()/getField()方法,这两个方法获取特定的成员变量,其中getDeclaredField()方法获取所有的成员变量,但不包括继承的成员变量,而getField()仅获取public的成员变量,包括继承的成员变量

Field field = clazz.getDeclaredField("id");
System.out.println(field.getName());

ClassLoader

ClassLoader是java.lang包中的一个抽象类,可用于将Class的字节码加载为Class对象的的加载器,字节码可以来源于.class文件,也可以是jar包中的.class文件,还可以是远程服务器上的字节流,它的本质是一个[]byte字节数组。

双亲委派原则

在Java中有4种类加载器:

  • Bootstrap ClassLoader 启动类加载器

    由C/C++实现,Java语言无法直接操作这个类,它用于加载<JAVA_HOME>/lib目录下的类库,没有父类加载器,它不继承自java.lang.ClassLoader抽象类,没有父类加载器

  • Extention ClassLoader 标准扩展类加载器

    sun.misc.Launcher$ExtClassLoader实现,派生继承自java.lang.ClassLoader抽象类,父类加载器为Bootstrap ClassLoader,用于加载<JAVA_HOME>/lib/ext目录下的类库,或者是java.ext.dirs系统变量指定目录下的类库

  • Application ClassLoader 应用类加载器

    sun.misc.Launcher$AppClassLoader实现,由于应用类加载器是ClassLoader.getSystemClassLoader()方法的返回值,所以也被叫做系统类加载器,派生继承自java.lang.ClassLoader抽象类,父类加载器为Extention ClassLoader,用于加载环境变量classpath或系统变量java.class.path指定目录下的类库

  • User ClassLoader 用户自定义类加载器

    当上面3中加载器无法满足我们的需求时,例如需要对Class进行加密操作,则我们可以使用自定义加载器,自定义加载器可以继承java.lang.ClassLoader类,然后重写findClass()方法,也可以直接继承自java.net.URLClassLoader类,重写loadClass()方法。

4种类加载器存在着如下图所示的层次关系:

ClassLoader

每个ClassLoader实例都有一个父类加载器的引用,但这种关系并非有继承实现,而是一直包含关系,在java.lang.ClassLoader源码中可以看到这种关系是由java.lang.ClassLoader中的parent成员属性实现的

private final ClassLoader parent;
private ClassLoader(Void unused, ClassLoader parent) {
   this.parent = parent;
}

JVM在加载Class时默认采用双亲委派原则,即在某个类加载器需要加载类时,会先将加载任务委托给父类加载器,仅当父类加载器无法完成加载请求时子类加载器才会处理类加载请求,而父类加载器如果还存在父类加载器则继续向上委托,以此递归下去直到遇到启动类加载器Bootstrap ClassLoader。

java.lang.ClassLoaderloadClass函数的部分源码大概长这样,

protected Class<?> loadClass(String name, boolean resolve)
   throws ClassNotFoundException {
   synchronized (getClassLoadingLock(name)) {
       // First, check if the class has already been loaded
       Class<?> c = findLoadedClass(name);
       if (c == null) {
           try {
               if (parent != null) {
                   c = parent.loadClass(name, false);
              } else {
                   c = findBootstrapClassOrNull(name);
              }
          } catch (ClassNotFoundException e) {
               // ClassNotFoundException thrown if class not found
               // from the non-null parent class loader
          }
           if (c == null) {
               // If still not found, then invoke findClass in order
               // to find the class.
               c = findClass(name);
          }
      }
       if (resolve) {
           resolveClass(c);
      }
       return c;
  }
}

ClassLoader.loadClass()函数的源码可知ClassLoader.loadClass()加载类的逻辑大概是先检查类是否已经被加载,如果加载过则不再进行加载。然后如果存在父类加载器先使用父类加载器对类进行加载,如果不存在父类加载器则先使用Bootstrap ClassLoader对类进行加载。父类加载器加载失败则调用自身的findClass()函数对类进行加载。因此我们可以通过继承ClassLoader抽象类并重写findClass()方法实现自定义类加载器

实现自定义类加载器

实现自定义加载器需要继承ClassLoader抽象类,并将parent属性置为Application ClassLoader,然后重写findClass()方法。下面是一个自定义类加载器的简单实现:

package com.eastjun.MyReflect;
​
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
​
public class MyClassLoader extends ClassLoader {
   public MyClassLoader(){
       super(ClassLoader.getSystemClassLoader());
  }
   @Override
   protected Class<?> findClass(String name) throws ClassNotFoundException {
       String filename = "/tmp/"+name.replace(".","/")+".class";
       try {
           DataInputStream dis = new DataInputStream(
                   new BufferedInputStream(
                           new FileInputStream(filename)
                  )
          );
           int len = dis.available();
           byte[] data = new byte[len];
           int res = dis.read(data);
           return defineClass(name, data, 0, data.length);
      } catch (Exception e) {
           e.printStackTrace();
      }
       return super.findClass(name);
  }
   public static void main(String[] args) throws Exception {
       ClassLoader loader1 = new MyClassLoader();
       Class<?> clazz = loader1.loadClass("com.eastjun.MyReflect.Student");
       clazz.getDeclaredConstructor().newInstance();
  }
}

代理

代理是Java的一种设计模式,可以做到在不修改目标对象的前提下对目标对象的功能进行扩展

静态代理

需要被代理的对象与目标对象实现同样的接口或继承相同的父类,然后代理对象通过调用相同的方法来调用目标对象的方法,下面是一个静态代理的例子: Study接口:

package com.eastjun.MyProxy;
​
public interface Study {
   public void startstudy();
}

目标对象Student:

package com.eastjun.MyProxy;
​
public class Student implements Study{
   @Override
   public void startstudy() {
       System.out.println("Start Class");
  }
}

代理对象:

package com.eastjun.MyProxy;
​
public class GoodStudent implements Study{
   Study study;
   GoodStudent(Study study){
       this.study = study;
  }
   @Override
   public void startstudy() {
       System.out.println("Start Preview");
       study.startstudy();
       System.out.println("Start Review");
  }
   public static void main(String[] args){
       Study student = new Student();
       Study goodStudent = new GoodStudent(student);
       goodStudent.startstudy();
  }
}

动态代理

动态代理不需要代理对象实现与目标对象相同的接口,是使用java.lang.reflect包中的Proxy类和InvocationHandler接口实现的。代理对象需要实现InvocationHandler接口,然后使用Proxy.newProxyInstance()函数创建代理类。下面是一个动态代理的例子

Study接口:

package com.eastjun.MyProxy;
​
public interface Study {
   public void startstudy();
}

目标对象Student:

package com.eastjun.MyProxy;
​
public class Student implements Study{
   @Override
   public void startstudy() {
       System.out.println("Start Class");
  }
}

代理对象:

package com.eastjun.MyProxy;
​
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
​
public class GoodStudent implements InvocationHandler {
​
   private final Object target;
​
   GoodStudent(Object target){
       this.target = target;
  }
​
   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("Start Preview");
       Object obj = method.invoke(this.target,args);
       System.out.println("Start Review");
       return obj;
  }
​
   public static void main(String[] args){
       Student student = new Student();
       InvocationHandler invocationHandler = new GoodStudent(student);
       Study study = (Study) Proxy.newProxyInstance(Student.class.getClassLoader(),Student.class.getInterfaces(),invocationHandler);
       study.startstudy();
  }
}

Proxy.newProxyInstance()函数的源码可知动态代理生成代理对象的原理:

首先在Proxy.newProxyInstance()中可以该函数获取Class对象是用了下面这一行代码:

Class<?> cl = getProxyClass0(loader, intfs);

getProxyClass0()函数的源码如下

private static Class<?> getProxyClass0(ClassLoader loader,
                                          Class<?>... interfaces) {
   if (interfaces.length > 65535) {
       throw new IllegalArgumentException("interface limit exceeded");
  }
   // If the proxy class defined by the given loader implementing
   // the given interfaces exists, this will simply return the cached copy;
   // otherwise, it will create the proxy class via the ProxyClassFactory
   return proxyClassCache.get(loader, interfaces);
}

然后可知getProxyClass0()函数是从缓存(proxyClassCache静态属性)中获取到目标对象,注释中也说过如果代理对象没有定义,则会通过ProxyClassFactory创建一个代理对象

在Proxy类的定义中找到proxyClassCache静态属性的定义如下

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
   proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

WeakCache.get()的源码中可知它触发了ProxyClassFactoryapply()方法

value = Objects.requireNonNull(valueFactory.apply(key, parameter));

然后在ProxyClassFactory类的apply()方法中可以找到创建代理类的关键代码

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
   proxyName, interfaces, accessFlags);
try {
   return defineClass0(loader, proxyName,
                       proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
   throw new IllegalArgumentException(e.toString());
}

首先调用ProxyGenerator.generateProxyClass()方法生成代理类的字节码,然后调用defineClass0()这样一个native方法加载字节码返回Class对象

CGLib动态代理

CGLib是一个基于ASM字节码生成框架实现的第三方工具类库,使用CGLib的目标类无需实现任何接口

首先需要引入依赖:

<dependency>
   <groupId>cglib</groupId>
   <artifactId>cglib</artifactId>
   <version>3.3.0</version>
</dependency>

目标对象Student无需实现任何接口:

package com.eastjun.MyProxy;
​
public class Student{
   public void startstudy() {
       System.out.println("Start Class");
  }
}

代理对象:

package com.eastjun.MyProxy;
​
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
​
public class MyCGLib implements MethodInterceptor {
   @Override
   public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
       System.out.println("Start Preview");
       Object obj = methodProxy.invokeSuper(o, objects);
       System.out.println("Start Review");
       return obj;
  }
​
   public static void main(String[] args) {
       Enhancer enhancer = new Enhancer();
       enhancer.setSuperclass(Student.class);
       enhancer.setCallback(new MyCGLib());
       Student student = (Student)enhancer.create();
       student.startstudy();
  }
}

Referer

《Java代码审计》

http://fengziji.com/blog/10

https://jishuin.proginn.com/p/763bfbd38d57

https://juejin.cn/post/7001856930981347358#heading-0

https://dunwu.github.io/javacore/basics/java-reflection.html