取得參數

你將無法同時取得 @PathVariable 和 @RequestParam

如果最後一個是 @PathVariable @RequestParam 適用於 URL 查詢參數 (? stockQuantity=100) 或表單資料 (form-urlencoded)。 而不適用於put put 需要 pojo 傳遞

參數案例
  • 陣列參數

對於多選操作 assets/JavaWeb/image-20230809102733219.png 接收 assets/JavaWeb/image-20230809102854144.png

array: 請求參數名=形參中陣列變數名,直接可以封裝

collection: 請求參數名=集合變數名,透過 @RequestParam 綁定參數關係

  • 日期參數

使用 @DateTimeFormat(pattern= "") 指定接受參數格式

assets/JavaWeb/image-20230809104109364.png 必須和傳遞格式一樣 assets/JavaWeb/image-20230809123944488.png

  • Json 參數

使用 @RequestBody

assets/JavaWeb/image-20230809124901273.png

Json 資料 key name 與形參物件 property name 相同

  • 路徑參數 localhost:8080/path/1 localhost:8080/path/100 請求參數 (1,100)成為了請求路徑的一部分,這種參數就稱為路徑參數

@RequestMapping("/path/{id}") 使用 @pathVariable 取得路徑中的參數綁定到形參

assets/JavaWeb/image-20230809135426642.png assets/JavaWeb/image-20230809135805144.png

@DeleteMapping("/{ids}")
public Result delete(@PathVariable List<Integer> ids){

assets/JavaWeb/image-20230809140520042.png

分層解耦

assets/JavaWeb/image-20240307154743209.png

檔案上傳

接受檔案類: MultipartFile

常用方法
String getOriginalFilename(); //取得原始檔案名稱
void transferTo(File dest); //將接收的檔案轉存到磁碟檔案中
long getSize(); //取得檔案的大小,單位:位元組
byte[] getBytes(); //取得檔案內容的位元組陣列
InputStream getInputStream(); //取得接收到的檔案內容的輸入串流

assets/JavaWeb/image-20230828223003194.png

檔案儲存

  • springboot 檔案預設上傳大小為 1M
#設定單個檔案最大上傳大小
spring.servlet.multipart.max-file-size=10MB
#設定單個請求最大上傳大小(一次請求可以上傳多個檔案)
spring.servlet.multipart.max-request-size=100MB
本機儲存方式

    @PostMapping("/upload")
    Result upload(String username , Integer age , MultipartFile image) throws IOException {
        log.info("文件上传: {}, {}, {}", username, age, image);
        String originalFilename = image.getOriginalFilename();
        int index = originalFilename.lastIndexOf(".");//取得最後一個.的位置
        String extname = originalFilename.substring(index);//取得檔案副檔名
        String newFileName = UUID.randomUUID().toString()+extname;
        log.info("新的文件名: {}", newFileName);
        image.transferTo(new File("D:\\Users\\XCJY\\Desktop\\"+newFileName));
        return Result.success();
    }

阿里雲 OSS

依賴 (可選)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
  • 阿里雲
aliyun.oss.endpoint = https://oss-cn-beijing.aliyuncs.com  
aliyun.oss.bucketName = web-tlias1122233

assets/JavaWeb/image-20230829104400590.png

web-tlias1122233
AccessKey ID:LTAI5t5YvwjEkM88f6m8ExCS
AccessKey Secret:wO895xzzup9fBtouTWFZzs7z2Memym
Ssky:  
  alioss:  
    access-key-id: LTAI5t5YvwjEkM88f6m8ExCS  
    access-key-secret: wO895xzzup9fBtouTWFZzs7z2Memym  
    bucket-name: web-tlias1122233  
    endpoint: https://oss-cn-beijing.aliyuncs.com 


或設定環境變數
OSS_ACCESS_KEY_ID=LTAI5t5YvwjEkM88f6m8ExCS
OSS_ACCESS_KEY_SECRET=wO895xzzup9fBtouTWFZzs7z2Memym
OSS_STS_ROLE_ARN=acs:ram::1547130400260530:role/xxx666

如何使用 OSS Java SDK 完成常見操作_物件儲存 OSS-阿里雲說明中心 (aliyun.com)

阿里雲儲存方式
public class UploadController {  
    @Autowired  
    AliOSSUtils aliOSSUtils;  
    
    @PostMapping("/upload")  
    Result upload(MultipartFile image) throws IOException {  
        log.info("文件上传, 文件名: {}", image.getOriginalFilename());  
        String url = aliOSSUtils.upload(image);  
        log.info("文件上传完成,文件访问的url", url);  
        return Result.success(url);  
    }  
}

/**
 * 阿里雲 OSS 工具類
 */
@Component
public class AliOSSUtils {
    @Autowired
    private AliOSSProperties aliOSSProperties;

    EnvironmentVariableCredentialsProvider credentialsProvider;

    {
        try {
            credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        } catch (ClientException e) {
            e.printStackTrace();
        }
    }

    /**
     * 實現上傳圖片到OSS
     */
    public String upload(MultipartFile file) throws IOException {
        String endpoint = aliOSSProperties.getEndpoint();
        String bucketName = aliOSSProperties.getBucketName();

        // 取得上傳檔案的輸入串流
        InputStream inputStream = file.getInputStream();

        // 避免檔案覆蓋
        String originalFilename = file.getOriginalFilename();
        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

        //上傳檔案到 OSS
        //OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
        ossClient.putObject(bucketName, fileName, inputStream);

        //檔案存取路徑
        String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
        // 關閉ossClient
        ossClient.shutdown();
        return url;// 把上傳到oss的路徑回傳
    }

}

登入驗證技術

由於 http 和 https 都是無狀態的,每次存取都是獨立的,造成在伺服器端無法僅透過 http 協定來區分兩次請求是否來自同一個瀏覽器、同一個工作階段,此時就需要工作階段追蹤技術

工作階段技術

資料共享: 非常重要,例如登入後,其他頁面依然取得登入資訊; 驗證碼: 取得自己填寫的驗證碼和上一次請求當中所產生的驗證碼的值 (在一次工作階段當中的兩次請求共享資料)

但 http 支援 cookie 的攜帶,可以進行傳送資料 登入一次後就建立了一個工作階段,在一次工作階段中可以發送多個請求

  • Cookie 工作階段追蹤技術,儲存在用戶端,用戶端儲存了整個 cookie 資料,容易被偽造
  • Session 工作階段追蹤技術,儲存在伺服器端,用戶端的 cookie 只儲存 key 值,去伺服器端查找cookie資料
  • 權杖技術工作階段追蹤技術: 用戶端請求不再使用 cookie, 而是使用 token, 儲存在用戶端,伺服器端只負責驗證

assets/JavaWeb/image-20230902145151850.png

JWT 權杖

就是為了保證傳輸的內容是可靠的,沒有被第三方竄改的,可以使用這個沒有被竄改的 token 登入,否則即使資訊洩露,也會拒絕登入

注意 : JWT 權杖本身並不加密,只是透過簽章來確保其完整性, 避免偽造,第三部分就是為了避免偽造, 伺服器端拿到 JWT 權杖後根據第一部分的演算法配合自己金鑰來得出數位簽章,如果和傳遞過來的不一樣就是偽造

第三部分採用第一部分的如 HMAC-SHA256 演算法, 傳入自己的金鑰, 如 itheima,將第二部分的資料進行簽章計算, 產生數位簽章,而駭客不知道金鑰,就無法製造出合法的數位簽章,傳出去的數位簽章也只有自己能進行驗證,解決了叢集認證問題,使用相同金鑰就能解開

用於在通訊雙方以 json 資料格式安全地傳輸資訊,由於數位簽章的存在,這些資訊是可靠的

  • 加密 (Encryption): 核心目標是保密性。將原始資料(稱為明文)轉換為不可讀的格式(稱為密文),只有擁有金鑰的人才能將密文還原回明文。

  • 簽章 (Signature): 核心目標是完整性和身分驗證。它不隱藏資訊,而是透過產生一個附加到資料的數位簽章來證明資料的來源和是否被竄改。JWT 並不加密第二部分的資訊

jwt 本身無狀態,後端無需記錄資訊,只需要驗證第三部分,簽章部分可靠就相當於資訊可靠

  1. 第一部分: Header (標頭): 記錄平台類型 (如:JWT),簽章演算法 (任意 HS 256),如 {"alg": "HS 256","type": "JWT"}
  2. 第二部分: payload (有效載荷): 攜帶一些自訂資訊、預設資訊等。例如:{"id": "1","username": "Tom"}
  3. 第三部分:數位簽章, Signature (簽章),防止 Token 被竄改、確保安全性。將 header、payload,並加入指定密鑰,透過第一部分的簽章演算法計算而來

第三部分:.signWith(signatureAlgorithm,secretKey.getBytes(StandardCharsets.UTF_8))

assets/JavaWeb/image-20230902233328545.png 原始資料是 json 格式的資料,透過 base 64 編碼 (非加密)轉換成下面的格式 Base 64:是一種基於 64 個可列印字元(A-Z a-z 0-9 + /)來表示二進位資料的編碼方式。 eyJhbGciOiJIUzI1NiJ9 -> base 64 解碼 -> { "alg": "HS 256"}

eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY5MzY3NDQyN30 -> base64解碼 -> { "name": "tom".....}

使用 jwts

加密解密都是為了確保 claims 內容沒有被竄改

加密採用 builder

  1. 內容
  2. 過期時間
  3. 加密演算法+金鑰
  4. builder.compact();

解密 從 http 取得 token

  1. 加密演算法+金鑰
  2. .parseClaimsJws(token).getBody(); 取得其中的claims
JWt

public class JwtUtil {  
    /**  
     * 產生jwt  
     * 使用Hs256演算法, 私鑰使用固定密鑰  
     *  
     * @param secretKey jwt密鑰  
     * @param ttlMillis jwt過期時間(毫秒)  
     * @param claims    設定的資訊  
     * @return  
     */  
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {  
        // 指定簽章時所使用的簽章演算法,也就是header那個部分  
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;  
  
        // 產生JWT的時間
        long expMillis = System.currentTimeMillis() + ttlMillis;  
        Date exp = new Date(expMillis);  
  
        // 設定jwt的body  
        JwtBuilder builder = Jwts.builder()  
                // 如果有私有宣告,一定要先設定這個自己建立的私有宣告,這個是給builder的claim賦值,一旦寫在標準的宣告賦值之後,就會覆蓋那些標準的宣告
                .setClaims(claims)
                // 設定簽章使用的簽章演算法和簽章使用的密鑰  
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))  
                // 設定過期時間  
                .setExpiration(exp);  
  
        return builder.compact();  
    }  
  
    /**  
     * Token解密  
     *  
     * @param secretKey jwt密鑰 此密鑰一定要保留好在伺服器端, 不能暴露出去, 否則sign就可以被偽造, 如果對接多個用戶端建議改造成多個  
     * @param token     加密後的token  
     * @return  
     */  
    public static Claims parseJWT(String secretKey, String token) {  
        // 得到DefaultJwtParser  
        Claims claims = Jwts.parser()  
                // 設定簽章的密鑰  
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))  
                // 設定需要解析的jwt  
                .parseClaimsJws(token).getBody();  
        return claims;  
    }  
  
}

setSigningKey 為了驗證 JWT 的完整性和真實性,雖然 claims 能直接取得,但不知真偽

eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY5MzY3NDQyN30.Z0Eac2WZQzHGkBIHDIZF8hjNDlvMOg9ijdSvlAj_u9g

一個小時後的權杖->第一部分和第二部分是相同的,但是第三部分不同,報錯已過期 透過第三部分來驗證使用者和是否有效

eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY5MzcxMjQ4N30.h3f-C46NdJMnlwnzv0uUk9zqW-K-0z7JsGGmv56BNT8

知道金鑰的情況下,並不能用金鑰來直接解密JWT的簽章部分。相反,可以使用金鑰驗證JWT的簽章,以確定JWT是否受信任和完整。

最後一個數位簽章加密解不了

assets/JavaWeb/image-20230903000933091.png

統一攔截

過濾器 Filter

概念:Filter 過濾器,是 JavaWeb 三大元件 (Servlet、Filter、Listener)之一

過濾器可以把對資源的請求攔截下來,從而實現一些特殊的功能比如:登入校驗、統一編碼處理、敏感字元處理等

filter 先執行放行前邏輯,放行後 filterChain.doFilter(servletRequest,servletResponse); 可以存取對應資源,之後回到 filter 中,執行放行後的邏輯操作 assets/JavaWeb/image-20230903194706743.png

當再次存取相同或不同資源,依舊執行相同步驟

  • 過濾器鏈(使用多個過濾器)

優先順序指定有兩種方式,當不指定優先順序時,根據名字來

  1. 如@Order (2)預設為字母順序
  2. 使用 FilterRegistrationBean 來設定過濾器的優先順序
Define a Spring of Bean FilterRegistrationBean to wrap our custom Filter,so that Spring the container can manage our filter

Likewise in Servlet there is also a similar wrapper bean:ServletRegis registrationBean

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<FirstFilter> firstFilterRegistration() {
        FilterRegistrationBean<FirstFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new FirstFilter());
        registrationBean.setOrder(1); // set the priority to 1
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<SecondFilter> secondFilterRegistration() {
        FilterRegistrationBean<SecondFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new SecondFilter());
        registrationBean.setOrder(2); // set the priority to 2
        return registrationBean;
    }
}

assets/JavaWeb/image-20230903204940865.png

屬性名 型別 描述
filterName String 指定過濾器的 name 屬性,等價於
value String[] 該屬性等價於 urlPatterns 屬性。但是兩者不應該同時使用。
urlPatterns String[] 指定一組過濾器的 URL 匹配模式。等價於標籤。
servletNames String[] 指定過濾器將應用於哪些 Servlet。取值是 @WebServlet 中的 name 屬性的取值,或者是 web.xml 中的取值。
dispatcherTypes DispatcherType 指定過濾器的轉發模式。具體取值包括:ASYNC、ERROR、FORWARD、INCLUDE、REQUEST。
initParams WebInitParam[] 指定一組過濾器初始化參數,等價於標籤。
asyncSupported boolean 宣告過濾器是否支援非同步操作模式,等價於標籤。
description String 該過濾器的描述資訊,等價於標籤。
displayName String 該過濾器的顯示名,通常配合工具使用,等價於標籤。
  • 使用Filter 過濾器新增註解 @WebFilter(urlPatterns = { "/AddServlet","/qiantai/order.jsp","/RemoveServlet", "/UserOrderingServlet" })

  • 使用步驟

  1. springboot 啟動類新增註解 @ServletComponentScan (因為 WebListener/WebFilte r 註解不是 Spring 的規範,所以為了識別它,需要在啟動類上加入註解 @ servletcomponentscan Spring Boot 會自動掃描指定套件中的 Servlet、Filter 和 Listener 的實作類別,並將它們註冊到 Spring 的上下文中。這表示你不需要在設定檔中手動註冊這些元件。也可以不加,手動將相關的設定類別使用 @component 作為普通的 bean 回傳
  2. 建立實作 Filter 介面,加入 @WebFilter(urlPatterns = "/*") 註解
  3. 重寫方法 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
  4. 從 servletRequest 中取得資料,進行操作或判斷, filterChain.doFilter(servletRequest, servletResponse) 進行放行
FilterDemo


@WebFilter(urlPatterns = "/*")

public class DemoFileter implements Filter {

    @Override//初始化方法,只呼叫一次
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("init 初始化方法执行了");
    }

    //另外兩個方法不常用
    @Override//攔截後呼叫,呼叫多次
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Demo 拦截到了请求...放行前逻辑");
        //放行
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("Demo 拦截到了请求...放行后逻辑");
    }

    @Override
    public void destroy() {
        System.out.println("destroy 销毁方法执行了");
    }
}

標題

@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {


    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;//請求標頭資料
        HttpServletResponse resp = (HttpServletResponse) servletResponse;

        //1. 取得請求url
        String url = req.getRequestURI().toString();

        log.info("请求的url:{}", url);

        if(url.contains("login")){
            log.info("登陆放行");
            filterChain.doFilter(servletRequest, servletResponse);//login直接放行
            return;
        }

        String jwt = req.getHeader("token");
        if (!StringUtils.hasLength(jwt)){
            log.info("未登陆");
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return;
        }

        try {//如果出現異常表示驗證沒有通過,
            JwtUtils.parseJWT(jwt);
        } catch (Exception e) {
            e.printStackTrace();
            log.info("解析令牌失败, 返回未登录错误信息");
            Result error = Result.error("解析令牌失败");
            //Manual conversion object--json --------> Alibaba fastJSON
            String notLogin = JSONObject.toJSONString(error);
            resp.getWriter().write(notLogin);
            return;
        }

        //6.Release.
        log.info("令牌合法, 放行");
        filterChain.doFilter(servletRequest, servletResponse);//Release

    }
}

攔截器 Interceptor

assets/JavaWeb/image-20240122155336203.png DispacherServlet -> HandlerMapping -> Interceptor -> HandlerAadapter ->呼叫前準備 handleMethodArgumentResolver(參數解析器) 和 handleMethodReturnValueHandler(傳回值處理器)

是一種動態攔截方法呼叫的機制,類似於過濾器。Spring 框架中提供的,用來動態攔截控制器方法的執行

作用:攔截 spring 環境中請求,在指定的方法呼叫前後,根據業務需要執行預先設定的程式碼

  1. 定義攔截器:實作 HandlerInterceptor 介面,並交給 IOC 容器管理(加上 @Component),方法有 pre/post/afterHandle 方法,兩者都回傳 true 即放行
  2. 設定攔截器:建立實作 WebMvcConfigurer 的設定類別,注入剛才定義的攔截器使其生效,ctrl+o 覆寫 addInterceptors 方法,加入 registry.addInterceptor(loginCheckInterceptor).addPathPatterns("").excludePathPatterns();//設定攔截資源
  • 攔截路徑 assets/JavaWeb/image-20230904091533033.png assets/JavaWeb/image-20230904091619895.png 定義攔截器優先於設定器?定義如果放行了 login,即使設定攔截全部也會放行 ^trzlo0
簡介

攔截器-執行流程與 Filter 的差別 assets/JavaWeb/image-20230904093429385.png 過濾器實作 Filter 介面 攔截器實作 HandlerInterceptor 介面

攔截範圍不同:過濾器 Filter 會攔截所有的資源,而 Interceptor 只會攔截 Spring 環境中的資源。 tomcat 不識別編寫的 Controller 程式,但識別 servelet 程式,需要用到 DiapatcherServlet,會轉給 Controller

assets/JavaWeb/image-20240826125221152.png assets/JavaWeb/image-20240826125238821.png

WebConfig
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
		            registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns();//設定攔截資源
    }
}
  • 多個攔截器的執行順序
  1. 若每個攔截器的 preHandle () 都返回 true

此時多個攔截器的執行順序和攔截器在SpringMVC的設定檔的配置順序有關:

preHandle()會按照配置的順序執行,而postHandle()和afterComplation()會按照配置的反序執行

  1. 若某個攔截器的 preHandle ()返回了 false

preHandle() 返回 false 和它之前的攔截器的 preHandle() 都會執行,postHandle() 都不執行,返回 false 的攔截器之前的那一個攔截器的 afterComplation() 會執行 (索引)

全域異常處理器

@RestControllerAdvice 是 @ControllerAdvice 和 @ResponseBody 的組合註解

異常處理

assets/JavaWeb/image-20230905103345610.png 圖中的由於異常沒有處理,最終拋給框架,返回的 json 資料 assets/JavaWeb/image-20230905104525537.png assets/JavaWeb/image-20230905104540186.png

  • 使用方法 ^x4m5fo
  1. 建立一個類, 加入註解 @RestControllerAdvice
  2. 撰寫方法,方法註解 @ExceptionHandler(異常類型)
全域異常處理器
@RestControllerAdvice//代表定義了全域異常處理器
public class GlobeExceptionHandler {
	@ExceptionHandler(Exception.class)//捕獲異常類型:Exception.class所有異常
    public Result ex(Exception ex){
        ex.printStackTrace();
        return Result.error("对不起,操作失败,请联系管理员");
    }
}
全域異常處理器攔截自訂異常的拋出

BaseException為自訂異常類的父類,自訂異常類繼承BaseException即可被攔截


@ExceptionHandler
public Result exceptionHandler(BaseException ex){  
    return Result.error(ex.getMessage());  
}

事物管理

Spring 事務管理器

為什麼不用 AOP 實現事物,因為開發資料庫使用的工具是多種多樣的 (jdbc, mysql, hibernate),每種事物程式碼不同

  1. Spring 宣告式事務對應依賴 spring-tx: 包含宣告式事務實現的基本規範(事務管理器規範介面和事務增強等等) spring-jdbc: 包含 DataSource 方式事務管理器實現類 DataSourceTransactionManager spring-orm: 包含其他持久層框架的事務管理器實現類例如:Hibernate/Jpa 等

assets/JavaWeb/image-20240121211243948.png

我們現在要使用的事務管理器是 org.springframework.jdbc.datasource.DataSourceTransactionManager,將來整合 JDBC 方式、JdbcTemplate 方式、Mybatis 方式的事務實現!

DataSourceTransactionManager 類中的主要方法: doBegin():開啟事務 doSuspend():掛起事務 doResume():恢復掛起的事務 doCommit():提交事務 doRollback():回滾事務

概述

事物管理底層也是 AOP

springboot 啟動檔案上開啟事物管理,加入註解 @EnableTransactionManagement

  • 使用註解 @Transactional
    • 位置: service 層的 方法,類,介面 上
      • @Transactional 標識在方法上,則只會影響該方法
      • @Transactional 標識的類上,則會影響類中所有的方法
    • 作用: 將當前方法交給 spring 進行事務管理,方法執行前,開啟事務;成功執行完畢,提交事務;出現異常,回滾事務 rollback ;(預設執行時異常才會回滾,透過 @Transactional(rollbackFor = Exception.class) 可以指定什麼異常進行回滾, Exception.class 所有異常都會回滾)

對資料進行 修改 且 執行多次的程式碼 的需要進行事務管理,如果只執行一步,或者不需要進行修改,也不需要進行事務管理

宣告式: 基於 AOP 方式實現,程式碼更簡潔、使用更方便,理解更容易 程式設計式: 非常靈活,完全由使用者自己控制,可以最小粒度的控制事務範圍, 和鎖類似,按照最小範圍使用原則, 尤其程式設計式事務

@Transactional  
@Override  
public void delete(int id) {  
    deptMapper.delete(id);  
    empMapper.deleteByDeptId(id);  
}
  • 開啟事務管理日誌
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug

屬性

@Transactional屬性名 說明
name 當在設定檔中有多個TransactionManager,可以用該屬性指定選擇哪個事務管理器。
propagation 事務的傳播行為,預設值為REQUIRED。
isolation 事務的隔離度,預設值採用DEFAULT。
timeout= 事務的超時時間,預設值為-1。如果超過該時間限制但事務還沒有完成,則自動回滾事務。
超時異常org.springframework.transaction.**TransactionTimedOutException**: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022
readonly=false預設 對一個查詢操作來說,如果我們把它設定成唯讀,就能夠明確告訴資料庫,這個操作不涉及寫操作。這樣資料庫就能夠針對查詢操作來進行最佳化,對增刪改操作設定唯讀會拋出下面異常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
rollbackfor 用於指定能夠觸發事務回滾的異常類型,如果有多個異常類型需要指定,各類型之
間可以透過逗號分隔。
no-rollback-for 拋出no-rollback-for指定的異常類型,不回滾事務。
  • 回滾策略 rollbackfor

宣告式事務預設只針對執行時異常 RuntimeException及其子類進行回滾,編譯時異常或Exception不回滾。 ../../Java SE/assets/Java面试/image-20240507230430989.png

  • rollbackFor 屬性:需要設定一個 Class 類型的物件, 預設是 RuntimeException and Error 異常方可回滾
  • rollbackForClassName屬性:需要設定一個字串類型的全類名
  • noRollbackFor 屬性:需要設定一個 Class 類型的物件, 指定哪些異常不會回滾, 預設沒有指定, 如果指定, 應該在 rollbackFor 的範圍內
  • rollbackFor 屬性:需要設定一個字串類型的全類名

assets/JavaWeb/image-20230926104241294.png

  • 隔離級別

Mysql/Mysql進階 > SQL 中的四種隔離級別

assets/JavaWeb/image-20230926105657853.png assets/JavaWeb/image-20230926105707646.png

@Transactional(isolation = Isolation.DEFAULT)//使用資料庫預設的隔離級別
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//讀未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//Read Committed
@Transactional(isolation = Isolation.REPEATABLE_READ)//Repeatable Read
@Transactional(isolation = Isolation.SERIALIZABLE)//Serializable
  • 傳播行為 propagation

事務 a 方法,事務 b 方法,此時 b 在執行時究竟時加入 a 方法的事務還是不加入新建一個事務?涉及到事務的傳播行為

假設有方法 A 和方法 B, 以下是方法 B 加入 propagation 後的效果

A 呼叫 B 方法時

  • required(默认): B 需要事務 A 有事務,則 B 加入 A, A 沒有事務 , B 則自己建立一個事務
  • required_new B 需要新事務 A 有事務則掛起,B 新事務會優先保證自己執行
  • supports B 有無事務無所謂 A 有事務則加入,無事務就無所謂
  • not_supports A 呼叫無事務B, A 有事務 B 則掛起
  • never A 呼叫 B, A 也必須無事務,否則異常
  • mandatory A 呼叫 B, A 必須有事務,否則異常
  • nested A 呼叫新事務 B, A 有事務作為嵌套事務存在,A 沒事務以新的事務執行

assets/JavaWeb/image-20231130191610275.png assets/JavaWeb/image-20230926164605978.png

傳播行為

assets/JavaWeb/image-20230905144314809.png

assets/JavaWeb/image-20230905151145008.png

insert 應該設定為 @Transactional(propagation = propagation.requires_new),這樣 insert 在一個新的事務中, 即使 delete 回滾,insert 也不會回滾,當執行 insert 方法時,會先將當前環境中的事務掛起,然後再執行 insert 這個事務,即使刪除失敗,日誌也已經記錄

  1. delete 方法事務回滾後, 才會繼續執行 insert 方法中的邏輯。
  2. 也就是說,事務回滾是在異常發生時立即執行,而不會等到整個方法執行完後再回滾。
  3. insert 方法在 delete 方法回滾之後才會執行, 所以不受影響。

REQUIRED :大部分情況下都是用該傳播行為即可。 REQUIRES_NEW :當我們不希望事務之間相互影響時,可以使用該傳播行為。比如:下訂單前需要記錄日誌,不論訂單儲存成功與否,都需要保證日誌記錄能夠記錄成功。

什麼時候使用?

  • 當一個基礎的業務單元 (如對外提供的http介面,service方法),存在>=2的資料變更時,就需要考慮,部分更行成功,部分更新失敗是否會出現髒資料,是否會有影響

若是:則可以考慮引入事務 若否:不建議加入事務

  • 只有一個資料變更,但是後續有強依賴這個資料的邏輯,也需要加入事務

比如接到支付成功訊息,我要做一個校對的後台任務 我的業務邏輯是:先呼叫第三方校驗是否屬實,然後更新本地狀態為成功,最後MQ方式通知訂單狀態變更 此時我們需要將第二步資料變更與第三步訊息通知放在事務中,只要通知失敗,我們還是不更新本地狀態,避免出現我們校驗成功了,但是訂單還是未支付

  • 使用建議避免大事務 只在需要的地方加事務 注意分散式事務場景 所有的操作,走索引,避免出現鎖表 減少範圍、大批量的資料處理場景 如果業務支撐,可以降低隔離級別,一是減少隔離級別帶來的附加成本,二則是可以避免某些場景下的死鎖

失效情況

../SpringBoot/Spring总结#代理失效情况

  • 多執行緒時可能失效 assets/JavaWeb/file-20241020013148769.png

上面這種場景不生效很好理解,子執行緒的異常不會被外部的執行緒捕獲, testMultThread 這個方法的呼叫不拋異常,因此不會觸發事務回滾

AOP:Aspect Oriented Programming

AOP 本質上就是對代理模式的簡化

代理模式

代理模式

assets/Spring/image-20230925222438847.png assets/JavaWeb/image-20240120222341002.png

目標 : 放核心程式碼,減少了目標的呼叫和打擾 代理 : 放目標方法中的非核心功能

代理有兩種: 靜態代理 : 為每個類建立一個自己的代理 (太麻煩) 動態代理 : 不單獨為類建立單獨的代理,而是自動提供一個代理物件

代理解決附加程式碼干擾核心程式碼和不方便統一維護的問題

靜態代理

Calculator 介面 CalculatorPureImpl 目標類/房東 CalculatorStaticProxy 代理類/仲介

assets/JavaWeb/image-20240120233144131.png



public class CalculatorStaticProxy implements Calculator {
    
    // 將被代理的目標物件宣告為成員變數
    private Calculator target;
    // 呼叫目標方法
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    
    @Override
    public int add(int i, int j) {
        // 附加功能由代理類中的代理方法來實現
        System.out.println("参数是:" + i + "," + j);
        // 透過目標物件來實現核心業務邏輯
        int addResult = target.add(i, j);
        System.out.println("方法内部 result = " + result);
        return addResult;
    }

動態代理

分類 : JDK 動態代理 (介面) 和 cglib 動態代理 (繼承)

JDK原生的實現方式 :有介面時 , 目標類實現了介面 , JDK 建立的 代理類 也會自動實現介面,且代理類和目標類之間沒有繼承關係,無法代理沒有實現介面的類 cglib(spring中):沒有介面時, 目標類不需要實現 介面,cglib 建立的 代理類 會繼承於目標類來實現代理

JDK 動態代理相對於 CGLIB 動態代理來說,生成代理物件的效能更高,而 CGLIB 動態代理需要透過繼承和重寫目標類的方式進行代理, 在生成代理物件時需要生成位元碼和類別載入, 相對較慢

這是因為JDK動態代理使用Java標準函式庫提供的反射機制,而CGLIB動態代理需要透過繼承和重寫目標類的方式進行代理,在生成代理物件時需要生成位元碼和類別載入,相對較慢。

簡介

總結 : 介面實現目標類透過介面型態接受代理元件 , 非介面類可以直接使用類別接受代理元件

JDK原生的實現方式 可以根據介面名稱取得bean,不能根據類別名稱 assets/JavaWeb/image-20240121194639659.png

cglib可以根據類別名稱取得

assets/JavaWeb/image-20240121194747469.png

JDK動態代理工廠

public class JdkProxy {  
  
        private Object target;  
        public JdkProxy(Object target) {  
            this.target = target;  
        }  
        public Object getProxy(){  
  
            /**  
             * newProxyInstance():建立一個代理實例  
             * 其中有三個參數:  
             * 1、classLoader:載入動態生成的代理類的類別載入器  
             * 2、interfaces:目標物件實現的所有介面的class物件所組成的陣列  
             * 3、invocationHandler:設定代理物件實現目標物件方法的過程,即代理類中如何重  
             寫介面中的抽象方法             */  
  
            ClassLoader classLoader = target.getClass().getClassLoader();  
            Class<?>[] interfaces = target.getClass().getInterfaces();  
            InvocationHandler invocationHandler = new InvocationHandler() {  
  
                @Override  
                public Object invoke(Object proxy, Method method, Object[] args)  
                        throws Throwable {  
                    /**  
                     * proxy:代理物件  
                     * method:代理物件需要實現的方法,即其中需要重寫的方法  
                     * args:method所對應方法的參數  
                     */  
                    Object result = null;  
                    try {  
                        result = method.invoke(target, args);  
                        System.out.println(method.getName()+",结果:"+ result);  
                    } catch (Exception e) {  
                        e.printStackTrace();  
                        System.out.println(method.getName()+",异常:"+e.getMessage());  
                    } finally {  
                        System.out.println(method.getName()+",方法执行完毕");  
                    }  
                    return result;  
                }  
            };  
            /**  
             * jdk生成代理物件  
             *  參數一:類別載入器  
             *  參數二:目標類的介面  
             *  參數上:具體要進行的代理動作[非核心動作,呼叫目標方法]  
             */            return Proxy.newProxyInstance(classLoader, interfaces,  
                    invocationHandler);  
        }  
}

JdkProxy factory=new JdkProxy(目標類);

目標類的介面 proxy1= (目標類的介面)factory.getProxy(); //產生代理類
proxy1.add(1,1);//JdkProxy會進行攔截,先執行invoke方法,然後執行add方法

proxy1就是代理類,可以正常呼叫方法

AOP 的動態代理選擇

使用 AOP 的目標類有介面時,會自動選擇 JDK原生的實作方式 實作代理,沒有介面會自動選擇 cglib 來實作代理

使用

使用方法

定義註解類(@Component + @Aspect),並在方法上 (形參 ProceedingJoinPoint JoinPoint) 註解 @Around("execution( )")


//這是一個切面類

@Slf4j
@Component
@Aspect
public class TimeAspect {

	//切入點表達式 : 控制切入點
	
    @Around("execution(* com.ithiema.service.*.*(..))")//:指定公用邏輯作用在哪些方法上:service套件下所有方法都會執行封裝的方法
    //通知
    
    public Object recordTime(ProceedingJoinPoint JoinPoint) throws Throwable {//aspect提供共用的ProceedingJoinPoint類api
        long begin = System.currentTimeMillis();

        Object result = JoinPoint.proceed();

        long end = System.currentTimeMillis();

        log.info(JoinPoint.getSignature()+"方法执行耗时{}ms",end-begin);//JoinPoint.getSignature()取得方法簽章

        return result;
    }
}

///整个被称为切面

//生效的地方被稱為切入點
  • 核心概念 assets/JavaWeb/image-20230907110339042.png

Spring 中 aop 底層基於動態代理 (CGLIB 代理技術) 來實作的,程式執行時基於程式動態代理技術,為目標物件產生一個代理物件,對原始方法進行功能的增強,注入時就不再注入目標物件,而是注入代理物件,呼叫的是代理物件中的方法

通知類型

內部可以填 表達式 或者 註解,填什麼,什麼所在的地方成為 切入點

JAVA 微服務核心/spring/Spring > 4 2 1 相關術語

  1. @Around: 環繞通知,此註解標註的通知方法在目標方法前、後根據設定位置都可以被執行,異常程式碼後的程式碼不會執行
    1. 需要手動呼叫 proceedingJoinPoint.proceed(); 來執行原方法,且傳回值必須指定為 Object,來接受方法傳回值並傳回
  2. @Before: 前置通知,此註解標註的通知方法在目標方法前被執行
  3. @After: 後置通知,此註解標註的通知方法在目標方法後被執行,無論是否有異常都會執行
  4. @AfterReturning:返回後通知,此註解標註的通知方法在目標方法後被執行,有異常不會執行 returning="result"
  5. @AfterThrowing:異常後通知,此註解標註的通知方法在異常後執行 throwing = " "

assets/JavaWeb/image-20230926164032570.png

對於 @Around 通知,取得連接點資訊只能使用  ProceedingJoinPoint

對於,取得連接點資訊只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父類別

通知順序

assets/JavaWeb/image-20230908192924801.png 越先執行,在原始方法執行後的,越靠前越後執行

  1. 不同切面類別中,預設按照切面類別的類別名稱字母排序: 目標方法前的通知方法:字母排名靠前的先執行 目標方法後的通知方法:字母排名靠前的後執行
  2. 用@Order(數字)加在切面類別上來控制順序
    目標方法前的通知方法:數字小的先執行
    目標方法後的通知方法:數字小的後執行 assets/JavaWeb/image-20230919184230248.png

切入點表達式

@execution

execution([访问修饰符] 返回值 [包名.类名].方法名(方法形参) [throws 异常] ) [] 中的可省略

省略套件名稱. 類別名稱後,如果有同名方法,都會執行,所以不建議省略 assets/JavaWeb/image-20230908195744480.png assets/JavaWeb/image-20230908195731342.png

  • 統配符號 * 單個獨立的任意符號,可以通配任意返回值、包名、类名、方法名、任意類型的一個参数,也可以通配包、类、方法名的一部分 .. u 多個連續的任意符號,可以通配任意層級的套件,或 任意类型、任意个数的参数,不能用在套件開頭

assets/JavaWeb/image-20240121192501336.png assets/JavaWeb/image-20230908201025712.png

匹配 DeptService 下所有方法形參有 Integer 類型,返回值為 void 類型的方法

  • 可以使用且(&&)、或(||)、非(!) 來組合比較複雜的切入點表達式。

所有業務方法名稱在命名時盡量規範,方便切入點表達式快速匹配。例如:查詢類方法都是 find 開頭,更新類方法都是 update 開頭。

描述切入點方法通常基於介面描述,而不是直接描述實作類別,增強拓展性。

在滿足業務需要的前提下,盡量縮小切入點的匹配範圍。例如:套件名稱匹配盡量不使用 ..,使用 * 匹配單個套件

重用(提取)切點表達式 @Pointcut

某個方法添加切入點表達式的註解 @Pointcut,然後其他通知類型中引用該方法 assets/JavaWeb/image-20230908183332860.png

自訂註解類別

@annotation(註解類):匹配方法上有註解的方法 @within(註解類):匹配類別上有註解的所有方法

自訂註解,在需要用的方法上添加 簡化 execution 中複雜的內容 同樣建立一個 Aspect 類只是切入點表達式不需要設定範圍,只需要設定為註解類位置

@Retention (RetentionPolicy. RUNTIME): 表示這個註解的保留策略是RUNTIME,即執行時保留,可以透過反射讀取註解資訊 @Target(ElementType.TYPE): 表示這個註解可以應用在類、介面、列舉上 (TYPE 表示類類型) ^f3q55w

使用方法

  1. 建立一個名為 xxx 的 annotation 註解類,新增註解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) assets/JavaWeb/image-20231031132350583.png
  2. 依舊定義 Aspect 類,類中引用 @Around("@annotation(注解类位置)") @Around("@annotation(com.ithiema.anno.Log)")
  3. 在需要用旳方法上新增 @xxx 即可使用

而 execution 方法直接是 @Around("execution(位置)

宣告一個類,新增兩個註解@Detention@Target

案例

assets/JavaWeb/image-20230908202953092.png 在 Aspect 中設定@Around("@annotation(com.ithiema.Log)")

最後在需要的方法或類上新增註解即可,所在類稱成為切入點方法 assets/JavaWeb/image-20230908203214801.png

@Retention (RetentionPolicy. RUNTIME)  
@Target (ElementType. METHOD)  
public @interface Log {  
	String value () default "default value"; // 定義一個元素,帶有預設值
	OperationType value ();
}

public enum OperationType {  
  UPDATE, INSERT  
}

使用@AutoFill(OperationType.INSERT)

註解屬性設定為列舉,方便AOP

攔截
MethodSignature signature = (MethodSignature) joinPoint.getSignature ();//方法簽章物件  
AutoFill autoFill = signature.getMethod (). getAnnotation (AutoFill. class);//取得方法上的註解物件  
OperationType operationType = autoFill.value ();//取得資料庫操作類型

連接點或取得通知相關資訊

對於 @Around 通知,取得連接點資訊只能使用  ProceedingJoinPoint

對於其他四種通知,取得連接點資訊只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父類

assets/JavaWeb/image-20230908203953107.png

`joinPoint.getSignature().getDeclaringType().getAnnotation(MdcDot.class);`

注意: joinPoint 是 aspectj 中的,不是 aopalliance 中的

  • 取得目標方法的返回值
@AfterReturning(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}
  • 取得目標方法的異常 (出現異常時)
@AfterThrowing(value = "execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}

操作日誌

LogAspect

@Slf4j
@Component
@Aspect

public class LogAspect {
    @Autowired
    private OperateLogMapper operateLogMapper;
    @Autowired
    private HttpServletRequest request;
    @Around("@annotation(com.ithiema.anno.Log)")//指定annotation位置
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {

        //由於不能隨便定義變數
        //取得操作人
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUser = (Integer) claims.get("id");
        //操作時間
        LocalDateTime operateTime = LocalDateTime.now();
        //操作類名
        String className = joinPoint.getTarget().getClass().getName();
        //操作方法名
        String methodName = joinPoint.getSignature().getName();
        //操作方法參數
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);
        long begin = System.currentTimeMillis();
        //呼叫原始方法
        Object result = joinPoint.proceed();
        //上面原始目標方法返回值不是json
        String returnValue = JSONObject.toJSONString(result);
        //記錄執行時間
        long end = System.currentTimeMillis();
        long costTime= end - begin;
        //記錄操作日誌
        OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
        operateLogMapper.insert(operateLog);
        log.info("AOP记录操作日志: {}" , operateLog);
        return result;
    }
}

日誌資訊包含:操作人、操作時間、執行方法的全類名、執行方法名、方法執行時參數、返回值、方法執行時長

註解類

@Retention (RetentionPolicy. RUNTIME): 表示這個註解的保留策略是 RUNTIME, 即執行時保留, 可以透過反射讀取註解資訊 @Target(ElementType.TYPE): 表示這個註解可以應用在類、介面、列舉上 (TYPE 表示類類型)

  • 內部屬性格式 類型 名稱() default 值;

別的類 可以透過反射來取得其他類別上註解的值

java

您提到的 @AliasFor 注解是 Spring Framework 中的一部分,確實會在注解類別中使用自身的注解。這種情況是透過 Spring 框架的特殊處理機制實現的,而不是 Java 語言本身的特性。Java 不允許在注解類別中直接使用自身的注解。

@AliasFor 注解的 value 和 attribute 屬性互為別名,這表示這兩個屬性可以互相替代使用,設定其中一個屬性的值會自動套用到另一個屬性上。例如:

@MyAnnotation(value = "test"),attribute 也會被設定為 test

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface AliasFor {
    @AliasFor("attribute")
    String value() default "";

    @AliasFor("value")
    String attribute() default "";

    Class<? extends Annotation> annotation() default Annotation.class;
}

程式碼規範

業務層 (Controller, Service)方法名稱: login 等

持久層 (dao, mapper),是用來操作資料庫的,用 login 不合適,適合用資料庫操作相關的方法名稱,如 getByUsernameAndPassword

方法放到對應 mapper 中,例如刪除部門的同時員工也需要刪除,刪除部門的方法放到部門中,刪除員工的方法放到員工中,不要將刪除員工的程式碼放到部門中