## 目錄 - [Spring 「使用者資料管控專案Part1」 - Day 66~80](##Day66) - [Multi-Thread - Day 81~???](##Day81) ## Day66 #### 學習重點 : JWT token生成與驗證 - 2 - 密碼儲存業務邏輯 ⭐⭐⭐⭐ - 透過與大神的溝通稍微整理了下更新密碼的業務邏輯 : - 1️⃣ 建立帳號時,首次密碼**存取於temp_password欄位**。 - 2️⃣ **第一次**登入時,比對temp_password,若成功則放進password欄位儲存。 - 若忘記密碼,可發送臨時密碼(temp)至使用者email,待登入後選取更改密碼功能,若無,則**清空temp**。 - 密碼更新功能Controller測試 ⭐⭐⭐⭐⭐ - **Token認證** : 以token作為認證使用者方法,因此要在Request headers放入token。 - **密碼dto** : 建立PasswordUpdate接收RequestBody傳入的新舊密碼,舊密碼作為認證,若成功則將password欄位改為新密碼。 - JwtUtil實作認證 : ```java= public static void verify(String token, String secret) { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm).build(); verifier.verify(token); } ``` - 密碼更改功能於Controller測試 : ```java= @PutMapping("/users") public void update(@RequestBody PasswordUpdate passwordUpdate , HttpServletRequest request){ String token = request.getHeader("Authorization"); JwtUtil.verify(token, secret); } ``` - `Bearer <token>` 為固定的Authorization格式,因此在Header時需用此格式才能代表JWT驗證。 ## Day67 #### 學習重點 : JWT token生成與驗證 - 3 - 密碼更新Payload解析 ⭐⭐⭐⭐ - 透過在Jwt的sign中claim的account名稱,去資料庫搜尋使用者,並認證舊密碼是否跟傳進來的oldPassword相符。 - 不過我目前都是在Dao層判定,好像有點問題ww - JwtUtil建立取得payload的資訊 : ```java= public static String getString(String token, String key) { Claim claim = JWT.decode(token).getClaim(key); return claim.asString(); } ``` ```java= userService.updatePassword( passwordUpdate, JwtUtil.getString(token, "account") ); ``` - 判定測試 ⭐⭐⭐⭐⭐⭐ - 現在都先用簡單的分析來讓功能先做出來,之後再作優化! - 這邊是流程 : - 1️⃣ 利用account取得使用者當前密碼 - 2️⃣ 比對oldPassword與當前密碼 - 3️⃣ 若pass則寫入新密碼,若fail則不動作 ```java= String sql = "SELECT * FROM user WHERE account = :account"; // ...省略 User user = namedParameterJdbcTemplate.query( sql, map, new UserMapper() ).getFirst(); if (Objects.equals(user.getPassword(), passwordUpdate.getOldPassword())){ user.setPassword(passwordUpdate.getNewPassword()); String updateSql = "UPDATE user SET password = :password WHERE account = :account"; // ...省略 namedParameterJdbcTemplate.update(updateSql, param); } ``` ## Day68 #### 學習重點 : JWT token生成與驗證 - 4 - 密碼更新邏輯(Service層)⭐⭐⭐⭐⭐⭐ - 1️⃣ 以payload傳進來的account資訊呼叫 `userDao.findUserByAccount()` - 2️⃣ 接收到User後,確認是否為null,若否.則進入比較階段,若是.則回傳false。 - 3️⃣ 比較成功,則呼叫 `dao.updatePassword()` 傳入user並更新。 ```java= public boolean updatePassword(PasswordUpdate passwordUpdate, String account){ // user接收 User realUser = implementUserDao.findUserByAccount(account); if (realUser == null) return false; // 確認狀態 // 比較password if (Objects.equals(realUser.getPassword(), passwordUpdate.getOldPassword())){ // 成功則setPassword並傳入dao層 realUser.setPassword(passwordUpdate.getNewPassword()); implementUserDao.updatePassword(realUser); return true; } return false; } ``` - 密碼更新邏輯(Dao層) ⭐⭐ - 這邊就很簡單,Dao層 **只負責與資料庫溝通**,因此不參與比對環節。 ```java= @Override public void updatePassword(User user){ String updateSql = "UPDATE user SET password = :password WHERE account = :account"; BeanPropertySqlParameterSource param = new BeanPropertySqlParameterSource(user); namedParameterJdbcTemplate.update(updateSql, param); } ``` ## Day69 #### 學習重點 : Java BCrypt Introduction - Bcrypt架構 ⭐⭐⭐⭐⭐⭐ - 分層分為 -> alg、cost factor、salt、hashed - 與Jwt的驗證一樣都有分層!但實際結構還是有些許不同,主要是Jwt跟BCrypt實際用處也不太一樣。 ![image](https://hackmd.io/_uploads/S16_1Z6tZx.png) - **alg** : 演算法的種類。 - **cost factor** : 重複加密幾次 -> 越多次越安全,但加密時間越長。 - **salt** : 每次**隨機產生**,增加多樣性,驗證機制 - 從密文拿出salt區塊,加上使用者提供的密碼進行雜湊,若與hash區域相同則通過。 - **hash** : 由salt + password進行雜湊出來的東西,會儲存在使用者password欄位。 - 實際Java加密與驗證 ⭐⭐⭐ - 加密、驗證,兩個步驟,Java BCrypt其實很易懂,就稍微寫了下。 ```java= public class BcryptUtil { public static String genHashedPassword(String password){ int logsalt = 12; String salt = BCrypt.gensalt(logsalt); return BCrypt.hashpw(password, salt); } public static boolean verify(String insertPassword, String hashedPassword){ return BCrypt.checkpw(insertPassword, hashedPassword); } } ``` ## Day70 #### 學習重點 : BCrypt應用至Service層 - 生成加密密碼於Service ⭐⭐⭐⭐ - 在Service層中,我加了一個 `userBCryptSettings` 來處理傳進來的使用者數據。 ```java= public static void userBCryptSettings(User user){ String hashedP = BcryptUtil.genHashedPassword(user.getPassword()); user.setPassword(hashedP); } ``` - 由於後續可能會在user型別中加入其他需加密的東西,所以settins就不單純接收String而是User了。 #### 注意🚨🚨 - 這邊可以看到傳入的是User物件,也就是所謂的 **call by reference**,傳進來的是地址,並沒有 `new` 的動作,因此地址是原本外部的,更改的user也自然是外部的user。 - 修改Object.equals ⭐⭐⭐⭐ - 原本我是利用Object的比較method,**但** 現在是利用BCrypt的verify功能,直接將oldPassword與realUser的加密密碼進行驗證! - 我感覺我很容易就忘記如何verify,這邊先寫下來好了w : - 1️⃣ 從雜湊密碼中拿出中間段的salt。 - 2️⃣ 將 **salt** 與傳進來 **要驗證的密碼** 進行雜湊。 - 3️⃣ 比對 **驗證密碼的雜湊值** 與 **原本資料庫存的雜湊密碼**。 ```java= public User login(UserRequest userRequest){ User user = new User(); // ...省略設定 User realUser = implementUserDao.login(user); if (BcryptUtil.verify(user.getPassword(), realUser.getPassword())){ return realUser; }else{ return null; } } ``` - updatePassword也是同樣的道理,這邊就不放上來了。 ## Day71 #### 學習重點 : 忘記密碼功能實作(隨機密碼產生) - 引入 `apache.commons-text` ⭐ - 簡單來說就是引入一個能夠生成隨機密碼的東西。 - 原本是想要手刻一個啦w 但感覺目前先專注在系統的實作,之後有時間再來研究! ```html= <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.10.0</version> </dependency> ``` - 基本架構 ⭐⭐⭐ - 按照apache提供的文檔,我稍微模仿了一下 : ```java= String rsg = new RandomStringGenerator.Builder() .withinRange('0', 'z') .filteredBy(Character::isLetterOrDigit) .build().generate(12); ``` #### Method reference (::) - 我記得遙遠的Day?? 我有學習過lambda的用法,簡單來說就是interface的實作簡寫用法。 - 而這邊的method refernece就是lambda的更簡寫用法,連參數都省掉了,我自己覺得有點太簡化了w。 ## Day72 #### 學習重點 : 忘記密碼功能實作(mail發送) - 忘記密碼功能想法 ⭐⭐⭐ - 由於忘記密碼功能通常是 : - 1️⃣ 點選忘記密碼功能 - 2️⃣ 寄送臨時密碼 or 更改密碼按鈕至使用者設定之信箱 - 3️⃣ 使用臨時密碼登入後再更改 or 按按鈕後更改 - 我這邊選擇配合我的 `updatePassword` 與 `temp_password` 的 **臨時密碼登入**。 - 郵件工具建立 - 這邊需要利用spring.mail的工具 - `pom.xml` 引入 ⭐ ```html= <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> ``` - application.properties 設定郵件伺服器、傳送者等。 ⭐⭐⭐⭐ ```properties= spring.mail.host=smtp.gmail.com spring.mail.port=587 spring.mail.username= 我的gmail帳號 spring.mail.password= 到安全性步驟中申請應用程式密碼 spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true ``` - 接著是基本架構 (**mailService**) : ⭐⭐⭐⭐⭐⭐ ```java= @Service public class MailService { // 注入springboot的mailSender @Autowired JavaMailSender javaMailSender; public void sendMail(String mailReceiver, String subject, String body){ // 設定內容 SimpleMailMessage smm = new SimpleMailMessage(); smm.setTo(mailReceiver); smm.setSubject(subject); smm.setText(body); javaMailSender.send(smm); } } ``` - 再來是搭配Service當中的forgotPassword ⭐⭐⭐⭐⭐⭐⭐⭐ ```java= public void forgetPassword(String account){ // 先利用account找到使用者數據 User user = implementUserDao.findUserByAccount(account); // 生成隨機密碼 String rsg = new RandomStringGenerator.Builder() .withinRange('0', 'z') .filteredBy(Character::isLetterOrDigit) .build().generate(12); // 加密隨機密碼 String hashedRandomPassword = BcryptUtil.genHashedPassword(rsg); // 將加密密碼與帳號交由dao層放入資料庫 implementUserDao. forgetPassword(account, hashedRandomPassword); // 交由 mailService 處理寄發mail的事情 mailService.sendMail(account, "Temporary password", rsg); } ``` - **實際成果如下**,接下來就是處理temp_password登入後刪除臨時密碼的功能啦~不過今天先這樣就好! ![螢幕擷取畫面 2026-03-13 161831](https://hackmd.io/_uploads/SkWOFH-qWx.png) ## Day73 #### 學習重點 : 忘記密碼功能實作(臨時密碼登入) - 密碼驗證 - 新增可變參數動態陣列、方法性質修正 ⭐⭐⭐⭐⭐ - 在進入臨時密碼登入前,我先對BCryptUtil的verify功能做了稍微更改 : ```java= // public改private、static移除 : 由於service是bean,不應有類別屬性 private boolean verify( String insertPassword, String... hashedPassword){ // 迴圈跑過所有加密密碼 for (String hp : hashedPassword){ // 避免發生nullPointerExeption,我加了個保險 if (hp == null) continue; if (BCrypt.checkpw(insertPassword, hp)){ return true; } } return false; } ``` - 由於有臨時密碼,若在Service層一直用 **if-else重複呼叫verify** 會 **很繁瑣**,又剛好我之前有學過Varargs的用法,想說剛好派上用場ww - 臨時密碼登入 ⭐⭐⭐⭐⭐ - 當我忘記密碼並成功寄發臨時密碼後,temp_password欄位更新,我就可以 **接收temp_password** 並用於login的verify上了,以下是我的邏輯 : ```java= public User login(UserRequest userRequest){ User realUser = implementUserDao .findUserByAccount(userRequest.getAccount()); boolean isValid = BcryptUtil.verify( userRequest.getPassword(), // 看看原本password或者temp_password哪個可以對上 realUser.getPassword(), realUser.getTemp_password() ); // 【確認」登入成功後,將temp設為null保持淨空並更新至資料庫 if (realUser.getTemp_password() != null && isValid){ realUser.setTemp_password(null); implementUserDao.updatePassword(realUser); } return isValid ? realUser : null; } ``` ## Day74 #### 學習重點 : 刪除帳號實作 - 刪除帳號功能 ⭐⭐ - 其實也沒有甚麼好講的ww,就是單純的刪除動作,只是一樣要利用JWT token驗證,以下我在Controller中的實作。 ```java= @DeleteMapping("/users") public ResponseEntity<?> delete(HttpServletRequest request){ String token = request.getHeader("Authorization"); JwtUtil.verify(token, secret); String account = JwtUtil.getString(token, "account"); userService.deleteAccount(account); return ResponseEntity.ok().build(); } ``` - 對於某些部分,我想要在明後天做個修正 : - 1️⃣ JWT驗證需要有exception做攔截,因為現在沒有細分錯誤。 - 2️⃣ BCrypt的Bearer前輟要補上,並且在傳進token時再做字串分析。 - 3️⃣ token要加上更多訊息,像是exp之類的。 - 我有看到一張圖去解釋status code對應的token錯誤 : ![image](https://hackmd.io/_uploads/By4GbGEcbe.png) ## Day75 #### 學習重點 : JWT解析token、token過期時間新增 - Bearer標記 ⭐⭐⭐⭐⭐ - Bearer是一種認證機制,表示token是透過JWT格式加密的,前面也實作了一堆JWT的東西,這邊就不贅述了w - 在後端,我新增了Bearer標籤功能,當signature製作完畢後,我讓token加上 `Bearer` 字串,**表示是JWT形式的token**。 - 而解析的方式目前若發現是非Bearer就回傳null,目前尚未加入throws功能。 ```java= // 目錄 : Util/JwtUtil public static String authorization(String rawCode){ return String.format("Bearer %s", rawCode); } public static String decode(String code){ if (code.startsWith("Bearer ")) { return code.substring(7); }else { return null; } } ``` - 加入exp - token到期時間 ⭐⭐ - 這邊使用簡單的Date形式,並作相加 ```java= // 目錄 : Util/JwtUtil private static final long EXPIRED_TIME = 1000*10; // 10秒 public static String sign(User user, String secret) { Algorithm algorithm = Algorithm.HMAC256(secret); long time = System.currentTimeMillis(); return JWT.create() .withClaim("account", user.getAccount()) .withIssuedAt(new Date(time)) // 讓現在時間 + 簽署開始後多久到期,由EXPIRED_TIME定義 .withExpiresAt(new Date(time+EXPIRED_TIME)) .sign(algorithm); } ``` ## Day76 #### 學習重點 : ControllerAdvice - 例外處理 - 簡介 ⭐⭐⭐⭐ - 建立Exception的資料夾,在其中需要有**Handler**以及**例外的種類**。 - 1️⃣ 先定義例外的類型,並且定義errorCode以及errorMessage。 - 2️⃣ Handler專門處理例外的各種問題,決定要回應什麼樣的status code以及訊息。 - 程式架構 ⭐⭐⭐⭐⭐⭐ - 例外類型定義(以驗證來做測試) ```java= package org.system.exception; public class AuthException extends RuntimeException{ private int errorCode; private String errorMessage; public AuthException(int errorCode, String errorMessage){ this.errorCode = errorCode; this.errorMessage = errorMessage; } // getter & setter } ``` - Handler處理 ```java= package org.system.exception.handler; // ...省略 @ControllerAdvice public class testExceptionHandler { @ExceptionHandler({AuthException.class}) public ResponseEntity<?> handleAuthException(AuthException e){ if (e.errorCode == 1001){ return //...省略 } // ...省略 } } ``` - 由於今天真的超級忙,明後天再來完整處理其他錯誤w ## Day77 #### 學習重點 : ControllerAdvice - 例外處理分類 - 簡單分類 : ⭐⭐⭐⭐⭐⭐ - 我稍微統整了一下可能會出現的錯誤 : - **驗證錯誤類** : token過期、token有誤、Bearer錯誤、。 - **業務邏輯類** : 註冊時帳號已存在、登入時帳密有誤、更新密碼時舊密碼有誤。 - **資源找不到類** : 忘記密碼時填入的信箱並不在DB中。 - 由於 **驗證錯誤類** 會交由auth0內建的 `JWTVerificationException`,因此我分了 `BusinessLogicException`、`ResourcesNotFoundException`。 - 業務邏輯類 : ⭐⭐⭐ - 以下是我的分類程式 : ```java= @ExceptionHandler({BusinessLogicException.class}) public ResponseEntity<?> logicHandle(BusinessLogicException e){ if (e.getErrorCode() == 1001){ // 當註冊帳號時,帳號已存在 return ResponseEntity.status(409).build(); }else if (e.getErrorCode() == 1002){ // 重設密碼時,舊密碼不符 return ResponseEntity.status(400).build(); }else if(e.getErrorCode() == 1003){ // 登入時帳密有誤 return ResponseEntity.status(401).build(); }else{ // 未知錯誤先回傳BAD_REQUEST return ResponseEntity.status(400) .body(e.getErrorMessage()); } } ``` - 我利用registration來加上錯誤引流 : ```java= public void registration(UserRequest userRequest){ try{ //...省略 if (userDao.findUserByAccount(userRequest.getAccount()) != null){ throw new BusinessLogicException(1001, "Failed registration"); } userBCryptSettings(user); implementUserDao.registration(user); }catch (BusinessLogicException e){ throw e; }catch (Exception e){ System.out.println("錯誤訊息 : " + e.getMessage()); throw new BusinessLogicException( 9999, "系統出現未知錯誤,請稍後再試...。" ); } } ``` - 明天繼續努力將其完善! ## Day78 #### 學習重點 : 剩餘例外處理分流、Controller & Service層簡潔化 - 話說我覺得程式碼好亂,乾脆用照片貼上比較簡潔一些w,之後都改用這個方法好了。 - 剩餘例外處理分流 ⭐⭐⭐ - 我建立了 `ResourcesException`,並在Handler新增了處理資源找不到的方法。 ![image](https://hackmd.io/_uploads/BJexNcK9bl.png) - Controller & Service層簡潔化 ⭐⭐⭐⭐⭐⭐ - 由於Controller不知道被哪個~~阿呆~~用try-catch搞得烏煙瘴氣,導致職責分離不清。 - 因此我今天把整個Controller優化成 **只專注回應**、Service處理 **throw exception的問題**,而exception後續動作就 **交給handler** 去用了~ - 大概長這樣 : ![image](https://hackmd.io/_uploads/BJ1mS5Fcbl.png) ## Day79 #### 學習重點 : Response業務狀態回應 - Response類別 ⭐⭐⭐⭐⭐⭐⭐⭐ - 該類別通常會自行定義「**業務**」狀態碼,針對自己邏輯的各種可能,屬於 **業務邏輯狀態**,也可以存取user物件,讓回傳更加靈活! ![image](https://hackmd.io/_uploads/B1-jIkic-l.png) - 這邊有偷用了LOMBOOK的工具ww,簡單來說就是省去了getter&setter的部分~ - Response應用 ⭐⭐ - 而我目前針對login的部分,若臨時密碼欄位不為空,則導向更改密碼的頁面(雖然可以再優化,但目前先這樣ww),Response則可以 **設定rc** 使得後續可以決定要跳轉到哪個頁面(首頁or更改密碼頁面)。 ![image](https://hackmd.io/_uploads/Hy_dIkj9Wg.png) - 目前Exception的部分我還在修正,可能得再花個幾天時間QAQ。 ## Day80 #### 學習重點 : 使用者資料管控實作(Part-1結業式🥳) - 總結 ✨✨✨✨ - 儘管Exception並沒有設計的很理想,但我想,這個可以留給幾十天後的我再來處理!我覺得是時候先去學習更多概念了! - 另外,以下是我對這次系統的功能介紹 : #### 加密功能 🔑 - **JwtUtil** : 負責 `加密、驗證、Bearer前輟新增or刪除` ➜ [**Day65 JWT研究**](https://hackmd.io/@learning-official/Java_learning#Day64) - **BCryptUtil** : 負責 `加密、驗證` ➜ [**Day69 BCrypt研究**](##Day69) #### 註冊功能 ®️ - 要求使用者輸入 : `名稱、帳號、密碼`。 - 搭配 `registration` 方法接收 `userRequest` 的dto格式。 #### 登入功能 🔐 - 要求使用者輸入 : `帳號、密碼`。 - 搭配 `login` 方法接收 `userRequest`、`前端response結構`。 #### 臨時密碼功能 📩 ➜ [Day71 忘記密碼研究](##Day71) - 要求使用者輸入 : `帳號`。 - 以 `RandomStringGenerator` 生成隨機密碼並 **寄信至輸入帳號**。 - 搭配 `forgetPassword` 方法接收 `路徑參數account`,寄信到指定信箱。 - 無論資料庫是否有該帳號,一律回應ok。 #### 使用者介面重設密碼功能 🔄 - 要求使用者輸入 : `新密碼`、`舊密碼`。 - 以 `JwtUtil` 做舊密碼驗證。 #### 刪除帳號功能 🗑️ - 簡單來說是刪帳號ww,沒什麼好說的。 - 搭配前端架構呈現 ✨✨✨✨✨✨ - 我請AI幫我建置了一個 **static的檔案資源**(html、js、css),以介面來呈現我的成果! ![image](https://hackmd.io/_uploads/BkU4v739bx.png) ![image](https://hackmd.io/_uploads/S1-yum29be.png) ![image](https://hackmd.io/_uploads/BkJz_Xh9Zx.png) ## Day81 #### 學習重點 : Thread & Runnable 基本理解 - 前言 ⭐ - 從以前到現在,我對於這兩個詞都很不能理解,儘管知道是可以讓程式同時運作,但終究沒有實作經驗,因此我打算花個幾天來認真研究一下。 - Thread基本架構 ⭐⭐⭐⭐ - 首先,需要建一個class用於繼承Thread,並 Override父類的 `run()` method。 ```java= // Test.java public class Test extends Thread{ @Ovveride public void run(){ System.out.print("Thread test!"); } } ``` - 可以想像run是 **一條執行緒的進入點**(? - 接著在Main檔建立Test物件,並使用 `start()` 來 **啟動執行緒**。 ```java= // Main.java public class Main{ public static void main(String args[]){ Test test = new Test(); test.start(); } } ``` - Thread - 暫停當前執行緒用法 ⭐⭐⭐⭐⭐⭐ - 當我們有多條執行緒時,可以使用 `sleep()` 來暫停當前執行緒。 ```java= // Test.java public class Test extends Thread{ @Override public void run(){ System.out.println("Thread test!"); // 由於執行緒在暫停期間,可被中斷,因此要catch被中斷後該做甚麼動作 try{ Thread.sleep(1000); // 1000 ms = 1s }catch(InterruptedException e){} } } ``` ## Day82 #### 學習重點 : Runnable的運用方式 - Thread跟Runnable的差別在哪? ⭐⭐⭐⭐⭐⭐⭐⭐ - 其實點進去這兩個.class檔就會發現,Runnable是一個 `FunctionalInterface`,而Thread是實作Runnable的一個檔案。 #### Runnable比Thread好用的點在哪? - 1️⃣ **靈活性問題** - Runnable是介面,由於Java不允許多重繼承,但 **可以多重實作**,因此使用implements相較於extends能增加類別的靈活性。 - 2️⃣ **多工處理,可共用資料** - 若我有一個工作 (Task) 想要丟給多個執行緒去跑同一個Task物件。 - 若繼承Thread,必需建立多個Task物件,按照 `task1.start()、task2.start()...` 這樣子。 - 若我實作Runnable,則可以只建立一個task1,並 `new Thread(task1).start();` 很多次即可,其實質上只使用了task1一個物件。 - 3️⃣ **關注點分離** - Thread 關心的是 **執行緒本身**。 - Runnable 關心的是要 **執行的工作內容**。 - 4️⃣ **關於Thread pool存放問題** - 它有點像我之前學的Spring IoC容器(? 在這邊,我們會將實作Runnable的 **類別丟到Thread pool裡面去存放**,後續的動作,我還沒學到,之後再來~ - Runnable的實際code寫法 ⭐⭐⭐⭐ - 跟Thread大同迷你異,只差在它需要丟到Thread去start。 ```java= public class Main { public static void main(String[] args) { for (int i=0; i<5; i++){ // 這樣子是建立五個RunnableTest物件 RunnableTest rt = new RunnableTest(i); new Thread(rt).start(); } } } ``` ```java= public class Main { public static void main(String[] args) { // 這樣子是則是建立一個物件,讓5個執行緒去跑同一個物件 RunnableTest rt = new RunnableTest(1); for (int i=0; i<5; i++){ new Thread(rt).start(); } } } ``` ## Day83 #### 學習重點 : Callable簡單理解使用方法 - Callable是甚麼?⭐⭐⭐⭐ - Callable基本上跟Thread、Runnable是做同樣的事情 -> 多執行緒! - 但差別在於Callable有回傳值,且可以自定義型態,也就是泛型! - 直接來看程式碼比較清楚 : ```java= import java.util.concurrent.Callable; public class CallableTest { // 這邊傳入整數的執行緒工作 Callable<Integer> callable = () -> { int sum = 0; for (int i = 1; i <= 10; i++) { sum += i; } return sum; }; public static void main(String[] args) { CallableTest ct = new CallableTest(); try { // 使用call啟動執行緒! System.out.println(ct.callable.call()); } catch (Exception e) {} } } ``` ## Day84 #### 學習重點 : Callable的用法 - Callable與Runnable不同之處 ⭐⭐ - 儘管Callable與Runnable都是用於多執行緒,但Callable可以在task結束後,**回傳自訂義型別的值or物件**。 - Callable回傳以及後續用法 ⭐⭐⭐⭐ - 當我們需要取得Callable實作下所回傳的東西時,需要用到Future類別。 ```java= Callable<Integer> c = () -> {實作內容省略...} Future<Integer> f = new Executors. newSingleThreadExecutor().submit(c) ``` - 先將Callable送進 **執行緒池** 中(Executor這部分先省略,我之後再來研究ww),接著將Callable的 **回傳的結果送進Future物件** 中。 - 接著就可以利用Future物件去取值啦~ ```java= System.out.println(f.get()); // 回傳的型態由Callable決定 ``` ## Day85 #### 學習重點 : Executors處理 - 甚麼是ExecutorService?⭐⭐⭐ - 可以想像ExecutorService是一家公司,專門 **接收並管理Task**,而這個接收Task的地方就是前兩天一直提到的ThreadPool。 - 當我們建立了Callable物件時,需要submit該物件到ThreadPool當中排隊等待工作。 ```java= public static void main(String[] args) throws Exception{ // 該方法在Executors中會回傳Service物件,不須再自行new! ExecutorService service = Executors.newFixedThreadPool(3); // 使用List存放Callable所回傳的Future物件 List<Future<Integer>> list = new ArrayList<>(); for (int i=0; i<5; i++){ CallableTest<Integer> ct = new CallableTest<>(i+10); list.add(service.submit(ct)); } service.shutdown(); for (Future<Integer> future : list){ System.out.println(future.get()); } } ``` - 注意!儘管執行緒task會結束,但Pool不會關,因此我們需要自行shutdown該Service,否則它會一直占用資源。 ## Day86 #### 學習重點 : 甚麼是Synchronize? - Synchronize(同步)的意義 ⭐⭐⭐⭐ - 在前幾天有提及到Runnable可以建立同一個物件 **但建立多執行緒** 運行同一個物件,若今天需對成員變數做更改時,**同時讀取、更動、寫入** 的時間有 **可能會衝突**,造成資料錯誤,此時就需要靠Synchronize來協調! - Synchronize如何使用? ⭐⭐⭐⭐ - 它就像final、abstract一樣,直接加在method的前面! ```java= public class RunnableTest implements Runnable{ // ...省略 public synchronized void add() { this.num++; } @Override public void run(){ // ...省略 } } ``` - 而它的作用性就在於 : 當 `thread_1` 進入到同步方法後,`thread_2、thread_3...` 若也要進入同步方法時,將需要排隊等待,以避免資料同時讀取寫入,造成錯誤! ## Day87 #### 學習重點 : Concurrency - 資源可見性問題 & Lock解方 - 甚麼是資源可見性問題? ⭐⭐⭐⭐⭐⭐⭐⭐ - 當利用Concurrency概念實踐Multithread的時候,會遇到一個問題 ➞ 當執行緒在**修改「共享資源」時**,其他執行緒能否 **「即時看見」這個修改成果**? - 一般來說,執行緒在操作共享資源時,會先從主記憶體 **複製一份** 到自己的工作區,運算完畢後,再更新到主記憶體 。 - 但這會衍生出「**更新延遲**」的問題。 - **更新延遲** ➞ 亦即昨天所理解到的,同時讀取運算造成的時間差,使得資料更新上出現重疊、錯誤 ➞ 進而衍生出了資源可見性問題。 - 我們可以說操作共享資源的程式片段是 **Critical Section**。 - Lock解方 ⭐⭐⭐ - 為了因應多執行緒操作共享資源所造成的可見性問題,Java提供了 `Synchonized` 關鍵字作為一種 **鎖住Critical Section** 的方法,使得該區塊 **同時間只能夠讓單一執行緒操作**。 - **若Lock範圍過大**,反而失去了Concurrency的優點,因此通常在寫Synchronized時,只會針對「取用共享資源」的部分鎖住。 ```java= public void add(){ // 針對nun++上鎖,但method本身不是鎖 synchronized(this){ this.num++; System.out.println("task " + this.num); } } ``` ## Day88 #### 學習重點 : Concurrency - 原子性問題 - 甚麼是原子性問題? ⭐⭐⭐⭐⭐⭐ - 在程式中,式子像是 `i++`、`new Thread()` ..雖然只有一行,但轉譯到組語中,就會變成像 : 「加載變數至CPU cache ➞ 計算、建立... ➞ 賦值 ➞ 回傳」這樣的步驟,由於那些式子「**在程式碼中**」已是**不可分割**,因此稱其為**原子性**。 - 而問題就出在,當今天需要使用Concurrency多執行緒去操作同一物件時,就會有原子性問題出現,底下這張圖我覺得不錯! ![image](https://hackmd.io/_uploads/SkWFc3UiWl.png) - [(取自Java Concurrency #1: Concurrency 基礎)](https://medium.com/bucketing/java-concurrency-1-%E5%9C%A8%E9%96%8B%E5%A7%8B%E5%AF%ABcode%E5%89%8D%E5%85%88%E4%BA%86%E8%A7%A3%E4%B8%80%E4%B8%8Bconcurrency%E7%9A%84%E5%9F%BA%E7%A4%8E-8d1a6694eeff) - 由於在程式碼中,已經不能再切分 `cnt++` 了,因此在Java Memory Model中,會使用像前幾天所寫的Synchronized來將某些式子鎖住,流程不與其他執行緒交互作用! - Java Memory Model(JMM)的概念 ⭐⭐⭐ - JMM代表著「JVM如何管理Thread與Memory的互動」。 - 1️⃣ Happen Before Order : 亦即當Write Action執行完畢後,Read Action能夠取得正確的變數值,此時就會利用到 `volatile` 關鍵字來 **確保可見性**,不過使用volatile會限制變數於主執行緒中操作,且不能解決原子性問題,因此使用不廣。 - 2️⃣ 第二個概念也就是Synchronized的概念拉~ 我就不打了。 ## Day89 #### 學習重點 : 應用Callable、ExecutorService、Future - 搜尋File中的關鍵字 ⭐⭐⭐⭐⭐ - 我請AI給我了一個功能以實踐我這幾天學習的執行緒功能! - 功能實作步驟如下 : - 1️⃣ 建立 **搜尋** File中關鍵字數量的檔案(這邊不是主要學習部分,直接請AI給code) - 2️⃣ 建立Scanner要求使用者給予要搜尋的關鍵字,並建立 `Callable<Integer>` 後,submit該task後,以 `Future<Integer>` 接收。 - 3️⃣ 建立ArrayList **接收多個Future**,使用 `future.get()` 等待並取得在該檔案中搜到的關鍵字數量,最後加總。 - 實際code展示 ⭐⭐ - 這邊只展示了應用的部分,搜關鍵字的部分這邊先不研究w ```java= public class Main { static final ExecutorService service = Executors.newFixedThreadPool(4); public static void main(String[] args){ String[] files = { // ...省略 }; Scanner scanner = new Scanner(System.in); List<Future<Integer>> list = new ArrayList<>(); System.out.println("請要尋找的關鍵字:"); String keyword = scanner.next(); for(String path : files){ list.add(service.submit(new FileSearchTask(path, keyword))); } int count = 0; try{ for (Future<Integer> future : list){ count += future.get(); } System.out.println( "這10個檔案共包含 " + count + " 個 " + keyword ); }catch (InterruptedException iException){ iException.printStackTrace(); }catch (Exception e){ e.printStackTrace(); } service.shutdown(); scanner.close(); } } ``` ## Day90 #### 學習重點 : 執行緒應用 - 小專案寄發mail解決 - 寄發mail問題 ⭐⭐⭐⭐ - 在我之前的小專案實作中,寄發信箱這部分有個問題點,那就是當我POST後,頁面會進入待機狀態 ➞ **等成功sendmail後** 才會跳轉去更新密碼頁面。 - 而多執行緒剛好可以解決這個問題,一部份讓主執行緒去做跳轉,再另外開出一個 **子執行緒去做sendmail** 的動作! - ExecutorService小專案mail寄發應用 ⭐⭐⭐⭐ - 我這邊使用了Runnable介面搭配ExecutorService來開子執行緒進行mail寄發。 ```java= public UserService(){ // ...省略 this.mailExecutors = Executors.newFixedThreadPool(10); } Runnable r = () -> { // 子執行緒 : 寄發原始隨機密碼到使用者信箱 try{ mailService.sendMail(account, "Temporary password", rsg); }catch (Exception e){ System.out.println("寄信失敗 : " + e.getMessage()); } }; mailExecutors.submit(r); ``` ## Day91 #### 學習重點 : 序列化與Serializable簡單理解 - 怎麼理解序列化? ⭐⭐⭐⭐⭐ - 序列化是一種轉譯的概念,在程式語言當中,物件是主角,序列化的功能就是將物件轉 **成電腦所認得的格式**(btye stream),其中當然就帶有變數、方法。 - 尤其Java又是以物件導向為主軸,因此序列化在Java中算是很重要的一環。 - 我自己感覺Serializable跟Java的 `.class檔` 處理模式蠻像的?都是轉譯成二進制,不過一個是在轉譯物件,一個用來轉譯源代碼。 - 現在Serializable遇到的問題 ⭐⭐⭐⭐⭐⭐⭐ - 我在爬文的時候有得到一個不錯的結論 : - **更新問題** : 由於Serializable限制在Java程式之間的溝通,且受限在版本差異、函式庫差異等問題下,造成很多不必要的麻煩。 - **支援問題** : 目前程式庫中也不一定支援與 `ObjectOutputStream`、`ObjectInputStream` 這兩個API互動,但這又是Serializable的核心,因此yoyoyo受限住。 - **更好的格式** : 像是JSON就是目前最好用的格式,儘管Serializable可以更好儲存Java物件的完整性,但JSON還是利大於弊! ## Day92 #### 學習重點 : Java File IO - 如何寫檔? ⭐⭐⭐⭐ - 使用 `BufferedWriter`,以FileWriter作為參數傳入BufferedWriter作為物件執行的的目標檔案! - 最後需要配上 `.close()` **關閉Buffer中的資料輸入狀態**! ```java= public class Main { public static void main(String[] args){ try { BufferedWriter writer = new BufferedWriter( new FileWriter("FileIO\\output.txt") ); writer.write("Hello World!"); writer.write("\nSecond line!"); writer.close(); } catch (IOException e) { e.printStackTrace(); } } } ``` - 如何讀檔? ⭐⭐⭐⭐⭐ - 以readLine作為讀取的主要功能,存入String line中,這邊的寫法很特別!先賦值在判斷是否終止迴圈! ```java= public class Main { public static void main(String[] args){ try { BufferedReader reader = new BufferedReader( new FileReader("FileIO\\output.txt")); String line; while ((line = reader.readLine()) != null){ System.out.println(line); } reader.close(); }catch (IOException e) { e.printStackTrace(); } } } ``` ## Day93 #### 學習重點 : Java IO - 甚麼是Stream? - Stream是甚麼? ⭐⭐⭐⭐⭐ - 在Input跟Output中,程式的運作底層原理就是靠著Stream在運作!但到底甚麼是Stream(流)呢? - 所謂的Stream其實就是「一串長度不一的**Bytes資料序列**」,因此Input Stream讀取資料序列,而Output Stream就是將資料序列呈現or寫入! - 如果用簡單的比喻,像是**鍵盤**就是一個**Input Stream**,讀取使用者按下的按鍵,而**螢幕**則是**Output Stream**,呈現出使用者按下按鍵出現的畫面! - Java的IO Stream分類 ⭐⭐⭐ - Java中,Input Stream與Output Stream,**都是抽象類別**!因此會有許多實作,像是昨天看到的**FileWriter、BufferedWriter都是**!底下這張圖就是Stream的架構! ![image](https://hackmd.io/_uploads/HkdZ0LTjWg.png) - Made By [Kody Simpson](https://www.youtube.com/watch?v=7dmIVusn8mk&list=PLfu_Bpi_zcDO4CdNYNS2Wten1vLuQfgp7&index=1) ## Day94 #### 學習重點 : Java IO - OutputStream與PrintStream - OutputStream介紹 ⭐⭐⭐⭐ - 在寫程式之前,必須先理解輸出流是怎麼運作的,首先當我們write資料序列進輸出流的時候,資料會先「**流**」進Buffer區,等到flush後,資料才會被「**沖出**」至指定區域! - PrintStream是甚麼? ⭐⭐⭐ - 其實它就是我們最常使用的 `System.out...` 啦~ 可以說PrintStream就是OutputStream的一個實作子類別,它指定flush沖出的地方是「Terminal」! - 因此平常我們所使用的輸出形式 `System.out.println(參數)` 就是將 **參數丟進Buffer後再flush出來的**! - 如何實作呢? ⭐⭐⭐⭐⭐⭐ - 以下是我整理的三個重點 : ```java= package FileIO; import java.io.IOException; public class OutputStreamT{ public static void main(String[] args){ // 重點一 : 根據ASCII table輸出形式 int thing = 75; System.out.write(thing); System.out.flush(); // 重點二 : byte array被write進Buffer區再一次flush出來 for (int i=32; i<127; i++){ System.out.write(i); } System.out.flush(); // 重點三 : 使用wrtie(byte[] b)時,需加try-catch // 由於該方法並沒有被PrintStream先攔截,因此必須自行攔截 try{ String name = "JavaLearningEveryday"; byte[] bytes = name.getBytes(); System.out.write(bytes); System.out.flush(); }catch (IOException ex){ System.out.println(ex); } } } ``` ## Day95 #### 學習重點 : Java IO - InputStream - InputStream是甚麼概念? ⭐⭐⭐ - 記得在剛開始學習Java的時候,有使用到一個Scanner的物件!而建構式當中所放入的就是 **輸入流** `System.in`! - 因此InputSream相對應OutputStream就是在 **讀取** 而不是寫入! - 昨天我在write放參數,這個參數本身就是已經被建置好的資料序列,而輸入流就是 **讀取序列後放入write** 當中作為參數。 - 如何使用InputStream? ⭐⭐⭐ - 首先當然就是利用 `System.in` 來做事啦~ - 基本架構如下 : ```java= try{ int[] array = new int[10]; for (int i=0; i < array.length; i++){ // 讓使用者於Terminal輸入字串,一個一個wrtie進Buffer區 array[i] = System.in.read(); System.out.write(array[i]); } // 最後一次flush出來 System.out.flush(); }catch (IOException ex){ System.out.println(ex); } ``` - 如何搭配FileInputStream? ⭐⭐⭐⭐ - 首先要先宣告FileInputStream物件來作為讀取File的主要工具。 ```java= FileInputStream input = new FileInputStream("output.txt"); // 這邊使用的available可以讀取檔案中有多少的字元 byte[] arrayOfData = new byte[input.available()]; // 將讀取到的字元放進array中 input.read(arrayOfData); ``` - 接著將讀取到的byte陣列放入write參數當中,再沖出至Terminal。 ```java= System.out.write(arrayOfData); System.out.flush(); ``` ## Day96 #### 學習重點 : Java IO - FileStream - FileStream如何使用? ⭐⭐⭐ - 透過前幾天的IO練習,其實可以大概知道怎麼用,首先當然就是先建立FileIO的物件,接著透過讀取與與寫入進Buffer區,再來就是flush啦~ - 實際架構還是得搭配try-catch,因為對於write、read來說需要有IO例外的攔截! ```java= FileInputStream fin = null; FileOutputStream fout = null; try{ fin = new FileInputStream(new File("fin.txt")); // 這邊設置append屬性是true,亦即不是覆蓋而是新增 fout = new FileOutputStream(new File("fout.txt"), true); // 這邊使用readAllBytes直接抓取檔案所有的字元 // 若擔心檔案過大,也可以使用while(data != -1)來做偵測是否為檔案結尾 byte[] text = fin.readAllBytes(); for (int i=0; i<text.length; i++){ fout.write(text[i]); } fout.flush(); }catch (IOException ex){ System.out.println(ex); }finally{ try{ if (fin != null) fin.close(); if (fout != null) fout.close(); }catch (IOException ex){ System.out.println(ex); } } ``` - 關於Buffer區域問題 ⭐⭐⭐⭐⭐⭐⭐ - 我在學習FileIO的時候有遇到一個問題 --> **Buffer區容易搞混**,所以我稍微列出了關於Stream的Buffer區種類 : - **PrintStream** : 處理 `System.out` 的緩衝區。 - **InputStream** : 處理 `System.in`,當我read時會去Terminal拿取資料序列,待flush被呼叫或者close呼叫才會清空Buffer。 #### 關於FileOutputStream的Buffer區 - 在程式碼當中 `fout.flush` 其實沒什麼作用!在FileOutputStream中,**並沒有Override flush方法**,也沒有自己的Buffer區,所以寫這行只是一個習慣而已w - 深究該類別的寫入方式,其實就是單純**讀一個byte寫一個byte**,因此在使用fout.write時,可以想像他自動幫我們flush進去了! ## Day97 #### 學習重點 : Java IO - FilterStream - 甚麼是FilterStream? ⭐⭐⭐⭐ - 當我們針對IO Stream做讀取寫入時,可能想對某些字元做更改,或者說「**攔截**」,這時候會靠著**FilterStream來完成**。 - 儘管也可以直接在Main檔案中進行實作,但經過SpringBoot的洗禮,我也大概知道為何要有這種「職責分離」的概念啦~ - 如何使用FilterStream? ⭐⭐⭐⭐ - 在Stream大分類中,Filter(IO)Stream是一個父類,其中包含許多子類是專門處理特別的過濾,像是大約一個禮拜前所看過的Buffered(IO)Stream就是一種過濾類別! - 而我們也可以自行繼承Filter(IO)Stream來「客製化」過濾流。 ```java= public class CustomFilterStream extends FilterOutputStream{ // 因為有繼承,所以會用到super啦~~ // 因為要過濾,當然要有「被過濾者」out本人傳進來 public CustomFileStream(OutputStream out){ super(out); } // 這邊Override父類的寫入功能,並加上自己的過濾邏輯 @Override public void write(int b) throws IOException{ if (b >= '0' && b <= '9'){ super.write(b); }else{ super.write('?'); } } @Override public void write(byte b[], int off, int len) throws IOException{ for (int i=off, i < off+len; i++){ this.write(b[i]); } } } ``` - 在Main檔使用FilterStream ⭐⭐⭐⭐ - 在Main檔案中,自然會需要兩個物件,一個是寫入流,一個是過濾流! ```java= public class FilterStreamT { public static void main(String[] agrs){ FileOutputStream fout = null; CustomFilterStream filter = null; try{ fout = new FileOutputStream("filter.txt"); filter = new CustomFilterStream(fout); int i; while ((i = System.in.read()) != 'x'){ filter.write(i); } }catch (IOException ex){ System.out.println(ex); }finally{ try{ if (filter != null){ filter.close(); } }catch (IOException ex){ System.out.println(ex); } } } } ``` - 但這邊要注意的是,過濾流中所連結的Buffer區,其實就是寫入流的Buffer區! - 我覺得這張圖畫得很好,偷過來用一下w。 ![image](https://hackmd.io/_uploads/Hk1CuGG2bx.png) [- Made By Kody Simpson](https://www.youtube.com/watch?v=W5OChVAoYm0&list=PLfu_Bpi_zcDO4CdNYNS2Wten1vLuQfgp7&index=5) - 明天我應該會先把BufferedStream完結之後再來研究try-resources的用法! ## Day98 #### 學習重點 : Java IO - BufferedStream - BufferedStream的用處 ⭐⭐⭐⭐⭐⭐⭐⭐ - 回到剛學IOstream的時候,我對於BufferedStream的理解就是讀取寫入。 - 但經歷過這禮拜的學習,我對於Buffer與Filter有更深的理解了! - 所謂的BufferedStream,對於電腦讀取與寫入有專門處理方式,增加處理效能! - BufferedStream是怎麼運作的? - 1️⃣ 首先,BufferedStream在物件建立的時候,會開啟一個byte array用於「暫存資料」。 - 2️⃣ 當我們執行read、write等動作,BufferedStream會先一次性將一坨資料序列先送進byte array,接著再交由read、write讀寫。 #### 為甚麼要這樣做? - 會這樣做的原因很簡單 : 「省去 **來回** 硬碟拿取的次數」。 - **硬體原因** : 由於「從硬碟拿資料」 與 「從RAM拿資料」有**實質上的速度差異**,因此一次從硬碟拿一整坨byte丟到RAM會比一個一個拿還快上N倍! - 流程圖 : ⭐⭐⭐⭐⭐⭐ - 我又偷了兩張圖來這邊ww,這個人真的講得很好,推一個! - 當我們針對Output的Buffered類別過濾時,會先 **等byte array滿了自動flush** 或者 **被手動flush後**,才會再去硬碟拿資料! ![image](https://hackmd.io/_uploads/r1kZSk4nWl.png) - 這是Input的部分 : ![image](https://hackmd.io/_uploads/BkXBSJ43Ze.png) [Made By - Kody Simpson](https://www.youtube.com/watch?v=baHz_RmMt5I&list=PLfu_Bpi_zcDO4CdNYNS2Wten1vLuQfgp7&index=6) - 實際程式架構 : ⭐⭐ - 其實跟昨天的Filter一樣,只是名稱換一下,過濾功能不同而已w ```java= public class BufferedStreamT{ public static void main(String[] args){ BufferedInputStream input = null; BufferedOutputStream output = null; try{ input = new BufferedInputStream( new FileInputStream("bufferIn.txt") ); output = new BufferedOutputStream( new FileOutputStream("bufferOut.txt") ); int in; while ((in = input.read()) != -1){ output.write(in); } output.flush(); // 雖然會close的時候會flush,但還是寫一下w } // ...後續catch、finally省略 } } ```