# Spring Framework ## Project - Spring Framework - Spring Boot - Spring Data - [Spring Cloud](#Spring-Cloud) - Spring Cloud Data Flow - Spring Security - Spring Authorization Server - Spring for GraphQL - Spring Session - Spring Integration - Spring HATEOAS - Spring Modulith - Spring REST Docs - [Spring Batch](#Spring-Batch) - Spring AMQP - Spring Flo - Spring for Apache Kafka - Spring LDAP - Spring Shell - Spring Statemachine - Spring Web Flow - Spring Web Services ## Spring Framework @Danny幫忙補充 Spring,一般指代的是Spring Framework,它是一個開源的應用程式框架,提供了一個簡易的開發方式,通過這種開發方式,將避免那些可能致使程式碼變得繁雜混亂的大量的業務/工具物件 Spring MVC和Spring Boot都屬於Spring。 Spring MVC 是基於Spring的一個 MVC 框架, Spring Boot 是基於Spring的一套快速開發整合包 Spring Boot只是擴展了Spring framework,實現自動配置,降低專案搭建的複雜度。  spring-boot-starter-web-services,針對SOAP Web Services spring-boot-starter-web,針對Web應用與網路介面 spring-boot-starter-jdbc,針對JDBC spring-boot-starter-data-jpa,基於hibernate的持久層框架 spring-boot-starter-cache,針對快取支援 ## Spring IoC - 全稱: Inversion of Control - 中譯: 控制反轉 假設有一個 interface 為 Car ```java public interface Car { void drive(String location); } ``` Car 有兩個實現類,分別為 Tesla 及 Benz - Tesla.java ```java public class Tesla implements Car { @Override public void drive(String location) { System.out.println("[Tesla] drive to: " + location); } } ``` - Benz.java ```java public class Benz implements Car { @Override public void drive(String location) { System.out.println("[Benz] drive to: " + location); } } ``` 今天當父母想要開這 Tesla 出門時,必須這樣做: - Father.java ```java public class Father { private Car car = new Tesla(); public void travel() { car.drive("Taichung"); } } ``` - Mother.java ```java public class Mother { private Car car = new Tesla(); public void travel() { car.drive("Chiayi"); } } ``` 但若今天 Tesla 沒電了,想要改開 Benz 出門,就必須將每支 java 程式打開,將 new Tesla() 改成 new Benz(),非常麻煩! ### Solution 因此 Spring 就提出了一個想法,將 Car 這個 Object 拿來保管,當有人要用到 Car 時,就直接跟 Spring 拿即可。從此之後 Father 及 Mother 就不需要去 new 一個 Tesla 或 Benz 了,如下: - Father.java ```java public class Father { private Car car; public void travel() { car.drive("Taichung"); } } ``` - Mother.java ```java public class Mother { private Car car; public void travel() { car.drive("Chiayi"); } } ``` 而當 Spring application 啟動後,Spring 會啟動 Spring Container,Spring 會預先創建一個 Tesla 存放在 Spring Container 中,由 Spring 來管理這個 Tesla,當 Father 和 Mother 要開車時,Spring 就會把這個預先創建好、存放在 Spring Container 中的 Tesla 交給他們兩人,如此一來他們就可以正常使用車子了,他們也不需要在意車子是哪個廠牌的了,只需要知道有車可以開就好,這就是 Spring IoC 的概念。 回頭看一下 IoC 的定義,就能理解其中的含意了: > “將 Object 的控制權 交給外部的 Spring Container 來管理。” > #### Spring IoC 的優點 1. Loose coupling 降低耦合度 (e.g. Father 與 Tesla 之間的關聯性降低) 2. 統一生命週期管理,由 Spring 管理 Tesla 的創建、初始化及銷毀 3. 方便單元測試。 #### Spring IoC 與 Bean 如果要讓 Spring 去創建一個 Tesla 的 Object,並且預先存放在這個 Spring Container 中的話,就需要使用 `@Component` 此 Annotation #### `@Component` - 用法: 只能加在 **class** 上 - 作用: 將該 class 變成由 Spring Container 所管理的 Object - Tesla.java ```java @Component public class Tesla implements Car { @Override public void drive(String location) { System.out.println("[Tesla] drive to: " + location); } } ``` > 📝NOTE: 為 Tesla 加上 `@Component` 後,以後當啟動 Spring Boot application 時,Spring Container 就會同時被啟動起來,接著 Spring 就會為我們去創建一個 Tesla 的 Object,並存放在 Spring Container 中 > #### 注意事項 1. 只能加在 class 上 2. 被 Spring Container 創建的 Object,稱作「bean」 3. class 加上 `@Component` 後,此 bean 的名稱預設會是 class name 的首字母轉成小寫 接著,要將這個 Spring 所管理的 tesla bean 交給 Father 及 Mother 使用,要實現此需求有以下兩個步驟: 1. Step1: 將 Father 及 Mother 也加上 `@Component` 變成 bean 2. Step2: 將 Father 類中 car 加上 `@Autowired` Annotation > 📝NOTE: **「依賴注入(Dependency Injection, DI)」** > > > 加上 `@Autowired` 後, Spring 就會把你想要的 bean 交到你手上,Spring 將 bean 交到你手上這個動作,就稱為**「依賴注入(Dependency Injection, DI)」**。 > > 為何稱作**「依賴注入」**? > > 因為 Father 想要使用 tesla 這個 bean,因此 Father 依賴於 tesla bean,而 Spring 把 tesla 這個 bean 交給 Father ,就是把 Father 依賴的東西交給他、注入依賴給他,因此才會稱作**「依賴注入」** > - Father.java ```java @Component public class Father { @Autowired private Car car; public void travel() { car.drive("Taichung"); } } ``` 最後,當去執行 Father 的 travel() 方法時,輸出的結果就會是: ``` # console: [Tesla] drive to: Taichung ``` #### IoC 小結 - IoC 與 DI 是相輔相成的,IoC 將物件存放在 Spring Container 中,DI 讓我們去取得存放在 Spring Container 中的那些物件 - bean: 指存放在 Spring Container 中的那些物件(咖啡罐裡的咖啡豆) ### Bean 的注入 #### `@Autowired` (Henry補充盡量別用) 不使用原因及替代方法可參考該網站 https://kaisheng714.github.io/articles/analyzing-dependency-injection-patterns-in-spring - 用法: 通常是加在 class 變數上 - 作用: 根據變數的類型,到 Spring Container 中尋找有無符合類型的 bean - 注意事項: 1. 變數的類型盡量使用 interface ```java @Component public class Father { @Autowired private Car car; // (O) // private Tesla car; // (X) ... } ``` 2. 如果同時有多個 class 都實現了 Car interface 的情況下,那當 `@Autowired` 到底要將哪個實現類注入給 Father 呢? ⮕ 解決辦法: `@Qualifier` #### `@Qualifier` - 用法: 通常是加在 class 變數上,並會跟 `@Autowired` 同時使用 - 作用: 指定要載入 bean 的 name > 📝NOTE: 記得 bean name 首字母為”小寫” > - example: 在 car 變數加上 `@Qualifier`,並指定為 "tesla" 這個 bean 之後,此時 `@Autowired` 就會把類型為 Car 且 name 為 "tesla" 的那個 bean 注入到 Father 中 ```java @Component public class Father { @Autowired @Qualifier("tesla") // (O) 記得 bean name 首字母為小寫 // @Qualifier("Tesla") // (X) 記得 bean name 首字母為小寫 private Car car; ... } ``` ### Bean 的注入建議用Constructor Injection 替代 此方式最大的特點是: Bean 的建立與依賴的注入是同時發生的 ```java public class MyBean { private final AnotherBean anotherBean; public MyBean(final AnotherBean anotherBean) { this.anotherBean = anotherBean; } //Business logic... } ``` - 優點1. 容易發現 code smell - 優點2. 容易釐清依賴關係 - 優點3. 容易寫單元測試 - 優點4. Immutable Object - 缺點:循環依賴 只有在使用 constructor injection 時才會造成此問題。 舉個簡單的例子,若依賴關係: Bean C → Bean B → Bean A → Bean C ,則會造成造成此問題,程式在 Runtime 會拋出BeanCurrentlyInCreationException ### Bean 的創建 @Component、@Configuration、@Bean - Spring 檢查 class 上的 Annotation,有以下三種情況: 1. class 上有 `@Component` ⮕ Spring 會為我們創建一個該 class 的 bean,然後放到 Spring Container 中 2. class 上有 `@Configuration` ⮕ 執行這個 class 中的 code,去對 Spring Container 進行設定 (設定甚麼就根據 code 的內容) 3. class 上沒有任何 Annotation ⮕ 該 class 會被 Spring 當作一個普通的 Java class (與 Spring Container 沒有任何關係) #### Bean 的創建方式 1. 方式(一): 在 class 上加上 `@Component` 2. 方式(二): 使用 `@Configuration` + `@Bean` #### `@Configuration` - 用法: 只能加在 class 上 - 作用: Spring 中的 配置用Annotation,代表被綁定的 class 是拿來配置 Spring 用的 - example: - MyConfiguration.java ⮕ 對 Spring Container 進行某些設定 #### `@Bean` - 用法: 只能加在 帶有 `@Configuration` class 的「方法」上 - 作用: 在 Spring Container 中創建一個 bean - example: - MyConfiguration.java ⮕ 在 Spring Container 中創建一個 tesla bean ```java @Configuration public class MyConfiguration { /** * 在 Spring Container 中創建一個 tesla bean * (return 一個 Tesla 類物件,存放到 Spring Container 中) */ @Bean public Car myCar() { return new Tesla(); } } ``` - 注意事項: 1. `@Bean` 一定要加在帶有 `@Configuration` 的 class 裡面 2. 所生成的 bean 就是該 `@Bean` 方法 return 的物件 3. 使用 `@Bean` 方法創建的 bean name,預設會是該”方法的名稱”!!! 4. 若 @Bean("xxx") 有指定 bean name,就優先以此字串當作 bean name!!! ### Bean的初始化 `@PostConstruct`、`@InitializingBean` 若今天在 Tesla 類中添加 double remainingBattery ****剩餘電量的屬性 - Tesla.java ```java @Component public class Tesla implements Car { private Integer remainingBattery; **// <-- 添加剩餘電量屬性** @Override public void drive(String location) { System.out.println("[Tesla] drive to: " + location); } } ``` 但此時 Spring 只是為我們創建 tesla bean 並存放在 Spring Container 中,並沒有為它的 remainingBattery 進行初始化,因此該 tesla bean 的 remainingBattery 屬性值就預設為 0。 那如果要同時去初始化該 tesla bean 的 remainingBattery 屬性值,有以下 2 種方法: #### Spring 中初始化 bean 的方法 1. 使用 `@PostConstruct` 2. 實現 InitializingBean interface 的 afterPropertiesSet() 方法 (少用) #### 初始化方法(1) - `@PostConstruct` - 用法: 加在「方法」上 - 作用: 初始化 bean 中的內容 - 注意事項: - 被 `@PostConstruct` 註解的方法必須為 `public`、`void`,且 `不能有參數` - 與 方法(2) 擇一使用即可 - example: 在 Tesla 類中添加 `initialize()` 方法並未方法加上 `@PostConstruct` Annotation 後,當創建 tesla bean 後,Spring 就會接著執行被 `@PostConstruct` 註解的方法,去初始化 tesla bean 中的內容 - Tesla.java ```java @Component public class Tesla implements Car { private Integer remainingBattery; // <-- 添加剩餘電量屬性 /** * 用來初始化 Spring Container 中的 tesla bean 之 remainingBattery 屬性值 * 注意: @PostConstruct 方法必須為 public、void,且不能有參數 */ @PostConstruct public void initialize() { this.remainingBattery = 100; // 初始化為 100 } @Override public void drive(String location) { this.remainingBattery -= 25; // 每開一次消耗 25 電量 System.out.println("[Tesla] drive to: " + location); System.out.println("[Tesla] 剩餘電量: " + this.remainingBattery); } } ``` #### 初始化方法(2) - 實現 InitializingBean interface 的 afterPropertiesSet() 方法 (少用) - example: - Benz.java ```java public class Benz implements Car, InitializingBean { private Integer remainingFuel; // <-- 添加剩餘油量屬性 /** * 用來初始化 Spring Container 中的 benz bean 之 remainingFuel 屬性值 */ @Override public void afterPropertiesSet() throws Exception { this.remainingFuel = 100; // 初始化為 100 } @Override public void drive(String location) { this.remainingFuel -= 20; // 每開一次消耗 20 油量 System.out.println("[Benz] drive to: " + location); System.out.println("[Benz] 剩餘油量為: " + this.remainingFuel); } } ``` ### Bean 的生命週期 #### 單一 bean 的狀況 - 啟動 Spring Boot application ``` mvn spring-boot:run ``` - 啟動一個空的 Spring Container 出來 - 根據以下兩種方式 創建 bean,並放進 Spring Container 中: 1. `@Component` 2. `@Configuration` + `@Bean` - 根據以下兩種方式 初始化 bean: 1. @PostConstruct 2. 實現 InitializingBean interface 及 其 afterPropertiesSet() 方法 - 完成以上步驟,bean 就變成「可被使用」的狀態了 - Spring Boot application 運行成功,console 輸出: ```java Started XxxApplication in x.xxx seconds (JVM running for 3.xxx) ``` #### 多個 bean 的狀況 基本上與流程相同,但要注意以下事項: 1. 要所有的 bean 都變成「可被使用」狀態時,Sring Boot application 才會運行成功(任何一個失敗都不行) 2. bean 之間的依賴關係處理 ⮕ 當有多個 bean 時,Spring 會自動根據「 bean 之間的依賴關係」,去決定「創建和初始化 bean 的順序」,ex: - 創建 father bean 時,發現 Father 依賴於 Car,並 `@Qualifier` 指名為 tesla bean - 回頭先創建 tesla bean,並初始化 tesla bean,讓 tesla bean 變成「可被使用」狀態 - 接著再回頭繼續創建 father bean、初始化並讓 father bean 變成「可被使用」狀態 3. 因為 「Spring 會自動根據 bean 之間的依賴關係,去決定創建和初始化 bean 的順序」的特性,因此我們避免寫出以下這種「循環依賴」的程式: ## Spring Security 是一個安全相關的框架,提供授權、驗證的功能,如果單只有引入會導致所有api回傳401,需要覆寫去自定義驗證方式 只要指定限制特定URL的傳入(也可鎖定post或get),就能簡單做好登入頁面 ### Spring Security 主要依賴 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` ### Spring Security OAuths2.0(開放式授權) 要先登入google、fb、line等申請憑證(根據自己的需求選擇),並將憑證放在自己的xml或yml ex:google https://console.cloud.google.com/apis  加入第三方登入驗證,只要在自定義授權裡簡單設置oauth2Login()就能達成 ```java @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .antMatcher("/**").authorizeRequests() .antMatchers(new String[]{"/", "/not-restricted"}).permitAll() .anyRequest().authenticated() .and() .oauth2Login(); } } ``` ### SSO(Single Sign-On, 單點登入) 透過一次登入滿足在多個地方訪問不同資源 ### Spring Security OAuths 主要依賴 ```xml <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency> ``` ## Spring AOP AOP 是一種程式設計思想,它允許你將橫切關注點(Cross-Cutting Concerns)從主要的業務邏輯中分離出來,以模組化的方式進行管理。橫切關注點是指那些無法單獨劃分為一個模組的功能,Spring AOP 通過提供一種方法來定義切面(Aspects),讓你能夠將這些橫切關注點應用到程式碼中。 **主要概念:** * 切面(Aspect): 切面是一個模組化單元,它包含了橫切關注點的定義。切面描述了在何處以及何時應該執行某些操作。例如,你可以創建一個切面來記錄方法的執行時間。 * 連接點(Join Point): 連接點是在應用程式中可能被切面影響的點,例如方法的呼叫、執行等。 * 通知(Advice): 通知是切面的實際行動,它定義了在何時(例如方法的開始、結束)執行特定的程式碼。通知可以分為以下幾種類型: 1. 前置通知(Before Advice): 在連接點方法執行之前執行的通知。 2. 後置通知(After Advice): 在連接點方法執行之後執行的通知。 3. 返回通知(After Returning Advice): 在連接點方法正常返回之後執行的通知。 4. 異常通知(After Throwing Advice): 在連接點方法拋出異常之後執行的通知。 5. 環繞通知(Around Advice): 包圍連接點方法執行的通知,它可以完全控制方法的執行。 * 切入點(Pointcut): 切入點是一個表達式,用來匹配一組連接點。通過切入點,你可以指定在哪些連接點上應用特定的通知。 **創建 UserService 類** ``` public class UserService { public void getUser(String username) { System.out.println("Getting user: " + username); } public void saveUser(String username) { System.out.println("Saving user: " + username); } } ``` **創建切面類 LoggingAspect** ``` import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { @Pointcut("execution(* com.example.UserService.*(..))") private void userServiceMethods() {} @Before("userServiceMethods()") public void logBeforeMethodExecution() { System.out.println("Before method execution: Logging..."); } @After("userServiceMethods()") public void logAfterMethodExecution() { System.out.println("After method execution: Logging..."); } } ``` **範例(使用LoggingAspect)** ``` import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class MainApp { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); UserService userService = context.getBean(UserService.class); userService.getUser("john"); userService.saveUser("jane"); context.close(); } } ``` **範例(未使用LoggingAspect)** ``` public class MainApp { public static void main(String[] args) { UserService userService = new UserService(); logBeforeMethodExecution(); userService.getUser("john"); logAfterMethodExecution(); logBeforeMethodExecution(); userService.saveUser("jane"); logAfterMethodExecution(); } public static void logBeforeMethodExecution() { System.out.println("Before method execution: Logging..."); } public static void logAfterMethodExecution() { System.out.println("After method execution: Logging..."); } } ``` **執行結果** ``` Before method execution: Logging... Getting user: john After method execution: Logging... Before method execution: Logging... Saving user: jane After method execution: Logging... ``` **優點:** * 提高了程式碼的模組性和可讀性,將橫切關注點從業務邏輯中分離出來。 * 降低了程式碼的重複性,例如日誌記錄、事務管理等操作只需定義一次。 * 讓業務邏輯保持乾淨,不受非功能性操作的干擾。 **應用場景:** * 日誌記錄:記錄方法的執行、參數等信息。 * 安全性:執行安全性檢查,確保只有授權用戶能夠訪問特定的方法。 * 事務管理:處理方法的事務操作,確保方法執行過程中的一致性。 * 性能監測:測量方法的執行時間,進行性能分析。 ## Spring Boot Spring Boot,很多人會想到Spring 和Spring MVC。 究竟它們有什麼差別? 第一,Spring是一種框架,包含一系列的IoC容器的設計和依賴注入(DI)及整合AOP功能。 第二,Spring Boot 和 Spring MVC 都是一種框架,同時它們的核心是Spring。 第三,Spring Boot包含了Spring MVC,同時能簡化配置。 Spring Boot 的特色: 創建獨立的Spring應用程式 嵌入式Tomcat, Jetty, Undertow (不用部署WAR包) 提出自主的starter來簡化配置 隨時自動地配置Spring及相關的第3方Library 提供已隨時就緒的功能如Metrics, 程式的健康檢查及外部化配置 不會生成任何代碼及無任何XML配置的前設要求 創建一個簡單的Spring boot https://blog.miniasp.com/post/2022/09/19/Spring-Boot-Quick-Start-From-Scratch Spring boot的tomcat預設8080port也可以調整 https://www.javatpoint.com/spring-boot-change-port ## Spring Cloud **什麼是Spring Cloud ?** Spring Cloud是一組用於構建分散式、彈性和可伸縮**微服務**的框架和工具集合。它是基於Spring框架的一個專門設計用於協助開發和管理微服務架構的子項目。 Spring Cloud提供了各種模塊和功能,用於幫助開發人員在微服務架構下解決一些常見的問題,例如: 1. 服務註冊和發現:Spring Cloud提供了服務註冊和發現的功能,使得服務可以輕鬆註冊到服務註冊中心並被其他服務發現和訪問。 2. 負載平衡:Spring Cloud支援服務的負載平衡,可以將請求分發到多個實例上,以實現高可用性和性能優化。 3. 配置管理:Spring Cloud允許動態配置微服務的參數,並提供中心化的配置管理。 4. 斷路器和故障處理:Spring Cloud提供斷路器模式,當某個服務故障時,可以將流量快速轉移到其他可用的服務上。 5. API Gateway:Spring Cloud還提供了API Gateway,可以集中處理請求路由、過濾、安全性等。 6. 分佈式追蹤:Spring Cloud支援分佈式系統的追蹤和監控,以協助定位問題和優化性能。 總的來說,Spring Cloud為開發人員提供了一整套的工具和框架,用於開發、部署和管理微服務架構,幫助實現高效、可伸縮和可靠的分散式系統。 ### Spring Cloud Netflix **Spring Cloud Netflix**是Spring Cloud底下的一個子專案,將Netflix一系列開源工具和框架(Netflix OSS Integrations)整合到Spring Cloud中,以提供開發者更容易使用Netflix OSS(Open Source Software)中的各種服務,例如: 1. 微服務架構: Netflix OSS 提供了一些與微服務相關的工具和框架,如Eureka(服務發現)、Ribbon(客戶端負載均衡)、Hystrix(熔斷器)、Archaius(動態配置)等,這些可以幫助開發者在微服務架構下實現服務的可用性和可靠性。 2. 數據處理: Netflix OSS 包括了一些數據處理工具,如Genie(數據作業)、PigPen(數據處理腳本)等,這些工具可以幫助開發者處理和分析大量的數據。 3. 流媒體處理: Netflix OSS 還包括了用於流媒體處理的工具,如Kafka(消息隊列)和Samza(流處理框架),這些工具可以幫助處理即時數據流。 4. 診斷和監控: Netflix OSS 提供了一些用於診斷和監控的工具,如Atlas(實時數據監控)、Spectator(性能監控)等,這些工具可以幫助開發者監控應用程序的運行情況。 總之,Netflix OSS是一系列的開源項目和工具,可以幫助開發者在不同的場景下構建、運行和監控分佈式系統,並從Netflix的實踐中受益。 ### 實作參考 https://ithelp.ithome.com.tw/users/20107338/ironman/1445?page=1 ### 補充 :::warning Spring Cloud和Kubernates的差異 * Spring Cloud:是一個框架,提供了多個模組和工具,例如服務發現、負載平衡、熔斷、配置管理等。它主要針對在 Spring 應用程式中實現分散式系統模式的開發。 * Kubernates:是一個容器管理平台,主要針對容器化應用程式的部署和管理。 參考資料:https://dzone.com/articles/deploying-microservices-spring-cloud-vs-kubernetes ::: ## Spring Batch Spring batch是一個輕量級、全面的批處理框架,旨在開發對企業系統日常運營至關重要的強大批處理應用程序。 Spring Batch 提供了處理大量記錄所必需的可重用功能,包括日誌記錄/跟踪、事務管理、作業處理統計、作業重啟、跳過和資源管理。它還提供更先進的技術服務和功能,通過優化和分區技術實現極高容量和高性能的批處理作業。簡單和復雜的大批量批處理作業都可以以高度可擴展的方式利用該框架來處理大量信息。 ### 特徵 - 交易管理 - 基於塊的處理 - 聲明式 I/O - 啟動/停止/重新啟動 - 重試/跳過 - 基於 Web 的管理界面(Spring Cloud Data Flow) ### 架構說明 - JobRepository 負責提供 Launcher 、 Job 、 Step 的持久化機制,負責運行中的 CRUD 。 - 一個 JobLauncher 觸發相對應的 Job 。 - 一個 Job 有數個 Step 。 - Step 有三種應對:分別是 資料讀取( ItemReader )、資料處理( ItemProcessor )、資料寫入( ItemWriter )。  ### 範例 https://www.tpisoftware.com/tpu/articleDetails/831
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up