如何通過Spring Boot配置動態數據源訪問多個資料庫

来源:https://www.cnblogs.com/elon/archive/2018/03/01/8486618.html
-Advertisement-
Play Games

之前寫過一篇博客《Spring+Mybatis+Mysql搭建分散式資料庫訪問框架》描述如何通過Spring+Mybatis配置動態數據源訪問多個資料庫。但是之前的方案有一些限制(原博客中也描述了):只適用於資料庫數量不多且固定的情況。針對資料庫動態增加的情況無能為力。 下麵講的方案能支持資料庫動態 ...


之前寫過一篇博客《Spring+Mybatis+Mysql搭建分散式資料庫訪問框架》描述如何通過Spring+Mybatis配置動態數據源訪問多個資料庫。但是之前的方案有一些限制(原博客中也描述了):只適用於資料庫數量不多且固定的情況。針對資料庫動態增加的情況無能為力。

下麵講的方案能支持資料庫動態增刪,數量不限。

資料庫環境準備

下麵以Mysql為例,先在本地建3個資料庫用於測試。需要說明的是本方案不限資料庫數量,支持不同的資料庫部署在不同的伺服器上。如圖所示db_project_001、db_project_002、db_project_003。

 

 

搭建Java後臺微服務項目

創建一個Spring Boot的maven項目:

 

 

 

config:數據源配置。

datasource:自己實現的動態數據源相關類。

dbmgr:管理項目編碼與資料庫IP、名稱的映射關係(實際項目中這部分數據保存在redis緩存中,可動態增刪)。

mapper:mybatis的資料庫訪問介面。

model:映射模型。

rest:微服務對外發佈的restful介面,這裡用來測試。

application.yml:配置資料庫JDBC參數。

詳細的代碼實現

1. 數據源配置管理類(DataSourceConfig.java)

 1 package com.elon.dds.config;
 2 
 3 import javax.sql.DataSource;
 4 
 5 import org.apache.ibatis.session.SqlSessionFactory;
 6 import org.mybatis.spring.SqlSessionFactoryBean;
 7 import org.mybatis.spring.annotation.MapperScan;
 8 import org.springframework.beans.factory.annotation.Qualifier;
 9 import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
10 import org.springframework.boot.context.properties.ConfigurationProperties;
11 import org.springframework.context.annotation.Bean;
12 import org.springframework.context.annotation.Configuration;
13 
14 import com.elon.dds.datasource.DynamicDataSource;
15 
16 /**
17  * 數據源配置管理。
18  * 
19  * @author elon
20  * @version 2018年2月26日
21  */
22 @Configuration
23 @MapperScan(basePackages="com.elon.dds.mapper", value="sqlSessionFactory")
24 public class DataSourceConfig {
25 
26     /**
27      * 根據配置參數創建數據源。使用派生的子類。
28      * 
29      * @return 數據源
30      */
31     @Bean(name="dataSource")
32     @ConfigurationProperties(prefix="spring.datasource")
33     public DataSource getDataSource() {
34         DataSourceBuilder builder = DataSourceBuilder.create();
35         builder.type(DynamicDataSource.class);
36         return builder.build();
37     }
38     
39     /**
40      * 創建會話工廠。
41      * 
42      * @param dataSource 數據源
43      * @return 會話工廠
44      */
45     @Bean(name="sqlSessionFactory")
46     public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) {
47         SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
48         bean.setDataSource(dataSource);
49         
50         try {
51             return bean.getObject();
52         } catch (Exception e) {
53             e.printStackTrace();
54             return null;
55         }
56     }
57 }

 

2.  定義動態數據源

1)  首先增加一個資料庫標識類,用於區分不同的資料庫(DBIdentifier.java)

由於我們為不同的project創建了單獨的資料庫,所以使用項目編碼作為資料庫的索引。而微服務支持多線程併發的,採用線程變數。

 1 package com.elon.dds.datasource;
 2 
 3 /**
 4  * 資料庫標識管理類。用於區分數據源連接的不同資料庫。
 5  * 
 6  * @author elon
 7  * @version 2018-02-25
 8  */
 9 public class DBIdentifier {
10     
11     /**
12      * 用不同的工程編碼來區分資料庫
13      */
14     private static ThreadLocal<String> projectCode = new ThreadLocal<String>();
15 
16     public static String getProjectCode() {
17         return projectCode.get();
18     }
19 
20     public static void setProjectCode(String code) {
21         projectCode.set(code);
22     }
23 }

 

2)  從DataSource派生了一個DynamicDataSource,在其中實現資料庫連接的動態切換(DynamicDataSource.java)

 1 package com.elon.dds.datasource;
 2 
 3 import java.lang.reflect.Field;
 4 import java.sql.Connection;
 5 import java.sql.SQLException;
 6 
 7 import org.apache.logging.log4j.LogManager;
 8 import org.apache.logging.log4j.Logger;
 9 import org.apache.tomcat.jdbc.pool.DataSource;
10 import org.apache.tomcat.jdbc.pool.PoolProperties;
11 
12 import com.elon.dds.dbmgr.ProjectDBMgr;
13 
14 /**
15  * 定義動態數據源派生類。從基礎的DataSource派生,動態性自己實現。
16  * 
17  * @author elon
18  * @version 2018-02-25
19  */
20 public class DynamicDataSource extends DataSource {
21     
22     private static Logger log = LogManager.getLogger(DynamicDataSource.class);
23     
24     /**
25      * 改寫本方法是為了在請求不同工程的數據時去連接不同的資料庫。
26      */
27     @Override
28     public Connection getConnection(){
29         
30         String projectCode = DBIdentifier.getProjectCode();
31         
32         //1、獲取數據源
33         DataSource dds = DDSHolder.instance().getDDS(projectCode);
34         
35         //2、如果數據源不存在則創建
36         if (dds == null) {
37             try {
38                 DataSource newDDS = initDDS(projectCode);
39                 DDSHolder.instance().addDDS(projectCode, newDDS);
40             } catch (IllegalArgumentException | IllegalAccessException e) {
41                 log.error("Init data source fail. projectCode:" + projectCode);
42                 return null;
43             }
44         }
45         
46         dds = DDSHolder.instance().getDDS(projectCode);
47         try {
48             return dds.getConnection();
49         } catch (SQLException e) {
50             e.printStackTrace();
51             return null;
52         }
53     }
54     
55     /**
56      * 以當前數據對象作為模板複製一份。
57      * 
58      * @return dds
59      * @throws IllegalAccessException 
60      * @throws IllegalArgumentException 
61      */
62     private DataSource initDDS(String projectCode) throws IllegalArgumentException, IllegalAccessException {
63         
64         DataSource dds = new DataSource();
65         
66         // 2、複製PoolConfiguration的屬性
67         PoolProperties property = new PoolProperties();
68         Field[] pfields = PoolProperties.class.getDeclaredFields();
69         for (Field f : pfields) {
70             f.setAccessible(true);
71             Object value = f.get(this.getPoolProperties());
72             
73             try
74             {
75                 f.set(property, value);                
76             }
77             catch (Exception e)
78             {
79                 //有一些static final的屬性不能修改。忽略。
80                 log.info("Set value fail. attr name:" + f.getName());
81                 continue;
82             }
83         }
84         dds.setPoolProperties(property);
85 
86         // 3、設置資料庫名稱和IP(一般來說,埠和用戶名、密碼都是統一固定的)
87         String urlFormat = this.getUrl();
88         String url = String.format(urlFormat, ProjectDBMgr.instance().getDBIP(projectCode), 
89                 ProjectDBMgr.instance().getDBName(projectCode));
90         dds.setUrl(url);
91 
92         return dds;
93     }
94 }

 

3)  通過DDSTimer控制數據連接釋放(DDSTimer.java)

 1 package com.elon.dds.datasource;
 2 
 3 import org.apache.tomcat.jdbc.pool.DataSource;
 4 
 5 /**
 6  * 動態數據源定時器管理。長時間無訪問的資料庫連接關閉。
 7  * 
 8  * @author elon
 9  * @version 2018年2月25日
10  */
11 public class DDSTimer {
12     
13     /**
14      * 空閑時間周期。超過這個時長沒有訪問的資料庫連接將被釋放。預設為10分鐘。
15      */
16     private static long idlePeriodTime = 10 * 60 * 1000;
17     
18     /**
19      * 動態數據源
20      */
21     private DataSource dds;
22     
23     /**
24      * 上一次訪問的時間
25      */
26     private long lastUseTime;
27     
28     public DDSTimer(DataSource dds) {
29         this.dds = dds;
30         this.lastUseTime = System.currentTimeMillis();
31     }
32     
33     /**
34      * 更新最近訪問時間
35      */
36     public void refreshTime() {
37         lastUseTime = System.currentTimeMillis();
38     }
39     
40     /**
41      * 檢測數據連接是否超時關閉。
42      * 
43      * @return true-已超時關閉; false-未超時
44      */
45     public boolean checkAndClose() {
46         
47         if (System.currentTimeMillis() - lastUseTime > idlePeriodTime)
48         {
49             dds.close();
50             return true;
51         }
52         
53         return false;
54     }
55 
56     public DataSource getDds() {
57         return dds;
58     }
59 }

 

4)  通過DDSHolder來管理不同的數據源,提供數據源的添加、查詢功能(DDSHolder.java)

 1 package com.elon.dds.datasource;
 2 
 3 import java.util.HashMap;
 4 import java.util.Iterator;
 5 import java.util.Map;
 6 import java.util.Map.Entry;
 7 import java.util.Timer;
 8 
 9 import org.apache.tomcat.jdbc.pool.DataSource;
10 
11 /**
12  * 動態數據源管理器。
13  * 
14  * @author elon
15  * @version 2018年2月25日
16  */
17 public class DDSHolder {
18     
19     /**
20      * 管理動態數據源列表。<工程編碼,數據源>
21      */
22     private Map<String, DDSTimer> ddsMap = new HashMap<String, DDSTimer>();
23 
24     /**
25      * 通過定時任務周期性清除不使用的數據源
26      */
27     private static Timer clearIdleTask = new Timer();
28     static {
29         clearIdleTask.schedule(new ClearIdleTimerTask(), 5000, 60 * 1000);
30     };
31     
32     private DDSHolder() {
33         
34     }
35     
36     /*
37      * 獲取單例對象
38      */
39     public static DDSHolder instance() {
40         return DDSHolderBuilder.instance;
41     }
42     
43     /**
44      * 添加動態數據源。
45      * 
46      * @param projectCode 項目編碼 
47      * @param dds dds
48      */
49     public synchronized void addDDS(String projectCode, DataSource dds) {
50         
51         DDSTimer ddst = new DDSTimer(dds);
52         ddsMap.put(projectCode, ddst);
53     }
54     
55     /**
56      * 查詢動態數據源
57      * 
58      * @param projectCode 項目編碼
59      * @return dds
60      */
61     public synchronized DataSource getDDS(String projectCode) {
62         
63         if (ddsMap.containsKey(projectCode)) {
64             DDSTimer ddst = ddsMap.get(projectCode);
65             ddst.refreshTime();
66             return ddst.getDds();
67         }
68         
69         return null;
70     }
71     
72     /**
73      * 清除超時無人使用的數據源。
74      */
75     public synchronized void clearIdleDDS() {
76         
77         Iterator<Entry<String, DDSTimer>> iter = ddsMap.entrySet().iterator();
78         for (; iter.hasNext(); ) {
79             
80             Entry<String, DDSTimer> entry = iter.next();
81             if (entry.getValue().checkAndClose())
82             {
83                 iter.remove();
84             }
85         }
86     }
87     
88     /**
89      * 單例構件類
90      * @author elon
91      * @version 2018年2月26日
92      */
93     private static class DDSHolderBuilder {
94         private static DDSHolder instance = new DDSHolder();
95     }
96 }

 

5)  定時器任務ClearIdleTimerTask用於定時清除空閑的數據源(ClearIdleTimerTask.java)

 1 package com.elon.dds.datasource;
 2 
 3 import java.util.TimerTask;
 4 
 5 /**
 6  * 清除空閑連接任務。
 7  * 
 8  * @author elon
 9  * @version 2018年2月26日
10  */
11 public class ClearIdleTimerTask extends TimerTask {
12     
13     @Override
14     public void run() {
15         DDSHolder.instance().clearIdleDDS();
16     }
17 }

3.  管理項目編碼與資料庫IP和名稱的映射關係(ProjectDBMgr.java)

 1 package com.elon.dds.dbmgr;
 2 
 3 import java.util.HashMap;
 4 import java.util.Map;
 5 
 6 /**
 7  * 項目資料庫管理。提供根據項目編碼查詢資料庫名稱和IP的介面。
 8  * @author elon
 9  * @version 2018年2月25日
10  */
11 public class ProjectDBMgr {
12     
13     /**
14      * 保存項目編碼與數據名稱的映射關係。這裡是硬編碼,實際開發中這個關係數據可以保存到redis緩存中;
15      * 新增一個項目或者刪除一個項目只需要更新緩存。到時這個類的介面只需要修改為從緩存拿數據。
16      */
17     private Map<String, String> dbNameMap = new HashMap<String, String>();
18     
19     /**
20      * 保存項目編碼與資料庫IP的映射關係。
21      */
22     private Map<String, String> dbIPMap = new HashMap<String, String>();
23     
24     private ProjectDBMgr() {
25         dbNameMap.put("project_001", "db_project_001");
26         dbNameMap.put("project_002", "db_project_002");
27         dbNameMap.put("project_003", "db_project_003");
28         
29         dbIPMap.put("project_001", "127.0.0.1");
30         dbIPMap.put("project_002", "127.0.0.1");
31         dbIPMap.put("project_003", "127.0.0.1");
32     }
33     
34     public static ProjectDBMgr instance() {
35         return ProjectDBMgrBuilder.instance;
36     }
37     
38     // 實際開發中改為從緩存獲取
39     public String getDBName(String projectCode) {
40         if (dbNameMap.containsKey(projectCode)) {
41             return dbNameMap.get(projectCode);
42         }
43         
44         return "";
45     }
46     
47     //實際開發中改為從緩存中獲取
48     public String getDBIP(String projectCode) {
49         if (dbIPMap.containsKey(projectCode)) {
50             return dbIPMap.get(projectCode);
51         }
52         
53         return "";
54     }
55     
56     private static class ProjectDBMgrBuilder {
57         private static ProjectDBMgr instance = new ProjectDBMgr();
58     }
59 }

4.  編寫資料庫訪問的mapper(UserMapper.java)

 1 package com.elon.dds.mapper;
 2 
 3 import java.util.List;
 4 
 5 import org.apache.ibatis.annotations.Mapper;
 6 import org.apache.ibatis.annotations.Result;
 7 import org.apache.ibatis.annotations.Results;
 8 import org.apache.ibatis.annotations.Select;
 9 
10 import com.elon.dds.model.User;
11 
12 /**
13  * Mybatis映射介面定義。
14  * 
15  * @author elon
16  * @version 2018年2月26日
17  */
18 @Mapper
19 public interface UserMapper
20 {
21     /**
22      * 查詢所有用戶數據
23      * @return 用戶數據列表
24      */
25     @Results(value= {
26             @Result(property="userId", column="id"),
27             @Result(property="name", column="name"),
28             @Result(property="age", column="age")
29     })
30     @Select("select id, name, age from tbl_user")
31     List<User> getUsers();
32 } 

 

5. 定義查詢對象模型(User.java)

 1 package com.elon.dds.model;
 2 
 3 public class User
 4 {
 5     private int userId = -1;
 6 
 7     private String name = "";
 8     
 9     private int age = -1;
10     
11     @Override
12     public String toString()
13     {
14         return "name:" + name + "|age:" + age;
15     }
16 
17     public int getUserId()
18     {
19         return userId;
20     }
21 
22     public void setUserId(int userId)
23     {
24         this.userId = userId;
25     }
26 
27     public String getName()
28     {
29         return name;
30     }
31 
32     public void setName(String name)
33     {
34         this.name = name;
35     }
36 
37     public int getAge()
38     {
39         return age;
40     }
41 
42     public void setAge(int age)
43     {
44         this.age = age;
45     }
46 }

 

6.  定義查詢數據的restful介面(WSUser.java)

 1 package com.elon.dds.rest;
 2 
 3 import java.util.List;
 4 
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.web.bind.annotation.RequestMapping;
 7 import org.springframework.web.bind.annotation.RequestMethod;
 8 import org.springframework.web.bind.annotation.RequestParam;
 9 import org.springframework.web.bind.annotation.RestController;
10 
11 import com.elon.dds.datasource.DBIdentifier;
12 import com.elon.dds.mapper.UserMapper;
13 import com.elon.dds.model.User;
14 
15 /**
16  * 用戶數據訪問介面。
17  * 
18  * @author elon
19  * @version 2018年2月26日
20  */
21 @RestController
22 @RequestMapping(value="/user")
23 public class WSUser {
24 
25     @Autowired
26     private UserMapper userMapper;
27     
28     /**
29      * 查詢項目中所有用戶信息
30      * 
31      * @param projectCode 項目編碼
32      * @return 用戶列表
33      */
34     @RequestMapping(value="/v1/users", method=RequestMethod.GET)
35     public List<User> queryUser(@RequestParam(value="projectCode", required=true) String projectCode) 
36     {
37         DBIdentifier.setProjectCode(projectCode);
38         return userMapper.getUsers();
39     }
40 }

 

要求每次查詢都要帶上projectCode參數。

7.   編寫Spring Boot App的啟動代碼(App.java)

 1 package com.elon.dds;
 2 
 3 import org.springframework.boot.SpringApplication;
 4 import org.springframework.boot.autoconfigure.SpringBootApplication;
 5 
 6 /**
 7  * Hello world!
 8  *
 9  */
10 @SpringBootApplication
11 public class App 
12 {
13     public static void main( String[] args )
14     {
15         System.out.println( "Hello World!" );
16         SpringApplication.run(App.class, args);
17     }
18 }

 

8.  在application.yml中配置數據源

其中的資料庫IP和資料庫名稱使用%s。

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

-Advertisement-
Play Games
更多相關文章
  • Docker 是一個容器工具,提供虛擬環境。很多人認為,它改變了我們對軟體的認識。 站在 Docker 的角度,軟體就是容器的組合:業務邏輯容器、資料庫容器、儲存容器、隊列容器......,Docker 使得軟體可以拆分成若幹個標準化容器,然後像搭積木一樣組合起來。 這正是微服務(microserv ...
  • 本文是我原創,首發於美團點評技術團隊博客。原文地址是:https://mp.weixin.qq.com/s/fx6XfBpuzozsJCvllMcCqw。歡迎大家轉載,轉載請註明出處,謝謝~~。 背景 2017年8月25日,我懷著“再也不要在下班時間收到報警”的美好期待,加入美團金融智能支付負責核心 ...
  • 2013年發佈至今, Docker 一直廣受矚目,被認為可能會改變軟體行業。 但是,許多人並不清楚 Docker 到底是什麼,要解決什麼問題,好處又在哪裡?本文就來詳細解釋,幫助大家理解它,還帶有簡單易懂的實例,教你如何將它用於日常開發。 一、環境配置的難題 軟體開發最大的麻煩事之一,就是環境配置。 ...
  • 上篇的內容,我們探討了分散式計算中的 MapReduce與批處理 。所以本篇我們將繼續探索分散式計算優化的相關細節,並且分析MapReduce與批處理的局限性,看看 流式計算 是否能給我們在分散式計算層面提供一個更好的解決方案。 1.MapReduce的局限 MapReduce作業是獨立於其他作業, ...
  • 互聯網分層架構的本質?MVC,MVP,MVVM ? 上圖是一個典型的互聯網分層架構: 客戶端層:典型調用方是browser或者APP 站點應用層:實現核心業務邏輯,從下游獲取數據,對上游返回html或者json 數據-緩存層:加速訪問存儲 數據-資料庫層:固化數據存儲 客戶端層:典型調用方是brow ...
  • 大型站點高併發架構技術 高併發: 高併發主要是由於網站PV訪問量大,單台伺服器涌承載大量訪問所帶來的壓力,所以會採用多台伺服器進行分流,採用伺服器集群技術,對於每個訪問會被髮送到哪台伺服器,我們採取負載均衡策略,常見的技術有LVS,由於網站中有大量的靜態頁面,所以採用緩存伺服器和反向代理技術,包括H ...
  • #117. 歐拉迴路 統計 描述 提交 自定義測試 有一天一位靈魂畫師畫了一張圖,現在要你找出歐拉迴路,即在圖中找一個環使得每條邊都在環上出現恰好一次。 一共兩個子任務: 輸入格式 第一行一個整數 tt,表示子任務編號。t∈{1,2}t∈{1,2},如果 t=1t=1 則表示處理無向圖的情況,如果  ...
  • 文件處理 一.文件處理 1.介紹 電腦系統:電腦硬體,操作系統,應用程式 應用程式無法直接操作硬體,通過操作系統來操作文件,進而讀/寫硬體中的文件。 python打開文件過程: 2.打開文件的模式 a.打開文本文件 b.對於非文本文件,只能使用b模式,"b"表示以位元組的方式操作(而所有文件也都是 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...