决战圣地玛丽乔亚Day42---AOP实现相关以及Mybatis解析

AOP:

我们有一个简单的java类,我们希望给操作前后加日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserService {
     
    public void saveUser(User user) {
        // 保存用户到数据库
        System.out.println("Save user: " + user);
    }
     
    public User getUserById(int userId) {
        // 从数据库获取用户信息
        User user = new User(userId, "John Doe");
        System.out.println("Get user: " + user);
        return user;
    }
     
}

定义一个切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Aspect
public class LogAspect {
     
    @Before("execution(public * com.example.UserService.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before " + joinPoint.getSignature().getName() + "()");
    }
     
    @After("execution(public * com.example.UserService.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("After " + joinPoint.getSignature().getName() + "()");
    }
     
}

在这里:

1
@Before("execution(public * com.example.UserService.*(..))")

@Before注解中的表达式"execution(public * com.example.UserService.*(..))"表示切入点表达式,用来匹配目标方法。它的具体含义如下:

  • execution: 指定切入点类型为方法执行,即目标方法执行时触发通知。
  • public: 指定目标方法的访问修饰符为public。
  • *: 指定目标方法的返回值类型为任意类型。
  • com.example.UserService: 指定目标方法所在的类为com.example.UserService。
  • *: 指定目标方法的方法名为任意名称。
  • (..): 指定目标方法的参数为任意个数、任意类型的参数。

综上所述,该切入点表达式表示匹配com.example.UserService类中的所有公有方法,无论方法名和参数如何,只要是public修饰符的方法都会被匹配到。在LogAspect切面中使用@Before注解标注的logBefore方法会在匹配到的目标方法执行前执行。

使用@Aspect注解标注的切面类会被Spring容器自动扫描并注册成一个切面,无需在其他类中进行显式声明。当目标类中的方法符合指定的切入点表达式时,切面类中对应的通知方法会被自动执行,从而实现AOP的功能。

需要注意的是,使用@Aspect注解标注的切面类,还需要同时使用@Component或@Configuration等注解将其标注为一个Bean,以使其能够被Spring容器正确地管理和注入到其他Bean中。

 

具体来说,在Spring容器实例化所有bean的过程中,如果发现某个bean需要进行AOP增强,Spring就会为这个bean创建一个代理对象,并将代理对象替换掉原来的bean对象。

为bean创建代理对象:

1.有接口,动态代理。

2.无接口的类,进行CGLIB代理。CGLIB库是一个强大的第三方库,它通过在运行时动态生成目标对象的子类来实现代理。

这样,当其他bean调用该bean的方法时,实际上是调用了代理对象的方法,从而实现了AOP增强。这个过程是在bean的post-processing阶段完成的。

Spring都会为代理对象添加拦截器,从而实现AOP增强。

动态代理是如何对实现AOP增强的类创建代理对象的?

  1. 首先,需要定义一个实现了InvocationHandler接口的拦截器类,该拦截器类中实现了需要增强的目标方法的增强逻辑。
  2. 当需要创建代理对象时,Spring会使用Java自带的Proxy类动态地生成一个(指定拦截器)的代理对象,将该代理对象返回给调用方。
  3. 当调用代理对象的某个方法时,该方法会被拦截器的invoke()方法拦截。在invoke()方法中,我们可以在目标方法执行前后执行一些额外的操作,比如记录日志、处理事务等。
  4. 最后,拦截器将增强后的方法结果返回给代理对象,从而实现了AOP增强。 需要注意的是,基于接口的代理只能代理实现了接口的类,如果目标对象没有实现接口,就需要使用基于类的代理来实现AOP增强。在基于类的代理中,Spring会使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 定义一个接口
public interface HelloService {
    void sayHello(String name);
}
// 实现接口的类
public class HelloServiceImpl implements HelloService {
    public void sayHello(String name) {
        System.out.println("Hello, " + name);
    }
}
// 拦截器类,在目标方法执行前后记录日志
public class LogInterceptor implements InvocationHandler {
    private Object target;
    public LogInterceptor(Object target) {
        this.target = target;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoking " + method.getName());
        Object result = method.invoke(target, args);  //
        System.out.println("After invoking " + method.getName());
        return result;
    }
}
// 创建代理对象
public class ProxyFactory {
    public static <T> T createProxy(T target, Class<?> iface) {
        return (T) Proxy.newProxyInstance(
                iface.getClassLoader(),
                new Class<?>[]{iface},
                new LogInterceptor(target)
        );
    }
}
// 测试代码
public class Test {
    public static void main(String[] args) {
        HelloService helloService = new HelloServiceImpl();
        // 创建代理对象
        HelloService proxy = ProxyFactory.createProxy(helloService, HelloService.class);
        // 调用代理对象的方法
        proxy.sayHello("world");
    }

在上面的例子中,我们定义了一个接口HelloService和一个实现类HelloServiceImpl,并且定义了一个拦截器类LogInterceptor,并重写拦截器的invoke方法,具体的AOP的逻辑。

ProxyFactory类中,我们使用Java自带的Proxy类动态地为HelloServiceImpl创建了一个代理对象,并将该代理对象返回给调用方。

1
2
如果createProxy生成HelloService的代理对象。应该是<br>HelloService helloService = new HelloServiceImpl();<br><br>ProxyFactory.createProxy(helloService, HelloService.class){<br>return (T) Proxy.newProxyInstance(
                HelloService.class.getClassLoader(),<br>                new Class<?>[]{HelloService.class},<br>                new LogInterceptor(helloService)<br>        );<br>}<br>第一个参数是类加载器。 <br>第二个参数是代理对象需要实现的接口。<br>第三个参数是指定的拦截器对象,在代理对象方法执行的前后进行拦截。

 

Test类中,我们创建了一个实现了HelloService接口的代理对象,并调用了代理对象的sayHello()方法。 

当调用代理对象的sayHello()方法时,该方法会被拦截器的invoke()方法拦截。在invoke()方法中,我们在目标方法执行前后记录了日志,从而实现了AOP增强。最后,拦截器将增强后的方法结果返回给代理对象,从而实现了AOP增强。  

如果是使用CGLIB进行增强,CGLIB库生成一个目标对象的子类,并在子类中重写需要增强的方法,从而实现代理对象的创建和AOP增强。

 

总结:

首先在Spring容器实例化的过程中,会发现需要AOP增强的bean,会为这种bean通过JDK动态代理或者CGLIB库的方式创建一个代理对象(取决于是否有接口)

这个代理对象的创建,以JDK动态代理为例,首先需要一个实现拦截器的InvocationHandler接口,并在invoke方法写AOP的增强实现。

需要创建代理bean的时候,java通过proxy.newProxyInstance方法(指定增强方法的拦截器和需要增强的接口)生成代理对象

我们通过这个代理对象去调用需要增强的接口的方法时,就会调用拦截器的invoke方法对接口方法进行增强。

 

 

 

MYBATIS:

拦截器插件

本质上是jdk动态代理和责任链设计模式的综合运用

使用场景:

  1. SQL语句性能监控:可以通过拦截器插件统计SQL语句的执行时间、执行次数等信息,以便进行性能监控和优化。
  2. 分页查询:可以通过拦截器插件在查询SQL语句前后进行分页处理,以实现分页查询功能。
  3. 数据库分库分表:可以通过拦截器插件在查询SQL语句前后进行分库分表处理,以实现数据库分库分表功能。
  4. 缓存处理:可以通过拦截器插件在查询SQL语句前后进行缓存处理,以提高查询性能。
  5. 数据库路由:可以通过拦截器插件在查询SQL语句前后进行数据库路由处理,以实现数据库读写分离等功能。
  6. 数据脱敏:可以通过拦截器插件在查询SQL语句结果集处理前进行数据脱敏处理,以保护敏感数据的安全性。 总之,Mybatis的拦截器插件可以应用于任何需要在Mybatis执行SQL语句过程中进行拦截和修改的场景,具有很大的扩展性和灵活

首先要知道mybatis的执行过程:

 

 

 

  • mybatis在执行过程中按照Executor => StatementHandler => ParameterHandler => ResultSetHandler。
  • Executor在执行过程中会创建StatementHandler,在创建StatementHandler过程中会创建 ParameterHandler和ResultSetHandler

我们可以再以下的四个方法使用拦截器进行拦截:

Executor 【SQL执行器】【update,query,commit,rollback】
StatementHandler 【Sql语法构建器对象】【prepare,parameterize,batch,update,query等】
ParameterHandler 【参数处理器】【getParameterObject,setParameters等】
ResultSetHandler 【结果集处理器】【handleResultSets,handleOuputParameters等

一个自定义拦截器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {<br>//拦截的逻辑
    return invocation.proceed();
  }
  public Object plugin(Object target) {<br>//生成代理类
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {<br>//属性的操作
  }
}

全局配置的xml文件:

1
2
3
<plugins>
    <plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"></plugin>
</plugins>

XMLConfigBuilder解析MyBatis全局配置文件的pluginElement私有方法:

1
2
3
4
5
6
7
8
9
10
11
private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        <strong>String interceptor = child.getStringAttribute("interceptor")</strong>;
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) <strong>resolveClass(interceptor).newInstance()</strong>;
        interceptorInstance.setProperties(properties);
        <strong>configuration.addInterceptor(interceptorInstance);</strong>
      }
    }
}

xml配置文件通过获取属性为 "interceptor"  ,去反射的方式解析对应的类并实例化。

然后调用 configuration.addInterceptor(interceptorInstance); 

这一步相当于在InterceptorChain中把拦截器存起来

 

 

 

 什么是 InterceptorChain?

 

 这里最重要的方法就是pluginAll,他会对List<Interceptor> interceptors进行遍历,调用每一个拦截器的plugin方法。

intercept:在拦截目标对象的方法时,实际执行的增强逻辑,我们一般在该方法中实现自定义逻辑

plugin:用于返回原生目标对象或它的代理对象,当返回的是代理对象的时候,会调用intercept方法

setProperties:可以用于读取配置文件中通过property标签配置的一些属性,设置一些属性变量

Plugin.wrap(target, this) : target是被代理的对象,this是使用的拦截器,这个方法是将被代理对象和拦截器绑定,生成一个代理对象。

调用这个代理对象的时候,会通过拦截器进行处理。

target 参数通常是一个 Executor 对象,表示执行 SQL 语句的执行器,因为拦截器通常是用来拦截 SQL 语句执行的过程。而 this 参数则表示拦截器对象本身,即实现了 Interceptor 接口的对象,它的作用是对 SQL 语句执行过程进行拦截和处理。

1
invocation.proceed()的理解:

执行 SQL 语句的过程中,每次方法调用都会被封装成一个 Invocation 对象

invocation 参数就表示一次方法调用的信息,包括目标对象、目标方法和方法参数等信息。拦截器在拦截目标方法时,可以通过 invocation.proceed() 方法来执行目标方法,并获取方法执行的结果。

 对于plugin.wrap方法的解析:

 

这个方法的作用是根据@Interceptors注解和@Signature的method和args属性,得到一个type为key,value为Set<Method>

例如:
@Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})

会返回一个key为Executor,value为集合(这个集合只有一个元素,也就是Method实例,这个Method实例就是Executor接口的update方法,且这个方法带有MappedStatement和Object类型的参数

【Excutor,{(Exceutor.method(MappedStatement.class,Object.class))}】

再来看wrap的getAllInterface方法。

 根据目标实例target(这个target就是之前所说的MyBatis拦截器可以拦截的类,Executor,ParameterHandler,ResultSetHandler,StatementHandler)和它的父类们,返回signatureMap中含有target实现的接口数组。

所以综上所述,plugin这个类就是根据@Interceptors注解和@Signature找到对应方法,最终根据调用的target对象实现的接口决定是否返回一个代理对象替代原先的target对象。

代理对象会拦截target的所有方法,并在方法执行前后执行intercept逻辑

 

在Configuration类中,我们实例化四个核心方法的时候,会调用拦截器链的pluginAll方法,对拦截器的plugin依次调用

 

总结:首先需要在mybatis-config.xml文件中配置自定义插件:

1
2
3
4
5
<configuration>
    <plugins>
        <plugin interceptor="com.example.mybatis.interceptor.PerformanceInterceptor"/>
    </plugins>
</configuration>

然后 config类,调用pluginElement根据xml配置解析对应的拦截器,加入拦截器链。

在四个核心方法实例化的时候,调用pluginAll,pluginAll方法再去分别调用每个拦截器链的每个拦截器的plugin方法,生成代理对象。

这样就可以把拦截器应用到四个核心方法上。

 

 

多级缓存的原理

  1. 一级缓存:SqlSession级别的缓存,它默认开启,只在当前SqlSession中有效。当同一个SqlSession对象执行相同的SQL语句时,如果查询的数据在一级缓存中存在,则直接从缓存中获取,而不需要再次查询数据库。
  2. 二级缓存:Mapper级别的缓存,它默认关闭,可以通过在Mapper文件中配置<cache>标签开启。当多个SqlSession对象执行相同的SQL语句时,如果查询的数据在二级缓存中存在,则直接从缓存中获取,而不需要再次查询数据库。二级缓存是跨SqlSession缓存的,即多个SqlSession可以共享同一个二级缓存。作用于是mapper下的同一个namespace。
  3. 三级缓存:全局缓存,它默认关闭,可以通过在配置文件中配置<cache>标签开启。当多个应用共享同一个SqlSessionFactory对象时,如果查询的数据在三级缓存中存在,则直接从缓存中获取,而不需要再次查询数据库。三级缓存是跨应用缓存的,即多个应用可以共享同一个三级缓存。 Mybatis中的缓存是基于装饰器模式实现的,它允许开发者自定义缓存实现方式。Mybatis在默认情况下使用PerpetualCache作为缓存实现,该缓存实现是基于HashMap实现的,缓存数据会一直存在内存中,不会过期和失效。开发者可以通过继承Cache接口并实现自定义的缓存实现,来满足不同的缓存需求。

二级缓存,同一个mapper下的sqlsession共享缓存,可以适当减轻数据库的压力但是缺点也有很多。

1.并发sqlsession的数据不一致问题。 (需要设置合理的缓存失效时间,或者手动清除)

2.二级缓存缓存到内存中,如果数据量大就会占大量内存空间(设置合适的大小,超出自动清除)

3.由于二级缓存是跨SqlSession共享的,当多个SqlSession并发地修改了同一个数据时,更新操作可能会被延时执行,因为需要等待其他SqlSession释放缓存(使用合适的锁机制来解决并发更新问题)

所以相比于使用二级缓存,redis等分布式缓存的方式来存储热点数据可能更加合适。

mybatis是如何运行的

 

 

 

1.读取配置文件:Mybatis会读取mybatis-config.xml配置文件,解析其中的配置信息,包括数据库连接信息、插件信息、类型别名信息等。

2.创建SqlSessionFactory:Mybatis会使用配置文件中的信息创建SqlSessionFactory对象,该对象是Mybatis操作数据库的核心对象,它会负责创建SqlSession对象。

3.创建SqlSession:通过SqlSessionFactory对象创建SqlSession对象,SqlSession是Mybatis与数据库交互的会话对象,它封装了对数据库的操作,包括插入、更新、删除、查询等操作。

   每个线程都应该有它自己的 SqlSession 实例。SqlSession的实例不是线程安全的,因此是不能被共享的

4.解析Mapper文件:Excetor执行器是Mybatis的核心,用于SQL语句的生成和查询缓存的维护,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

5.将其解析成一个个MappedStatement对象,每个MappedStatement对象封装了一个SQL语句和对应的参数映射。

例如

<!--一个动态sql标签就是一个`MappedStatement`对象-->

<select id="selectUserList" resultType="com.mybatis.User">

select * from t_user

</select>

执行SQL语句:通过SqlSession对象,调用相应的操作方法(如insertupdatedeleteselect等方法)执行SQL语句。

封装结果集:Mybatis会将数据库查询结果封装成一个个Java对象或Map对象,返回给调用者。

事务提交或回滚:在执行完SQL语句后,根据事务管理器的配置,Mybatis会自动提交或回滚事务。

释放资源:在SqlSession对象的作用域结束后,Mybatis会自动调用SqlSession的close()方法,释放资源,包括数据库连接和内存对象等。 在以上执行过程中,Mybatis还提供了拦截器插件机制,可以通过自定义拦截器实现对SQL语句和执行过程的拦截和修改。

 

Mybatis接口和XML里面的sql语句是如何建立联系的?

在初始化SqlSessionFactoryBean的时候,找到mapperLocations路径去解析里面所有XML文件。
1.Mybatis会把每个SQL标签封装成SqlSource对象。静态SQL、动态SQL
2.每个SQL标签对应一个MppedStatement对象
Id:全限定类名+方法名
sqlSource:当前Sql标签对应的SqlSource对象
创建完MappedStatement对象,缓存到Configuration中。

xml解析完成,执行Mybatis方法通过全限定名+方法名就可以找到对应的MappedStatement对象,然后解析里面的SQL内容,执行即可。

posted @   NobodyHero  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
历史上的今天:
2021-03-27 Java-Core之集合框架
点击右上角即可分享
微信分享提示