<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
1.如下圖在程式碼開發中使用mybatis時,通過一個介面UserDao對應的方法selectUserNameById執行xml裡設定的selectUserNameById查詢sql語句。介面dao沒有具體的實現方法,那真正執行時mybatis是通過哪個類的哪個方法去找到對應的sql語句並執行的?
package com.changshin.dao; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.changshin.entity.po.User; /** * 後臺管理使用者表 dao 注意:只是一個介面沒有對應的實現類 * */ public interface UserDao extends BaseMapper<User> { public User selectUserNameById(Integer id); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.changshin.dao.UserDao"> <select id="selectUserNameById" parameterType="Integer" resultType="com.changshin.entity.po.User"> select username from t_user where id = #{id} </select> </mapper>
application.yml中關於mybatis-plus的設定
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
typeAliasesPackage: >
com.changshin.entity.po
global-config:
id-type: 0 # 0:資料庫ID自增 1:使用者輸入id 2:全域性唯一id(IdWorker) 3:全域性唯一ID(uuid)
db-column-underline: false
refresh-mapper: true
configuration:
map-underscore-to-camel-case: true
cache-enabled: true #設定的快取的全域性開關
lazyLoadingEnabled: true #延時載入的開關
multipleResultSetsEnabled: true #開啟的話,延時載入一個屬性時會載入該物件全部屬性,否則按需載入屬性
在springboot開發過程中專案整合了mybatis-plus,在整合時做了如下設定,通過@MapperScan來掃描com.changshin.dao包下的dao介面,dao是介面不能直接生成範例,在呼叫時只能時通過代理的方式執行代理物件的方法。
package com.changshin.config; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * MybatisPlus設定 */ @Configuration @EnableTransactionManagement(order = 2) @MapperScan(value = "com.changshin.dao") public class MybatisPlusConfig { /** * mybatis-plus分頁外掛 */ @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } }
進入MapperScan類,在類註解上發現@Import(MapperScannerRegistrar.class)這行程式碼,@Import註解是用來匯入設定類或者一些需要前置載入的類,類似原來Spring XML 裡面 的 一樣。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(MapperScannerRegistrar.class) //關注該行程式碼 @Repeatable(MapperScans.class) public @interface MapperScan {
在啟動springboot專案時,根據匯入的設定類的型別有以下四種處理方式:
1. 如果Abc類實現了ImportSelector介面,spring容器就會範例化Abc類,並且呼叫其selectImports方法;
2. DeferredImportSelector是ImportSelector的子類,如果Abc類實現了DeferredImportSelector介面,spring容器就會範例化Abc類,並且呼叫其selectImports方法,和ImportSelector的範例不同的是,DeferredImportSelector的範例的selectImports方法呼叫時機晚於ImportSelector的範例,要等到@Configuration註解中相關的業務全部都處理完了才會呼叫(具體邏輯在ConfigurationClassParser.processDeferredImportSelectors方法中),想了解更多DeferredImportSelector和ImportSelector的區別,請參考《ImportSelector與DeferredImportSelector的區別(spring4) 》;
3. 如果Abc類實現了ImportBeanDefinitionRegistrar介面,spring容器就會範例化Abc類,並且呼叫其registerBeanDefinitions方法;
4. 如果Abc沒有實現ImportSelector、DeferredImportSelector、ImportBeanDefinitionRegistrar等其中的任何一個,spring容器就會範例化Abc類。
在MapperScannerRegistrar類中實現了ImportBeanDefinitionRegistrar介面。
//實現了ImportBeanDefinitionRegistrar介面 public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
那麼spring在初始化上下文時會呼叫registerBeanDefinitions方法。
@Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AnnotationAttributes mapperScanAttrs = AnnotationAttributes .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName())); if (mapperScanAttrs != null) { //注意該方法 registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0)); } }
載入MapperScannerConfigurer類的bean定義。
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) { //獲取MapperScannerConfigurer的bean定義,注意該行方法 BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class); builder.addPropertyValue("processPropertyPlaceHolders", true); Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass"); if (!Annotation.class.equals(annotationClass)) { builder.addPropertyValue("annotationClass", annotationClass); } //省略..... }
MapperScannerConfigurer類實現了BeanDefinitionRegistryPostProcessor介面。這個介面支援自定義beanDefinition的註冊,在標準的註冊完成後(解析xml或者註解),在與範例化物件之前,實現這個介面
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware { //省略.... }
實現介面對應的postProcessBeanDefinitionRegistry方法,可以修改增加BeanDefinition。
此特性可以用來動態生成bean,比如讀取某個設定項,然後根據設定項動態生成bean。在程式碼中
初始化ClassPathMapperScanner類來掃描包裡的介面資訊
@Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { //該方法可以載入包資訊 //PropertyValues values = mapperScannerBean.getPropertyValues(); //this.basePackage = updatePropertyValue("basePackage", values); processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); scanner.setSqlSessionFactory(this.sqlSessionFactory); scanner.setSqlSessionTemplate(this.sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass); if (StringUtils.hasText(lazyInitialization)) { scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization)); } scanner.registerFilters(); //掃描mapperscan裡設定的com.changshin.dao包 scanner.scan( StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
ClassPathMapperScanner繼承了ClassPathBeanDefinitionScanner,因ClassPathMapperScanner沒有scan方法,呼叫scan方法時呼叫了ClassPathBeanDefinitionScanner裡的scan方法,
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner { //省略。。。。。 }
scan方法中呼叫了doScan方法,在繼承時,子類呼叫了父類別的方法,父類別的方法中呼叫的方法被子類重寫時會呼叫子類的重寫方法而不是父類別自己的方法,ClassPathBeanDefinitionScanner裡的doScan方法,呼叫的並非本類的doScan方法而是ClassPathMapperScanner重寫的doScan方法。
public int scan(String... basePackages) { int beanCountAtScanStart = this.registry.getBeanDefinitionCount(); //注意該方法呼叫的並非是ClassPathBeanDefinitionScanner裡的doScan方法,而是ClassPathMapperScanner重寫的doScan方法 doScan(basePackages); // Register annotation config processors, if necessary. if (this.includeAnnotationConfig) { AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); } return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart); }
在ClassPathMapperScanner的doScan方法中呼叫父類別ClassPathBeanDefinitionScanner的doScan載入bean定義
@Override public Set<BeanDefinitionHolder> doScan(String... basePackages) { //呼叫父類別ClassPathBeanDefinitionScanner的doScan載入bean定義 Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); if (beanDefinitions.isEmpty()) { LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration."); } else { //注意該方法 processBeanDefinitions(beanDefinitions); } return beanDefinitions; }
protected Set<BeanDefinitionHolder> doScan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); for (String basePackage : basePackages) { //獲取符合要求的bean定義 Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); candidate.setScope(scopeMetadata.getScopeName()); String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } if (candidate instanceof AnnotatedBeanDefinition) { AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } if (checkCandidate(beanName, candidate)) { BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); beanDefinitions.add(definitionHolder); //將bean定義註冊到registry中 registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; }
在載入bean定義後執行ClassPathMapperScanner的doScan中的processBeanDefinitions方法。在方法中將bean對應的class替換成MapperFactoryBean,這樣呼叫dao介面時,指向的類被替換成
MapperFactoryBean。
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); String beanClassName = definition.getBeanClassName(); //省略。。。。。 //將bean對應的class替換成MapperFactoryBean //private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class; definition.setBeanClass(this.mapperFactoryBeanClass); //省略。。。。。 if (!explicitFactoryUsed) { LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'."); //通過AUTOWIRE_BY_TYPE注入bean,注意此處在後面會用到,bean在初始化時會呼叫beanClass裡的set方法 definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } definition.setLazyInit(lazyInitialization); } }
替換前:
替換後:
繼續研究MapperFactoryBean,該類繼承了SqlSessionDaoSupport類,實現了FactoryBean介面。
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { //省略。。。。。。。。。。。 }
在繼承的SqlSessionDaoSupport類中有兩個set方法,分別是setSqlSessionFactory和setSqlSessionTemplate方法,在上面說到MapperFactoryBean在設定bean定義時,會將AutowireMode設定為AUTOWIRE_BY_TYPE,bean在初始化時會呼叫beanClass裡的set方法。
上述呼叫set方法可以參考
AbstractAutowireCapableBeanFactory#populateBean方法中呼叫的autowireByType方法的邏輯。
在這兩個set方法中setSqlSessionFactory和setSqlSessionTemplate都對sqlSessionTemplate進行了賦值,但是setSqlSessionTemplate後賦值,對setSqlSessionFactory賦的值進行了覆蓋。繼續觀察setSqlSessionTemplate方法,該方法直接將上下文中的sqlSessionTemplate bean物件直接賦值到SqlSessionDaoSupport的sqlSessionTemplate屬性中。
public abstract class SqlSessionDaoSupport extends DaoSupport { private SqlSessionTemplate sqlSessionTemplate; /** * Set MyBatis SqlSessionFactory to be used by this DAO. Will automatically create SqlSessionTemplate for the given * SqlSessionFactory. * * @param sqlSessionFactory * a factory of SqlSession */ public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) { this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory); } } /** * Create a SqlSessionTemplate for the given SqlSessionFactory. Only invoked if populating the DAO with a * SqlSessionFactory reference! * <p> * Can be overridden in subclasses to provide a SqlSessionTemplate instance with different configuration, or a custom * SqlSessionTemplate subclass. * * @param sqlSessionFactory * the MyBatis SqlSessionFactory to create a SqlSessionTemplate for * @return the new SqlSessionTemplate instance * @see #setSqlSessionFactory */ @SuppressWarnings("WeakerAccess") protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } /** * Return the MyBatis SqlSessionFactory used by this DAO. * * @return a factory of SqlSession */ public final SqlSessionFactory getSqlSessionFactory() { return (this.sqlSessionTemplate != null ? this.sqlSessionTemplate.getSqlSessionFactory() : null); } /** * Set the SqlSessionTemplate for this DAO explicitly, as an alternative to specifying a SqlSessionFactory. * * @param sqlSessionTemplate * a template of SqlSession * @see #setSqlSessionFactory */ public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) { this.sqlSessionTemplate = sqlSessionTemplate; } /** * Users should use this method to get a SqlSession to call its statement methods This is SqlSession is managed by * spring. Users should not commit/rollback/close it because it will be automatically done. * * @return Spring managed thread safe SqlSession */ public SqlSession getSqlSession() { return this.sqlSessionTemplate; } /** * Return the SqlSessionTemplate for this DAO, pre-initialized with the SessionFactory or set explicitly. * <p> * <b>Note: The returned SqlSessionTemplate is a shared instance.</b> You may introspect its configuration, but not * modify the configuration (other than from within an {@link #initDao} implementation). Consider creating a custom * SqlSessionTemplate instance via {@code new SqlSessionTemplate(getSqlSessionFactory())}, in which case you're * allowed to customize the settings on the resulting instance. * * @return a template of SqlSession */ public SqlSessionTemplate getSqlSessionTemplate() { return this.sqlSessionTemplate; } /** * {@inheritDoc} */ @Override protected void checkDaoConfig() { notNull(this.sqlSessionTemplate, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required"); } }
sqlSessionTemplate bean物件的來源是通過MybatisPlusAutoConfiguration類(被@Configuration註解)中sqlSessionTemplate放到上下文管理的。在程式碼中@Configuration下的@Bean註解會將方法返回的物件放入上下文管理並且beanName預設為方法名。
@Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType != null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } }
在生成SqlSessionTemplate bean物件時,SqlSessionFactory作為屬性傳入,SqlSessionFactory物件的生成是在MybatisPlusAutoConfiguration類的sqlSessionFactory方法獲取的。在該方法中的
factory.getObject()呼叫了MybatisSqlSessionFactoryBean中的getObject方法。
@Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { // TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setVfs(SpringBootVFS.class); if (StringUtils.hasText(this.properties.getConfigLocation())) { factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation())); } applyConfiguration(factory); if (this.properties.getConfigurationProperties() != null) { factory.setConfigurationProperties(this.properties.getConfigurationProperties()); } if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); } if (this.databaseIdProvider != null) { factory.setDatabaseIdProvider(this.databaseIdProvider); } if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) { factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage()); } if (this.properties.getTypeAliasesSuperType() != null) { factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType()); } if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) { factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage()); } if (!ObjectUtils.isEmpty(this.typeHandlers)) { factory.setTypeHandlers(this.typeHandlers); } // 注意該行程式碼獲取xml設定路徑 Resource[] mapperLocations = this.properties.resolveMapperLocations(); if (!ObjectUtils.isEmpty(mapperLocations)) { factory.setMapperLocations(mapperLocations); } // TODO 修改原始碼支援定義 TransactionFactory this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory); // TODO 對原始碼做了一定的修改(因為原始碼適配了老舊的mybatis版本,但我們不需要適配) Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver(); if (!ObjectUtils.isEmpty(this.languageDrivers)) { factory.setScriptingLanguageDrivers(this.languageDrivers); } Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver); // TODO 自定義列舉包 if (StringUtils.hasLength(this.properties.getTypeEnumsPackage())) { factory.setTypeEnumsPackage(this.properties.getTypeEnumsPackage()); } // TODO 此處必為非 NULL GlobalConfig globalConfig = this.properties.getGlobalConfig(); // TODO 注入填充器 this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler); // TODO 注入主鍵生成器 this.getBeanThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerator(i)); // TODO 注入sql注入器 this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector); // TODO 注入ID生成器 this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator); // TODO 設定 GlobalConfig 到 MybatisSqlSessionFactoryBean factory.setGlobalConfig(globalConfig); //注意該行方法 return factory.getObject(); }
MybatisSqlSessionFactoryBean#getObject方法中的afterPropertiesSet方法中的buildSqlSessionFactory方法有一段這樣的程式碼
final SqlSessionFactory sqlSessionFactory = new MybatisSqlSessionFactoryBuilder().build(targetConfiguration);
在程式碼中通過builder方法可以找到MybatisSqlSessionFactoryBuilder類裡的builder方法
SqlSessionFactory sqlSessionFactory = super.build(configuration);
通過該方法裡的super.build發現呼叫的是父類別SqlSessionFactoryBuilder裡的builder方法
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
該方法返回的是一個DefaultSqlSessionFactory物件。作為sqlSessionFactory Bean對應的物件。也就是說sqlSessionTemplate bean範例裡的sqlSessionFactory屬性是一個DefaultSqlSessionFactory型別的範例。
分析完SqlSessionDaoSupport,繼續分析MapperFactoryBean實現的FactoryBean,FactoryBean是一個工廠Bean,可以生成某一個型別Bean範例,它最大的一個作用是:可以讓我們自定義Bean的建立過程。在FactoryBean中有一個方法getObject可以返回該bean定義的範例。在MapperFactoryBean中檢視getObject方法的實現方法。該方法中getSqlSession返回sqlSessionTemplate物件。
@Override public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); }
getMapper方法執行的是sqlSessionTemplate的getMapper方法。其中getConfiguration方法執行的是SqlSessionFactory的getConfiguration方法,SqlSessionFactory是一個介面,實現類是DefaultSqlSessionFactory。getConfiguration方法獲取的是getConfiguration方法獲取的是DefaultSqlSessionFactory中的configuration屬性,該屬性是MybatisConfiguration型別的。
@Override public <T> T getMapper(Class<T> type) { return getConfiguration().getMapper(type, this); }
MybatisConfiguration中的getMapper方法是從MybatisMapperRegistry類的getMapper方法。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mybatisMapperRegistry.getMapper(type, sqlSession); }
MybatisMapperRegistry#getMapper方法通過type(mapperInterface即介面全路徑)從knownMappers(HashMap中儲存的MybatisMapperProxyFactory)中獲取MybatisMapperProxyFactory
public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // TODO 這裡換成 MybatisMapperProxyFactory 而不是 MapperProxyFactory final MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry."); } try { //注意該行程式碼 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } }
通過newInstance方法返回一個MybatisMapperProxy的範例。
@SuppressWarnings("unchecked") protected T newInstance(MybatisMapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy); } public T newInstance(SqlSession sqlSession) { //該sqlSession是sqlSessionTemplate型別的物件 final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
這樣,這樣客戶只用呼叫dao介面實際上呼叫的是MybatisMapperProxy類裡的invoke方法。
總結:該文章講述了springboot在啟動時是如何通過MapperScan註解的來實現Mybatis動態代理的,dao介面世界上的代理物件其實就是MybatisMapperProxy型別的範例。下一篇文章我們會講述dao方法是如何同呼叫的sql語句對應起來的。
到此這篇關於Mybatis基於MapperScan註解的動態代理載入機制詳解的文章就介紹到這了,更多相關Mybatis動態代理載入內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45