决战圣地玛丽乔亚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增强的类创建代理对象的?
- 首先,需要定义一个实现了InvocationHandler接口的拦截器类,该拦截器类中实现了需要增强的目标方法的增强逻辑。
- 当需要创建代理对象时,Spring会使用Java自带的Proxy类动态地生成一个(指定拦截器)的代理对象,将该代理对象返回给调用方。
- 当调用代理对象的某个方法时,该方法会被拦截器的invoke()方法拦截。在invoke()方法中,我们可以在目标方法执行前后执行一些额外的操作,比如记录日志、处理事务等。
- 最后,拦截器将增强后的方法结果返回给代理对象,从而实现了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动态代理和责任链设计模式的综合运用
使用场景:
- SQL语句性能监控:可以通过拦截器插件统计SQL语句的执行时间、执行次数等信息,以便进行性能监控和优化。
- 分页查询:可以通过拦截器插件在查询SQL语句前后进行分页处理,以实现分页查询功能。
- 数据库分库分表:可以通过拦截器插件在查询SQL语句前后进行分库分表处理,以实现数据库分库分表功能。
- 缓存处理:可以通过拦截器插件在查询SQL语句前后进行缓存处理,以提高查询性能。
- 数据库路由:可以通过拦截器插件在查询SQL语句前后进行数据库路由处理,以实现数据库读写分离等功能。
- 数据脱敏:可以通过拦截器插件在查询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方法,生成代理对象。
这样就可以把拦截器应用到四个核心方法上。
多级缓存的原理
- 一级缓存:SqlSession级别的缓存,它默认开启,只在当前SqlSession中有效。当同一个SqlSession对象执行相同的SQL语句时,如果查询的数据在一级缓存中存在,则直接从缓存中获取,而不需要再次查询数据库。
- 二级缓存:Mapper级别的缓存,它默认关闭,可以通过在Mapper文件中配置
<cache>
标签开启。当多个SqlSession对象执行相同的SQL语句时,如果查询的数据在二级缓存中存在,则直接从缓存中获取,而不需要再次查询数据库。二级缓存是跨SqlSession缓存的,即多个SqlSession可以共享同一个二级缓存。作用于是mapper下的同一个namespace。 - 三级缓存:全局缓存,它默认关闭,可以通过在配置文件中配置
<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对象,调用相应的操作方法(如insert
、update
、delete
、select
等方法)执行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内容,执行即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
2021-03-27 Java-Core之集合框架