反射
反射可以在运行时获取一个类的所有信息,(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。把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(实例,参数数据);//让指定实例来执行该方法
- Method m = Clazz.getDeclaredMethod(方法名,参数类型列表);
动态代理
JDK动态代理
Proxy.newProxyInstance()
- newPoxyInstance 新建代理对象
- 通过这个方法可以创建代理对象
- 本质上这个 Proxy.newProxyInstance()方法的执行,做了两件事
- 在内存中动态的生成了一个代理类的字节码class
- new对象了,通过内存中生成的代理类代码,实例化了代理对象
- 关于 newProxyInstance() 方法的三个重要参数,每一个有什么含义,有什么用?
- ClassLoader loader
- 类加载器
- 在内存中生成的字节码也是class文件,要执行也得先加载到内存中,加载类就需要类加载器,所以需要指定类加载器,并且jdk要求目标类的加载器和代理类的加载器要使用同一个
- 类加载器
- Class<?>[] interfaces
- 代理类和目标类要实现同一个接口或者同一些接口
- 在内存中生成代理类的时候,这个代理类是需要告诉你告诉他实现哪些接口的
- InvocationHandler h
- 调用处理器,是一个接口
- 在调用处理器中编写的就是:增强代码
- 因为具体要增强啊什么代码,JDK动态代理技术是猜不到的
- 既然是一个接口,就要写接口的实现类
- 调用处理器,是一个接口
- ClassLoader loader
InvocationHandler
- 为什么强行要求必须实现InvocationHandler接口
- 因为一个类实现接口必须实现接口中的方法
- 以下方法必须是 invoke() ,因为JDK在底层调用 invoke() 方法的程序提前写好了
- Invoke() 方法是JDK底层负责调用的
- 因为一个类实现接口必须实现接口中的方法
- Invoke() 方法什么时候被调用
- 当代理对象调用代理方法时,注册在 InvocationHandler 调用处理器当中的 invoke() 方法被调用
- invoke() 方法三个参数
- invoke 方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这三个参数
- Object proxy 代理对象的引用,这个参数使用少
- Method method 目标对象上的目标方法 (要执行的目标方法就是它)
- Object[] arges 目标方法上的实参
- invoke 方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这三个参数
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
- intercept() 的四个参数
-
通过 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的实现类为例
-
获取类池
- ClassPool pool = ClassPool.getDefault();
-
制造类
- CtClass ctClass = pool.makeClass(String className)
- 全限定类名:com.mikasa.bank.dao.impl.AccountDaoImpl
- CtClass ctClass = pool.makeClass(String className)
-
制造接口
- CtClass ctInterface = pool.makerInterface(String interfaceName)
- 全限定接口名:com.mikasa.bank.dao.impl.AccountDao
- CtClass ctInterface = pool.makerInterface(String interfaceName)
-
实现接口 相当于: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
-
- 被织入通知的对象。
切点表达式
切点表达式用来定义通知(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
(如Attribute
、Element
或ProcessingInstruction
)上。这允许使用一行代码在整个文档中进行复杂的导航。例如
public void bar(Document document) {
List<Node> list = document.selectNodes("//foo/bar");
Node node = document.selectSingleNode("//foo/bar/author");
String name = node.valueOf("@name");
}