Spring
1:Spring 自动装配机制
1.容器在启动的时候会调用 EnableAutoConfigurationImportSelector.class 的 selectImports方法「获取一个全面的常用 BeanConfiguration 列表]
2.之后会读取 spring-boot-autoconfigure.jar 下面的spring.factories,「获取到所有的 Spring 相关的 Bean 的全限定名 ClassName]
3.之后继续「调用 filter 来一一筛选」,过滤掉一些我们不需要不符合条件的 Bean
4.最后把符合条件的 BeanConfiguration 注入默认的 EnableConfigurationPropertie 类里面的属性值,并且「注入到 IOC 环境当中」
2:BeanFactory 和 FactoryBean
BeanFactory是个Factory,也就是IOC容器或对象工厂,FactoryBean是个Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的.
举例如Mybatis @Mapper扫描的注解,会生成一个FactoryBean对象,最终调用getObject()获取代理类
3:BeanFactoryPostProcessor 和 BeanDefinitionRegisterPostProcessor
BeanFactoryPostProcessor: beanFactory 的后置处理器,在BeanFactory 标准初始化之后调用, 所有的 bean 定义 已经保存加载到 beanFactory 中, 但是 bean 的实例还没有创建
BeanDefinitionRegistryPostProcessor:继承于 BeanFactoryPostProcessor,postProcessBeanDefinitionRegistry()。在所有 bean 自定义信息将要被加载, bean 实例还未创建。 优先于 BeanFactoryPostProcessor 执行,利用 BeanDefinitionRegistryPostProcessor 给容器中添加一些组件
BeanDefinitionRegistryPostProcessor通常用来添加第三方组件,比如mybatis配置实现BeanDefinitionRegistryPostProcessor用来加载@Mapper注解。
4:BeanFactoryPostProcessor 和 BeanPostProcessor
BeanFactoryPostProcessor:是针对于beanFactory的扩展点,即spring会在beanFactory初始化之后,beanDefinition都已经loaded,但是bean还未创建前进行调用,可以修改,增加beanDefinition
BeanPostProcessor:是针对bean的扩展点,即spring会在bean初始化前后 调用方法对bean进行处理.
5:Spring @import ImportSelector 相关拓展
动态注入bean:bean的注入不是写死在逻辑里面的,是依据某些用户使用条件的。这也是框架所需要的,我们应该依据用户怎么配置,决定我们给容器注入什么bean。
例如:@EnableAsync
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
Class<? extends Annotation> annotation() default Annotation.class;
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME =
"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";
@Override
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] { ProxyAsyncConfiguration.class.getName() };
case ASPECTJ:
return new String[] { ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME };
default:
return null;
}
}
}
6:Spring ioc理解
传统的创建对象的方法是直接通过 new 关键字,而 spring 则是通过 IOC 容器来创建对象,也就是说我们将创建对象的控制权交给了 IOC 容器。我们可以用一句话来概括 IOC:
IOC 让程序员不在关注怎么去创建对象,而是关注与对象创建之后的操作,把对象的创建、初始化、销毁等工作交给spring容器来做。
7:Spring Bean 生命周期
Bean 容器找到配置文件中 Spring Bean 的定义。
Bean 容器利用 Java Reflection API 创建一个Bean的实例。
如果涉及到一些属性值 利用 set()方法设置一些属性值。
如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入Bean的名字。
如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
如果Bean实现了 BeanFactoryAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoade r对象的实例。
与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。
如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。
如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法
当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。
8:Spring 注入方式
我们知道一个对象由两部分组成:属性+行为(方法),可以说Spring通过属性注入+方法注入的方式掌控的整个bean。
属性注入跟方法注入都是Spring提供给我们用来处理Bean之间协作关系的手段
属性注入有两种方式:构造函数,Setter方法。
方法注入(LookUp Method跟Replace Method)需要依赖动态代理完成
方法注入对属性注入进行了一定程度上的补充,因为属性注入的情况下,原型对象可能会失去原型的意义
9:Spring 循环依赖 及三级缓存
Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),
二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,
在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,
如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,
那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,
所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,
第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,
得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。
当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!
”为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?
如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,
这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,
而不是在实例化后就立马进行AOP代理。循环依赖和注入方式
依赖情况 | 依赖方式 | 循环依赖是否被解决 |
---|---|---|
AB相互依赖(循环依赖) | 均采用setter方法注入 | 是 |
AB相互依赖(循环依赖) | 均采用构造器注入 | 否 |
AB相互依赖(循环依赖) | A中注入B的方式为setter方法,B中注入A的方式为构造器 | 是 |
AB相互依赖(循环依赖) | B中注入A的方式为setter方法,A中注入B的方式为构造器 | 否 |
10:Spring AOP (切点,通知,连接点,切面,织入)
切点:切点的主要作用是定义通知所要应用到的类跟方法。具体到哪个类,或者类里面哪个方法想要被增强。
public interface Pointcut {
/**
* 类级别上过滤
*/
ClassFilter getClassFilter();
/**
* 方法级别上过滤
*/
MethodMatcher getMethodMatcher();
/**
* 默认匹配所有
*/
Pointcut TRUE = TruePointcut.INSTANCE;
}
通知:就是抽取出来的通用功能,也就是上说的安全、事物、日志等。
@Around("log2()")
public Object around2(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("log2()调用前......");
Object object = joinPoint.proceed();
System.out.println("log2()调用后.......");
return object;
}
连接点:连接点是个虚拟的概念,大概意思是Spring给了几种通知:环绕通知(Interception Around Advice)、前置通知(Before Advice)
异常通知(Throws Advice)、后置通知(After Returning Advice),在调用某个方法触发了上面的通知,这个地方就叫连接点。切面:切点+通知 组成了切面。通知定义了切面是什么以及何时通知。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
@Component
@Aspect
public class AspFilter {
/**
* 定义切点
**/
@Pointcut("within(com.yijiupi.himalaya.settle.demo.proxy.ConfigCglib)")
public void log() {
}
@Pointcut("execution(public * com.yijiupi.himalaya.settle.demo.proxy..*(..))")
public void log2() {
}
/**
* 定义通知
**/
@Around("log2()")
public Object around2(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("log2()调用前......");
Object object = joinPoint.proceed();
System.out.println("log2()调用后.......");
return object;
}
@Around("log()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("调用前......");
Object object = joinPoint.proceed();
System.out.println("调用后.......");
return object;
}
}
织入:织入就是在切点的引导下,将通知逻辑插入到方法调用上,使得我们的通知逻辑在方法调用时得以执行。
代码层面
Spring在Bean完成populateBean注入属性后,执行initializeBean的过程中,会调用BeanPostProcessor的afterInitialization创建代理类。
代理类创建时候会根据目标类是否实现了接口来处用 JDK或者是CGLIB来产生代理对象。代理对象在创建的过程中会创建一个拦截链,当一个类或者方法存在多个通知时。
AOP设计模式
适配器模式:部分通知器是没有实现 MethodInterceptor 接口的,所以设计了 一些XXXAdviceAdapter将advisor转变为Interceptor
职责链模式 :拦截器链执行过程中,由每个拦截器调用proceed()方法以执行,链条的头部传递到各个节点,从而触发执行各个节点索要负责的业务逻辑。
代理模式:创建一个代理类注入。
11:Spring事物和事物传播机制
12:@Bean 和@Component
1:作用对象不同: @Component 注解作用于类,而@Bean注解作用于方法。
2:@Component通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。
@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了Spring这是某个类的示例,当我需要用它的时候还给我。3:@Bean 注解比 Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册bean。
比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。
举例:一个记录数据变更拦截mongo ,mysql 因为是一个通用的二方包有的业务只有mysql或者只有mongo驱动,所有需要@bean配合@Condition去按需加载
@Bean
@ConditionalOnClass(name = Constants.MONGO_REPOSITORY_CLASS_NAME)
public MongoDBAction mongoDBAction() {
return new MongoDBAction();
}
@Bean
@ConditionalOnClass(name = Constants.MONGO_REPOSITORY_CLASS_NAME)
public MongoDBActionAspect mongoDBActionAspect() {
return new MongoDBActionAspect();
}
@Bean
@ConditionalOnClass(name = Constants.MYSQL_REPOSITORY_CLASS_NAME)
public MySQLDBAction mySQLDBAction() {
return new MySQLDBAction();
}
@Bean
@ConditionalOnClass(name = Constants.MYSQL_REPOSITORY_CLASS_NAME)
public MySQLDBActionAspect mySQLDBActionAspect() {
return new MySQLDBActionAspect();
}
13:@Configuration 和@Component
如果是由@Configuration注解修饰的类,自身会生成一个cglib代理对象,在通过@Bean方式创建单例对象时,
经过增强,会尝试从BeanFactory里返回对象,如果是第一次创建@Bean要生成的对象,才会反射调用被@Bean修饰的方法。
不管是spring初次创建@Bean的对象,还是业务代码手动调用被@Bean方法修饰的方法,返回的永远是同一个被spring容器管理的对象。
疑问:cglib代理类中内部方法调用,普通代理内部方法调用?invokeSuper()
Dubbo面试题
1:Dubbo 基础概念
节点 | 角色说明 |
---|---|
Consumer | 需要调用远程服务的服务消费方 |
Registry | 注册中心 |
Provider | 服务提供方 |
Container | 服务运行的容器 |
Monitor | 监控中心 |
Provider(服务端)启动后会创建一个容器提供服务端运行环境,然后将可以提供的服务注册到Registry(注册中心)。Customer(消费者)启动时会
去Registry(注册中心)去订阅想要的服务,Customer(消费者)就可以获取到Provider提供服务的地址,Customer就可以去调用了。
当服务端提供服务发生变更,注册中心会notify给Customer。服务提供者和消费者都会在内存中记录着调用的次数和时间,然后定时的发送统计数据到监控中心
Dubbo分层
Service:业务层,就是咱们开发的业务逻辑层。
Config:配置层,主要围绕 ServiceConfig 和 ReferenceConfig,初始化配置信息。
Proxy:代理层,服务提供者还是消费者都会生成一个代理类,使得服务接口透明化,代理层做远程调用和返回结果。
Register:注册层,封装了服务注册和发现。
Cluster:路由和集群容错层,负责选取具体调用的节点,处理特殊的调用要求和负责远程调用失败的容错措施。
Monitor:监控层,负责监控统计调用时间和次数。
Portocol:远程调用层,主要是封装 RPC 调用,主要负责管理 Invoker,Invoker代表一个抽象封装了的执行体,之后再做详解。
Exchange:信息交换层,用来封装请求响应模型,同步转异步。
Transport:网络传输层,抽象了网络传输的统一接口,这样用户想用 Netty 就用 Netty,想用 Mina 就用 Mina。
Serialize:序列化层,将数据序列化成二进制流,当然也做反序列化。
2:Dubbo Spi 机制和 java Spi机制 (dubbo Ioc 和Aop)
Java SPI 和 Dubbo SPI 机制异同点
Java SPI:约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。
缺点:Java SPI 不能够按需加载;
Dubbo SPI: Dubbo 配置文件存放的是形式是key(类名)-value(类的全名),加载的时候通过key找到value加载实例化。通过ExtensionLoader.getExtension(name)实现。
加载文件地址为:
META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。
META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。
META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。
Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有个自适应扩展机制。
IOC:
IOC是通过反射setter注入到对象中。
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
/**
* Check {@link DisableInject} to see if we need auto injection for this property
*/
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
Class<?> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
eg:这边setXXX 是没有实际调用。都是通过反射注入到目标对象的。
public class DubboRegistryFactory extends AbstractRegistryFactory {
private Protocol protocol;
private ProxyFactory proxyFactory;
private Cluster cluster;
public void setProtocol(Protocol protocol) {
this.protocol = protocol;
}
public void setProxyFactory(ProxyFactory proxyFactory) {
this.proxyFactory = proxyFactory;
}
public void setCluster(Cluster cluster) {
this.cluster = cluster;
}
}
Dubbo AOP 是一种包装模式
Wapper规范:
该类要实现 SPI 接口
该类中要有 SPI 接口的引用
该类中必须含有一个含参的构造方法且参数只能有一个类型为SPI借口
在接口实现方法中要调用 SPI 接口引用对象的相应方法
该类名称以 Wrapper 结尾
在调用getExtension()遍历配置的资源文件时,会按照Wapper规范识别,然后将这些类放入到cachedWrapperClasses中,然后加载类时候会将这些Wapper类包装一遍返回。
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + "is not subtype of interface.");
}
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
} else if (isWrapperClass(clazz)) {
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} else {
clazz.getConstructor();
if (name == null || name.length() == 0) {
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
}
}
}
}
}
包装时候代码:
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
Adaptive 注解 - 自适应扩展
Dubbo 通过一个代理机制实现了自适应扩展,简单的说就是为你想扩展的接口生成一个代理类,
可以通过JDK 或者 javassist 编译你生成的代理类代码,然后通过反射创建实例。
这个实例里面的实现会根据本来方法的请求参数得知需要的扩展类,
然后通过 ExtensionLoader.getExtensionLoader(type.class).getExtension(从参数得来的name),
来获取真正的实例来调用。
Activate 注解
这个注解简单的说下,拿 Filter 举例,Filter 有很多实现类,在某些场景下需要其中的几个实现类,而某些场景下需要另外几个,
而 Activate 注解就是标记这个用的。
它有三个属性,group 表示修饰在哪个端,
是 provider 还是 consumer,value 表示在 URL参数中出现才会被激活,
order 表示实现类的顺序。
DubboSPI加载流程图:
3:Dubbo 服务暴露过程
4:Dubbo 服务引入过程
5:Dubbo 服务调用过程(负载,容错)
6:Dubbo 泛化调用
7:Dubbo 优雅停机
Mybatis面试题
Mybatis怎么融入Spring中?
Spring自动装配机制,会加载Mybatis配置类(配置类初始化的时候也会通过Spring构造函数注入Spring中的DataSource(自定义创建:Druid))。
添加Mybatis特有的扫描器(Scanner)会识别所有统计了@Mapper接口,将它们封装为MapperFactoryBean,然后创建代理类,代理类中主要属性有:SqlSession,SqlSessionFactory。
Mybatis缓存问题
一级缓存:第一次发出一个查询 sql,sql 查询结果写入 sqlsession 的一级缓存中,缓存使用的数据结构是一个 map。
key:MapperID+offset+limit+Sql+所有的入参
value:用户信息同一个 sqlsession
再次发出相同的 sql,就从缓存中取出数据。如果两次中间出现 commit 操作 (修改、添加、删除),
本 sqlsession 中的一级缓存区域全部清空,
下次再去缓存中查询不到所 以要从数据库查询,
从数据库查询到再写入缓存。
像下面(1)会执行,下面都不会查询数据库。
xxxxMapper.selectOne(id); (1)
xxxxMapper.selectOne(id);
xxxxMapper.selectOne(id);
这种方式会出现脏读,而且Mybatis一级缓存还不可以关。所以当mybatis交由Spring托管后,Spring在执行时候添加了一个拦截器,每次请求时候创建一个SqlSession,这样也避免了Mybatis缓存问题。
当然如果是当前执行的存在事务用的就是一个SqlSession.总结:如果在事务里,则Spring给你的sqlSession是一个,否则,每一个sql给你一个新的sqlSession。这里生成的sqlSession其实就是DefaultSqlSession了。
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
二级缓存:二级缓存的范围是 mapper 级别(mapper同一个命名空间,Namespace),
mapper 以命名空间为单位创建缓存数据结构,
结构是 map。mybatis 的二级缓存是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象。
所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存在,不存在则查询数据库。
key:MapperID+offset+limit+Sql+所有的入参
区分#{}和${}的是什么?
: #{}是预编译处理,${}是字符串替换。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性。
预编译语句的优势在于归纳 为:一次编译、多次运行,省去了解析优化等过程;此外预编译语句能防止sql注入。
Mybatis的分页原理?
分页插件的原理就是使用MyBatis提供的插件接口,实现自定义插件,在插件的拦截方法内,拦截待执行的SQL,然后根据设置的dialect(方言),和设置的分页参数,重写SQL ,生成带有分页语句的SQL,执行重写后的SQL,从而实现分页。
举例:select from student,拦截sql后重写为:select t. from (select * from student)t limit 0,10
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。