手撕源码手写框架的必备知识

反射

反射可以在运行时获取一个类的所有信息,(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。把java类中的各种成分映射成一个个的Java对象。
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把一个个组成部分映射成一个个对象。

获取类对应的字节码的对象

  • 调用某个类的对象的getClass()方法,即:对象.getClass()

    • Person p = new Person();
      Class clazz = p.getClass();
      
  • 调用类的class属性类获取该类对应的Class对象,即:类名.class

    • Class clazz = Person.class;
      
  • 使用**Class类中的forName()**静态方法(最安全,性能最好)即:Class.forName(“类的全路径”)

    • Class clazz = Class.forName("类的全路径");
      

常用方法

  • 获取包名、类名
    • clazz.getPackage().getName()//包名
    • clazz.getSimpleName()//类名
    • clazz.getName()//完整类名
  • 获取成员变量定义信息
    • getFields()//获取所有公开的成员变量,包括继承变量
    • getDeclaredFields()//获取本类定义的成员变量,包括私有,但不包括继承的变量
    • getField(变量名)
    • getDeclaredField(变量名)
  • 获取构造方法定义信息
    • getConstructor(参数类型列表)//获取公开的构造方法
    • getConstructors()//获取所有的公开的构造方法
    • getDeclaredConstructors()//获取所有的构造方法,包括私有
    • getDeclaredConstructor(int.class,String.class)
  • 获取方法定义信息
    • getMethods()//获取所有可见的方法,包括继承的方法
    • getMethod(方法名,参数类型列表)
    • getDeclaredMethods()//获取本类定义的的方法,包括私有,不包括继承的方法
    • getDeclaredMethod(方法名,int.class,String.class)
  • 反射新建实例
    • clazz.newInstance();//执行无参构造创建对象
    • clazz.newInstance(222,“韦小宝”);//执行有参构造创建对象
    • clazz.getConstructor(int.class,String.class)//获取构造方法
  • 反射调用成员变量
    • clazz.getDeclaredField(变量名);//获取变量
    • clazz.setAccessible(true);//使私有成员允许访问
    • f.set(实例,值);//为指定实例的变量赋值,静态变量,第一参数给null
    • f.get(实例);//访问指定实例变量的值,静态变量,第一参数给null
  • 反射调用成员方法
    • Method m = Clazz.getDeclaredMethod(方法名,参数类型列表);
      • m.setAccessible(true);//使私有方法允许被调用
      • m.invoke(实例,参数数据);//让指定实例来执行该方法

动态代理

JDK动态代理

Proxy.newProxyInstance()

  • newPoxyInstance 新建代理对象
    • 通过这个方法可以创建代理对象
    • 本质上这个 Proxy.newProxyInstance()方法的执行,做了两件事
      • 在内存中动态的生成了一个代理类的字节码class
      • new对象了,通过内存中生成的代理类代码,实例化了代理对象
  • 关于 newProxyInstance() 方法的三个重要参数,每一个有什么含义,有什么用?
    • ClassLoader loader
      • 类加载器
        • 在内存中生成的字节码也是class文件,要执行也得先加载到内存中,加载类就需要类加载器,所以需要指定类加载器,并且jdk要求目标类的加载器和代理类的加载器要使用同一个
    • Class<?>[] interfaces
      • 代理类和目标类要实现同一个接口或者同一些接口
      • 在内存中生成代理类的时候,这个代理类是需要告诉你告诉他实现哪些接口的
    • InvocationHandler h
      • 调用处理器,是一个接口
        • 在调用处理器中编写的就是:增强代码
        • 因为具体要增强啊什么代码,JDK动态代理技术是猜不到的
        • 既然是一个接口,就要写接口的实现类

InvocationHandler

  • 为什么强行要求必须实现InvocationHandler接口
    • 因为一个类实现接口必须实现接口中的方法
      • 以下方法必须是 invoke() ,因为JDK在底层调用 invoke() 方法的程序提前写好了
      • Invoke() 方法是JDK底层负责调用的
  • Invoke() 方法什么时候被调用
    • 当代理对象调用代理方法时,注册在 InvocationHandler 调用处理器当中的 invoke() 方法被调用
  • invoke() 方法三个参数
    • invoke 方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这三个参数
      • Object proxy 代理对象的引用,这个参数使用少
      • Method method 目标对象上的目标方法 (要执行的目标方法就是它)
      • Object[] arges 目标方法上的实参

CGLIB动态代理

CGLIB既可以代理接口,又可以代理类。底层采用继承的方式实现。所以被代理的目标类不能使用final修饰

  • 1、引入它的依赖

    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>3.3.0</version>
    </dependency>
    
  • 2、创建字节码增强器

    • Enhancer enhancer = new Enhancer();
  • 3、告诉 cglib要继承的类(代理目标类/接口)

    • enhancer.setSuperclass(xxx.class)
  • 4、设置回调接口

    • enhancer.setCallback(new MyMethodInterceptor() );

      • 在CGLIB中需要提供的不是InvocationHandler,而是:net.sf.cglib.proxy.MethodInterceptor

      • 编写MethodInterceptor接口实现类:

        • intercept() 的四个参数
          • 目标对象 Object target
          • 目标方法 Method method
          • 目标方法调用时的实参 Object[] objects
          • 代理方法 MethodProxy methodProxy
      • 通过 methodProxy.invokeSuper(target, objects) 调用目标方法并在前后添加增强代码

      • public class MyMethodInterceptor implements MethodInterceptor {
                  
                  
            @Override
            public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                  
                  
                // 前增强
                long begin = System.currentTimeMillis();
                // 调用目标
                Object retValue = methodProxy.invokeSuper(target, objects);
                // 后增强
                long end = System.currentTimeMillis();
                System.out.println("耗时" + (end - begin) + "毫秒");
                // 一定要返回
                return retValue;
            }
        }
        
  • 5、生成源码,编译class,加载到JVM,并创建代理对象 enhancer.create()

  • 示例

    • public class Client {
              
              
          public static void main(String[] args) {
              
              
              // 创建字节码增强器
              Enhancer enhancer = new Enhancer();
              // 告诉cglib要继承哪个类
              enhancer.setSuperclass(UserService.class);
              // 设置回调接口
              enhancer.setCallback(new TimerMethodInterceptor());
              // 生成源码,编译class,加载到JVM,并创建代理对象
              UserService userServiceProxy = (UserService)enhancer.create();
      
              userServiceProxy.login();
              userServiceProxy.logout();
      
          }
      }
      
  • 高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数

    • –add-opens java.base/sun.net.util=ALL-UNNAMED
    • –add-opens java.base/java.lang=ALL-UNNAMED

javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库。

引入依赖

<dependency>
  <groupId>org.javassist</groupId>
  <artifactId>javassist</artifactId>
  <version>3.29.1-GA</version>
</dependency>

使用步骤

以mybatis底层动态生成Dao的实现类为例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9kkYMmUd-1678850289028)(/Users/mikasa/Library/Application Support/typora-user-images/image-20230222085548908.png)]

  • 获取类池

    • ClassPool pool = ClassPool.getDefault();
  • 制造类

    • CtClass ctClass = pool.makeClass(String className)
      • 全限定类名:com.mikasa.bank.dao.impl.AccountDaoImpl
  • 制造接口

    • CtClass ctInterface = pool.makerInterface(String interfaceName)
      • 全限定接口名:com.mikasa.bank.dao.impl.AccountDao
  • 实现接口 相当于:AccountDaoImpl implements AccountDao

    • ctClass.addInterface(ctInterface);
  • 实现接口所有方法

    • 获取接口所有的方法
      • 获取到的是接口中的所有抽象方法,必须制造方法将其实现
  • 制造方法

    • CtMethod ctMethod = CtMethod.make(String src, CtClass ctClass)

      • src: 方法代码

      • 以mybatis底层动态生成Dao的实现类为例 :
         // 获取所有的方法
         Method[] methods = daoInterface.getDeclaredMethods();
        Arrays.stream(methods).forEach(method -> {
                  
                  
        
        // public void delete(){}
        // public int update(String actno, Double balance){}
          StringBuilder methodStr = new StringBuilder();
        // 追加修饰符列表 public
        	methodStr.append("public");
        // 拼接空格
        	methodStr.append(" ")
        // 追加返回值类型 void/int 
        	methodStr.append(method.getReturnType().getName());
        // 拼接空格
        	methodStr.append(" ")
        // 追加方法名
          String methodName = method.getName();
          methodStr.append(methodName);
        // 拼接左括号
        	methodStr.append("(");
        // 拼接参数 空/String actno,Double balance
        	Class<?>[] parameterTypes = method.getParameterTypes();
            for (int i = 0; i < parameterTypes.length; i++) {
                  
                  
              // 拼接参数类型名
              methodStr.append(parameterTypes[i].getName());
              methodStr.append(" ")
              // 拼接形参名(随意起)
              methodStr.append(" arg");
              // 为了不重复如 String arg0,Double arg1
              methodStr.append(i);
              // 不是最后一个参数就需要加 , 隔开
              if (i != parameterTypes.length - 1) {
                  
                  
              methodStr.append(",");
                        }
         }
        // 拼接右括号
        	methodStr.append("){");
        	
        // 方法体中的代码很关键
        // 其中javassist 的方法必须要用全限定名.方法名调用,这个决定了 mybatis的 namespace必须是接口类的全限定名,sqlId必须是方法名,才能实现方法体代码的拼接
          String sqlId = daoInterface.getName() + "." + methodName;
          // 根据sqlSession.getConfiguration获取会话配置信息,根据sqlId(命名空间+id)获取对应标签节点
          	// 获取SqlCommondType 是一个枚举 及 SELECT/UPDATE...
          String sqlCommondTypeName = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType().name();
          // 匹配类型进行拼串
          if ("SELECT".equals(sqlCommondTypeName)) {
                  
                  
                   methodStr.append("org.apache.ibatis.session.SqlSession sqlSession = com.mikasa.bank.utils.SqlSessionUtil.openSession();");
             methodStr.append("Object obj = sqlSession.selectOne(\"" + sqlId + "\", arg0);");
             methodStr.append("return (" + returnTypeName + ")obj;"); }
          else if ("UPDATE".equals(sqlCommondTypeName)){
                  
                         methodStr.append("org.apache.ibatis.session.SqlSession sqlSession=com.mikasa.bank.utils.SqlSessionUtil.openSession();");
        	methodStr.append("int count = sqlSession.update(\"" +sqlId + "\", arg0);");
        	methodStr.append("return count;");
          }
          methodStr.append("}");
          
          
          try {
                  
                  
                        // 创建CtMethod对象
                        CtMethod ctMethod = CtMethod.make(methodStr.toString(), ctClass);
                        ctMethod.setModifiers(Modifier.PUBLIC);
                        // 将方法添加到类
                        ctClass.addMethod(ctMethod);
                    } catch (CannotCompileException e) {
                  
                  
                        throw new RuntimeException(e);
                    }
                });
                try {
                  
                  
                    // 创建代理对象
                    Class<?> aClass = ctClass.toClass();
                    Constructor<?> defaultCon = aClass.getDeclaredConstructor();
                    Object o = defaultCon.newInstance();
                    return o;
                } catch (Exception e) {
                  
                  
                    throw new RuntimeException(e);
                }
            }
        
      • ctClass: 方法的所属类

  • 将方法添加到类

    • ctClass.addMethod(CtMethod ctMethod)
  • 在内存中生成class并加载进虚拟机

    • Class<?> clazz = ctClass.toClass()
  • 创建对象

注意事项

运行要注意:加两个参数,要不然会有异常。

  • –add-opens java.base/java.lang=ALL-UNNAMED
  • –add-opens java.base/sun.net.util=ALL-UNNAMED

AOP

将与核心业务无关的代码独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。

AOP的七大术语

  • 连接点 Joinpoint

    • 在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。
  • 切点 Pointcut

    • 在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点)
  • 通知 Advice

    • 通知又叫增强,就是具体你要织入的代码。
    • 通知包括:
      • 前置通知
      • 后置通知
      • 环绕通知
      • 异常通知
      • 最终通知
  • 切面 Aspect

    • 切点 + 通知就是切面。
  • 织入 Weaving

    • 把通知应用到目标对象上的过程。
  • 代理对象 Proxy

    • 一个目标对象被织入通知后产生的新对象。
  • 目标对象 Target

    • 被织入通知的对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VjKHBh4S-1678850289029)(/Users/mikasa/Library/Application Support/typora-user-images/image-20230221155459167.png)]

切点表达式

切点表达式用来定义通知(Advice)往哪些方法上切入。

切入点表达式语法格式:

execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])

访问控制权限修饰符:

  • 可选项。
  • 没写,就是4个权限都包括。
  • 写public就表示只包括公开的方法。

返回值类型:

  • 必填项。
  • * 表示返回值类型任意。

全限定类名:

  • 可选项。
  • 两个点“…”代表当前包以及子包下的所有类。
  • 省略时表示所有的类。

方法名:

  • 必填项。
  • *表示所有方法。
  • set*表示所有的set方法。

形式参数列表:

  • 必填项

  • () 表示没有参数的方法

  • (…) 参数类型和个数随意的方法

  • (*) 只有一个参数的方法

  • (*, String) 第一个参数类型随意,第二个参数是String的。

异常:

  • 可选项。
  • 省略时表示任意异常类型。

理解以下的切点表达式 (mall下有类且有service子包)

service包下所有的类中以delete开始的所有方法

excution(* com.mikasa.mall.service.delete*(..))

mall包下所有的类的所有的方法

excution(* com.mikasa.mall..*(..))

所有类的所有方法

excution(* *(..))

Spring框架结合AspectJ框架实现的AOP

依赖

<!--spring context依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>6.0.0-M2</version>
</dependency>
<!--spring aop依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>6.0.0-M2</version>
</dependency>
<!--spring aspects依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>6.0.0-M2</version>
</dependency>

基于AspectJ的AOP注解式开发

第一步:定义目标类以及目标方法

// 目标类
public class OrderService {
    
    
    // 目标方法
    public void generate(){
    
    
        System.out.println("订单已生成!");
    }
}

第二步:定义切面类

import org.aspectj.lang.annotation.Aspect;

// 切面类
@Aspect
public class MyAspect {
    
    
}

第三步:目标类和切面类都纳入spring bean管理

  • 在目标类OrderService上添加**@Component**注解。

  • 在切面类MyAspect类上添加**@Component**注解。

第四步:在spring配置文件中添加组建扫描

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--开启组件扫描-->
    <context:component-scan base-package="com.mikasa.spring6.service"/>
</beans>

第五步:在切面类中添加通知

package com.mikasa.spring6.service;

import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;

// 切面类
@Aspect
@Component
public class MyAspect {
    
    
    // 这就是需要增强的代码(通知)
    public void advice(){
    
    
        System.out.println("我是一个通知");
    }
}

第六步:在通知上添加切点表达式

import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;

// 切面类
@Aspect
@Component
public class MyAspect {
    
    
    
    // 切点表达式
    @Before("execution(* com.mikasa.spring6.service.OrderService.*(..))")
    // 这就是需要增强的代码(通知)
    public void advice(){
    
    
        System.out.println("我是一个通知");
    }
}

注解@Before表示前置通知。

第七步:在spring配置文件中启用自动代理

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--开启组件扫描-->
    <context:component-scan base-package="com.mikasa.spring6.service"/>
    <!--开启自动代理-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
  • <aop:aspectj-autoproxy proxy-target-class=“true”/> 开启自动代理之后,凡事带有@Aspect注解的bean都会生成代理对象。

  • proxy-target-class=“true” 表示采用cglib动态代理。

  • proxy-target-class=“false” 表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类。

通知类型

通知类型包括:

  • 前置通知:@Before 目标方法执行之前的通知
  • 后置通知:@AfterReturning 目标方法执行之后的通知
  • 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
  • 异常通知:@AfterThrowing 发生异常之后执行的通知
  • 最终通知:@After 放在finally语句块中的通知

接下来,编写程序来测试这几个通知的执行顺序:

package com.mikasa.spring6.service;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 切面类
@Component
@Aspect
public class MyAspect {
    
    

    @Around("execution(* com.mikasa.spring6.service.OrderService.*(..))")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    
    
        System.out.println("环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    @Before("execution(* com.mikasa.spring6.service.OrderService.*(..))")
    public void beforeAdvice(){
    
    
        System.out.println("前置通知");
    }

    @AfterReturning("execution(* com.mikasa.spring6.service.OrderService.*(..))")
    public void afterReturningAdvice(){
    
    
        System.out.println("后置通知");
    }

    @AfterThrowing("execution(* com.mikasa.spring6.service.OrderService.*(..))")
    public void afterThrowingAdvice(){
    
    
        System.out.println("异常通知");
    }

    @After("execution(* com.mikasa.spring6.service.OrderService.*(..))")
    public void afterAdvice(){
    
    
        System.out.println("最终通知");
    }

}

切面的先后顺序

我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高

@Aspect
@Component
@Order(1) //设置优先级
public class YourAspect

@Pointcut 优化使用切点表达式

将切点表达式单独的定义出来,在需要的位置引入即可

// 切面类
@Component
@Aspect
@Order(2)
public class MyAspect {
    
    
    
    @Pointcut("execution(* com.mikasa.spring6.service.OrderService.*(..))")
    public void pointcut(){
    
    }

    @Around("pointcut()")
    public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    
    
        System.out.println("环绕通知开始");
        // 执行目标方法。
        proceedingJoinPoint.proceed();
        System.out.println("环绕通知结束");
    }

    @Before("pointcut()")
    public void beforeAdvice(){
    
    
        System.out.println("前置通知");
    }
  
}

全注解式开发AOP

就是编写一个类,在这个类上面使用大量注解来代替spring的配置文件,spring配置文件消失了,如下:

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("com.powernode.spring6.service")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Spring6Configuration {
    
    
}

测试程序也变化了:

@Test
public void testAOPWithAllAnnotation(){
    
    
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
    OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
    orderService.generate();
}

解析xml文件

dom4j

https://dom4j.github.io/#xpath

  • 创建解析器对象

    • SAXReader saxReader = new SAXReader();
      
  • 使用解析器对象读取XML文档(类路径下)生成Document对象

    • Document document = saxReader.read(Thread.currentThread().getContextClassLoader().getResource("godbatis-config.xml"));
      

常用API说明

  • org.dom4j**.Document**常用方法

    • Element getRootElement() 获取XML文件的根节点
  • org.dom4j.Element常用方法

    • getName() 返回元素名称 return String
    • elements() 获取标签的子标签 return List
    • attributeValue(String name) 获取指定属性名称的属性值 return String
    • getText() 获取标签的文本 return String
    • elementText(String name) 获取指定名称的子标签的文本 return String

XPath快速导航xml

XPath api https://blog.csdn.net/weixin_45468845/article/details/108657663

XPath表达式可以在Document或树中的任何Node(如AttributeElementProcessingInstruction)上。这允许使用一行代码在整个文档中进行复杂的导航。例如

public void bar(Document document) {
    
    
  
    List<Node> list = document.selectNodes("//foo/bar");

    Node node = document.selectSingleNode("//foo/bar/author");

    String name = node.valueOf("@name");
}

猜你喜欢

转载自blog.csdn.net/weixin_52156647/article/details/129549309