设计模式学习笔记 - 开源实战五(下):总结Mybatis中用到的10种设计模式

news2024/12/23 23:28:00

概述

本章再对 Mybatis 用到的设计模式做一个总结。它用到的设计模式也不少。有些前面章节已经经过了,有些则比较简单。


SqlSessionFactoryBuilder:为什么要用建造者模式来创建 SqlSessionFactory?

在《Mybatis如何权衡易用性、性能和灵活性?》章节,通过一个查询用户的例子,展示了用 Mybatis 进行数据库编程。为方便查看,代码重新摘抄到这里。

public class MybatisDemo {
    public static void main(String[] args) throws IOException {
        Reader reader = Resources.getResourceAsReader("mybatis.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);
        SqlSession session = sessionFactory.openSession();
        UserMapper userMapper = session.getMapper(UserMapper.class);
        UserDo userDo = userMapper.selectById(8);
        // ...
    }
}

针对这段代码,请你思考下下面这个问题。

之前讲到建造者模式时,我们使用 Builder 类来创建对象,一般都是先级联一组 setXXX() 方法来设置属性,然后再调用 builder() 方法创建最终的对象。但是,在上面这段代码中,通过 SqlSessionFactoryBuilder 来创建 SqlSessionFactory 并不符合这个套路。它既没有 setter 方法,而且 builder() 方法也并非无参,需要传递参数。此外,从上面的代码来看,SqlSessionFactory 对象的创建过程也并不复杂。那直接通过构造函数来创建 SqlSessionFactory 不就行了吗?为什么还要借助建造者模式创建 SqlSessionFactory 呢?

要回答这个问题,先要看下 SqlSessionFactoryBuilder 类的源码。源码如下所示:

public class SqlSessionFactoryBuilder {

  public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
  }

  public SqlSessionFactory build(Reader reader, String environment) {
    return build(reader, environment, null);
  }

  public SqlSessionFactory build(Reader reader, Properties properties) {
    return build(reader, null, properties);
  }

  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

  public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment) {
    return build(inputStream, environment, null);
  }

  public SqlSessionFactory build(InputStream inputStream, Properties properties) {
    return build(inputStream, null, properties);
  }

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }
}

SqlSessionFactoryBuilder 类中有大量的 build() 重载函数。为了方便你查看,以及待会儿跟 SqlSessionFactory 类的代码做对比,我们把重载函数抽象出来,贴到这里。

public class SqlSessionFactoryBuilder {
  public SqlSessionFactory build(Reader reader);
  public SqlSessionFactory build(Reader reader, String environment);
  public SqlSessionFactory build(Reader reader, Properties properties);
  public SqlSessionFactory build(Reader reader, String environment, Properties properties);
  public SqlSessionFactory build(InputStream inputStream);
  public SqlSessionFactory build(InputStream inputStream, String environment);
  public SqlSessionFactory build(InputStream inputStream, Properties properties);
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) ;
  // 上面的所有方法,最终都会调用这个方法
  public SqlSessionFactory build(Configuration config);
}

我们知道,如果一个类包含很多成员变量,而构建对象并不需要设置所有的成员变量,只需要选择性地设置其中几个就可以了。为了满足这样的构建需求,就要定义多个包含不同参数列表的构造函数。为了避免构造函数过多、参数列表过长,我们一般通过无参构造函数加 setter 方法或者通过建造者模式来解决。

从建造者模式的设计初衷上来看,SqlSessionFactoryBuilder 虽然带有 Builder() 后缀,但不要被它的名字所迷惑,它并不是标准的建造者模式。一方面,原始类 SqlSessionFactory 只需要一个参数,并不复杂。另一方面,Builder 类 SqlSessionFactoryBuilder 仍然定义了 n 多个包含不同参数列表的构造函数。

实际上,SqlSessionFactoryBuilder 设计的初衷只不过是为了简化开发。因为构建 SqlSessionFactory 需要先构建 Configuration,而构建 Configuration 是非常复杂的,需要做很多工作,比如配置的读取、解析、创建 n 多对象等。为了将构建 SqlSessionFactory 的过程隐藏起来,对程序员透明,Mybatis 就设计了 SqlSessionFactoryBuilder 类封装这些构建细节。

SqlSessionFactory:到底属于工厂模式还是建造者模式?

在上面那段 Mybatis 示例代码中,我们通过 SqlSessionFactoryBuilder 创建了 SqlSessionFactory,然后再通过 SqlSessionFactory 创建了 SqlSession。刚刚讲了 SqlSessionFactoryBuilder,现在再来看下 SqlSessionFactory

从名字上,你可能已经猜到,SqlSessionFactory 是一个工厂类,用到的设计模式是工厂模式。不过,它跟 SqlSessionFactoryBuilder 类似,名字有很大的迷惑性。实际上,它并不是标准的工厂模式。为什么这么说呢?我们先来看下 SqlSessionFactory 类的源码。

public interface SqlSessionFactory {
  SqlSession openSession();
  SqlSession openSession(boolean autoCommit);
  SqlSession openSession(Connection connection);
  SqlSession openSession(TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);
  Configuration getConfiguration();
}

SqlSessionFactory 是一个接口,DefaultSqlSessionFactory 是它的唯一实现类。DefaultSqlSessionFactory 源码如下所示:

public class DefaultSqlSessionFactory implements SqlSessionFactory {
  private final Configuration configuration;
  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }

  @Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

  @Override
  public SqlSession openSession(boolean autoCommit) {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, autoCommit);
  }

  @Override
  public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
  }

  @Override
  public SqlSession openSession(TransactionIsolationLevel level) {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), level, false);
  }

  @Override
  public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
    return openSessionFromDataSource(execType, level, false);
  }

  @Override
  public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
    return openSessionFromDataSource(execType, null, autoCommit);
  }

  @Override
  public SqlSession openSession(Connection connection) {
    return openSessionFromConnection(configuration.getDefaultExecutorType(), connection);
  }

  @Override
  public SqlSession openSession(ExecutorType execType, Connection connection) {
    return openSessionFromConnection(execType, connection);
  }

  @Override
  public Configuration getConfiguration() {
    return configuration;
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
      boolean autoCommit;
      try {
        autoCommit = connection.getAutoCommit();
      } catch (SQLException e) {
        // Failover to true, as most poor drivers
        // or databases won't support transactions
        autoCommit = true;
      }
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      final Transaction tx = transactionFactory.newTransaction(connection);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
  // ...
}

SqlSessionFactoryDefaultSqlSessionFactory 的源码来看,它的设计非常类似刚刚的 SqlSessionFactoryBuilder,通过重载多个 openSession() 函数,支持通过组合 autoCommitExecutorTransaction 等不同的参数,创建 SqlSession 对象。标准的工厂模式通过 type 来创建继承同一个父类的不同子类对象,而这里只不过是通过传递进来的不同参数,来创建同一个类的对象。所以,它更像建造者模式。

虽然设计思路基本一致,但一个叫 xxxBuilderSqlSessionFactoryBuilder),一个叫 xxxFactorySqlSessionFactory)。而且,叫 xxxBuilder 的也并非标准的建造者模式,叫 xxxFactory 的也并非标准的工厂模式。所以,我个人觉得,Mybatis 对这部分代码的设计还是值得优化的。

实际上,这两个类的作用只不过是为了创建 SqlSession 对象,没有其他作用。所以,我更建议参照 Spring 的设计思路,把 SqlSessionFactoryBuilderSqlSessionFactory 的逻辑,放到一个叫 “ApplicationContext” 的类中。让这个类来全权负责读入配置文件,创建 Configuration,生成 SqlSession

BaseExecutor:模板模式跟普通的继承有什么区别?

如果查阅 SqlSessionDefaultSqlSession 的源码,你会发现,SqlSession 执行 SQL 的业务逻辑,都是委托给了 Executor 来实现。Executor 相关的类主要是用来执行 SQL。其中,Executor 本身是一个接口;BaseExecutor 是一个抽象类,实现了 Executor 接口;而 BatchExecutorSimpleExecutorReuseExecutor 三个类继承 BaseExecutor 抽象类。

BatchExecutorSimpleExecutorReuseExecutor 三个类跟 BaseExecutor 是简单的继承关系,还是模板模式关系呢?我们看一下 BaseExecutor 的源码就清楚了。

public abstract class BaseExecutor implements Executor {
  // ...
  
  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

  @Override
  public List<BatchResult> flushStatements() throws SQLException {
    return flushStatements(false);
  }

  public List<BatchResult> flushStatements(boolean isRollBack) throws SQLException {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    return doFlushStatements(isRollBack);
  }
  
  // ...
  
  @Override
  public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    return doQueryCursor(ms, parameter, rowBounds, boundSql);
  }
  
  // ...
  
  protected abstract int doUpdate(MappedStatement ms, Object parameter)
      throws SQLException;

  protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
      throws SQLException;

  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;

  protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
      throws SQLException;
      
  // ...
  
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
  
  // ...
  
}

模板模式基于继承来实现代码复用。如果抽象类中包含模板方法,模板方法调用待子类实现的抽象方法,那这一般就是模板模式的代码实现。而且,在命名上,模板方法与抽象方法一般是一一对应地,抽象方法在模板方法前面多一个 “do”,比如,在 BaseExecutor 类中,其中一个模板方法叫做 update(),那对应地抽象方法叫做 doUpdate()

SqlNode:如何利用解释器模式来解析动态 SQL?

支持配置文件中编写动态 SQL,是 Mybatis 一个非常强大的功能。所谓动态 SQL,就是在 SQL 中可以包含在 trim、if、#{} 等语法标签,在运行时根虎条件来生成不同的 SQL。这么说比较抽象,我举个例子解释下。

<update id="update" parameterType="com.example.User">
	UPDATE user 
	<trim prefix="SET" prefixOverrides=",">
		<if test="name != null and name != ''">
			name = #{name}
		</if>
		<if test="age != null and age != ''">
			, age = #{age}
		</if>
		<if test="birthday != null and birthday != ''">
			, birthday = #{birthday}
		</if>
	</trim>
	where id = #{id}
</update>

显然,动态 SQL 的语法规则是 Mybtis 自定义的。如果想要根据语法规则,替换掉动态 SQL 中的动态元素,生成真正可以执行的 SQL 语句,Mybatis 还需要实现对应的解释器。这一部分功能就可以看作是解释器模式的应用。实际上,如果你去查看它的代码实现,你会发现,它跟我们在前面讲解解释器模式时举的例子的代码结构非常相似。

前面提到,解释器模式在解释语法规则时,一般会把语法规则分割成小的单元,特别是可以嵌套的小单元,针对每个小单元来解析,最终再把解析结果合并在一起。这里也不例外。Mybatis 把每个语法小单元叫 SqlNodeSqlNode 的定义如下所示:

public interface SqlNode {
  boolean apply(DynamicContext context);
}

对应不同的语法小单元,Mybatis 定义的不同 SqlNode 实现类。

在这里插入图片描述

整个解释器的调用入口在 DynamicSqlSource.getBoundSql() 方法中,它调用了 rootSqlNode.apply(context) 方法。

ErrorContext:如何实现一个线程唯一的单例模式?

在单例模式章节,我们讲到单例模式时进程唯一的。同时,还讲到单例模式的几种变形,比如线程唯一的单例、集群唯一的单例等等。在 Mybatis 中,ErrorContext 这个类就是标准的单例的变形:现成唯一实例。

它的代码实现如下所示。它基于 Java 的 ThreadLocal 类实现。

public class ErrorContext {

  private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();

  private ErrorContext stored;
  private String resource;
  private String activity;
  private String object;
  private String message;
  private String sql;
  private Throwable cause;

  private ErrorContext() {
  }

  public static ErrorContext instance() {
    ErrorContext context = LOCAL.get();
    if (context == null) {
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }
  // ...
}

Cache:为什么要用装饰器模式而不设计成继承子类?

前面提到,Mybatis 是一个 ORM 框架。实际上,它不只是简单地完成了对象和数据库之间的互相转化,还提供了很多其他功能,比如缓存、事务等。接下来,再讲讲它的缓存实现。

在 Mybatis 中,缓存功能由接口 Cache 定义。PrepetualCache 类是最基础的缓存类,是一个大小无限的缓存。此外,Mybatis 还设计了 9 个包裹 PrepetualCache 的类装饰器,用来实现功能增强。它们分别是:FifoCacheLoggingCacheLruCacheScheduleCacheSerializedCacheSoftCacheSynchronizedCacheWeakCacheTransactionCache

public interface Cache {
  String getId();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  Object removeObject(Object key);
  void clear();
  int getSize();
  ReadWriteLock getReadWriteLock();
}

public class PerpetualCache implements Cache {

  private final String id;

  private Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }
}

另外 9 个装饰器的代码结构都类似,我们只浆砌砖的 LruCache 的源码贴到这里。从代码中可以看出,它是标准的装饰器模式的代码实现。

public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); //touch
    return delegate.getObject(key);
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    delegate.clear();
    keyMap.clear();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }
}

之所以 Mybatis 采用装饰器模式来实现缓存功能,是因为装饰器模式采用了组合,而非继承,更加灵活,能够有效地避免继承关系的组合爆炸。

PropertyTokenizer:如何利用迭代器模式实现一个属性解析器?

前面章节讲过,迭代器模式常用来替代 for 循环遍历集合。Mybatis 的 PropertyTokenizer 类实现了 Java Iterator 接口,是一个迭代器,用来对配置属性进行解析。具体代码如下所示:

public class PropertyTokenizer implements Iterator<PropertyTokenizer> {
  private String name;
  private final String indexedName;
  private String index;
  private final String children;

  public PropertyTokenizer(String fullname) {
    int delim = fullname.indexOf('.');
    if (delim > -1) {
      name = fullname.substring(0, delim);
      children = fullname.substring(delim + 1);
    } else {
      name = fullname;
      children = null;
    }
    indexedName = name;
    delim = name.indexOf('[');
    if (delim > -1) {
      index = name.substring(delim + 1, name.length() - 1);
      name = name.substring(0, delim);
    }
  }

  public String getName() {
    return name;
  }

  public String getIndex() {
    return index;
  }

  public String getIndexedName() {
    return indexedName;
  }

  public String getChildren() {
    return children;
  }

  @Override
  public boolean hasNext() {
    return children != null;
  }

  @Override
  public PropertyTokenizer next() {
    return new PropertyTokenizer(children);
  }

  @Override
  public void remove() {
    throw new UnsupportedOperationException("Remove is not supported, as it has no meaning in the context of properties.");
  }
}

实际上, PropertyTokenizer 也并非标准的迭代器类。它将配置的解析、解析之后的元素、迭代器,这三部分代码都耦合在一个类中,所以看起来稍微有点难懂。不过这样做的好处是能够做到惰性解析。我们不需要事先将整个配置,解析成多个 PropertyTokenizer 对象。只有当我们在调用 next() 函数时,才会解析其中的部分配置。

Log:如何使用适配器模式来适配不同的日志框架

在适配器模式章节我们讲过,Sl4j 为了统一各个不同的日志框架(Log4j、JCL、Logback 等),提供了一套统一的日志接口。不过,Mybatis 并没有直接使用 Sl4j 提供的统一日志规范,而是自己又重复造轮子,定义了一套自己的日志访问接口。

public interface Log {
  boolean isDebugEnabled();
  boolean isTraceEnabled();
  void error(String s, Throwable e);
  void error(String s);
  void debug(String s);
  void trace(String s);
  void warn(String s);
}

针对 Log 接口,Mybatis 还提供了各种不同的实现类,分别使用不同的日志框架来实现 Log 接口。
在这里插入图片描述

这几个类的代码结构基本一致。我们把其中的 Log4jImpl 的源码贴到下方。在适配器模式中,传递给适配器构造函数的是被适配的类对象,而这里是 clazz (相当于日志名称 name),所以,从代码实现上来讲,它并非标准的适配器模式。但是,从应用场景上看,这里确实又起到了适配的作用,是典型的适配器模式的应用场景。

public class Log4jImpl implements Log {

  private static final String FQCN = Log4jImpl.class.getName();

  private final Logger log;

  public Log4jImpl(String clazz) {
    log = Logger.getLogger(clazz);
  }

  @Override
  public boolean isDebugEnabled() {
    return log.isDebugEnabled();
  }

  @Override
  public boolean isTraceEnabled() {
    return log.isTraceEnabled();
  }

  @Override
  public void error(String s, Throwable e) {
    log.log(FQCN, Level.ERROR, s, e);
  }

  @Override
  public void error(String s) {
    log.log(FQCN, Level.ERROR, s, null);
  }

  @Override
  public void debug(String s) {
    log.log(FQCN, Level.DEBUG, s, null);
  }

  @Override
  public void trace(String s) {
    log.log(FQCN, Level.TRACE, s, null);
  }

  @Override
  public void warn(String s) {
    log.log(FQCN, Level.WARN, s, null);
  }

}

总结

本章,讲解了 Mybatis 中用到的 8 种设计模式,它们分别是:建造者模式、工程模式、模板模式、解释权模式、单例模式、装饰器模式、适配器模式。再加上上篇文章的职责链模式和动态代理,总共讲了 10 种设计模式。

从两篇文章的讲解中,不知道你发现没有,Mybatis 对很多设计模式的实现,都并非标准的代码实现,都做了比较多的自我改进。实际上,这就是所谓的灵活应用,只借鉴不照搬,根据具体问题针对性地去解决。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1627264.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nvm的下载与安装

nvm&#xff08;Node Version Manager&#xff09;是一个用于管理 Node.js 版本的工具&#xff0c;它允许您在同一台计算机上安装和切换不同的 Node.js 版本。 一、下载地址 https://github.com/coreybutler/nvm-windows/releases 二、安装nvm 三、设置环境变量 在命令提示…

python之List列表

1. 高级数据类型 Python中的数据类型可以分为&#xff1a;数字型&#xff08;基本数据类型&#xff09;和非数字型&#xff08;高级数据类型&#xff09; 数字型包含&#xff1a;整型int、浮点型float、布尔型bool、复数型complex 非数字型包含&#xff1a;字符串str、列表l…

URL路由基础与Django处理请求的过程分析

1. URL路由基础 对于高质量的Web应用来讲&#xff0c;使用简洁、优雅的URL设计模式非常有必要。Django框架允许设计人员自由地设计URL模式&#xff0c;而不用受到框架本身的约束。对于URL路由来讲&#xff0c;其主要实现了Web服务的入口。用户通过浏览器发送过来的任何请求&am…

张小泉签约实在智能,用实在Agent打造自动化高

在不少老杭州人的童年记忆里&#xff0c;妈妈裁剪衣服、料理食材、修剪各种物品&#xff0c;用的都是张小泉刀剪。 近日&#xff0c;实在智能与“刀剪第一股”张小泉&#xff08;股票代码&#xff1a;301055.SZ&#xff09;正式达成合作&#xff0c;实在Agent数字员工助力张小…

PT Knockin - 仅需两分钟的在线电子邮件安全检查

我们很高兴向您介绍电子邮件安全评估工具 PT Knockin。 PT Knockin 是一个基于云的 SaaS 解决方案。这意味着企业无需下载或安装任何东西。他们只需访问 PT Knockin 网页&#xff0c;输入电子邮件地址并登录&#xff0c;然后在两分钟内收到电子邮件安全有效性分析报告以及解决…

Unreal Engine添加UGameInstanceSubsystem子类

点击C类文件夹&#xff0c;在右边的区域点击鼠标右键&#xff0c;在弹出的菜单中选择“新建C类”在弹出的菜单中选中“显示所有类”&#xff0c;选择GameInstanceSubsystem作为父类, 点击“下一步”按钮输入子类名称“UVRVIUOnlineGameSubsystem”&#xff0c;选择插件作为新类…

HTTP网络协议的请求方法,具体详解(2024-04-26)

1、HTTP 即超文本传输协议&#xff0c;是一种实现客户端和服务器之间通信的响应协议&#xff0c;它是用作客户端和服务器之间的请求 根据 HTTP 标准&#xff0c;HTTP 请求可以使用多种请求方法。 2、方法分类 HTTP1.0 定义了三种请求方法&#xff1a; GET, POST 和 HEAD 方…

[Linux][网络][网络基础][协议][网络传输基本流程][数据包封装和分用]详细讲解

目录 1.认识协议1."协议"本质就是一种约定2.计算机中的协议 2.网络协议初识1.协议分层2.OSI七层模型3.TCP/IP五层(四层)模型 3.网络传输基本流程0.预备知识1.跨网络的两台主机通信2.网络通信的基本轮廓 4.数据包封装和分用1.数据封装的过程2.数据分用的过程3.总结 5.…

杰发科技AC7840——ADC简介(1)_双路ADC同时使用

0. 简介 1. 特性 2. 双路ADC Sample里面没有双路的&#xff0c;以为那个规则组只有一个通道&#xff0c;看了外设寄存器才发现&#xff0c;原来他的通道是双路的。 注意1: ADC硬件引脚的配置 注意2: 规则组长度设置和 RSEQ序列号和CH通道号组合应该就对应了转换顺序&#xff0…

玩转手机在AidLux上安装宝塔面板

AidLux&#xff0c;手机不用刷机、不用root&#xff0c;直接在手机应用市场就能下载使用。 1.4G的应用包&#xff0c;看起来挺大的&#xff0c;那是因为内嵌了一套完整的AIoT应用开发和部署平台。 不仅Android手机可以玩&#xff0c;华为的Harmony系统也可以使用。 使用它最主…

认识HTTP

HTTP缺点 通信使用明文&#xff08;不加密&#xff09;&#xff0c;内容可能会被窃听 不验证通信方的身份&#xff0c;可能遭遇伪装 无法证明报文的完整性&#xff0c;所以有可能遭篡改 一、通信使用明文&#xff08;不加密&#xff09;&#xff0c;内容可能会被窃听 TCP/…

dpdk 总线设备管理

背景 dpdk版本: 22.11 1、dpdk支持的bus类型注册 注册当前能够处理bus类型,放在全局 rte_bus_list 链表中。 struct rte_bus: A structure describing a generic bus,描述总线类型的结构 注册方法 void rte_bus_register(struct rte_bus *bus); //函数功能: 将支持的bus结…

C++-4

在Complex类的基础上&#xff0c;完成^&#xff0c;>&#xff0c;~运算符的重载 #include <iostream>using namespace std; class Complex {int rel; //实部int vir; //虚部 public:Complex(){}Complex(int rel,int vir):rel(rel),vir(vir){}/* Complex operato…

5、Flink事件时间之Watermark详解

1&#xff09;生成 Watermark 1.Watermark 策略简介 为了使用事件时间语义&#xff0c;Flink 应用程序需要知道事件时间戳对应的字段&#xff0c;即数据流中的每个元素都需要拥有可分配的事件时间戳。 通过使用 TimestampAssigner API 从元素中的某个字段去访问/提取时间戳。…

服务器数据恢复—Storwize V3700存储数据恢复案例

服务器存储数据恢复环境&#xff1a; 某品牌Storwize V3700存储&#xff0c;10块硬盘组建了2组Mdisk加入到一个存储池中&#xff0c;一共创建了1个通用卷来存放数据&#xff0c;主要数据为oracle数据库。 服务器存储故障&#xff1a; 其中一组Mdisk中两块磁盘出现故障离线&…

【Linux】进程间通信(共享内存、消息队列、信号量)

一、System V —— 共享内存&#xff08;详解&#xff09; 共享内存区是最快的 IPC 形式。一旦这样的内存映射到共享它的进程的地址空间&#xff0c;这些进程间数据传递不再涉及到内核&#xff0c;换句话说&#xff0c;就是进程不再通过执行进入内核的系统调用来传递彼此的数…

DaVinci Resolve Studio 19(达芬奇19调色剪辑)win/mac激活版

DaVinci Resolve Studio是一个结合专业的8k 编辑&#xff0c;颜色混合&#xff0c;视觉效果和音频后期制作的软件。只需点击一下&#xff0c;你就可以立即在编辑、混音、特效和音频流之间切换。此外&#xff0c;达芬奇解决(达芬奇)是一个多用户协作的解决方案&#xff0c;使编辑…

实验7:路由冗余协议HSRP配置管理(课内实验以及解答)

实验目的及要求&#xff1a; 理解首跳冗余协议&#xff08;FHRP&#xff09;的工作原理&#xff0c;掌握热备份路由器协议 (HSRP)&#xff08;思科私有协议&#xff09;原理和配置。能够实现网络终端设备虚拟网关的配置和网络故障的灵活切换&#xff0c;完成相应网络的联通性测…

配置opencv属性表

编译opencv cmake -DCMAKE_INSTALL_PREFIX./install -G "Visual Studio 15 2017" -A x64 .. cmake -DCMAKE_INSTALL_PREFIX./install -G "Visual Studio 15 2017" -A Win32 ..cmake --build . --config Release -j 2cmake --build . --config Release --t…

uniapp制作分页查询功能

效果 代码 标签中 <uni-pagination change"pageChanged" :current"pageIndex" :pageSize"pageSize" :total"pageTotle" class"pagination" /> data中 pageIndex: 1, //分页器页码 pageSize: 10, //分页器每页显示…