开始学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种方法,其中init
、service
、destroy
是Servlet生命周期的方法。还有另外两种方法为getServletConfig
和getServletInfo
方法
方法 | 描述 |
---|---|
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抽象类中帮我们封装了doGet
、doPost
、doHead
等方法分别用于处理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抽象类然后重写doGet
、doPost
等方法实现对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实例都有一个父类加载器的引用,但这种关系并非有继承实现,而是一直包含关系,在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.ClassLoader
中loadClass
函数的部分源码大概长这样,
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()
的源码中可知它触发了ProxyClassFactory
的apply()
方法
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代码审计》
https://jishuin.proginn.com/p/763bfbd38d57
https://juejin.cn/post/7001856930981347358#heading-0