mybatis独立使用及源码分析

mybatis独立使用及源码分析

不使用mybatis使用jdbc

JdbcExample

public class JdbcExample {

    public static void main(String[] args) throws Exception{
        Class.forName("org.postgresql.Driver");
        String url = "jdbc:postgresql://10.25.76.198:5432/artifact";
        String username = "dev_root";
        String password = "dev_root";
        Connection connection = DriverManager.getConnection(url, username, password);

        Statement stmt = connection.createStatement();
        PreparedStatement preparedStatement = connection.prepareStatement("select * from clean_policy where id = ?");
        preparedStatement.setLong(1,292L);

        //prepare sql进行预处理
        ResultSet resultSet = preparedStatement.executeQuery();
        while (resultSet.next()) {
            String name = resultSet.getString("name");
            System.out.println("name:" + name);
        }
        //statement 不预处理直接替换
        String sql = "select * from clean_policy where id = {0}";
        sql = MessageFormat.format(sql, 292L);
        ResultSet resultSet1 = stmt.executeQuery(sql);
        while (resultSet1.next()) {
            String name = resultSet1.getString("name");
            System.out.println("name1:" + name);
        }

    }
}

我们直接使用jdbc查询语句有两种方式:
1. 预处理
2. 直接替换

两种方式的本质区别:预处理可以避免sql注入;直接替换无法避免预处理

SPI

Class.forName

//加载pg的驱动,这里加载类会触发类的静态代码块
Class.forName("org.postgresql.Driver");


org.postgresql.Driver

class Driver{

//org.postgresql.Driver类的静态代码块,再类加载的时候就执行
  static {
        try {
            register();
        } catch (SQLException var1) {
            throw new ExceptionInInitializerError(var1);
        }
  }


  public static void register() throws SQLException {
      if (isRegistered()) {
            throw new IllegalStateException("Driver is already registered. It can only be registered once.");
      } else {
          Driver registeredDriver = new Driver();
          // 这里把驱动注册到驱动管理器 DriverManager的registeredDrivers集合中
          DriverManager.registerDriver(registeredDriver);
          Driver.registeredDriver = registeredDriver;
      }
  }
}

获取到数据库连接从已加载的驱动中获取

DriverManager.getConnection(url, username, password);

private static Connection getConnection(){

  for(DriverInfo aDriver : registeredDrivers) {

    Connection con = aDriver.driver.connect(url, info);
    return con
  }
}

SPI的机制

上面JdbcExample中main方法我们去掉Class.forName方法,发现还是可以正常获取数据连接,这说明数据库驱动是被
正常加载的,这种机制是如何实现的呢? 这种就是我们接下来要了解的SPI机制(service provider interface)

public class DriverManager {

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
            while(driversIterator.hasNext()) {
              driversIterator.next();
          }
    }
}


// 通过下面的源码我们可以看到driversIterator == lookupIterator

class ServiceLoader{

   public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
}


//driversIterator.hasNext()
private static final String PREFIX = "META-INF/services/";
 
private boolean hasNextService() {      
  if (configs == null) {
     try {
          // service.getName() == java.sql.Driver
           String fullName = PREFIX + service.getName();
           if (loader == null)
               configs = ClassLoader.getSystemResources(fullName);
            else
               configs = loader.getResources(fullName);
       } catch (IOException x) {
               fail(service, "Error locating configuration files", x);
       }
  }
     
     while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        //读取到jar包org.postgresql的META-INF下的services的java.sql.Driver的内容org.postgresql.Driver
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

// driversIterator.next();

private S nextService() {
  // 这里nextName就是从hasNextService方法中读到的org.postgresql.Driver,
  String cn = nextName;
  //和加载org.postgresql.Driver类,执行静态代码块,把自己注册到DriverManager的registeredDrivers集合中
  c = Class.forName(cn, false, loader);
}

总结:通过约定jar包具体目录存放接口的实现类,来动态可插拔加载类的方式
这里通过: 接口 + 配置文件 通过策略模式来实现spi机制,

  1. springboot中stater中就是通过这种方式来实现自动加载注入配置的。
  2. java中领域参数校验Validation也有采用这种机制

不依赖spring使用mybaits简单实例

创建Maven项目

MybatisUse

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;

/**
 * @author xuelongjiang109
 * @description
 **/
public class MybatisUse {

    public static void main(String[] args) throws Exception{
        String mybatisXml = "mybatis.xml";
        InputStream inputStream = Resources.getResourceAsStream(mybatisXml);

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        CleanPolicyMapper cleanPolicyMapper = sqlSession.getMapper(CleanPolicyMapper.class);

        CleanPolicy cleanPolicy = cleanPolicyMapper.selectById(292L);
        System.out.println("name:" + cleanPolicy.getName());

        sqlSession.commit();
        sqlSession.flushStatements();
        sqlSession.close();

    }
}

mybaits.xml

位于resources目录下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- configuration 核心配置文件 -->
<configuration>


    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="org.postgresql.Driver"/>
                <property name="url" value="jdbc:postgresql://10.25.76.198:5432/artifact"/>
                <property name="username" value="dev_root"/>
                <property name="password" value="dev_root"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 每一个 Mapper.XML 都需要在 Mybatis 核心配置文件中注册!!-->
    <mappers>
        <mapper class="com.xuelongjiang.testanyone.mybatis.CleanPolicyMapper"/>
        <!--<mapper class="com.song.dao.UserMapper"/>-->
        <!--<package name="com.song.dao"/>-->
    </mappers>
</configuration>

CleanPolicyMapper

import org.apache.ibatis.annotations.Select;

public interface CleanPolicyMapper {

    @Select("select * from clean_policy where id = #{id} ")
    CleanPolicy selectById(Long id);

}

CleanPolicy实体


/**
 * @author xuelongjiang109
 * @description
 **/
public class CleanPolicy {

    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

源码分析

下面源码会省略部分代码,我们只要焦距在主流程就行。

生成sql会话工厂

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//sqlSessionFactory是sqlSession的工厂,sqlSession是对数据库一次会话的抽象

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {

      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      // parser.parse() 返回org.apache.ibatis.session.Configuration
      return build(parser.parse());
}

//返回默认的会话工厂 DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

parser.parse()

 public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 读取到xml的根节点configuration
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

// 解析xml中的节点及其内容
private void parseConfiguration(XNode root) {
    environmentsElement(root.evalNode("environments"));
    mapperElement(root.evalNode("mappers"));
    // ....省略了解析其他节点如:properties,plugins,plugins
}
解析数据库配置
// 解析读取数据库配置 结果存放到configuration的environment属性
private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
          //得到指定数据源的id 对用我们配置的 development
        environment = context.getStringAttribute("default");
      }
      // 读取environment节点的内容
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        //如果当前环境的id等于指定的id则继续读取数据库相关配置
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);   
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

解析mapper

依次读取resource、url、class 注意这里如果mapper中的这个三个属性只能配置一个

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
          //如果配置的是package则加载其下的所有类
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
            // 如果是mapper 依次读取resource、url、class 注意这里如果mapper中的这个三个属性只能配置一个
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

// 继续解析mappers中的sql
mapperParser.parse();
// 解析xml中的select,iseret,update,delete语句
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
// 解析sql语句的各种配置以及Sql内容构造出MappedStatement来存储这些信息
statementParser.parseStatementNode();
//注意:MappedStatement中的id是namespace.id
MappedStatement
//MappedStatement接下来被放到configuration.mappedStatements中(这一个map StrictMap是mybatis继承HashMap重写了部分方法)
configuration.addMappedStatement(statement);
public V put(String key, V value) {
    //如果已经put过sql了直接报异常,如mapper接口有注解sql和xml也有sql,则直接报错
    if (containsKey(key)) {
        throw new IllegalArgumentException(name + " already contains value for " + key);
    }
    // ... 省略  
}

CleanPolicyMapper cleanPolicyMapper = sqlSession.getMapper(CleanPolicyMapper.class);如何执行的

CleanPolicyMapper是一个接口,我们并没有实现这个接口,那么getMapper返回的是什么呢?
通过下面源码,我们看到getMapper返回的一个动态代理对象

bindMapperForNamespace: 解析mapper对应的namespace,创建CleanPolicyMapper的代理类()

mapperParser.parse() ----> bindMapperForNamespace() ----> configuration.addMapper() —> knownMappers.put(type, new MapperProxyFactory(type));

sqlSession.getMapper(CleanPolicyMapper.class) —> mapperProxyFactory = knownMappers.get(type); —> mapperProxyFactory.newInstance(sqlSession); --> Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); --> MapperProxy.invoke() ----> mapperMethod.execute(sqlSession, args); —> sqlSession.selectOne(); --> DefaultSqlSession.selectList() -->
MappedStatement ms = configuration.getMappedStatement(); --> executor.query()

// 

mapperParser.parse();

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

  //解析namespace对应的类,加入到MapperProxyFactory代理工厂
  private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          // cleanPolicyMapper创建MapperProxyFactory加入到mapper中
          configuration.addMapper(boundType);
        }
      }
    }
  }


  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }


  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }


  @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

// 最终执行
 protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }


// 实现jdk的InvocationHandler接口实现动态代理
public class MapperProxy<T> implements InvocationHandler, Serializable {

}

加载:

  1. MappedStatement构造出
  2. 接口构造出MapperProxy动态代理

mybatis如何处理有无@param

selectById(@param(id)long id)
selectById(long id)

如果有@param则下方代码中names为
key=1,name= id
如果没有@param则
key=1,name=arg0


Object param = method.convertArgsToSqlCommandParam(args);
paramNameResolver.getNamedParams(args);


  public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    //如果没有参数直接直接返回null
    if (args == null || paramCount == 0) {
      return null;
    }
    //如果只有一个参数直接返回传入的值
    else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    }
    //如果有多个参数返回map结构 
    else {
      final Map<String, Object> param = new ParamMap<Object>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

这里我们重点看下多个参数的情况

selectById(@param(id)long id, @param(“name”) String name)
selectById(long id, String name)

id =292
name= zhang

1.有@param返回的map
key=id, value = 292
key=name,value=zhang
key=param1,value=292
key=param2,value=zhang

2.没有@param返回的map
key=arg0, value = 292
key=arg1,value=zhang
key=param1,value=292
key=param2,value=zhang

param1,param2的作用是为了第三方框架方便和mybatis继承。

mybatis处理sql注入

mybaits处理sql注入的原理是利用jdbc的PreparedStatement的set值而不是简单的替换。
#{}: 使用PreparedStatement来进行set值
${}: 简单替换


handler.parameterize(stmt);

public void parameterize(Statement statement) throws SQLException {
  parameterHandler.setParameters((PreparedStatement) statement);
}

void setParameters(PreparedStatement ps) {
  typeHandler.setParameter(ps, i + 1, value, jdbcType);
}

处理带有${}的动态sql

可以看到这里是简单替换有sql注入风险

executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

BoundSql boundSql = ms.getBoundSql(parameterObject);

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);


public BoundSql getBoundSql(Object parameterObject) {
   rootSqlNode.apply(context);
}

// TextSqlNode
public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
}
//解析替换${}
TextSqlNode.parse()

上述源码中的常见对象

Configuration: 存储解析的xml的属性,sql,自定义插件
SqlSessionFactory:sql会话工厂
SqlSession:sql会话
XMLConfigBuilder:解析mybatis的xml配置文件以及mapper对应的xml文件
MappedStatement mapper对应xml中sql的解析封装
StrictMap: mybaits继承hashmap重写put方法,key不能重复加入
MapperProxyFactory: mapper接口的代理对象
MapperProxy: 实现jdk动态代理InvocationHandler接口,实际执行增删改查调用它的invoke方法
MapperMethod: 内部封装sql类型,以及id可以找到MappedStatement,以及方法的签名信息,解析方法参数
executor: 方法执行器这个过程会涉及到mybatis的核心组件ParameterHanlder、ResultSetHanler、StatementHanlder、以及自身Executor

总结点

  1. JDBC加载驱动器有两种方式Class.forName()和SPI机制来加载驱动
  2. springboot中stater中就是通过这种方式来实现自动加载注入配置的。
  3. configuration存放了myabtis的整个配置文件的信息以及抽象出sql的MappedStatemen
  4. MappedStatement: 解析sql语句的各种配置以及Sql内容构造出MappedStatement来存储这些信息
  5. 中依次读取resource、url、class 注意这里如果mapper中的这个三个属性只能配置一个
  6. 解析sql语句的各种配置以及Sql内容构造出MappedStatement来存储这些信息
  7. MappedStatement中的id是namespace.id,也就是用nameSpace和id来定位唯一sql
  8. StrictMap的Put实现,如果put过sql了直接报异常(如mapper接口有注解sql和xml也有sql,则直接报错)
  9. interface中的方法与xml中sql的关联通过id(inteface全路径 + 方法名)
  10. mapper接口最终会被jdk动态代理为MapperProxy
  11. param1,param2的作用是为了第三方框架方便和mybatis继承。
  12. ${}有sql注入风险,#{}没有sql注入风险使用了jdbc的PreparedStatement

热门文章

暂无图片
编程学习 ·

Java输出数组的内容

Java输出数组的内容_一万个小时-CSDN博客_java打印数组内容1. 输出内容最常见的方式// List<String>类型的列表List<String> list new ArrayList<String>();list.add("First");list.add("Second");list.add("Third");list.ad…
暂无图片
编程学习 ·

母螳螂的“魅惑之术”

在它们对大蝗虫发起进攻的时候&#xff0c;我认认真真地观察了一次&#xff0c;因为它们突然像触电一样浑身痉挛起来&#xff0c;警觉地面对限前这个大家伙&#xff0c;然后放下自己优雅的身段和祈祷的双手&#xff0c;摆出了一个可怕的姿势。我被眼前的一幕吓到了&#xff0c;…
暂无图片
编程学习 ·

疯狂填词 mad_libs 第9章9.9.2

#win7 python3.7.0 import os,reos.chdir(d:\documents\program_language) file1open(.\疯狂填词_d9z9d2_r.txt) file2open(.\疯狂填词_d9z9d2_w.txt,w) words[ADJECTIVE,NOUN,VERB,NOUN] str1file1.read()#方法1 for word in words :word_replaceinput(fEnter a {word} :)str1…
暂无图片
编程学习 ·

HBASE 高可用

为了保证HBASE是高可用的,所依赖的HDFS和zookeeper也要是高可用的. 通过参数hbase.rootdir指定了连接到Hadoop的地址,mycluster表示为Hadoop的集群. HBASE本身的高可用很简单,只要在一个健康的集群其他节点通过命令 hbase-daemon.sh start master启动一个Hmaster进程,这个Hmast…
暂无图片
编程学习 ·

js事件操作语法

一、事件的绑定语法 语法形式1 事件监听 标签对象.addEventListener(click,function(){}); 语法形式2 on语法绑定 标签对象.onclick function(){} on语法是通过 等于赋值绑定的事件处理函数 , 等于赋值本质上执行的是覆盖赋值,后赋值的数据会覆盖之前存储的数据,也就是on…
暂无图片
编程学习 ·

Photoshop插件--晕影动态--选区--脚本开发--PS插件

文章目录1.插件界面2.关键代码2.1 选区2.2 动态晕影3.作者寄语PS是一款栅格图像编辑软件&#xff0c;具有许多强大的功能&#xff0c;本文演示如何通过脚本实现晕影动态和选区相关功能&#xff0c;展示从互联网收集而来的一个小插件&#xff0c;供大家学习交流&#xff0c;请勿…
暂无图片
编程学习 ·

vs LNK1104 无法打开文件“xxx.obj”

写在前面&#xff1a; 向大家推荐两本新书&#xff0c;《深度学习计算机视觉实战》和《学习OpenCV4&#xff1a;基于Python的算法实战》。 《深度学习计算机视觉实战》讲了计算机视觉理论基础&#xff0c;讲了案例项目&#xff0c;讲了模型部署&#xff0c;这些项目学会之后可以…
暂无图片
编程学习 ·

工业元宇宙的定义与实施路线图

工业元宇宙的定义与实施路线图 李正海 1 工业元宇宙 给大家做一个关于工业元宇宙的定义。对于工业&#xff0c;从设计的角度来讲&#xff0c;现在的设计人员已经做到了普遍的三维设计&#xff0c;但是进入元宇宙时代&#xff0c;就不仅仅只是三维设计了&#xff0c;我们的目…
暂无图片
编程学习 ·

【leectode 2022.1.15】完成一半题目

有 N 位扣友参加了微软与力扣举办了「以扣会友」线下活动。主办方提供了 2*N 道题目&#xff0c;整型数组 questions 中每个数字对应了每道题目所涉及的知识点类型。 若每位扣友选择不同的一题&#xff0c;请返回被选的 N 道题目至少包含多少种知识点类型。 示例 1&#xff1a…
暂无图片
编程学习 ·

js 面试题总结

一、js原型与原型链 1. prototype 每个函数都有一个prototype属性&#xff0c;被称为显示原型 2._ _proto_ _ 每个实例对象都会有_ _proto_ _属性,其被称为隐式原型 每一个实例对象的隐式原型_ _proto_ _属性指向自身构造函数的显式原型prototype 3. constructor 每个prot…
暂无图片
编程学习 ·

java练习代码

打印自定义行数的空心菱形练习代码如下 import java.util.Scanner; public class daYinLengXing{public static void main(String[] args) {System.out.println("请输入行数");Scanner myScanner new Scanner(System.in);int g myScanner.nextInt();int num g%2;//…
暂无图片
编程学习 ·

RocketMQ-什么是死信队列?怎么解决

目录 什么是死信队列 死信队列的特征 死信消息的处理 什么是死信队列 当一条消息初次消费失败&#xff0c;消息队列会自动进行消费重试&#xff1b;达到最大重试次数后&#xff0c;若消费依然失败&#xff0c;则表明消费者在正常情况下无法正确地消费该消息&#xff0c;此时…
暂无图片
编程学习 ·

项目 cg day04

第4章 lua、Canal实现广告缓存 学习目标 Lua介绍 Lua语法 输出、变量定义、数据类型、流程控制(if..)、循环操作、函数、表(数组)、模块OpenResty介绍(理解配置) 封装了Nginx&#xff0c;并且提供了Lua扩展&#xff0c;大大提升了Nginx对并发处理的能&#xff0c;10K-1000K Lu…
暂无图片
编程学习 ·

输出三角形

#include <stdio.h> int main() { int i,j; for(i0;i<5;i) { for(j0;j<i;j) { printf("*"); } printf("\n"); } }
暂无图片
编程学习 ·

stm32的BOOTLOADER学习1

序言 最近计划学习stm32的BOOTLOADER学习,把学习过程记录下来 因为现在网上STM32C8T6还是比较贵的,根据我的需求flash空间小一些也可以,所以我决定使用stm32c6t6.这个芯片的空间是32kb的。 #熟悉芯片内部的空间地址 1、flash ROM&#xff1a; 大小32KB&#xff0c;范围&#xf…
暂无图片
编程学习 ·

通过awk和shell来限制IP多次访问之学不会你打死我

学不会你打死我 今天我们用shell脚本&#xff0c;awk工具来分析日志来判断是否存在扫描器来进行破解网站密码——限制访问次数过多的IP地址&#xff0c;通过Iptables来进行限制。代码在末尾 首先我们要先查看日志的格式&#xff0c;分析出我们需要筛选的内容&#xff0c;日志…
暂无图片
编程学习 ·

Python - 如何像程序员一样思考

在为计算机编写程序之前&#xff0c;您必须学会如何像程序员一样思考。学习像程序员一样思考对任何学生都很有价值。以下步骤可帮助任何人学习编码并了解计算机科学的价值——即使他们不打算成为计算机科学家。 顾名思义&#xff0c;Python经常被想要学习编程的人用作第一语言…
暂无图片
编程学习 ·

蓝桥杯python-数字三角形

问题描述 虽然我前后用了三种做法&#xff0c;但是我发现只有“优化思路_1”可以通过蓝桥杯官网中的测评&#xff0c;但是如果用c/c的话&#xff0c;每个都通得过&#xff0c;足以可见python的效率之低&#xff08;但耐不住人家好用啊&#xff08;哭笑&#xff09;&#xff09…