分類
發燒車訊

基於NACOS和JAVA反射機制動態更新JAVA靜態常量非@Value註解

1.前言

項目中都會使用常量類文件, 這些值如果需要變動需要重新提交代碼,或者基於@Value註解實現動態刷新, 如果常量太多也是很麻煩; 那麼 能不能有更加簡便的實現方式呢?

本文講述的方式是, 一個JAVA類對應NACOS中的一個配置文件,優先使用nacos中的配置,不配置則使用程序中的默認值;

2.正文

nacos的配置如下圖所示,為了滿足大多數情況,配置了 namespace命名空間和group;

 

 

 新建個測試工程 cloud-sm.

bootstrap.yml 中添加nacos相關配置;

為了支持多配置文件需要注意ext-config節點,group對應nacos的添加的配置文件的group; data-id 對應nacos上配置的data-id

配置如下:

server:
  port: 9010
  servlet:
    context-path: /sm
spring:
  application:
    name: cloud-sm
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.100.101:8848 #Nacos服務註冊中心地址
        namespace: 1
      config:
        server-addr: 192.168.100.101:8848 #Nacos作為配置中心地址
        namespace: 1
        ext-config:
          - group: TEST_GROUP
            data-id: cloud-sm.yaml
            refresh: true
          - group: TEST_GROUP
            data-id: cloud-sm-constant.properties
            refresh: true

接下來是本文重點:

1)新建註解ConfigModule,用於在配置類上;一個value屬性;

2)新建個監聽類,用於獲取最新配置,並更新常量值

實現流程:

1)項目初始化時獲取所有nacos的配置

2)遍歷這些配置文件,從nacos上獲取配置

3)遍歷nacos配置文件,獲取MODULE_NAME的值

4)尋找配置文件對應的常量類,從spring容器中尋找 常量類 有註解ConfigModule 且值是 MODULE_NAME對應的

5)使用JAVA反射更改常量類的值

6)增加監聽,用於動態刷新

 

import org.springframework.stereotype.Component;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Component
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ConfigModule {
    /**
     *  對應配置文件裏面key為( MODULE_NAME ) 的值
     * @return
     */
    String value();
}
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.client.utils.LogUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 * nacos 自定義監聽
 *
 * @author zch
 */
@Component
public class NacosConfigListener {
    private Logger LOGGER = LogUtils.logger(NacosConfigListener.class);
    @Autowired
    private NacosConfigProperties configs;
    @Value("${spring.cloud.nacos.config.server-addr:}")
    private String serverAddr;
    @Value("${spring.cloud.nacos.config.namespace:}")
    private String namespace;
    @Autowired
    private ApplicationContext applicationContext;
    /**
     * 目前只考慮properties 文件
     */
    private String fileType = "properties";
    /**
     * 需要在配置文件中增加一條 MODULE_NAME 的配置,用於找到對應的 常量類
     */
    private String MODULE_NAME = "MODULE_NAME";

    /**
     * NACOS監聽方法
     *
     * @throws NacosException
     */
    public void listener() throws NacosException {
        if (StringUtils.isBlank(serverAddr)) {
            LOGGER.info("未找到 spring.cloud.nacos.config.server-addr");
            return;
        }
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr.split(":")[0]);
        if (StringUtils.isNotBlank(namespace)) {
            properties.put(PropertyKeyConst.NAMESPACE, namespace);
        }

        ConfigService configService = NacosFactory.createConfigService(properties);
        // 處理每個配置文件
        for (NacosConfigProperties.Config config : configs.getExtConfig()) {
            String dataId = config.getDataId();
            String group = config.getGroup();
            //目前只考慮properties 文件
            if (!dataId.endsWith(fileType)) continue;

            changeValue(configService.getConfig(dataId, group, 5000));

            configService.addListener(dataId, group, new Listener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    changeValue(configInfo);
                }

                @Override
                public Executor getExecutor() {
                    return null;
                }
            });
        }
    }

    /**
     * 改變 常量類的 值
     *
     * @param configInfo
     */
    private void changeValue(String configInfo) {
        if(StringUtils.isBlank(configInfo)) return;
        Properties proper = new Properties();
        try {
            proper.load(new StringReader(configInfo)); //把字符串轉為reader
        } catch (IOException e) {
            e.printStackTrace();
        }
        String moduleName = "";
        Enumeration enumeration = proper.propertyNames();
        //尋找MODULE_NAME的值
        while (enumeration.hasMoreElements()) {
            String strKey = (String) enumeration.nextElement();
            if (MODULE_NAME.equals(strKey)) {
                moduleName = proper.getProperty(strKey);
                break;
            }
        }
        if (StringUtils.isBlank(moduleName)) return;
        Class curClazz = null;
        // 尋找配置文件對應的常量類
        // 從spring容器中 尋找類的註解有ConfigModule 且值是 MODULE_NAME對應的
        for (String beanName : applicationContext.getBeanDefinitionNames()) {
            Class clazz = applicationContext.getBean(beanName).getClass();
            ConfigModule configModule = (ConfigModule) clazz.getAnnotation(ConfigModule.class);
            if (configModule != null && moduleName.equals(configModule.value())) {
                curClazz = clazz;
                break;
            }
        }
        if (curClazz == null) return;
        // 使用JAVA反射機制 更改常量
        enumeration = proper.propertyNames();
        while (enumeration.hasMoreElements()) {
            String key = (String) enumeration.nextElement();
            String value = proper.getProperty(key);
            if (MODULE_NAME.equals(key)) continue;
            try {
                Field field = curClazz.getDeclaredField(key);
                //忽略屬性的訪問權限
                field.setAccessible(true);
                Class<?> curFieldType = field.getType();
                //其他類型自行拓展
                if (curFieldType.equals(String.class)) {
                    field.set(null, value);
                } else if (curFieldType.equals(List.class)) { // 集合List元素
                    field.set(null, JSONUtils.parse(value));
                } else if (curFieldType.equals(Map.class)) { //Map
                    field.set(null, JSONUtils.parse(value));
                }
            } catch (NoSuchFieldException | IllegalAccessException e) {
                LOGGER.info("設置屬性失敗:{} {} = {} ", curClazz.toString(), key, value);
            }
        }
    }

    @PostConstruct
    public void init() throws NacosException {
        listener();
    }
}

 3.測試

1)新建常量類Constant,增加註解@ConfigModule(“sm”),盡量測試全面, 添加常量類型有 String, List,Map

@ConfigModule("sm")
public class Constant {

    public static volatile String TEST = new String("test");

    public static volatile List<String> TEST_LIST = new ArrayList<>();
    static {
        TEST_LIST.add("默認值");
    }
    public static volatile Map<String,Object> TEST_MAP = new HashMap<>();
    static {
        TEST_MAP.put("KEY","初始化默認值");
    }
    public static volatile List<Integer> TEST_LIST_INT = new ArrayList<>();
    static {
        TEST_LIST_INT.add(1);
    }
}

2)新建個Controller用於測試這些值

@RestController
public class TestController {

    @GetMapping("/t1")
    public Map<String, Object> test1() {
        Map<String, Object> result = new HashMap<>();

        result.put("string" , Constant.TEST);
        result.put("list" , Constant.TEST_LIST);
        result.put("map" , Constant.TEST_MAP);
        result.put("list_int" , Constant.TEST_LIST_INT);
        result.put("code" , 1);
        return result;
    }
}

3)當前nacos的配置文件cloud-sm-constant.properties為空

 4)訪問測試路徑localhost:9010/sm/t1,返回為默認值

{
    "code": 1,
    "string": "test",
    "list_int": [
        1
    ],
    "list": [
        "默認值"
    ],
    "map": {
        "KEY": "初始化默認值"
    }
}

5)然後更改nacos的配置文件cloud-sm-constant.properties;

 6)再次訪問測試路徑localhost:9010/sm/t1,返回為nacos中的值

{
    "code": 1,
    "string": "12351",
    "list_int": [
        1,
        23,
        4
    ],
    "list": [
        "123",
        "sss"
    ],
    "map": {
        "A": 12,
        "B": 432
    }
}

4.結語

這種實現方式優點如下:

1)動態刷新配置,不需要重啟即可改變程序中的靜態常量值

2)使用簡單,只需在常量類上添加一個註解

3)避免在程序中大量使用@Value,@RefreshScope註解

 不足:

此代碼是個人業餘時間的想法,未經過生產驗證,實現的數據類型暫時只寫幾個,其餘的需要自行拓展

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

聚甘新

分類
發燒車訊

學習ASP.NET Core(11)-解決跨域問題與程序部署

上一篇我們介紹了系統日誌與測試相關的內容並添加了相關的功能;本章我們將介紹跨域與程序部署相關的內容

一、跨域

1、跨域的概念

1、什麼是跨域?

一個請求的URL由協議,域名,端口號組成,以百度的https://www.baidu.com為例,協議為https,域名由子域名www和主域名baidu組成,端口號若為80會自動隱藏(也可以配置為其它端口,通過代理服務器將80端口請求轉發給實際的端口號)。而當請求的URL的協議,域名,端口號任意一個於當前頁面的URL不同即為跨域

2、什麼是同源策略?

瀏覽器存在一個同源策略,即為了防範跨站腳本的攻擊,出現跨域請求時瀏覽器會限制自身不能執行其它網站的腳本(如JavaScript)。所以說當我們把項目部署到Web服務器后,通過瀏覽器進行請求時就會出現同源策略問題;而像PostMan軟件因其是客戶端形式的,所以不存在此類問題

3、跨域會導致什麼問題?

同源策略會限制以下行為:

  • Cookie、LocalStorage和IndexDb的讀取
  • DOM和JS對象的獲取
  • Ajax請求的發送

2、常用的解決方法

這裏我們將簡單介紹針對跨域問題常用的幾種解決辦法,並就其中的Cors方法進行配置,若對其它方式感興趣,可參照老張的哲學的文章,⅖ 種方法實現完美跨域

2.1、JsonP

1、原理

上面有提到瀏覽器基於其同源策略會限制部分行為,但對於Script標籤是沒有限制的,而JsonP就是基於這一點,它會在頁面種動態的插入Script標籤,其Src屬性對應的就是api接口的地址,前端會以Get方式將處理函數以回調的形式傳遞給後端,後端響應後會再以回調的方式傳遞給前端,最終頁面得以显示

2、優缺點

JsonP出現時間較早,所以對舊版本瀏覽器支持性較好;但自身只支持Get請求,無法確認請求是否成功

2.2、 CORS

1、原理

CORS的全稱是Corss Origin Resource Sharing,即跨域資源共享,它允許將當前域下的資源被其它域的腳本請求訪問。其實現原理就是在響應的head中添加Access-Control-Allow-Origin,只要有該字段就支持跨域請求

2、優缺點

Cors支持所有Http方法,不用考慮接口規則,使用簡單;但是對一些舊版本的瀏覽器支持性欠佳

3、使用

其使用非常簡單,以我們的項目為例,在BlogSystem.Core項目的Startup類的ConfigureServices方法中進行如下配置

同時需要開啟使用中間件,如下:

2.3、Nginx

1、原理

跨域問題是指在一個地址中發起另一個地址的請求,而Nginx可以利用其反向代理的功能,接受請求后直接請求該地址,類似打開了一個新的頁面,所以可以避開跨域的問題

2、優缺點

配置簡單,可以降低開發成本,方便配置負載均衡;靈活性差,每個環境都需要進行不同的配置

二、程序部署

1、部署模式

在.NET Core中,有兩種部署模式,分別為FDD(Framework-dependent)框架依賴發布模式和SCD(Self-contained)自包含獨立發布模式

  • FDD:此類部署需要服務器安裝.NET Core SDK環境,部署的包容量會比較小,但可能因SDK版本存在兼容性問題;
  • SCD:此類部署自包含.NET Core SDK的環境,不同.NET Core版本可以共存,其部署包容量會較大,且需要對服務器進行相關配置

2、常用部署方式

以下內容均參考老張的哲學的文章最全的部署方案 & 最豐富的錯誤分析,有興趣的朋友可以參考原文

2.1、Windows平台

  • 直接運行:發布目標選擇windows時會在文件夾中生成一個exe文件,我們可以直接執行exe文件或使用CLI命令調用dll運行;這種方式雖然方便,卻存在一些弊端,比如說部署多個的情況下會存在很多控制台窗口,如誤操作會導致窗口關閉等;
  • 部署服務:除了上述直接運行的方式外,我們還可以將程序發布為服務,發布后我們可以像控制系統服務一樣控製程序的啟動和關閉

需要注意的是上述兩類方法都需要藉助IIS或者是代理服務器進行服務的轉發,否則只能在本地進行訪問;

2.2、Linux平台

Linux平台常用的部署方式即為程序+代理服務器,但是當我們配置完成后運行程序時,該運行命令會一直佔用操作窗口,所以我們需要使用“守護進程”來解決這個問題,簡單來說就是將程序放到後台運行,不影響我們進行其他操作

綜上,部署模式、部署方式及部署平台有多種組合方式,接下來我們挑選下述3種方法進行演示:

方案 依賴運行時/宿主機 依賴代理服務器 其它配置
Windows程序(SCD)+Nginx
Windows服務(FDD)+IIS 設置為服務
Linux程序(FDD)+Nginx 守護進程

3、程序發布

1、這裏我們右擊BlogSystem.Core項目,選擇發布,選擇文件夾后,點擊高級

2、為了演示後面的發布實例,這裏我們分別選擇3種組合模式,①獨立+win-x64;②框架依賴+win-x64;③框架依賴+linux-x64

3、將發布實例拷貝到單獨的文件夾種,這裏我們使用SCD-Window驗證下程序能否直接運行,運行BlogSystem.Core.exe,報錯:

原來還是老問題,BLL沒有添加到發布文件中,我們到項目的bin文件夾下將BLL和DAL的dll文件分別拷貝至3個文件夾,再次運行,出現404錯誤,經過確認發現,首頁對應的是Swagger文檔頁面,而在配置中間件時我們有添加開發環境才配置swagger的邏輯,所以這裏我們可以根據個人需求決定是否添加。

這裏我為了方便確認發布是否成功,所以將其從判斷邏輯中取出了。重新生成發布文件,拷貝BLL和DAL的dll文件,再次運行,還是報錯。原來時Swagger的XML文件缺失,從bin文件夾下拷貝添加至發布文件,運行后成功显示頁面

4、有的朋友會說了,每次都要拷貝這兩個dll和這兩個xml文件,太麻煩了。其實也是有對應的解決辦法的,我們可以使用dotnet的CLI命令進行發布,選擇引用的發布文件夾為bin文件夾,拷貝至發布文件夾即可,有興趣的朋友可以自行研究

三、服務器發布

這裏我用的是阿里雲服務器,Window系統版本是Window Server2012 R2,Linux系統版本是CentOS 8.0;在操作前記得確認拷貝的發布文件能否在本地正常運行

1、Windows程序(SCD)+Nginx

1、解壓后雙擊exe文件網站可以正常運行,如下:

2、這個時候我們發現了一個問題,服務器上沒有數據庫,所以無法確認功能是否正常,這裏我們先下載安裝一個Microsoft SQL Server 2012 Express數據庫(建項目時沒有考慮到發布后測試的問題,實際上像SQLite數據庫是非常符合這類場景的)

安裝完成后我們新建一個BlogSystem的數據庫,通過Sql文件的形式將數據庫結構和數據導入至服務器數據庫,這時候又發現一個問題,由於我們連接數據庫的邏輯放置在model層的BlogSystemContext文件夾下,所以需要將連接中的DataSource更改為Express數據庫,重新發布后覆蓋舊的發布文件(系統設計有缺陷,可以將EF上下文文件放在應用程序層或單獨一層),再次運行,成功執行查詢,如下:

3、這個時候本地已經可以進行正常的訪問了,但是外部網絡是無法訪問調用接口的,這裏我們藉助Nginx進行服務的轉發。下載Nginx后解壓對conf文件夾下的nginx.conf文件進行如下配置:

4、在nginx.exe文件所在目錄的文件路徑輸入cmd,鍵入nginx啟動服務訪問8081端口,成功显示頁面(確保core程序正常運行)如下:

5、這個時候我們使用其它電腦訪問接口,發現還是無法訪問,經過查詢是阿里雲服務器進行了相關的限制,在阿里雲控制台配置安全組規則后即可正常訪問,如下:

6、配置完成后運行,成功訪問該網站且功能正常。這類方法不需要藉助Core的運行時環境,可以說十分便捷

2、Windows服務(FDD)+IIS

1、首先我們將FDD發布文件壓縮后拷貝至Window Server主機,因FDD的部署方法需要藉助.NET Core運行時環境,所以這裏我們首先到官網https://dotnet.microsoft.com/download/dotnet-core/current/runtime下載安裝.NET Core運行時,這裏我們選擇的是右邊這個,安裝完需要重新啟動

2、上一個方法中桌面显示控制台窗口顯然不是一個較佳的方案,所以這裏我們將其註冊為服務。官方提供了ASP.NET Core服務託管的方法,但使用較為複雜,這裏我們藉助一個名為nssm的工具來達到同樣的目的。我們下載nssm后,在其exe路徑運行cmd命令,執行nssm install,在彈出的窗口中進行如下配置:

3、我們在系統服務中開啟BlogSytem.Core_Server,在控制面版中選擇安裝IIS服務,併發布對應的項目,安裝完成后,添加部署為8082端口,將應用程序池修改為無託管,如下:

4、運行網站,成功显示頁面,但是進行功能試用時發現報錯;經過確認是由於IIS應用程序池的用戶驗證模式和sqlserver的驗證模式不同,解決辦法有三種①修改應用程序池高級設置中的進程模型中的標識②將連接數據庫字符串中的Integrated Security=True去除,並添加數據庫連接對應的賬號密碼③在數據庫的“安全性”>“登錄名”裏面,添加對應IIS程序池的名稱,並在這個用戶的“服務器角色”和“用戶映射”中給他對應的權限

後續嘗試方案一失敗,嘗試方案二成功,方案三由於要安裝SSMS所以沒有嘗試,有遇到相同問題的朋友可以自己試下

3、Linux程序(FDD)+Nginx

1、首先我們使用MobaXterm工具登錄至Linux主機(選擇此工具是由於其)同時支持文件傳送和命令行操作),這裏使用的Linux版本是CentOS 8.0;藉助MobaXterm工具在home文件夾下創建WebSite文件夾,並在其內部創建BlogSystem文件夾,將我們準備好的FDD部署方式的發布文件上傳至此文件夾后,使用命令sudo dnf install dotnet-sdk-3.1安裝.net core sdk,如下圖

2、輸入cd /home/WebSite/BlogSystem切換至項目文件夾后,使用dotnet BlogSystem.Core.dll運行程序,成功執行,但是由於我們沒有數據庫,且未配置代理服務器,所以無法驗證服務是否正常運行;所以這裏我們先參照微軟doc快速入門:在 Red Hat 上安裝 SQL Server 並創建數據庫安裝Sql Server數據庫(阿里雲默認安裝了python3作為解釋器所以無需重複安裝),安裝完成后我們開放在阿里雲實例中開放1433端口,使用可視化工具導入表結構和數據

3、完成上述操作后我們需要配置守護進程,將程序放在後台運行。首先我們在/etc/systemd/system下新建守護進程文件,文件名以.service結尾,這裏我們新建名為BlogSystem.service文件,使用MobaXterm自帶的編輯器打開文件後進行如下配置,注意後面的中文備註需要去除否則會報錯

[Unit]
Description=BlogSystem    #服務描述,隨便填就好

[Service]
WorkingDirectory=/home/WebSite/BlogSystem/   #工作目錄,填你應用的絕對路徑
ExecStart=/usr/bin/dotnet /home/WebSite/BlogSystem/BlogSystem.Core.dll    #啟動:前半截是你dotnet的位置(一般都在這個位置),後半部分是你程序入口的dll,中間用空格隔開
Restart=always  
RestartSec=25 #如果服務出現問題會在25秒后重啟,數值可自己設置
SyslogIdentifier=BlogSystem  #設置日誌標識,此行可以沒有
User=root   #配置服務用戶,越高越好
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target

我們使用cd /etc/systemd/system/切換至BlogSystem.service對應的目錄,使用systemctl enable BlogSystem.service設置為開機運行后,再使用systemctl start BlogSystem.service啟動服務,另外可以使用systemctl status BlogSystem確認服務狀態

4、接下來我們安裝代理Nginx代理默認的5000端口,使用sudo yum install nginx安裝nginx后,我們到\etc\nginx文件夾下打開nginx.conf文件進行如下配置:

配置完成我們進入\etc\nginx文件夾下,使用systemctl enable nginx將nginx設置為開機啟動,並使用systemctl start nginx啟用服務,同樣可以使用systemctl status nginx確認其狀態。確認無誤后在阿里雲中開放8081端口,外網可正常訪問,但功能試用時報錯,原來是數據庫連接錯誤,重新設置后即可正常訪問

本章完~

本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。

本文部分內容參考了網絡上的視頻內容和文章,僅為學習和交流,地址如下:

老張的哲學,系列一、ASP.NET Core 學習視頻教程

solenovex,ASP.NET Core 3.x 入門視頻

聲明

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

聚甘新

分類
發燒車訊

【Spring】循環依賴 Java Vs Spring

菜瓜:水稻,這次我特意去看了java的循環依賴

水稻:喲,有什麼收穫

菜瓜:兩種情況,構造器循環依賴,屬性循環依賴

  • 構造器循環依賴在邏輯層面無法通過。對象通過構造函數創建時如果需要創建另一個對象,就會存在遞歸調用。棧內存直接溢出
  • 屬性循環依賴可以解決。在對象創建完成之後通過屬性賦值操作。
  • package club.interview.base;
    
    /**
     * 構造器循環依賴 - Exception in thread "main" java.lang.StackOverflowError
     * toString()循環打印也會異常 - Exception in thread "main" java.lang.StackOverflowError
     * @author QuCheng on 2020/6/18.
     */
    public class Circular {
    
        class A {
            B b;
    
    //        public A() {
    //            b = new B();
    //        }
    
    //        @Override
    //        public String toString() {
    //            return "A{" +
    //                    "b=" + b +
    //                    '}';
    //        }
        }
    
        class B {
            A a;
    
    //        public B() {
    //            a = new A();
    //        }
    
    //        @Override
    //        public String toString() {
    //            return "B{" +
    //                    "a=" + a +
    //                    '}';
    //        }
        }
    
        private void test() {
            B b = new B();
            A a = new A();
            a.b = b;
            b.a = a;
            System.out.println(a);
            System.out.println(b);
        }
    
        public static void main(String[] args) {
            new Circular().test();
        }
    }

水稻:厲害啊,Spring也不支持構造函數的依賴注入,而且也不支持多例的循環依賴。同樣的,它支持屬性的依賴注入。

  • 看效果 – 如果toString()打印同樣會出現棧內存溢出。
  • package com.vip.qc.circular;
    
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    
    /**
     * @author QuCheng on 2020/6/18.
     */
    @Component("a")
    public class CircularA {
    
        @Resource
        private CircularB circularB;
    
    //    @Override
    //    public String toString() {
    //        return "CircularA{" +
    //                "circularB=" + circularB +
    //                '}';
    //    }
    }
    
    
    package com.vip.qc.circular;
    
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    
    /**
     * @author QuCheng on 2020/6/18.
     */
    @Component("b")
    public class CircularB {
    
        @Resource
        private CircularA circularA;
    
    //    @Override
    //    public String toString() {
    //        return "CircularB{" +
    //                "circularA=" + circularA +
    //                '}';
    //    }
    }
    
    
        @Test
        public void testCircular() {
            String basePackages = "com.vip.qc.circular";
            new AnnotationConfigApplicationContext(basePackages);
        }

菜瓜:看來spring的實現應該也是通過屬性注入的吧

水稻:你說的對。先給思路和demo,之後帶你掃一遍源碼,follow me !

  • spring的思路是給已經初始化的bean標記狀態,假設A依賴B,B依賴A,先創建A
    • 先從緩存容器(總共三層,一級拿不到就拿二級,二級拿不到就從三級緩存中拿正在創建的)中獲取A,未獲取到就執行創建邏輯
    • 對象A在創建完成還未將屬性渲染完之前標記為正在創建中,放入三級緩存容器。渲染屬性populateBean()會獲取依賴的對象B。
    • 此時B會走一次getBean邏輯,B同樣會先放入三級緩存,然後渲染屬性,再次走getBean邏輯注入A,此時能從三級緩存中拿到A,並將A放入二級容器。B渲染完成放入一級容器
    • 回到A渲染B的方法populateBean(),拿到B之後能順利執行完自己的創建過程。放入一級緩存
  •  為了證實結果,我把源碼給改了一下,看結果

    • package com.vip.qc.circular;
      
      import org.springframework.stereotype.Component;
      
      import javax.annotation.Resource;
      
      /**
       * @author QuCheng on 2020/6/18.
       */
      @Component("a")
      public class CircularA {
      
          @Resource
          private CircularB circularB;
      
          @Override
          public String toString() {
              return "CircularA{" +
                      "circularB=" + circularB +
                      '}';
          }
      }
      
      
      package com.vip.qc.circular;
      
      import org.springframework.stereotype.Component;
      
      import javax.annotation.Resource;
      
      /**
       * @author QuCheng on 2020/6/18.
       */
      @Component("b")
      public class CircularB {
      
          @Resource
          private CircularA circularA;
      
          @Override
          public String toString() {
              return "CircularB{" +
                      "circularA=" + circularA +
                      '}';
          }
      }
      
      
      測試代碼
      @Test
          public void testCircular() {
              String basePackages = "com.vip.qc.circular";
              new AnnotationConfigApplicationContext(basePackages);
      }
      
      測試結果(我改過源碼了)
      ---- 
      將a放入三級緩存
      將b放入三級緩存
      將a放入二級緩存
      將b放入一級緩存
      從二級緩存中拿到了a
      將a放入一級緩存

        

  • 再看源碼
    • 關鍵類處理getSingleton邏輯 – 緩存容器
      • public class DefaultSingletonBeanRegistry 
        
          /** Cache of singleton objects: bean name to bean instance. */
          // 一級緩存
            private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
        
            /** Cache of singleton factories: bean name to ObjectFactory. */
          // 三級緩存
            private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
        
            /** Cache of early singleton objects: bean name to bean instance. */
          // 二級緩存
            private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    • 主流程 AbstractApplicationContext#refresh() -> DefaultListableBeanFactory#preInstantiateSingletons() -> AbstractBeanFactory#getBean() & #doGetBean()
      • protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
              @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
           /**
            * 處理FactoryBean接口名稱轉換 {@link BeanFactory#FACTORY_BEAN_PREFIX }
            */
           final String beanName = transformedBeanName(name);
                ...
           // ①從緩存中拿對象(如果對象正在創建中且被依賴注入,會放入二級緩存)
           Object sharedInstance = getSingleton(beanName);
           if (sharedInstance != null && args == null) {
              ...
           }else {      
                    ...
                 if (mbd.isSingleton()) {
                   // ② 將創建的對象放入一級緩存
                    sharedInstance = getSingleton(beanName, () -> {
                       try {
                             // ③ 具體創建的過程,每個bean創建完成之後都會放入三級緩存,然後渲染屬性
                          return createBean(beanName, mbd, args);
                       }catch (BeansException ex) {
                         ...
           ...
           return (T) bean;
        } 
    • ①getSingleton(beanName) – 二級緩存操作
      • protected Object getSingleton(String beanName, boolean allowEarlyReference) {
           // 實例化已經完成了的放在singletonObjects
           Object singletonObject = this.singletonObjects.get(beanName);
           // 解決循環依賴
           if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
              synchronized (this.singletonObjects) {
                 singletonObject = this.earlySingletonObjects.get(beanName);
                 if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                       singletonObject = singletonFactory.getObject();
                       this.earlySingletonObjects.put(beanName, singletonObject);
                       if(beanName.equals("a")||beanName.equals("b")||beanName.equals("c"))
                          System.out.println("將"+beanName+"放入二級緩存");;
                       this.singletonFactories.remove(beanName);
                    }
                 }else if(singletonObject != null){
                    System.out.println("從二級緩存中拿到了"+beanName);
                 }
              }
           }
           return singletonObject;
        }
    • ② getSingleton(beanName,lamdba) – 一級緩存操作
      • public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
              Assert.notNull(beanName, "Bean name must not be null");
              synchronized (this.singletonObjects) {
                 Object singletonObject = this.singletonObjects.get(beanName);
                 if (singletonObject == null) {
                    if (this.singletonsCurrentlyInDestruction) {
                    ...
                    // 正在創建的bean加入singletonsCurrentlyInCreation - 保證只有一個對象創建,阻斷循環依賴
                    beforeSingletonCreation(beanName);
                              ...
                    try {
                       singletonObject = singletonFactory.getObject();
                    ...
                    finally {
                    ...
                       // 從singletonsCurrentlyInCreation中移除
                       afterSingletonCreation(beanName);
                    }
                    if (newSingleton) {
                       // 對象創建完畢 - 放入一級緩存(從其他緩存移除)
                       addSingleton(beanName, singletonObject);
                    }
                 }
                 return singletonObject;
              }
           }
                
         //  -----  內部調用一級緩存操作
            protected void addSingleton(String beanName, Object singletonObject) {
                synchronized (this.singletonObjects) {
                    if(beanName.equals("a")||beanName.equals("b")||beanName.equals("c"))
                        System.out.println("將"+beanName+"放入一級緩存");;
                    this.singletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                    this.earlySingletonObjects.remove(beanName);
                    this.registeredSingletons.add(beanName);
                }
            }        
           
    • ③createBean(beanName, mbd, args) – 三級緩存操作
      • protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {
             ...
           if (instanceWrapper == null) {
              // 5* 實例化對象本身
              instanceWrapper = createBeanInstance(beanName, mbd, args);
           }
           ...
           boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                 isSingletonCurrentlyInCreation(beanName));
           if (earlySingletonExposure) {
              ...
              // 將創建好還未渲染屬性的bean 放入三級緩存
              addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
           }
        
           Object exposedObject = bean;
           try {
              // 渲染bean自身和屬性
              populateBean(beanName, mbd, instanceWrapper);
              // 實例化之後的後置處理 - init
              exposedObject = initializeBean(beanName, exposedObject, mbd);
           }
           catch (Throwable ex) {
           ...
           return exposedObject;
        }
          
          
           // ------------- 內部調用三級緩存操作 
           protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
                Assert.notNull(singletonFactory, "Singleton factory must not be null");
                synchronized (this.singletonObjects) {
                    if (!this.singletonObjects.containsKey(beanName)) {
                        if(beanName.equals("a")||beanName.equals("b")||beanName.equals("c"))
                            System.out.println("將"+beanName+"放入三級緩存");;
                        this.singletonFactories.put(beanName, singletonFactory);
                        this.earlySingletonObjects.remove(beanName);
                        this.registeredSingletons.add(beanName);
                    }
                }
            }       

菜瓜:demo比較簡單,流程大致明白,源碼我還需要斟酌一下,整體有了概念。這個流程好像是摻雜在bean的創建過程中,結合bean的生命周期整體理解可能會更深入一點

水稻:是的。每個知識點都不是單一的,拿着bean的生命周期再理解一遍可能會更有收穫。

 

討論

  • 為什麼是三級緩存,兩級不行嗎?
    • 猜測:理論上兩級也可以實現。多一個二級緩存可能是為了加快獲取的速度。假如A依賴B,A依賴C,B依賴A,C依賴A,那麼C在獲取A的時候只需要從二級緩存中就能拿到A了

總結

  • Spring的處理方式和java處理的思想一致,構造器依賴本身是破壞語義和規範的
  • 屬性賦值–> 依賴注入 。 先創建對象,再賦值屬性,賦值的時候發現需要創建便生成依賴對象,被依賴對象需要前一個對象就從緩存容器中拿取即可

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

聚甘新

分類
發燒車訊

菜渣開源一個基於 EMIT 的 AOP 庫(.NET Core)

目錄

  • 1,快速入門
    • 1.1 繼承 ActionAttribute 特性
    • 1.2 標記代理類型
    • 2,如何創建代理類型
      • 2.1 通過API直接創建
  • 2,創建代理類型
    • 通過API
    • 通過 Microsoft.Extensions.DependencyInjection
      • 通過 Autofac
  • 3,深入使用
    • 代理類型
    • 方法、屬性代理
    • 上下文
      • 攔截方法或屬性的參數
    • 非侵入式代理

Nuget 庫地址:https://www.nuget.org/packages/CZGL.AOP/

Github 庫地址:https://github.com/whuanle/CZGL.AOP

CZGL.AOP 是 基於 EMIT 編寫的 一個簡單輕量的AOP框架,支持非侵入式代理,支持.NET Core/ASP.NET Core,以及支持多種依賴注入框架。

1,快速入門

CZGL.AOP 使用比較簡單,你只需要使用 [Interceptor] 特性標記需要代理的類型,然後使用繼承 ActionAttribute 的特性標記要被代理的方法或屬性。

1.1 繼承 ActionAttribute 特性

ActionAttribute 是用於代理方法或屬性的特性標記,不能直接使用,需要繼承后重寫方法。

示例如下:

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");
            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }

Before 會在被代理的方法執行前或被代理的屬性調用時生效,你可以通過 AspectContext 上下文,獲取、修改傳遞的參數。

After 在方法執行后或屬性調用時生效,你可以通過上下文獲取、修改返回值。

1.2 標記代理類型

在被代理的類型中,使用 [Interceptor] 特性來標記,在需要代理的方法中,使用 繼承了 ActionAttribute 的特性來標記。

此方法是侵入式的,需要在編譯前完成。

[Interceptor]
public class Test : ITest
{
    [Log] public virtual string A { get; set; }
    [Log]
    public virtual void MyMethod()
    {
        Console.WriteLine("運行中");
    }
}

注意的是,一個方法或屬性只能設置一個攔截器。

2,如何創建代理類型

CZGL.AOP 有多種生成代理類型的方式,下面介紹簡單的方式。

請預先創建如下代碼:

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");
            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }

    public interface ITest
    {
        void MyMethod();
    }

    [Interceptor]
    public class Test : ITest
    {
        [Log] public virtual string A { get; set; }
        public Test()
        {
            Console.WriteLine("構造函數沒問題");
        }
        [Log]
        public virtual void MyMethod()
        {
            Console.WriteLine("運行中");
        }
    }

2.1 通過API直接創建

通過 CZGL.AOP 中的 AopInterceptor 類,你可以生成代理類型。

示例如下:

            ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>();
            Test test2 = AopInterceptor.CreateProxyOfClass<Test>();
            test1.MyMethod();
            test2.MyMethod();

CreateProxyOfInterface 通過接口創建代理類型;CreateProxyOfClass 通過類創建代理類型;

默認調用的是無參構造函數。

2,創建代理類型

通過API

你可以參考源碼解決方案

中的 ExampleConsole 項目。

如果要直接使用 AopInterceptor.CreateProxyOfInterfaceAopInterceptor.CreateProxyOfClass 方法,通過接口或類來創建代理類型。

        ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>();
        Test test2 = AopInterceptor.CreateProxyOfClass<Test>();

如果要指定實例化的構造函數,可以這樣:

            // 指定構造函數
            test2 = AopInterceptor.CreateProxyOfClass<Test>("aaa", "bbb");
            test2.MyMethod();

通過 Microsoft.Extensions.DependencyInjection

Microsoft.Extensions.DependencyInjection 是 .NET Core/ASP.NET Core 默認的依賴注入容器。

如果需要支持 ASP.NET Core 中使用 AOP,你可以在 Nuget 包中安裝 CZGL.AOP.MEDI

如果你在控制台下使用 Microsoft.Extensions.DependencyInjection,你可以使用名為 BuildAopProxyIServiceCollection 拓展方法來為容器中的類型,生成代理類型。

示例如下:

            IServiceCollection _services = new ServiceCollection();
            _services.AddTransient<ITest, Test>();
            var serviceProvider = _services.BuildAopProxy().BuildServiceProvider();
            serviceProvider.GetService<ITest>();
            return serviceProvider;

你可以參考源碼解決方案中的 ExampleMEDI 項目。

如果你要在 ASP.NET Core 中使用,你可以在 Startup 中,ConfigureServices 方法的最後一行代碼使用 services.BuildAopProxy();

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.BuildAopProxy();
        }

還可以在 ProgramIHostBuilder 中使用 .UseServiceProviderFactory(new AOPServiceProxviderFactory()) 來配置使用 CZGL.AOP。

示例:

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .UseServiceProviderFactory(new AOPServiceProxviderFactory())
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });

可以參考解決方案中的 ExampleConsoleExampleWebMEDI 兩個項目。

你不必擔心引入 CZGL.AOP 后,使用依賴注入會使程序變慢或者破壞容器中的原有屬性。CZGL.AOP 只會在創建容器時處理需要被代理的類型,不會影響容器中的服務,也不會幹擾到依賴注入的執行。

通過 Autofac

如果需要在 Autofac 中使用 AOP,則需要引用 CZGL.AOP.Autofac 包。

如果你在控制台程序中使用 Autofac,則可以在 Build() 後面使用 BuildAopProxy()

            ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<Test>().As<ITest>();
            var container = builder.Build().BuildAopProxy();

            using (ILifetimeScope scope = container.BeginLifetimeScope())
            {
                // 獲取實例
                ITest myService = scope.Resolve<ITest>();
                myService.MyMethod();
            }

            Console.ReadKey();
        }

要注意的是,在已經完成的組件註冊創建一個新的容器后,才能調用 BuildAopProxy() 方法,

這樣針對一個新的容器你可以考慮是否需要對容器中的組件進行代理。

如果在 ASP.NET Core 中使用 Autofac,你需要在 Program 類的 IHostBuilder 中使用:

.UseServiceProviderFactory(new AutofacServiceProviderFactory())

如果需要代理已經註冊的組件,則將其替換為:

 .UseServiceProviderFactory(new CZGL.AOP.Autofac.AOPServiceProxviderFactory())

請參考 源碼解決方案中的 ExampleAutofacExampleWebAutofac 兩個項目。

3,深入使用

代理類型

要被代理的類型,需要使用 [Interceptor]來標記,例如:

    [Interceptor]
    public class Test : ITest
    {
    }

支持泛型類型。

被代理的類型必須是可被繼承的。

類型的構造函數沒有限制,你可以隨意編寫。

在使用 API 創建代理類型並且實例化時,你可以指定使用哪個構造函數。

例如:

			string a="",b="",c="";
			ITest test1 = AopInterceptor.CreateProxyOfInterface<ITest, Test>(a,b,c);

API 會根據參數的多少以及參數的類型自動尋找合適的構造函數。

方法、屬性代理

為了代理方法或屬性,你需要繼承 ActionAttribute 特性,然後為方法或屬性標記此特性,並且將方法或屬性設置為 virtual

一個類型中的不同方法,可以使用不同的攔截器。

        [Log1]
        public virtual void MyMethod1(){}
        
        [Log2]
        public virtual void MyMethod2(){}

對於屬性,可以在屬性上直接使用特性,或者只在 get 或 set 構造器使用。

        [Log] public virtual string A { get; set; }
        
        // 或
        public virtual string A { [Log] get; set; }
        
        // 或
        public virtual string A { get; [Log] set; }

如果在屬性上使用特性,相當於 [Log] get; [Log] set;

上下文

一個簡單的方法或屬性攔截標記是這樣的:

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");
            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }

AspectContext 的屬性說明如下:

字段 說明
Type 當前被代理類型生成的代理類型
ConstructorParamters 類型被實例化時使用的構造函數的參數,如果構造函數沒有參數,則 MethodValues.Length = 0,而不是 MethodValues 為 null。
IsProperty 當前攔截的是屬性
PropertyInfo 當前被執行的屬性的信息,可為 null。
PropertyValue 但調用的是屬性時,返回 get 的結果或 set 的 value 值。
IsMethod 當前攔截的是方法
MethodInfo 當前方法的信息
MethodValues 方法被調用時傳遞的參數,如果此方法沒有參數,則 MethodValues.Length = 0,而不是 MethodValues 為 null
MethodResult 方法執行返回的結果(如果有)

攔截方法或屬性的參數

通過上下文,你可以修改方法或屬性的參數以及攔截返回結果:

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            // 攔截並修改方法的參數
            for (int i = 0; i < context.MethodValues.Length; i++)
            {
                context.MethodValues[i] = (int)context.MethodValues[i] + 1;
            }
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");

            // 攔截方法的執行結果
            context.MethodResult = (int)context.MethodResult + 664;

            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }

    [Interceptor]
    public class Test
    {
        [Log]
        public virtual int Sum(int a, int b)
        {
            Console.WriteLine("運行中");
            return a + b;
        }
    }
            Test test = AopInterceptor.CreateProxyOfClass<Test>();

            Console.WriteLine(test.Sum(1, 1));

方法的參數支持 inrefout;支持泛型方法泛型屬性;支持異步方法;

非侵入式代理

此種方式不需要改動被代理的類型,你也可以代理程序集中的類型。

    public class LogAttribute : ActionAttribute
    {
        public override void Before(AspectContext context)
        {
            Console.WriteLine("執行前");
        }

        public override object After(AspectContext context)
        {
            Console.WriteLine("執行后");
            if (context.IsMethod)
                return context.MethodResult;
            else if (context.IsProperty)
                return context.PropertyValue;
            return null;
        }
    }
    public class TestNo
    {
        public virtual string A { get; set; }
        public virtual void MyMethod()
        {
            Console.WriteLine("運行中");
        }
    }
            TestNo test3 = AopInterceptor.CreateProxyOfType<TestNo>(new ProxyTypeBuilder()
                .AddProxyMethod(typeof(LogAttribute), typeof(TestNo).GetMethod(nameof(TestNo.MyMethod)))
                .AddProxyMethod(typeof(LogAttribute), typeof(TestNo).GetProperty(nameof(TestNo.A)).GetSetMethod()));

通過 ProxyTypeBuilder 來構建代理類型。

代理方法或屬性都是使用 AddProxyMethod,第一個參數是要使用的攔截器,第二個參數是要攔截的方法。

如果要攔截屬性,請分開設置屬性的 getset 構造。

如果多個方法或屬性使用同一個攔截器,則可以這樣:

            TestNo test3 = AopInterceptor.CreateProxyOfType<TestNo>(
                new ProxyTypeBuilder(new Type[] { typeof(LogAttribute) })
                .AddProxyMethod("LogAttribute", typeof(TestNo).GetMethod(nameof(TestNo.MyMethod)))
                .AddProxyMethod("LogAttribute", typeof(TestNo).GetProperty(nameof(TestNo.A)).GetSetMethod()));
            TestNo test3 = AopInterceptor.CreateProxyOfType<TestNo>(
                new ProxyTypeBuilder(new Type[] { typeof(LogAttribute) })
                .AddProxyMethod("LogAttribute", typeof(TestNo).GetMethod(nameof(TestNo.MyMethod)))
                .AddProxyMethod(typeof(LogAttribute2), typeof(TestNo).GetProperty(nameof(TestNo.A)).GetSetMethod()));

在構造函數中傳遞過去所需要的攔截器,然後在攔截時使用。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

※推薦評價好的iphone維修中心

聚甘新

分類
發燒車訊

Java併發編程(05):悲觀鎖和樂觀鎖機制

本文源碼:GitHub·點這裏 || GitEE·點這裏

一、資源和加鎖

1、場景描述

多線程併發訪問同一個資源問題,假如線程A獲取變量之後修改變量值,線程C在此時也獲取變量值並且修改,兩個線程同時併發處理一個變量,就會導致併發問題。

這種并行處理數據庫的情況在實際的業務開發中很常見,兩個線程先後修改數據庫的值,導致數據有問題,該問題復現的概率不大,處理的時候需要對整個模塊體系有概念,才能容易定位問題。

2、演示案例

public class LockThread01 {
    public static void main(String[] args) {
        CountAdd countAdd = new CountAdd() ;
        AddThread01 addThread01 = new AddThread01(countAdd) ;
        addThread01.start();
        AddThread02 varThread02 = new AddThread02(countAdd) ;
        varThread02.start();
    }
}
class AddThread01 extends Thread {
    private CountAdd countAdd  ;
    public AddThread01 (CountAdd countAdd){
        this.countAdd = countAdd ;
    }
    @Override
    public void run() {
        countAdd.countAdd(30);
    }
}
class AddThread02 extends Thread {
    private CountAdd countAdd  ;
    public AddThread02 (CountAdd countAdd){
        this.countAdd = countAdd ;
    }
    @Override
    public void run() {
        countAdd.countAdd(10);
    }
}
class CountAdd {
    private Integer count = 0 ;
    public void countAdd (Integer num){
        try {
            if (num == 30){
                count = count + 50 ;
                Thread.sleep(3000);
            } else {
                count = count + num ;
            }
            System.out.println("num="+num+";count="+count);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

這裏案例演示多線程併發修改count值,導致和預期不一致的結果,這是多線程併發下最常見的問題,尤其是在併發更新數據時。

出現併發的情況時,就需要通過一定的方式或策略來控制在併發情況下數據讀寫的準確性,這被稱為併發控制,實現併發控制手段也很多,最常見的方式是資源加鎖,還有一種簡單的實現策略:修改數據前讀取數據,修改的時候加入限制條件,保證修改的內容在此期間沒有被修改。

二、鎖的概念簡介

1、鎖機制簡介

併發編程中一個最關鍵的問題,多線程併發處理同一個資源,防止資源使用的衝突一個關鍵解決方法,就是在資源上加鎖:多線程序列化訪問。鎖是用來控制多個線程訪問共享資源的方式,鎖機制能夠讓共享資源在任意給定時刻只有一個線程任務訪問,實現線程任務的同步互斥,這是最理想但性能最差的方式,共享讀鎖的機制允許多任務併發訪問資源。

2、悲觀鎖

悲觀鎖,總是假設每次每次被讀取的數據會被修改,所以要給讀取的數據加鎖,具有強烈的資源獨佔和排他特性,在整個數據處理過程中,將數據處於鎖定狀態,例如synchronized關鍵字的實現就是悲觀機制。

悲觀鎖的實現,往往依靠數據庫提供的鎖機制,只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據,悲觀鎖主要分為共享讀鎖和排他寫鎖。

排他鎖基本機制:又稱寫鎖,允許獲取排他鎖的事務更新數據,阻止其他事務取得相同的資源的共享讀鎖和排他鎖。若事務T對數據對象A加上寫鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的寫鎖。

3、樂觀鎖

樂觀鎖相對悲觀鎖而言,採用更加寬鬆的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務的開銷非常的占資源,樂觀鎖機制在一定程度上解決了這個問題。

樂觀鎖大多是基於數據版本記錄機制實現,為數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過為數據庫表增加一個version字段來實現。讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號等於數據庫表當前版本號,則予以更新,否則認為是過期數據。樂觀鎖機制在高併發場景下,可能會導致大量更新失敗的操作。

樂觀鎖的實現是策略層面的實現:CAS(Compare-And-Swap)。當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能成功更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。

4、機制對比

悲觀鎖本身的實現機制就以損失性能為代價,多線程爭搶,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,加鎖的機制會產生額外的開銷,還有增加產生死鎖的概率,引發性能問題。

樂觀鎖雖然會基於對比檢測的手段判斷更新的數據是否有變化,但是不確定數據是否變化完成,例如線程1讀取的數據是A1,但是線程2操作A1的值變化為A2,然後再次變化為A1,這樣線程1的任務是沒有感知的。

悲觀鎖每一次數據修改都要上鎖,效率低,寫數據失敗的概率比較低,比較適合用在寫多讀少場景。

樂觀鎖並未真正加鎖,效率高,寫數據失敗的概率比較高,容易發生業務形異常,比較適合用在讀多寫少場景。

是選擇犧牲性能,還是追求效率,要根據業務場景判斷,這種選擇需要依賴經驗判斷,不過隨着技術迭代,數據庫的效率提升,集群模式的出現,性能和效率還是可以兩全的。

三、Lock基礎案例

1、Lock方法說明

lock:執行一次獲取鎖,獲取后立即返回;

lockInterruptibly:在獲取鎖的過程中可以中斷;

tryLock:嘗試非阻塞獲取鎖,可以設置超時時間,如果獲取成功返回true,有利於線程的狀態監控;

unlock:釋放鎖,清理線程狀態;

newCondition:獲取等待通知組件,和當前鎖綁定;

2、應用案例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockThread02 {
    public static void main(String[] args) {
        LockNum lockNum = new LockNum() ;
        LockThread lockThread1 = new LockThread(lockNum,"TH1");
        LockThread lockThread2 = new LockThread(lockNum,"TH2");
        LockThread lockThread3 = new LockThread(lockNum,"TH3");
        lockThread1.start();
        lockThread2.start();
        lockThread3.start();
    }
}
class LockNum {
    private Lock lock = new ReentrantLock() ;
    public void getNum (){
        lock.lock();
        try {
            for (int i = 0 ; i < 3 ; i++){
                System.out.println("ThreadName:"+Thread.currentThread().getName()+";i="+i);
            }
        } finally {
            lock.unlock();
        }
    }
}
class LockThread extends Thread {
    private LockNum lockNum ;
    public LockThread (LockNum lockNum,String name){
        this.lockNum = lockNum ;
        super.setName(name);
    }
    @Override
    public void run() {
        lockNum.getNum();
    }
}

這裏多線程基於Lock鎖機制,分別依次執行任務,這是Lock的基礎用法,各種API的詳解,下次再說。

3、與synchronized對比

基於synchronized實現的鎖機制,安全性很高,但是一旦線程失敗,直接拋出異常,沒有清理線程狀態的機會。顯式的使用Lock語法,可以在finally語句中最終釋放鎖,維護相對正常的線程狀態,在獲取鎖的過程中,可以嘗試獲取,或者嘗試獲取鎖一段時間。

四、源代碼地址

GitHub·地址
https://github.com/cicadasmile/java-base-parent
GitEE·地址
https://gitee.com/cicadasmile/java-base-parent

推薦閱讀:Java基礎系列

序號 文章標題
A01 Java基礎:基本數據類型,核心點整理
A02 Java基礎:特殊的String類,和相關擴展API
B01 Java併發:線程的創建方式,狀態周期管理
B02 Java併發:線程核心機制,基礎概念擴展
B03 Java併發:多線程併發訪問,同步控制
B04 Java併發:線程間通信,等待/通知機制

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

聚甘新

分類
發燒車訊

文本挖掘之情感分析(一)

一、文本挖掘  

     文本挖掘則是對文本進行處理,從中挖掘出來文本中有用的信息和關鍵的規則,在文本挖掘領域應用最往廣泛的是對文本進行分類和聚類,其挖掘的方法分為無監督學習和監督學習。文本挖掘還可以劃分為7大類:關鍵詞提取、文本摘要、文本主題模型、文本聚類、文本分類、觀點提取、情感分析。

   關鍵詞提取:對長文本的內容進行分析,輸出能夠反映文本關鍵信息的關鍵詞。

   文本摘要:許多文本挖掘應用程序需要總結文本文檔,以便對大型文檔或某一主題的文檔集合做出簡要概述。

   文本聚類:主要是對未標註的文本進行標註,常見的有 K均值聚類和層次聚類。

   文本分類:文本分類使用監督學習的方法,以對未知數據的分類進行預測的機器學習方法。

   文本主題模型 LDA:LDA(Latent Dirichlet Allocation)是一種文檔主題生成模型,也稱為一個三層貝恭弘=叶 恭弘斯概率模型,包含詞、主題和文檔三層結構,該模型可以用於獲取語料的主題提取和對不同類別的文檔進行分類。

   觀點抽取:對文本(主要針對評論)進行分析,抽取出核心觀點,並判斷極性(正負面),主要用於電商、美食、酒店、汽車等評論進行分析。

   情感分析:對文本進行情感傾向判斷,將文本情感分為正向、負向、中性。用於口碑分析、話題監控、輿情分析。

   因為自己的論文寫的是關於情感分析方面的內容,因此打算接下來主要寫情感分析系列的內容,今天主要寫關於情感分析的介紹以及發展史。

二、情感分析

1. 含義

     情感分析主要是通過分析人們對於服務、產品、事件、話題來挖掘出說話人/作者觀點、情感、情緒等的研究。情感分析按照研究內容的不同,可以分為:意見挖掘 / 意見提取 / 主觀性分析 / 情感傾向分析、情感情緒分析、情感打分等。情感傾向問題,即是指挖掘出一段語料中說話人/作者對於某一話題/事件所持有的態度,如褒義、貶義、中性、兩者兼有。情感情緒,則是將情感傾向進行更進一步的細化,依據“大連理工大學的情感詞彙本體庫”可知,可以將情感傾向可以細化為:“喜歡”、“憤怒”、“討厭”等具體的7個大類——21個小類別。情感打分,即根據情感態度對於某一事物進行評分,如淘寶系統的1-5分。 文本中的情感分析還可以分為:顯式情感、隱式情感,顯式情感是指包含明顯的情感詞語(如:高興、漂亮、討厭等),隱式情感則是指不包含情感詞語的情感文本,如:“這個杯子上面有一層灰”。由於隱式情感分析難度較大,因此目前的工作多集中在顯式情感分析領域。

     情感分析按照不同的分析對象,可以分為:文章級別的情感分析、句子級別的情感分析、詞彙級別的情感分析。按照不同的研究內容以及研究的粒度的不同,其研究情感分析的方法也有很大的變化。

     目前的情感分析研究可歸納為:情感資源構建、情感元素抽取、情感分類及情感分析應用系統;

     情感資源構建:情感資源一般來說有:情感詞典、情感語料庫。情感詞典的構建即是將現有的、整理好的情感詞典資源進行整合,比如中文情感詞典有:大連理工大學的情感詞彙本體庫、知網Hownet情感詞典、台灣大學的NTUSD簡體中文情感詞典等,根據不同的需求,應用這些情感詞典。情感語料庫,則是我們要分析的文本,如關於新聞的文本、微博評論文本、商品評論文本、電影評論文本等,這些語料的獲取可以是尋找已經整理好的數據,或者自己爬蟲獲取。推薦一個比較全的中文語料庫網站:中文NLP語料庫。

    情感元素抽取:情感元素抽取則是從語料中抽取出來能夠代表說話人/作者情感態度問題的詞彙,也稱為細粒度情感分析。語料中的評價對象和表達抽取是情感元素抽取的核心內容。評價對象是指語料中被討論的主題,比如對於商品評論來說,用戶常提起的“外觀”、“快遞”、“包裝”等方面;表達抽取主要針對顯式情感表達的文本,是指文本抽取出來能夠代表說話人/作者情感、情緒、意見等的詞彙,比如“漂亮”、“贊同”、“不贊同”等。一般來說,評價對象和表達抽取也可以作為相互獨立的兩個任務。一般來說,分析這兩者的方法有:基於規則、基於機器學習。對於評價對象來說,現如今使用最多的方法是利用主題模型中的LDA(Latent Dirichlet Allocation)模型進行分析;對於表達抽取則有:深度學習的方法、基於JST (Joint Sentiment/Topic )模型的方法等。

     情感分類:情感分類則是將文本分為一個具體的類別,比如情感傾向分析,則是將文檔分為:褒義、貶義、中性等。一般來說,進行情感分類的方法有,基於情感詞典、基於機器學習。基於情感詞典,最典型的方法則是基於知網Hownet情感詞典的So-Hownet指標進行情感分類,基於機器學習的方法則有監督學習方法、半監督學習方法等。

    針對於情感分析,現已經存在一些專有平台,如:基於Boson 數據的情感分析平台,基於產品評論的平台Google Shopping。情感分析除了在電商平台應用廣泛之外,情感分析技術還被引入到對話機器人領域。例如,微軟的“小冰”機器人 可以通過分析用戶的文本輸入和表情貼圖,理解用戶當前的情緒狀況,並據此回復文本或者語音等情感回應。部分研究機構還將情感分析技術融入實體機器人中。

 2. 發展史

     V. H. 和 K. R. McKeown 於 1997 年發表的論文 [1],該論文使用對數線性回歸模型從大量語料庫中識別形容詞的正面或負面語義,同時藉助該模型對語料中出現的形容詞進行分類預測。

     Peter Turney在 2002年在論文 [2] 提出了一種無監督學習的算法,其可以很好的將語料中的詞語分類成正面情感詞和負面情感詞。

    2002 年 Bo Pang 等人在論文 [3] 中使用了傳統的機器學習方法對電影評論數據進行分類,同時也驗證了機器學習的方法的確要比之前基於規則的方法要優。

    2003 年 Blei 等人在論文 [4] 中提出了 LDA(Latent Dirichlet Allocation)模型,在之後的情感分析領域的工作中,很多學者/研究人員都使用主題模型來進行情感分析,當然也不只是基於主題模型來進行情感分析研究,還有很多利用深度學習方法來進行情感分析的研究。

   Lin 和 He在 2009 年的論文 [5] 提出了一種基於主題模型的模型 —JST(Joint Sentiment/Topic),其有效的將情感加入到了經典的主題模型當中,因此利用該模型可以獲取到不同情感極性標籤下不同主題的分佈情況。傳統的主題模型獲取文檔的主題以及詞的分佈情況,但並沒有關注到情感的存在,因此基於該模型可以對文檔的情感傾向進行分析。利用 JST 模型可以有效的直接將語料的主題、情感信息挖掘出來,同時 JST 模型還考慮到了主題、文檔、情感、詞之間的聯繫。

    基於對於語義和句法的考慮,Jo 和 H.OH 在 2011 年提出了 ASUM(Aspect and Sentiment Unification Model)模型,該模型和 JST 模型很相似都是四層的貝恭弘=叶 恭弘斯網絡結構 [6] 。

    基於神經網絡的語義組合算法被驗證是一種非常有效的特徵學習手段,2013年,Richard Socher和Christopher Potts等人提出多個基於樹結構的Recursive Neural Network,該方法通過迭代運算的方式學習變量長度的句子或短語的語義表示,在斯坦福情感分析樹庫(Stanford Sentiment Treebank)上驗證了該方法的有效性 [7]。Nal Kalchbrenner等人描述了一個卷積體繫結構,稱為動態卷積神經網絡(DCNN),他們採用它來進行句子的語義建模。 該網絡使用動態k-Max池,這是一種線性序列的全局池操作。 該網絡處理不同長度的輸入句子,並在句子上引入能夠明確捕獲短程和長程關係的特徵圖。 網絡不依賴於解析樹,並且很容易適用於任何語言。該模型在句子級情感分類任務上取得了非常出色的效果[8]。2015年,Kai Sheng Tai,Richard Socher, Christopher D. Manning在序列化的LSTM (Long Short-Term Memory)模型的基礎上加入了句法結構的因素,該方法在句法分析的結果上進行語義組合,在句子級情感分類和文本蘊含任務(text entailment)上都取得了很好的效果[9]。

   2016年,Qiao Qian, Xiaoyan Zhu等人在LSTM和Bi-LSTM模型的基礎上加入四種規則約束,這四種規則分別是: Non-Sentiment Regularizer,Sentiment Regularizer, Negation Regularizer, Intensity Regularizer,利用語言資源和神經網絡相結合來提升情感分類問題的精度。

  除了上面的一些研究,關於情感分析領域的應用仍然有很多,比如:2015 年鄭祥雲等人通過主題模型提取出來圖書館用戶的主題信息,最後利用這些信息來進行個性化圖書的有效推薦。將 JST 模型中直接引入了情感詞典作為外部先驗知識來對新聞文本進行分析,獲取其中的主旨句,並對主旨句進行情感打分,同時利用情感主旨句來代替全文,這樣能夠使得用戶更有效、更快速的閱讀文章。

  總的來說,情感分析在很多的領域被應用,當然情感分析也有很多的局限性,就是過多的依賴於語料庫信息,同時還需要使用自然語言、人工智能的方法來能夠最大化的挖掘出來其中的信息。

 

 參考文獻:

 [1] :Predicting the semantic orientation of adjectives

 [2]:  Thumbs up or thumbs down? Semantic orientation applied to unsupervised classification of reviews 

 [3] : Thumbs up? Sentiment Classification using Machine Learning Techniques

 [4] :   Latent Dirichlet Allocation

 [5] : Joint sentiment/topic model for sentiment analysis

 [6] : Aspect and sentiment unification model for online review analysis

 [7] : Recursive Deep Models for Semantic Compositionality Over a Sentiment Treebank

 [8]:  A Convolutional Neural Network for Modelling Sentences

 [9]: Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks

 [10] : Linguistically Regularized LSTMs for Sentiment Classification

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

聚甘新

分類
發燒車訊

服務設計思考:平台化

平台是一套完整的服務。也是一套內部自洽的系統。核心在於分離,業務與通用服務隔離,業務與通用功能隔離。

目標:

  • 對需求方: 快速響應。可以敏捷地進行需求迭代。

  • 對第三方業務方: 以產品的方式提供服務。所見即所得。所有功能對業務方透明。

  • 對測試方: 簡易明了的測試方式。利於自動化測試,灰度測試。

  • 對運維方: 持續集成,自動化編排,自動化部署。

  • 數據方: 提供多維度,詳盡的服務數據。可以給數據方提供簡便的數據分析。

  • 內部開發: 敏捷開發。迅速集成。

實現:

  • 如何實現需求的快速響應?
    明確的方向,清晰的邊界。確認通用語言,核心領域。敏捷開發,快速迭代。AB 測試。

  • 如何為第三方提供產品式的服務?

    所見即所得。詳盡的文檔。第三方調試平台,第三方管理平台。

  • mock 服務,自動化測試,swagger 文檔。

  • Devops,CI,DI 等持續集成,服務監控。

  • 業務數據與分析數據異構存儲。提供易於分析的數據服務。

  • 組內服務負責制度,人類最佳的合作人數是 2-3 人。所以兩人維護一個項目,一人主導,一人輔助,兩人交叉合作是一個很好的團隊合作模式。如圖形成一個網狀模式(紅色線代表主導,黑色線輔助)。這樣每一個項目都將有兩個熟悉的人。

原則

  1. 單一職責。
  2. 業務關注業務,功能關注功能。
  3. 確認邊界,確認核心領域。
  4. 所見即所得。

實施

如何推進業務開發快速響應?

  1. 抽離變化與不變。形成基礎服務

    如下面一套用戶體系,將服務抽離,將變與不變隔離。

    用戶 api: 主要提供用戶相關的接口,變化大,更偏向於業務;

    用戶中心: 主要管理用戶核心領域,變動不大,需穩定高可用的服務;

    鑒權授權中心: 變動不大,主要管理用戶憑證核心領域;

  1. 抽離通用功能。

    那些非業務的通用功能應隔離於業務之外:組件化工具化服務化

    來源監控接口限流日誌分析應用監控服務依賴配置管理系統部署等(業務人員不必關心這些功能相關的事情,只需要關注於具體的業務領域)。關注點分離。

    如上面所涉及的,從Spring Cloud的各大組件可以看出,最終的方案都將走上相近的道路。

  1. 領域上下文劃分。劃分微服務項目。業務隔離,數據去中心化。服務組件化。

    Spring cloud 技術棧:

    • 服務治理: 註冊中心,服務調用,衍生的容錯(熔斷器)
    • api 網關: 來源監控,接口限流(Spring Cloud gateway、zuul)
    • **配置中心: ** 配置管理(Apollo)
    • 自動化部署: Jenkins、docker、k8s
    • 日誌與監控: prometheus、influxdb、skywalking、elk
    • 數據可視化: druid、kylin、superset
  1. 細節管控

    接口版本管理, gitflow 管理,項目迭代 release 版本管理,標準化,敏捷開發。

歡迎關注我的公眾號。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

聚甘新

分類
發燒車訊

從一個計算器開始說起——C#中的工廠方法模式

工廠模式作為很常見的設計模式,在日常工作中出鏡率非常高,程序員們一定要掌握它的用法喲,今天跟着老胡一起來看看吧。

舉個例子

現在先讓我們來看一個例子吧,比如,要開發一個簡單的計算器,完成加減功能,通過命令行讀入形如1+1的公式,輸出2這個結果,讓我們看看怎麼實現吧。

 

第一個版本

這個版本裏面,我們不考慮使用模式,就按照最簡單的結構,怎麼方便怎麼來。

思路非常簡單,僅需要實現以下幾個方法

  • 取運算數
  • 取運算符
  • 輸出結果
     class Program
    {
        static int GetOperatorIndex(string input)
        {
            int operatorIndex = 0;
            for (; operatorIndex < input.Length; operatorIndex++)
            {
                if (!char.IsDigit(input[operatorIndex]))
                    break;
            }
            return operatorIndex;
        }

        static int GetOp(string input, int startIndex, int size = -1)
        {
            string subStr;
            if (size == -1)
            {
                subStr = input.Substring(startIndex);
            }
            else
            {
                subStr = input.Substring(startIndex, size);
            }
            return int.Parse(subStr);
        }

        static int CalculateExpression(string input)
        {
            var operatorIndex = GetOperatorIndex(input); //得到運算符索引
            var op1 = GetOp(input, 0, operatorIndex); //得到運算數1
            var op2 = GetOp(input, operatorIndex + 1); //得到運算數2
            switch (input[operatorIndex])
            {
                case '+':
                    return op1 + op2;
                case '-':
                    return op1 - op2;
                default:
                    throw new Exception("not support");
            }
        }

        static void Main(string[] args)
        {
            string input = Console.ReadLine();
            while(!string.IsNullOrEmpty(input))
            {
                var result = CalculateExpression(input);
                Console.WriteLine("={0}", result);
                input = Console.ReadLine();
            }            
        }
}

代碼非常簡單,毋庸置疑,這個運算器是可以正常工作的。這也可能是我們大部分人剛剛踏上工作崗位的時候可能會寫出的代碼。但它有着以下這些缺點:

  • 缺乏起碼的抽象,至少加和減應該能抽象出操作類。
  • 缺乏抽象造成了巨型客戶端,所有的邏輯都嵌套在了客戶端裏面。
  • 使用switch case缺乏擴展性,同時switch case也暗指了這部分代碼是屬於變化可能性比較高的地方,我們應該把它們封裝起來。而且不能把他們放在和客戶端代碼一起

接下來,我們引入我們的主題,工廠方法模式。
 

工廠方法模式版本

工廠方法模式使用一個虛擬的工廠來完成產品構建(在這裡是運算符的構建,因為運算符是我們這個程序中最具有變化的部分),通過把可變化的部分封裝在工廠類中以達到隔離變化的目的。我們看看UML圖:

依葫蘆畫瓢,我們設計思路如下:

  • 設計一個IOperator接口,對應抽象的Product
  • 設計AddOperator和SubtractOperator,對應具體Product
  • 設計IOperatorFactory接口生產Operator
  • 設計OperatorFactory實現抽象IFactory

關鍵代碼如下,其他讀取操作數之類的代碼就不在贅述。

  • IOperator接口
       interface IOperator
       {
           int Calculate(int op1, int p2);
       }
  • 具體Operator
	class AddOperator : IOperator
        {
            public int Calculate(int op1, int op2)
            {
                return op1 + op2;
            }
        }

        class SubtractOperator : IOperator
        {
            public int Calculate(int op1, int op2)
            {
                return op1 - op2;
            }
        }
  • Factory接口
	interface IOperatorFactory
        {
            IOperator CreateOperator(char c);
        }
  • 具體Factory
	class OperatorFactory : IOperatorFactory
        {
            public IOperator CreateOperator(char c)
            {
                switch(c)
                {
                    case '+':
                        return new AddOperator();
                    case '-':
                        return new SubtractOperator();
                    default:
                        throw new Exception("Not support");
                }
            }
        }
  • 在CalculateExpression裏面使用他們
   static IOperator GetOperator(string input, int operatorIndex)
       {
           IOperatorFactory f = new OperatorFactory();
           return f.CreateOperator(input[operatorIndex]);
       }

       static int CalculateExpression(string input)
       {
           var operatorIndex = GetOperatorIndex(input);
           var op1 = GetOp(input, 0, operatorIndex);
           var op2 = GetOp(input, operatorIndex + 1);
           IOperator op = GetOperator(input, operatorIndex);
           return op.Calculate(op1, op2);                    
       }

這樣,我們就用工廠方法重新寫了一次計算器,現在看看,好處有

  • 容易變化的創建部分被工廠封裝了起來,工廠和客戶端以接口的形式依賴,工廠內部邏輯可以隨時變化而不用擔心影響客戶端代碼
  • 工厂部分可以放在另外一個程序集,項目規劃會更加合理
  • 客戶端僅僅需要知道工廠和抽象的產品類,不需要再知道每一個具體的產品(不需要知道如何構建每一個具體運算符),符合迪米特法則
  • 擴展性增強,如果之後需要添加乘法multiple,那麼僅需要添加一個Operator類代表Multiple並且修改Facotry裏面的生成Operator邏輯就可以了,不會影響到客戶端
     

自此,我們已經在代碼裏面實現了工廠方法模式,但可能有朋友就會想,雖然現在擴展性增強了,但是新添加運算符還是需要修改已有的工廠,這不是違反了開閉原則么。。有沒有更好的辦法呢?當然是有的。
 

反射版本

想想工廠方法那個版本,我們為什麼增加新的運算符就會不可避免的修改現有工廠?原因就是我們通過switch case來硬編碼“教導”工廠如何根據用戶輸入產生正確的運算符,那麼如果有一種方法可以讓工廠自動學會發現新的運算符,那麼我們的目的不就達到了?

嗯,我想聰明的朋友們已經知道了,用屬性嘛,在C#中,這種方法完成類的自描述,是最好不過了的。

我們的設計思路如下:

  • 定義一個描述屬性以識別運算符
  • 在運算符中添加該描述屬性
  • 在工廠啟動的時候,掃描程序集以註冊所有運算符

代碼如下:

  • 描述屬性
    class OperatorDescriptionAttribute : Attribute
    {
        public char Symbol { get; }
        public OperatorDescriptionAttribute(char c)
        {
            Symbol = c;
        }
    }
  • 添加描述屬性到運算符
    [OperatorDescription('+')]
    class AddOperator : IOperator
    {
        public int Calculate(int op1, int op2)
        {
            return op1 + op2;
        }
    }

    [OperatorDescription('-')]
    class SubtractOperator : IOperator
    {
        public int Calculate(int op1, int op2)
        {
            return op1 - op2;
        }
    }
  • 讓工廠使用描述屬性
    class OperatorFactory : IOperatorFactory
    {
        private Dictionary<char, IOperator> dict = new Dictionary<char, IOperator>();
        public OperatorFactory()
        {
            Assembly assembly = Assembly.GetExecutingAssembly();
            foreach (var type in assembly.GetTypes())
            {
                if (typeof(IOperator).IsAssignableFrom (type) 
                    && !type.IsInterface)
                {
                    var attribute = type.GetCustomAttribute<OperatorDescriptionAttribute>();
                    if(attribute != null)
                    {
                        dict[attribute.Symbol] = Activator.CreateInstance(type) as IOperator;
                    }
                }
            }
        }
        public IOperator CreateOperator(char c)
        {
            if(!dict.ContainsKey(c))
            {
                throw new Exception("Not support");
            }
            return dict[c];
        }
    }

經過這種改造,現在程序對擴展性支持已經很友好了,需要添加Multiple,只需要添加一個Multiple類就可以,其他代碼都不用修改,這樣就完美符合開閉原則了。

  [OperatorDescription('*')]
  class MultipleOperator : IOperator
  {
      public int Calculate(int op1, int op2)
      {
          return op1 * op2;
      }
  }

這就是我們怎麼一步步從最原始的代碼走過來,一點點重構讓代碼實現工廠方法模式,最終再完美支持開閉原則的過程,希望能幫助到大家。

其實關於最後那個通過標記屬性實現擴展,微軟有個MEF框架支持的很好,原理跟這個有點相似,有機會我們再聊聊MEF。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

聚甘新

分類
發燒車訊

【Flutter實戰】六大布局組件及半圓菜單案例

老孟導讀:Flutter中布局組件有水平 / 垂直布局組件( RowColumn )、疊加布局組件( StackIndexedStack )、流式布局組件( Wrap )和 自定義布局組件(Flow)。

水平、垂直布局組件

Row 是將子組件以水平方式布局的組件, Column 是將子組件以垂直方式布局的組件。項目中 90% 的頁面布局都可以通過 Row 和 Column 來實現。

將3個組件水平排列:

Row(
  children: <Widget>[
    Container(
      height: 50,
      width: 100,
      color: Colors.red,
    ),
    Container(
      height: 50,
      width: 100,
      color: Colors.green,
    ),
    Container(
      height: 50,
      width: 100,
      color: Colors.blue,
    ),
  ],
)

將3個組件垂直排列:

Column(
  mainAxisSize: MainAxisSize.min,
  children: <Widget>[
    Container(
      height: 50,
      width: 100,
      color: Colors.red,
    ),
    Container(
      height: 50,
      width: 100,
      color: Colors.green,
    ),
    Container(
      height: 50,
      width: 100,
      color: Colors.blue,
    ),
  ],
)

在 Row 和 Column 中有一個非常重要的概念:主軸( MainAxis )交叉軸( CrossAxis ),主軸就是與組件布局方向一致的軸,交叉軸就是與主軸方向垂直的軸。

具體到 Row 組件,主軸 是水平方向,交叉軸 是垂直方向。而 Column 與 Row 正好相反,主軸 是垂直方向,交叉軸 是水平方向。

明白了 主軸 和 交叉軸 概念,我們來看下 mainAxisAlignment 屬性,此屬性表示主軸方向的對齊方式,默認值為 start,表示從組件的開始處布局,此處的開始位置和 textDirection 屬性有關,textDirection 表示文本的布局方向,其值包括 ltr(從左到右) 和 rtl(從右到左),當 textDirection = ltr 時,start 表示左側,當 textDirection = rtl 時,start 表示右側,

Container(
  decoration: BoxDecoration(border: Border.all(color: Colors.black)),
  child: Row(
    children: <Widget>[
      Container(
        height: 50,
        width: 100,
        color: Colors.red,
      ),
      Container(
        height: 50,
        width: 100,
        color: Colors.green,
      ),
      Container(
        height: 50,
        width: 100,
        color: Colors.blue,
      ),
    ],
  ),
)

黑色邊框是Row控件的範圍,默認情況下Row鋪滿父組件。

主軸對齊方式有6種,效果如下圖:

spaceAround 和 spaceEvenly 區別是:

  • spaceAround :第一個子控件距開始位置和最後一個子控件距結尾位置是其他子控件間距的一半。
  • spaceEvenly : 所有間距一樣。

和主軸對齊方式相對應的就是交叉軸對齊方式 crossAxisAlignment ,交叉軸對齊方式默認是居中。Row控件的高度是依賴子控件高度,因此子控件高都一樣時,Row的高和子控件高相同,此時是無法體現交叉軸對齊方式,修改3個顏色塊高分別為50,100,150,這樣Row的高是150,代碼如下:

Container(
      decoration: BoxDecoration(border: Border.all(color: Colors.black)),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Container(
            height: 50,
            width: 100,
            color: Colors.red,
          ),
          Container(
            height: 100,
            width: 100,
            color: Colors.green,
          ),
          Container(
            height: 150,
            width: 100,
            color: Colors.blue,
          ),
        ],
      ),
    )

主軸對齊方式效果如下圖:

mainAxisSize 表示主軸尺寸,有 min 和 max 兩種方式,默認是 maxmin 表示盡可能小,max 表示盡可能大。

Container(
	decoration: BoxDecoration(border: Border.all(color: Colors.black)),
	child: Row(
		mainAxisSize: MainAxisSize.min,
		...
	)
)

看黑色邊框,正好包裹子組件,而 max 效果如下:

textDirection 表示子組件主軸布局方向,值包括 ltr(從左到右) 和 rtl(從右到左)

Container(
  decoration: BoxDecoration(border: Border.all(color: Colors.black)),
  child: Row(
    textDirection: TextDirection.rtl,
    children: <Widget>[
      ...
    ],
  ),
)

verticalDirection 表示子組件交叉軸布局方向:

  • up :從底部開始,並垂直堆疊到頂部,對齊方式的 start 在底部,end 在頂部。
  • down: 與 up 相反。
Container(
  decoration: BoxDecoration(border: Border.all(color: Colors.black)),
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.start,
    verticalDirection: VerticalDirection.up,
    children: <Widget>[
      Container(
        height: 50,
        width: 100,
        color: Colors.red,
      ),
      Container(
        height: 100,
        width: 100,
        color: Colors.green,
      ),
      Container(
        height: 150,
        width: 100,
        color: Colors.blue,
      ),
    ],
  ),
)

想一想這種效果完全可以通過對齊方式實現,那麼為什麼還要有 textDirectionverticalDirection 這兩個屬性,官方API文檔已經解釋了這個問題:

This is also used to disambiguate start and end values (e.g. [MainAxisAlignment.start] or [CrossAxisAlignment.end]).

用於消除 MainAxisAlignment.start 和 CrossAxisAlignment.end 值的歧義的。

疊加布局組件

疊加布局組件包含 StackIndexedStack,Stack 組件將子組件疊加显示,根據子組件的順利依次向上疊加,用法如下:

Stack(
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Container(
      height: 170,
      width: 170,
      color: Colors.blue,
    ),
    Container(
      height: 140,
      width: 140,
      color: Colors.yellow,
    )
  ],
)

Stack 對未定位(不被 Positioned 包裹)子組件的大小由 fit 參數決定,默認值是 StackFit.loose ,表示子組件自己決定,StackFit.expand 表示盡可能的大,用法如下:

Stack(
  fit: StackFit.expand,
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Container(
      height: 170,
      width: 170,
      color: Colors.blue,
    ),
    Container(
      height: 140,
      width: 140,
      color: Colors.yellow,
    )
  ],
)

效果只有黃色(最後一個組件的顏色),並不是其他組件沒有繪製,而是另外兩個組件被黃色組件覆蓋。

Stack 對未定位(不被 Positioned 包裹)子組件的對齊方式由 alignment 控制,默認左上角對齊,用法如下:

Stack(
  alignment: AlignmentDirectional.center,
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Container(
      height: 170,
      width: 170,
      color: Colors.blue,
    ),
    Container(
      height: 140,
      width: 140,
      color: Colors.yellow,
    )
  ],
)

通過 Positioned 定位的子組件:

Stack(
  alignment: AlignmentDirectional.center,
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Container(
      height: 170,
      width: 170,
      color: Colors.blue,
    ),
    Positioned(
      left: 30,
      right: 40,
      bottom: 50,
      top: 60,
      child: Container(
        color: Colors.yellow,
      ),
    )
  ],
)

topbottomleftright 四種定位屬性,分別表示距離上下左右的距離。

如果子組件超過 Stack 邊界由 overflow 控制,默認是裁剪,下面設置總是显示的用法:

Stack(
  overflow: Overflow.visible,
  children: <Widget>[
    Container(
      height: 200,
      width: 200,
      color: Colors.red,
    ),
    Positioned(
      left: 100,
      top: 100,
      height: 150,
      width: 150,
      child: Container(
        color: Colors.green,
      ),
    )
  ],
)

IndexedStack 是 Stack 的子類,Stack 是將所有的子組件疊加显示,而 IndexedStack 通過 index 只显示指定索引的子組件,用法如下:

class IndexedStackDemo extends StatefulWidget {
  @override
  _IndexedStackDemoState createState() => _IndexedStackDemoState();
}

class _IndexedStackDemoState extends State<IndexedStackDemo> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        SizedBox(height: 50,),
        _buildIndexedStack(),
        SizedBox(height: 30,),
        _buildRow(),
      ],
    );
  }

  _buildIndexedStack() {
    return IndexedStack(
      index: _index,
      children: <Widget>[
        Center(
          child: Container(
            height: 300,
            width: 300,
            color: Colors.red,
            alignment: Alignment.center,
            child: Icon(
              Icons.fastfood,
              size: 60,
              color: Colors.blue,
            ),
          ),
        ),
        Center(
          child: Container(
            height: 300,
            width: 300,
            color: Colors.green,
            alignment: Alignment.center,
            child: Icon(
              Icons.cake,
              size: 60,
              color: Colors.blue,
            ),
          ),
        ),
        Center(
          child: Container(
            height: 300,
            width: 300,
            color: Colors.yellow,
            alignment: Alignment.center,
            child: Icon(
              Icons.local_cafe,
              size: 60,
              color: Colors.blue,
            ),
          ),
        ),
      ],
    );
  }

  _buildRow() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        IconButton(
          icon: Icon(Icons.fastfood),
          onPressed: () {
            setState(() {
              _index = 0;
            });
          },
        ),
        IconButton(
          icon: Icon(Icons.cake),
          onPressed: () {
            setState(() {
              _index = 1;
            });
          },
        ),
        IconButton(
          icon: Icon(Icons.local_cafe),
          onPressed: () {
            setState(() {
              _index = 2;
            });
          },
        ),
      ],
    );
  }
}

流式布局組件

Wrap 為子組件進行水平或者垂直方向布局,且當空間用完時,Wrap 會自動換行,也就是流式布局。

創建多個子控件做為 Wrap 的子控件,代碼如下:

Wrap(
  children: List.generate(10, (i) {
    double w = 50.0 + 10 * i;
    return Container(
      color: Colors.primaries[i],
      height: 50,
      width: w,
      child: Text('$i'),
    );
  }),
)

direction 屬性控制布局方向,默認為水平方向,設置方向為垂直代碼如下:

Wrap(
  direction: Axis.vertical,
  children: List.generate(4, (i) {
    double w = 50.0 + 10 * i;
    return Container(
      color: Colors.primaries[i],
      height: 50,
      width: w,
      child: Text('$i'),
    );
  }),
)

alignment 屬性控制主軸對齊方式,crossAxisAlignment 屬性控制交叉軸對齊方式,對齊方式只對有剩餘空間的行或者列起作用,例如水平方向上正好填充完整,則不管設置主軸對齊方式為什麼,看上去的效果都是鋪滿。

說明 :主軸就是與當前組件方向一致的軸,而交叉軸就是與當前組件方向垂直的軸,如果Wrap的布局方向為水平方向 Axis.horizontal,那麼主軸就是水平方向,反之布局方向為垂直方向 Axis.vertical ,主軸就是垂直方向。

Wrap(
	alignment: WrapAlignment.spaceBetween,
	...
)

主軸對齊方式有6種,效果如下圖:

spaceAroundspaceEvenly 區別是:

  • spaceAround:第一個子控件距開始位置和最後一個子控件距結尾位置是其他子控件間距的一半。
  • spaceEvenly:所有間距一樣。

設置交叉軸對齊代碼如下:

Wrap(
	crossAxisAlignment: WrapCrossAlignment.center,
	...
)

如果 Wrap 的主軸方向為水平方向,交叉軸方向則為垂直方向,如果想要看到交叉軸對齊方式的效果需要設置子控件的高不一樣,代碼如下:

Wrap(
  spacing: 5,
  runSpacing: 3,
  crossAxisAlignment: WrapCrossAlignment.center,
  children: List.generate(10, (i) {
    double w = 50.0 + 10 * i;
    double h = 50.0 + 5 * i;
    return Container(
      color: Colors.primaries[i],
      height: h,
      alignment: Alignment.center,
      width: w,
      child: Text('$i'),
    );
  }),
)

runAlignment 屬性控制 Wrap 的交叉抽方向上每一行的對齊方式,下面直接看 runAlignment 6中方式對應的效果圖,

runAlignmentalignment 的區別:

  • alignment :是主軸方向上對齊方式,作用於每一行。
  • runAlignment :是交叉軸方向上將每一行看作一個整體的對齊方式。

spacingrunSpacing 屬性控制Wrap主軸方向和交叉軸方向子控件之間的間隙,代碼如下:

Wrap(
	spacing: 5,
    runSpacing: 2,
	...
)

textDirection 屬性表示 Wrap 主軸方向上子組件的方向,取值範圍是 ltr(從左到右) 和 rtl(從右到左),下面是從右到左的代碼:

Wrap(
	textDirection: TextDirection.rtl,
	...
)

verticalDirection 屬性表示 Wrap 交叉軸方向上子組件的方向,取值範圍是 up(向上) 和 down(向下),設置代碼如下:

Wrap(
	verticalDirection: VerticalDirection.up,
	...
)

注意:文字為0的組件是在下面的。

自定義布局組件

大部分情況下,不會使用到 Flow ,但 Flow 可以調整子組件的位置和大小,結合Matrix4繪製出各種酷炫的效果。

Flow 組件對使用轉換矩陣操作子組件經過系統優化,性能非常高效。

基本用法如下:

Flow(
  delegate: SimpleFlowDelegate(),
  children: List.generate(5, (index) {
    return Container(
      height: 100,
      color: Colors.primaries[index % Colors.primaries.length],
    );
  }),
)

delegate 控制子組件的位置和大小,定義如下 :

class SimpleFlowDelegate extends FlowDelegate {
  @override
  void paintChildren(FlowPaintingContext context) {
    for (int i = 0; i < context.childCount; ++i) {
      context.paintChild(i);
    }
  }

  @override
  bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
    return false;
  }
}

delegate 要繼承 FlowDelegate,重寫 paintChildrenshouldRepaint 函數,上面直接繪製子組件,效果如下:

只看到一種顏色並不是只繪製了這一個,而是疊加覆蓋了,和 Stack 類似,下面讓每一個組件有一定的偏移,SimpleFlowDelegate 修改如下:

class SimpleFlowDelegate extends FlowDelegate {
  @override
  void paintChildren(FlowPaintingContext context) {
    for (int i = 0; i < context.childCount; ++i) {
      context.paintChild(i,transform: Matrix4.translationValues(0,i*30.0,0));
    }
  }

  @override
  bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
    return false;
  }
}

每一個子組件比上一個組件向下偏移30。

仿 掘金-我的效果

效果如下:

到拿到一個頁面時,先要將其拆分,上面的效果拆分如下:

總體分為3個部分,水平布局,紅色區域圓形頭像代碼如下:

_buildCircleImg() {
  return Container(
    height: 60,
    width: 60,
    decoration: BoxDecoration(
        shape: BoxShape.circle,
        image: DecorationImage(image: AssetImage('assets/images/logo.png'))),
  );
}

藍色區域代碼如下:

_buildCenter() {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    crossAxisAlignment: CrossAxisAlignment.start,
    children: <Widget>[
      Text('老孟Flutter', style: TextStyle(fontSize: 20),),
      Text('Flutter、Android', style: TextStyle(color: Colors.grey),)
    ],
  );
}

綠色區域是一個圖標,代碼如下:

Icon(Icons.arrow_forward_ios,color: Colors.grey,size: 14,),

將這3部分組合在一起:

Container(
  color: Colors.grey.withOpacity(.5),
  alignment: Alignment.center,
  child: Container(
    height: 100,
    color: Colors.white,
    child: Row(
      children: <Widget>[
        SizedBox(
          width: 15,
        ),
        _buildCircleImg(),
        SizedBox(
          width: 25,
        ),
        Expanded(
          child: _buildCenter(),
        ),
        Icon(Icons.arrow_forward_ios,color: Colors.grey,size: 14,),
        SizedBox(
          width: 15,
        ),
      ],
    ),
  ),
)

最終的效果就是開始我們看到的效果圖。

水平展開/收起菜單

使用Flow實現水平展開/收起菜單的功能,代碼如下:

class DemoFlowPopMenu extends StatefulWidget {
  @override
  _DemoFlowPopMenuState createState() => _DemoFlowPopMenuState();
}

class _DemoFlowPopMenuState extends State<DemoFlowPopMenu>
    with SingleTickerProviderStateMixin {
  //動畫必須要with這個類
  AnimationController _ctrlAnimationPopMenu; //定義動畫的變量
  IconData lastTapped = Icons.notifications;
  final List<IconData> menuItems = <IconData>[
    //菜單的icon
    Icons.home,
    Icons.new_releases,
    Icons.notifications,
    Icons.settings,
    Icons.menu,
  ];

  void _updateMenu(IconData icon) {
    if (icon != Icons.menu) {
      setState(() => lastTapped = icon);
    } else {
      _ctrlAnimationPopMenu.status == AnimationStatus.completed
          ? _ctrlAnimationPopMenu.reverse() //展開和收攏的效果
          : _ctrlAnimationPopMenu.forward();
    }
  }

  @override
  void initState() {
    super.initState();
    _ctrlAnimationPopMenu = AnimationController(
      //必須初始化動畫變量
      duration: const Duration(milliseconds: 250), //動畫時長250毫秒
      vsync: this, //SingleTickerProviderStateMixin的作用
    );
  }

//生成Popmenu數據
  Widget flowMenuItem(IconData icon) {
    final double buttonDiameter =
        MediaQuery.of(context).size.width * 2 / (menuItems.length * 3);
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: RawMaterialButton(
        fillColor: lastTapped == icon ? Colors.amber[700] : Colors.blue,
        splashColor: Colors.amber[100],
        shape: CircleBorder(),
        constraints: BoxConstraints.tight(Size(buttonDiameter, buttonDiameter)),
        onPressed: () {
          _updateMenu(icon);
        },
        child: Icon(icon, color: Colors.white, size: 30.0),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Flow(
        delegate: FlowMenuDelegate(animation: _ctrlAnimationPopMenu),
        children: menuItems
            .map<Widget>((IconData icon) => flowMenuItem(icon))
            .toList(),
      ),
    );
  }
}

FlowMenuDelegate 定義如下:

class FlowMenuDelegate extends FlowDelegate {
  FlowMenuDelegate({this.animation}) : super(repaint: animation);
  final Animation<double> animation;

  @override
  void paintChildren(FlowPaintingContext context) {
    double x = 50.0; //起始位置
    double y = 50.0; //橫向展開,y不變
    for (int i = 0; i < context.childCount; ++i) {
      x = context.getChildSize(i).width * i * animation.value;
      context.paintChild(
        i,
        transform: Matrix4.translationValues(x, y, 0),
      );
    }
  }

  @override
  bool shouldRepaint(FlowMenuDelegate oldDelegate) =>
      animation != oldDelegate.animation;
}

半圓菜單展開/收起

代碼如下:

import 'dart:math';

import 'package:flutter/material.dart';

class DemoFlowMenu extends StatefulWidget {
  @override
  _DemoFlowMenuState createState() => _DemoFlowMenuState();
}

class _DemoFlowMenuState extends State<DemoFlowMenu>
    with TickerProviderStateMixin {
  //動畫需要這個類來混合
  //動畫變量,以及初始化和銷毀
  AnimationController _ctrlAnimationCircle;

  @override
  void initState() {
    super.initState();
    _ctrlAnimationCircle = AnimationController(
        //初始化動畫變量
        lowerBound: 0,
        upperBound: 80,
        duration: Duration(milliseconds: 300),
        vsync: this);
    _ctrlAnimationCircle.addListener(() => setState(() {}));
  }

  @override
  void dispose() {
    _ctrlAnimationCircle.dispose(); //銷毀變量,釋放資源
    super.dispose();
  }

  //生成Flow的數據
  List<Widget> _buildFlowChildren() {
    return List.generate(
        5,
        (index) => Container(
              child: Icon(
                index.isEven ? Icons.timer : Icons.ac_unit,
                color: Colors.primaries[index % Colors.primaries.length],
              ),
            ));
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Positioned.fill(
          child: Flow(
            delegate: FlowAnimatedCircle(_ctrlAnimationCircle.value),
            children: _buildFlowChildren(),
          ),
        ),
        Positioned.fill(
          child: IconButton(
            icon: Icon(Icons.menu),
            onPressed: () {
              setState(() {
                //點擊后讓動畫可前行或回退
                _ctrlAnimationCircle.status == AnimationStatus.completed
                    ? _ctrlAnimationCircle.reverse()
                    : _ctrlAnimationCircle.forward();
              });
            },
          ),
        ),
      ],
    );
  }
}

FlowAnimatedCircle 代碼如下:

class FlowAnimatedCircle extends FlowDelegate {
  final double radius; //綁定半徑,讓圓動起來
  FlowAnimatedCircle(this.radius);

  @override
  void paintChildren(FlowPaintingContext context) {
    if (radius == 0) {
      return;
    }
    double x = 0; //開始(0,0)在父組件的中心
    double y = 0;
    for (int i = 0; i < context.childCount; i++) {
      x = radius * cos(i * pi / (context.childCount - 1)); //根據數學得出坐標
      y = radius * sin(i * pi / (context.childCount - 1)); //根據數學得出坐標
      context.paintChild(i, transform: Matrix4.translationValues(x, -y, 0));
    } //使用Matrix定位每個子組件
  }

  @override
  bool shouldRepaint(FlowDelegate oldDelegate) => true;
}

交流

老孟Flutter博客地址(330個控件用法):http://laomengit.com

歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司“嚨底家”!

※推薦評價好的iphone維修中心

聚甘新

分類
發燒車訊

JAVA設計模式 1【創建型】設計模式介紹、單例模式的理解與使用

數據結構我們已經學了一部分了。是該了解了解設計模式了。習慣了CRUD的你,也該了解了解這一門神器、我為啥要說是神器呢?

因為在大廠的面試環節、以及很多的比如

  • Springboot
  • Mybatis

等開源框架中、大量的使用到了設計模式。為了我們在之後學習源代碼的時候不再懵逼,為啥這代碼能這樣寫?為啥巴拉巴拉xxx

設計模式必須要肝完

簡介

設計模式,是一套被反覆使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結
它是解決特定問題的一系列套路,是前輩們的代碼設計經驗的總結,具有一定的普遍性,可以反覆使用。其目的是為了提高代碼的可重用性、代碼的可讀性和代碼的可靠性。

總結下來說就是:一種設計經驗、一種設計套路

想一下,被前輩們總結下來的東西。使用這麼多年、凝結為精華的東西、該學

創建型

我們首先來了解一下什麼是創建型,創建型 作為設計模式的一種分類,是描述如何將一個對象創建出來的。

我們都知道,JAVA 作為一種面向對象編程,最關鍵的關鍵字new 用來實例化一個對象。創建型分類、則是描述:如何更好的創建出一個對象

單例模式理解

單例模式,從字面意思上了解就是:它只負責創建出一個對象。因此被稱為單例模式。理解一下:我們的計算機都會有一個任務管理器。而在一台windows 的電腦上。任何時候都只會實例化一個任務管理器對象。進而可以理解為單例模式

在一個任務管理器被調用的時候(對象已經被創建),再次使用ctrl+shift+esc 則任然返回這個已經被創建的任務管理器

UML 圖理解

可能我首次提到這個概念,所以簡介一下。

UML圖是用圖形化的方式表示軟件類與類之間的關係。用圖形化的方式,展示出眾多類之間的關聯關係。

類圖

如下圖,我用圖形的方式、描述了一個任務管理器類JobManagement.class
它有一個 私有化private的屬性management 類型為本身。
它有一個 公開的public 的方法getManagement() 返回類型為本身

  • 常用的類型修飾符就是 + 與 –

注意:“可見性”表示該屬性對類外的元素是否可見,包括公有(Public)、私有(Private)、受保護(Protected)和朋友(Friendly)4 種,在類圖中分別用符號+、-、#、~表示。
http://c.biancheng.net/view/1319.html

關聯關係

關聯關係就是用來表示:多個類之間存在怎麼樣的關係的表示方法。常用箭頭來表示。

虛線箭頭 依賴關係

虛線箭頭用來表示依賴關係 從使用類指向被依賴的類。這裏使用的類就是我們的main 方法。而被依賴類則是我們的任務管理器對象

菱形箭頭 聚合關係

聚合管理作為一種強關聯管理。一般用於成員變量的引用。

http://c.biancheng.net/view/1319.html

單例模式的特點

  • 對象只會被創建一次,並且重複使用
  • 全局提供一個訪問點。靜態訪問點
  • 構造方法私有

學以致用

public class JobManagement {

    private static volatile JobManagement management;

    private JobManagement() {

    }

    public static synchronized JobManagement getManagement() {

        if (null == management) {
            System.out.println("未創建任務管理器,正在創建。。");
            management = new JobManagement();
        } else {
            System.out.println("已經存在創建的任務管理器");
        }
        return management;
    }
}

任務管理器對象包含以及靜態類型的自身對象引用。以及將自身的構造方法進行私有化、使得外部無法直接創建對象。而需要這個對象,則需要調用get()方法進行獲取。

  • volatile 關鍵字將屬性在所有線程中同步

懶漢模式

	if (null == management) {
            System.out.println("未創建任務管理器,正在創建。。");
            management = new JobManagement();
        } 

懶漢模式則是在對象首次被訪問的時候才進行創建的。否則、若這個對象從未被引用、則對象是不會被創建的。而餓漢模式,剛剛相反。

餓漢模式

private static JobManagement management = new JobManagement();

餓漢模式則是則是在類被虛擬機加載的時候就創建一個示例出來。這樣在訪問之前就已經有對象被創建、線程也是安全的。

測試使用

    public static void main(String[] args) {

        JobManagement management1 = JobManagement.getManagement();
        System.out.println(management1);
        JobManagement management2 = JobManagement.getManagement();
        System.out.println(management2);

    }
----------------------------
未創建任務管理器,正在創建。。
JobManagement@1b6d3586
已經存在創建的任務管理器
JobManagement@1b6d3586

小結

單例模式在Java 的學習中還是有很多地方會使用到,對於我們學習的第一個,也是最簡單的模式,也是最常用的模式。記住它的特點:

  • 構造方法私有
  • 提供一個全局訪問點

參考

http://c.biancheng.net/view/1338.html

歡迎關注

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

聚甘新