MyBatis詳解(一)

来源:https://www.cnblogs.com/chafry/archive/2022/11/27/16876309.html
-Advertisement-
Play Games

MyBatis簡單介紹 【1】MyBatis是一個持久層的ORM框架【Object Relational Mapping,對象關係映射】,使用簡單,學習成本較低。可以執行自己手寫的SQL語句,比較靈活。但是MyBatis的自動化程度不高,移植性也不高,有時從一個資料庫遷移到另外一個資料庫的時候需要自 ...


MyBatis簡單介紹

【1】MyBatis是一個持久層的ORM框架【Object Relational Mapping,對象關係映射】,使用簡單,學習成本較低。可以執行自己手寫的SQL語句,比較靈活。但是MyBatis的自動化程度不高,移植性也不高,有時從一個資料庫遷移到另外一個資料庫的時候需要自己修改配置,所以稱只為半自動ORM框架。

 

傳統JDBC介紹

【1】簡單使用

@Test
public  void test() throws SQLException {
    Connection conn=null;
    PreparedStatement pstmt=null;
    try {
        // 1.載入驅動,其實這一步可以不加因為DriverManager裡面會有自動載入驅動的一步
        Class.forName("com.mysql.jdbc.Driver");

        // 2.創建連接
        conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis_example", "root", "123456");

        //開啟事務
        conn.setAutoCommit(false);

        // SQL語句
        String sql="select id,user_name,create_time from t_user where id=? ";

        // 獲得sql執行者
        pstmt=conn.prepareStatement(sql);
        pstmt.setInt(1,1);

        // 執行查詢
        //ResultSet rs= pstmt.executeQuery();
        pstmt.execute();
        ResultSet rs= pstmt.getResultSet();

        rs.next();
        User user =new User();
        user.setId(rs.getLong("id"));
        user.setUserName(rs.getString("user_name"));
        user.setCreateTime(rs.getDate("create_time"));
        System.out.println(user.toString());
    } catch (Exception e) {
        e.printStackTrace();
    }
    finally{
        // 關閉資源
        try {
            if(conn!=null){
                conn.close();
            }
            if(pstmt!=null){
                pstmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

 

【1.1】DriverManager如何自動載入驅動【利用靜態代碼塊調用初始化方法,至於為什麼要使用SPI機制,主要是為了實現解耦和可插拔,因為驅動有多種】

  註:驅動展示【位於mysql-connector-java-5.1.22.jar/META-INF/services/java.sql.Driver  內容為:com.mysql.jdbc.Driver】

static {
    // 啟動類載入器載入DriverManager類,觸發靜態方法執行
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

    // 載入java.sql.Driver驅動的實現
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            // 1、創建一個 ServiceLoader對象,【這裡就將上下文類載入器設置到ServiceLoader對象的變數上了】
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 2、創建一個迭代器對象
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{
                // 3、這裡調用driversIterator.hasNext()的時候,觸發將 META-INF/services 下的配置文件中的數據讀取進來,方便下麵的next方法使用
                while(driversIterator.hasNext()) {
                    // 4、【關鍵】:觸發上面創建的迭代器對象的方法調用。這裡才是具體載入的實現邏輯,非常不好找
                    driversIterator.next();
                }
            } catch(Throwable t) {}
            return null;
        }
    });

    //判斷有沒有載入到
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {...}
    }
}

//ServiceLoader類#hasNext方法
public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // 1、拼湊要讀取的文件的全名
            // final String PREFIX = "META-INF/services/";
            String fullName = PREFIX + service.getName();

            // 2、根據 fullName 去到META-INF/services/目錄下尋找配置文件
            // 如果類載入器為空,則使用系統類載入器,如果不為空則使用指定的類載入器
            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;
        }

        // 3、使用parse方法解析配置文件中的每一行數據
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}

 

【2】當然正常情況一般是封裝成類來使用的,如

//數據訪問基類
public class BaseDao {
    // 驅動
    private static String DRIVER = null;
    // 鏈接字元串
    private static String URL = null;
    // 用戶名
    private static String USERNAME = null;
    // 密碼
    private static String PASSWORD = null;

    //初始化
    static {  init();  }

    // 初始化
    private static void init() {
        try {
            // 使用Properties對象讀取資源文件屬性
            Properties pro = new Properties();
            // 獲得資源文件輸入流
            InputStream inStream = BaseDao.class.getClassLoader().getResourceAsStream("db.properties");
            // 載入輸入流
            pro.load(inStream);
            DRIVER = pro.getProperty("mysql.driverClass");
            URL = pro.getProperty("mysql.jdbcUrl");
            USERNAME = pro.getProperty("mysql.user");
            PASSWORD = pro.getProperty("mysql.password");

            Class.forName(DRIVER);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //獲取資料庫連接對象
    protected Connection getConnection() {
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return conn;
    }

    /**
     * 關閉所有鏈接
     * @param conn
     * @param stmt
     * @param rs
     */
    protected void CloseAll(Connection conn, Statement stmt, ResultSet rs) {
        try {
            if (conn != null) {
                conn.close();
            }
            if (stmt != null) {
                stmt.close();
            }
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * 執行 增、刪、 改的公共方法
     * @param sql     SQL語句
     * @param prams   參數
     * @return           受影響的行數
     */
    protected int executeUpdate(String sql, Object... prams) {
        // 獲得資料庫鏈接對象
        Connection conn = getConnection();
        // 聲明SQL執行者
        PreparedStatement pstmt = null;
        try {
            // 獲得SQL執行者
            pstmt = conn.prepareStatement(sql);

            // 迴圈載入參數
            for (int i = 0; i < prams.length; i++) {
                pstmt.setObject(i + 1, prams[i]);
            }
            // 執行executeUpdate 返回受影響行數
            return pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 關閉所有需要關閉的對象
            CloseAll(conn, pstmt, null);
        }

        return 0;
    }

    /**
     * 執行查詢 返回單個值
     * @param sql         SQL語句
     * @param prams     參數
     * @return             OBJECT
     */
    protected Object executeScaler(String sql, Object... prams) {
        // 獲得資料庫鏈接對象
        Connection conn = getConnection();
        // 聲明SQL執行者
        PreparedStatement pstmt = null;
        // 聲明查詢結果集
        ResultSet rs = null;
        // 接收單個值
        Object value = null;
        try {
            // 獲得SQL執行者
            pstmt = conn.prepareStatement(sql);

            // 迴圈載入參數
            for (int i = 0; i < prams.length; i++) {
                pstmt.setObject(i + 1, prams[i]);
            }
            // 執行executeUpdate 返回受影響行數
            rs = pstmt.executeQuery();

            if (rs.next()) {
                value = rs.getObject(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            CloseAll(conn, pstmt, rs);
        }
        return value;
    }

    /**
     * 執行查詢返回list
     * 
     * @param sql         SQL語句
     * @param clazz     類的類型
     * @return List
     */
    public <T> List<T> executeList(String sql, Class<T> clazz, Object... prams) {
        // 數據集合
        List<T> list = new ArrayList<T>();
        // 獲得資料庫連接
        Connection conn = getConnection();
        // 聲明SQL執行者
        PreparedStatement pstmt = null;
        // 聲明查詢結果集
        ResultSet rs = null;
        try {

            // 3. 通過鏈接創建一個SQL執行者
            pstmt = conn.prepareStatement(sql);

            // 迴圈載入參數
            for (int i = 0; i < prams.length; i++) {
                //內部會通過instance of判斷這個參數到底是哪個類型的具體對象
                pstmt.setObject(i + 1, prams[i]);
            }

            // 4 執行查詢SQL 返回查詢結果
            rs = pstmt.executeQuery();

            // 獲得結果集的列信息對象
            ResultSetMetaData rsmd = rs.getMetaData();

            while (rs.next()) {
                // 通過類反射實例化
                T obj = clazz.newInstance();

                // 迴圈所有的列
                for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                    /* 通過屬性名稱使用反射給泛型實例賦值 Begin */

                    // 獲得每一列的列名
                    String cloName = rsmd.getColumnName(i);
                    // 根據列名反射到類的欄位
                    Field filed = clazz.getDeclaredField(cloName);
                    // 設置私有屬性的訪問許可權
                    filed.setAccessible(true);

                    // 給泛型實例的某一個屬性賦值
                    filed.set(obj, rs.getObject(cloName));
                    /* 通過屬性名稱使用反射給泛型實例賦值 End */
                }
                // 將泛型實例添加到 泛型集合中
                list.add(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return list;
    }

    /**
     * 執行查詢返回JavaBean
     * 
     * @param sql         SQL語句
     * @param clazz     類的類型
     * @return JavaBean
     */
    public <T> T executeJavaBean(String sql, Class<T> clazz, Object... prams) {
        // 聲明數據對象
        T obj=null;
        // 獲得資料庫連接
        Connection conn = getConnection();
        // 聲明SQL執行者
        PreparedStatement pstmt = null;
        // 聲明查詢結果集
        ResultSet rs = null;
        try {

            // 3. 通過鏈接創建一個SQL執行者
            pstmt = conn.prepareStatement(sql);

            // 迴圈載入參數
            for (int i = 0; i < prams.length; i++) {
                pstmt.setObject(i + 1, prams[i]);
            }

            // 4 執行查詢SQL 返回查詢結果
            rs = pstmt.executeQuery();

            // 獲得結果集的列信息對象
            ResultSetMetaData rsmd = rs.getMetaData();

            if (rs.next()) {
                // 通過類反射實例化
                obj = clazz.newInstance();

                // 迴圈所有的列
                for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                    /* 通過屬性名稱使用反射給泛型實例賦值 Begin */

                    // 獲得每一列的列名
                    String cloName = rsmd.getColumnName(i);
                    // 根據列名反射到類的欄位
                    Field filed = clazz.getDeclaredField(cloName);
                    // 設置私有屬性的訪問許可權
                    filed.setAccessible(true);

                    // 給泛型實例的某一個屬性賦值
                    filed.set(obj, rs.getObject(cloName));
                    /* 通過屬性名稱使用反射給泛型實例賦值 End */
                } 
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return obj;
    }
}

 

【3】總結JDBC的四大核心對象

1DriverManager(驅動管理對象):獲取資料庫連接;
2Connection(資料庫連接對象):獲取執行sql對象;管理事務;
3Statement(執行sql對象):executeUpdate執行DML語句(增刪改)DDL語句;executeQuery 執行DQL語句;
4ResultSet(結果集對象)

 

【4】傳統JDBC的問題

1)資料庫連接創建,釋放頻繁造成西戎資源的浪費,從而影響系統性能,使用資料庫連接池可以解決問題。
2)sql語句在代碼中硬編碼,造成代碼的不已維護,實際應用中sql的變化可能較大,sql代碼和java代碼沒有分離開來維護不方便。
3)使用preparedStatement向有占位符傳遞參數存在硬編碼問題因為sql中的where子句的條件不確定,同樣是修改不方便。
4)對結果集中解析存在硬編碼問題,sql的變化導致解析代碼的變化,系統維護不方便。

【5】針對問題的優化

1、資料庫連接創建、釋放頻繁造成系統資源浪費從而影響系統性能,如果使用資料庫連接池可解決此問題。
優化部分,如mybatis:在SqlMapConfig.xml中配置數據連接池,使用連接池管理資料庫鏈接。

2、Sql語句寫在代碼中造成代碼不易維護,實際應用sql變化的可能較大,sql變動需要改變java代碼。
優化部分,如mybatis:將Sql語句配置在XXXXmapper.xml文件中與java代碼分離。

3、向sql語句傳參數麻煩,因為sql語句的where條件不一定,可能多也可能少,占位符需要和參數一一對應。
優化部分,如mybatis:自動將java對象映射至sql語句,通過statement中的parameterType定義輸入參數的類型。

4、對結果集解析麻煩,sql變化導致解析代碼變化,且解析前需要遍歷,如果能將資料庫記錄封裝成pojo對象解析比較方便。
優化部分,如mybatis:自動將sql執行結果映射至java對象,通過statement中的resultType定義輸出結果的類型。

 

Mybaits整體體系圖

【1】圖示

          

【2】分析

把Mybatis的功能架構分為三層:
1基礎支撐層:負責最基礎的功能支撐,包括連接管理、事務管理、配置載入和緩存處理,這些都是共用的東西,將他們抽取出來作為最基礎的組件。為上層的數據處理層提供最基礎的支撐。
對應示例中的部分為:  SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

2API介面層:提供給外部使用的介面API,開發人員通過這些本地API來操縱資料庫。介面層一接收到調用請求就會調用數據處理層來完成具體的數據處理。
對應示例中的部分為:  SqlSession session = sqlMapper.openSession();

3數據處理層:負責具體的SQL查找、SQL解析、SQL執行和執行結果映射處理等。它主要的目的是根據調用的請求完成一次資料庫操作。
對應示例中的部分為:  
User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1);
或者
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1L);

 

【3】簡單示例

public static void main(String[] args) {
    String resource = "mybatis-config.xml";
    Reader reader;
    try {
        //將XML配置文件構建為Configuration配置類
        reader = Resources.getResourceAsReader(resource);
        // 通過載入配置文件流構建一個SqlSessionFactory  DefaultSqlSessionFactory
        SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
        // 數據源 執行器  DefaultSqlSession
        SqlSession session = sqlMapper.openSession();
        try {
            // 執行查詢 底層執行jdbc
            //User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1);

            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.selectById(1L);
            
            session.commit();
            System.out.println(user.getUserName());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            session.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

Mybaits插件的分析

【1】插件主要作用於四⼤組件對象

1)執⾏器 Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 等⽅法)
2)參數處理器 ParameterHandler (getParameterObject, setParameters 等⽅法)
3)結果集處理器 ResultSetHandler (handleResultSets, handleOutputParameters 等⽅法)
4)SQL語法構建處理器 StatementHandler (prepare, parameterize, batch, update, query 等⽅法)    

 

源碼分析部分

MyBatis解析全局配置文件的源碼分析

【1】分析怎麼通過載入配置文件流構建一個SqlSessionFactory

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

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.
      }
    }
}

// 到這裡配置文件已經解析成了Configuration
public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

 

【1.1】分析XMLConfigBuilder類怎麼將xml的資源文件流解析成Configuration

【1.1.1】新建XMLConfigBuilder過程【所以說XMLConfigBuilder並不負責解析,解析的是它裡面的XPathParser類,】

/**
* 創建一個用於解析xml配置的構建器對象
* @param inputStream 傳入進來的xml的配置
* @param environment 我們的環境變數
* @param props:用於保存我們從xml中解析出來的屬性
*/
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    /**
     * 該方法做了二個事情
     * 第一件事情:創建XPathParser 解析器對象,在這裡會把我們的
     * 把我們的mybatis-config.xml解析出一個Document對象
     * 第二節事情:調用重寫的構造函數來構建我XMLConfigBuilder
     */
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    //調用父類的BaseBuilder的構造方法:給configuration賦值,typeAliasRegistry別名註冊器賦值,TypeHandlerRegistry賦值
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");

    //把props綁定到configuration的props屬性上
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
}

//又因為class XMLConfigBuilder extends BaseBuilder,所以看一下他的父類
public abstract class BaseBuilder {
    // mybatis的全局配置文件
    protected final Configuration configuration;
    // 用於保存我們的Entity的別名
    protected final TypeAliasRegistry typeAliasRegistry;
    // 用戶保存我們java類型和jdbc資料庫類型的
    protected final TypeHandlerRegistry typeHandlerRegistry;

    public BaseBuilder(Configuration configuration) {
        this.configuration = configuration;
        this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
    }
    ....
}

 

【1.1.2】parser.parse()解析過程【本質上就是從根結點開始解析

public Configuration parse() {
    //若已經解析過了 就拋出異常
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    //設置解析標誌位,保證只解析一次
    parsed = true;
    /**
     * 解析我們的mybatis-config.xml的節點
     * <configuration> </configuration>
     */
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

//方法實現說明:解析我們mybatis-config.xml的 configuration節點
private void parseConfiguration(XNode root) {
    try {
      // 解析 properties節點
      // 如:<properties resource="mybatis/db.properties" />
      propertiesElement(root.evalNode("properties"));
      
      // 解析我們的mybatis-config.xml中的settings節點
      // 如:<setting name="mapUnderscoreToCamelCase" value="false"/>
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      
      // 基本沒有用過該屬性
      // VFS含義是虛擬文件系統;主要是通過程式能夠方便讀取本地文件系統、FTP文件系統等系統中的文件資源。
      // Mybatis中提供了VFS這個配置,主要是通過該配置可以載入自定義的虛擬文件系統應用程式
      loadCustomVfs(settings);
      
      // 指定 MyBatis 所用日誌的具體實現,未指定時將自動查找。
      // SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
      // 解析到org.apache.ibatis.session.Configuration#logImpl
      loadCustomLogImpl(settings);

      // 解析別名typeAliases節點
      typeAliasesElement(root.evalNode("typeAliases"));

      // 解析插件節點(比如分頁插件),解析到 interceptorChain.interceptors
      pluginElement(root.evalNode(	   

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1,首先創建一個普通的Java項目 2,將項目變為web項目 在項目上單機右鍵-->選擇 Add FrameWorks Support 選擇第一項 web application-->OK 此時項目已經變為web項目,已經可以添加tomcat運行,但是一般web項目都會使用maven管理,所以可以添 ...
  • 代碼1 class Base { public: Base(int data=10):ma(data){ cout<<"Base()"<<endl; } void show(){cout<<"Base Show()"<<endl;} void show(int){cout<<"Base Show(i ...
  • 7. Spring整合Drools 全套代碼及資料全部完整提供,點此處下載 7.1 Spring簡單整合Drools 在項目中使用Drools時往往會跟Spring整合來使用。具體整合步驟如下: 第一步:創建maven工程drools_spring並配置pom.xml <?xml version=" ...
  • Listener監聽器 1.Listener監聽器介紹 Listener監聽器是JavaWeb三大組件之一。JavaWeb三大組件分別是:Servlet程式,Listener監聽器,Filter過濾器。 Listener是JavaEE的規範,即介面。 監聽器的作用是,監聽某種變化(一般就是對象的創建 ...
  • 初始化 Date date = new Date(); 輸出時間字元串 System.out.println(date.toString()); 字母 描述 示例G 紀元標記 ADy 四位年份 2001M 月份 July or 07d 一個月的日期 10h A.M./P.M. (1~12)格式小時 ...
  • 如果只是想簡單地對整個程式做計算統計,通常使用UNIX下的time命令就足夠了。由於我用的是Mac系統,和Linux系統的輸出可能有不同,不過關鍵都是這三個時間:user: 運行用戶態代碼所花費的時間,也即CPU實際用於執行該進程的時間,其他進程和進程阻塞的時間不計入此數字;system: 在內核中... ...
  • 本文author:@愷龍 報錯情況 在一次使用JSP的jstl時候按照正常引入jstl和使用for each標簽出現瞭如下報錯: 分析原因 經過一番調查研究發現原因如下: JavaEE被Oracle捐獻給Apache了。目前最高版本是 JavaEE8; Apache把JavaEE換名了,以後不叫Ja ...
  • 一、概念辨析:網路延遲與網路波動 (1) 網路延遲 網路延遲是指各式各樣的數據在網路介質中通過網路協議(如TCP/IP)進行傳輸,如果信息量過大不加以限制,超額的網路流量就會導致設備反應緩慢,造成網路延遲。受限於光速,網路延遲總是有一個下限,是無法超越物理極限的。 (2) 網路波動(丟包率) 網路波 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...