From 7dcaebcd75ffd1c5a034252bb5cdcb6af6b3b3b2 Mon Sep 17 00:00:00 2001 From: zhangsan <646228430@qq.com> Date: Wed, 9 Apr 2025 17:23:21 +0800 Subject: [PATCH] =?UTF-8?q?Commit=20on=202025/04/09=20=E5=91=A8=E4=B8=89?= =?UTF-8?q?=2017:23:21.27?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 自学/JavaWeb——后端.md | 885 +++++++++++++++++++++++++++++------------- 自学/Java笔记本.md | 540 +++++++++++++++----------- 自学/linux服务器.md | 28 +- 自学/力扣Hot 100题.md | 36 +- 自学/苍穹外卖.md | 2 - 自学/草稿.md | 157 ++++++-- 6 files changed, 1122 insertions(+), 526 deletions(-) diff --git a/自学/JavaWeb——后端.md b/自学/JavaWeb——后端.md index 5c857a7..47f2ef8 100644 --- a/自学/JavaWeb——后端.md +++ b/自学/JavaWeb——后端.md @@ -908,7 +908,9 @@ Component衍生注解 -### 配置优先级 +### 配置文件 + +#### 配置优先级 在SpringBoot项目当中,常见的属性配置方式有5种, 3种配置文件,加上2种外部属性的配置(Java系统属性、命令行参数)。优先级(从低到高): @@ -930,6 +932,102 @@ java -Dserver.port=9000 -jar XXXXX.jar --server.port=10010 +#### **properties** + +位置:`src/main/resources/application.properties` + +将配置信息写在application.properties,用注解@Value获取配置文件中的数据 + +![image-20230102173905913](https://pic.bitday.top/i/2025/03/19/u6osck-2.png) + +`@Value("${aliyun.oss.endpoint}")` + + + +#### **yml配置文件**(推荐!!!) + +位置:`src/main/resources/application.yml` + +![image-20230102181215809](https://pic.bitday.top/i/2025/03/19/u6txwe-2.png) + +了解下yml配置文件的基本语法: + +- 大小写敏感 +- **数据前边必须有空格**,作为分隔符 +- 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格) +- 缩进的空格数目不重要,只要**相同层级的元素左侧对齐**即可 +- `#`表示注释,从这个字符一直到行尾,都会被解析器忽略 + +对象/map集合 + +```java +user: + name: zhangsan + detail: + age: 18 + password: "123456" +``` + +数组/List/Set集合 + +```java +hobby: + - java + - game + - sport + +//获取示例 +@Value("${hobby}") +private List hobby; +``` + +以上获取配置文件中的属性值,需要通过@Value注解,有时过于繁琐!!! + + + +#### **@ConfigurationProperties** + +是用来**将外部配置(如 `application.properties` / `application.yml`)映射到一个 POJO** 上的。 + +在 **Spring Boot** 中,根据 **驼峰命名转换**规则,自动将 `YAML` 配置文件中的 **键名**(例如 `user-token-name` `user_token_name`)映射到 **Java 类中的属性**(例如 `userTokenName`)。 + +```java +@Data +@Component +@ConfigurationProperties(prefix = "aliyun.oss") +public class AliOssProperties { + private String endpoint; + private String accessKeyId; + private String accessKeySecret; + private String bucketName; +} +``` + +Spring提供的简化方式套路: + +1. 需要创建一个实现类,且实体类中的**属性名**和配置文件当中**key**的名字必须要**一致** + + > 比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法 **==》@Data** + +2. 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象 **==>@Component** + +3. 在实体类上添加`@ConfigurationProperties`注解,并通过**perfix属性**来指定配置参数项的前缀 + +![image-20230103210827003](https://pic.bitday.top/i/2025/03/19/u6taji-2.png) + +4. (可选)引入依赖pom.xml (自动生成配置元数据,让 IDE 能识别并补全你在 `application.properties/yml` 中的自定义配置项,提高开发体验) + +```java + + org.springframework.boot + spring-boot-configuration-processor + +``` + + + + + ### Bean 的获取和管理 #### 获取Bean @@ -946,7 +1044,9 @@ public class MyService { **2.手动获取(ApplicationContext)** -`@Autowired` 自动将 Spring 创建的 `ApplicationContext` 注入到 `applicationContext` 字段中,这样你就可以通过该对象访问容器中的所有 Bean。 +- `@Autowired` 自动将 Spring 创建的 `ApplicationContext` 注入到 `applicationContext` 字段中, + +- 再通过 `applicationContext.getBean(...)` 拿到其他 Bean Spring 会默认采用类名并将首字母小写作为 Bean 的名称。例如,类名为 `DeptController` 的组件默认名称就是 `deptController`。 @@ -998,9 +1098,9 @@ public class CalculationService { -#### 第三方 Bean +#### 第三方 Bean配置 -- 如果要**管理的bean对象来自于第三方**(不是自定义的),是无法用@Component 及衍生注解声明bean的,就需要用到**@Bean**注解。 +- 如果要管理的bean对象**来自于第三方**(不是自定义的),是无法用@Component 及衍生注解声明bean的,就需要用到**@Bean**注解。 - 如果需要定义第三方Bean时, 通常会单独定义一个**配置类** ```java @@ -1015,12 +1115,31 @@ public class CommonConfig { } ``` -在应用启动时,Spring 会调用配置类中标注 `@Bean` 的方法,将方法返回值注册为容器中的 Bean 对象。 +在应用启动时,Spring 会调用配置类中标注 `@Bean` 的方法,将方法**返回值注册**为容器中的 Bean 对象。 默认情况下,该 Bean 的名称就是**该方法的名字**。本例 Bean 名称默认就是 `"reader"`。 +使用: + +```java +@Service +public class XmlProcessingService { + + // 按类型注入 + @Autowired + private SAXReader reader; + + public void parse(String xmlPath) throws DocumentException { + Document doc = reader.read(new File(xmlPath)); + // ... 处理 Document ... + } +} +``` + + + ### SpirngBoot原理 #### **起步依赖** @@ -1910,7 +2029,7 @@ spring.servlet.multipart.max-request-size=100MB -#### 云存储 +#### 阿里云OSS存储 pom文件中添加如下依赖: @@ -1999,101 +2118,6 @@ public class AliOSSUtils { -### 配置文件 - -**properties** - -将配置信息写在application.properties,用注解@Value获取配置文件中的数据 - -![image-20230102173905913](https://pic.bitday.top/i/2025/03/19/u6osck-2.png) - -@Value("${aliyun.oss.endpoint}") - - - -**yml配置文件** - -![image-20230102181215809](https://pic.bitday.top/i/2025/03/19/u6txwe-2.png) - -了解下yml配置文件的基本语法: - -- 大小写敏感 -- 数据前边必须有空格,作为分隔符 -- 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格) -- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 -- `#`表示注释,从这个字符一直到行尾,都会被解析器忽略 - -对象/map集合 - -```java -user: - name: zhangsan - age: 18 - password: 123456 -``` - -数组/List/Set集合 - -```java -hobby: - - java - - game - - sport -``` - - - -**@ConfigurationProperties** - -前面获取配置项中的属性值,需要通过@Value注解,有时过于繁琐!!! - -```java -@Component -public class AliOSSUtils { - - @Value("${aliyun.oss.endpoint}") - private String endpoint; - - @Value("${aliyun.oss.accessKeyId}") - private String accessKeyId; - - @Value("${aliyun.oss.accessKeySecret}") - private String accessKeySecret; - - @Value("${aliyun.oss.bucketName}") - private String bucketName; - - //省略其他代码... - } -``` - -Spring提供的简化方式套路: - -1. 需要创建一个实现类,且实体类中的**属性名**和配置文件当中**key**的名字必须要一致 - - > 比如:配置文件当中叫endpoints,实体类当中的属性也得叫endpoints,另外实体类当中的属性还需要提供 getter / setter方法 **==》@Data** - -2. 需要将实体类交给Spring的IOC容器管理,成为IOC容器当中的bean对象 **==>@Component** - -3. 在实体类上添加`@ConfigurationProperties`注解,并通过**perfix属性**来指定配置参数项的前缀 - -![image-20230103210827003](https://pic.bitday.top/i/2025/03/19/u6taji-2.png) - -4. (可选)引入依赖pom.xml - -```java - - org.springframework.boot - spring-boot-configuration-processor - -``` - -**使用:** - -![image-20240327124923629](https://pic.bitday.top/i/2025/03/19/u6s60c-2.png) - - - ## 登录校验 ### 会话技术 @@ -2102,30 +2126,76 @@ Spring提供的简化方式套路: 会话是和浏览器关联的,当有三个浏览器客户端和服务器建立了连接时,就会有三个会话。同一个浏览器在未关闭之前请求了多次服务器,这多次请求是属于同一个会话。比如:1、2、3这三个请求都是属于同一个会话。当我们关闭浏览器之后,这次会话就结束了。而如果我们是直接把web服务器关了,那么所有的会话就都结束了。 -会话跟踪技术有两种: +会话跟踪技术有三种: 1. Cookie(客户端会话跟踪技术) - - 数据存储在客户端浏览器当中 2. Session(服务端会话跟踪技术) - - 数据存储在储在服务端 3. 令牌技术 + + **Cookie** +**原理**:会话数据**存储在客户端浏览器**中,通过浏览器自动管理。 + - 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的) - 缺点: - 移动端APP(Android、IOS)中无法使用Cookie - 不安全,用户可以自己禁用Cookie - - Cookie不能跨域 + - Cookie不能跨域传递 + + **Session** -- 优点:Session是存储在服务端的,安全 +**原理**:**服务端存储**会话数据(如内存、Redis),客户端**只保存**会话 ID。 + +Session 底层是基于Cookie实现的会话跟踪,因此Cookie的缺点他也有。 + +- 优点:Session是存储在服务端的,安全。会话数据存在客户端有篡改的风险。 - 缺点: - - 服务器集群环境下无法直接使用Session - - 移动端APP(Android、IOS)中无法使用Cookie - - 用户可以自己禁用Cookie - - Cookie不能跨域 + - 在分布式服务器集群环境下,Session 无法自动共享 + - 如果客户端禁用 Cookie,Session 会失效。 + - 需要在服务器端存储会话信息,可能带来性能压力,尤其是在高并发环境下。 + + + +**流程解析** + +![image-20230112101804878](https://pic.bitday.top/i/2025/04/08/nu2p10-0.png) + +1.**当用户登录时**,客户端(浏览器)向服务器发送请求(如用户名和密码)。 + +服务器验证用户身份,如果身份验证成功,服务器会生成一个 **唯一标识符**(例如 `userId` 或 `authToken`),并将其存储在 **Cookie** 中。服务器会通过 **`Set-Cookie`** HTTP 响应头将这个信息发送到浏览器:如: + +```text +Set-Cookie: userId=12345; Path=/; HttpOnly; Secure; Max-Age=3600; +``` + +`userId=12345` 是服务器返回的标识符。 + +`Path=/` 表示此 Cookie 对整个网站有效。 + +`HttpOnly` 限制客户端 JavaScript 访问该 Cookie,提高安全性。 + +`Secure` 指示该 Cookie 仅通过 HTTPS 协议传输。 + +`Max-Age=3600` 设置 Cookie 的有效期为一小时。 + +2.**浏览器存储 Cookie**: + +- 浏览器收到 `Set-Cookie` 响应头后,会自动将 **`userId`** 存储在客户端的 Cookie 中。 +- **`userId`** 会在 **本地存储**,并在浏览器的后续请求中自动携带。 + +3.**后续请求发送 Cookie** + +- 当浏览器再次向服务器发送请求时,它会自动在 HTTP 请求头中附带之前存储的 Cookie。 + +4.**服务器识别用户** + +- 服务器通过读取请求中的 `Cookie`,获取 **`userId`**(或其他标识符),然后可以从数据库或缓存中获取对应的用户信息。 + + **令牌(推荐)** @@ -2137,11 +2207,83 @@ Spring提供的简化方式套路: +### 跨域问题 + +**跨域问题**指的是在浏览器中,**一个网页**试图去访问**另一个域**下的资源时,浏览器出于安全考虑,默认会阻止这种操作。这是浏览器的同源策略(Same-Origin Policy)导致的行为。 + +**同源策略(Same-Origin Policy)** + +同源策略是浏览器的一种安全机制,它要求: + +- **协议**(如 `http`、`https`) +- **域名/IP**(如 `example.com`) +- **端口**(如 `80` 或 `443`) + +这三者必须完全相同,才能被视为同源。 + +举例: + +​ http://192.168.150.200/login.html ----------> https://192.168.150.200/login [协议不同,跨域] + +​ http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域] + +​ http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域] + +​ http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域] + + + +**解决跨域问题的方法:** + +**CORS**(Cross-Origin Resource Sharing)是解决跨域问题的标准机制。它允许服务器在响应头中加上特定的 **CORS** 头部信息,明确表示允许哪些外域访问其资源。 + +**服务器端配置**:服务器返回带有 `Access-Control-Allow-Origin` 头部的响应,告诉浏览器允许哪些域访问资源。 + +- `Access-Control-Allow-Origin: *`(表示允许所有域访问) +- `Access-Control-Allow-Origin: http://site1.com`(表示只允许 `http://site1.com` 访问) + +**全局统一配置** + +```java +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebCorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/**") // 匹配所有 /api/** 路径 + .allowedOrigins("http://allowed-domain.com") // 允许的域名 + .allowedMethods("GET","POST","PUT","DELETE","OPTIONS") + .allowedHeaders("Content-Type","Authorization") + .allowCredentials(true) // 是否允许携带 Cookie + .maxAge(3600); // 预检请求缓存 1 小时 + } +} + +``` + +**总结** + +普通的跨域请求依然会送达服务器,**服务器并不主动拦截**;它只是通过响应头声明哪些来源被允许访问,而真正的拦截与安全检查,则**由浏览器**根据同源策略来完成。 + + + ### JWT令牌 +| 特性 | Session | JWT(JSON Web Token) | +| -------- | ------------------------------------ | ----------------------------------------------- | +| 存储方式 | 服务端存储会话数据(如内存、Redis) | 客户端存储完整的令牌(通常在 Header 或 Cookie) | +| 标识方式 | 客户端持有一个 Session ID | 客户端持有一个自包含的 Token | +| 状态管理 | 有状态(Stateful),服务器要维护会话 | 无状态(Stateless),服务器不存会话 | + ![image-20240320191446966](https://pic.bitday.top/i/2025/03/19/u6rqof-2.png) -**生成和校验** + + +#### **生成和校验** 引入依赖 @@ -2153,13 +2295,13 @@ Spring提供的简化方式套路: ``` -生成与解析: +生成令牌与解析令牌: ```java public class JwtUtils { - private static String signKey = "itheima"; - private static Long expire = 43200000L; + private static String signKey = "zy123"; + private static Long expire = 43200000L; //单位毫秒 12小时 /** * 生成JWT令牌 @@ -2190,20 +2332,18 @@ public class JwtUtils { } ``` -**注意:**一旦生成token,在有效期内,关闭浏览器再重新打开,仍然可以通过登录校验,因为token仍然存在浏览器的“storage里面”。 - -在有效期内,发送的每个请求头部都会带上token - **令牌可以存储当前登录用户的信息:id、username等等,传入claims** +`Object` 类型能够容纳字符串、数字等各种对象。 + ```java Map claims = new HashMap<>(); -claims.put("id",emp.getId()); -claims.put("name",e.getName()); -claims.put("username",e.getUsername()); -String jwt=JwtUtils.generateJwt(claims); +claims.put("id", emp.getId()); // 假设 emp.getId() 返回一个数字(如 Long 类型) +claims.put("name", e.getName()); // 假设 e.getName() 返回一个字符串 +claims.put("username", e.getUsername()); // 假设 e.getUsername() 返回一个字符串 +String jwt = JwtUtils.generateJwt(claims); ``` **解析令牌:** @@ -2212,34 +2352,65 @@ String jwt=JwtUtils.generateJwt(claims); @Autowired private HttpServletRequest request; - String jwt = request.getHeader("token"); - Claims claims = JwtUtils.parseJWT(jwt); +String jwt = request.getHeader("token"); +Claims claims = JwtUtils.parseJWT(jwt); // 解析 JWT 令牌 + +// 获取存储的 id, name, username +Long id = (Long) claims.get("id"); // 如果 "id" 是 Long 类型 +String name = (String) claims.get("name"); +String username = (String) claims.get("username"); ``` +#### **JWT 登录认证流程** + +1. 用户登录 + 用户发起登录请求,登录成功后,生成 JWT 令牌,并将其返回给前端。 + +2. 前端存储令牌 + 前端接收到 JWT 令牌,**存储在浏览器中**(通常存储在 LocalStorage 或 Cookie 中)。 + + ```javascript + // 登录成功后,存储 JWT 令牌到 LocalStorage + const token = response.data.token; // 从响应中获取令牌 + localStorage.setItem('token', token); // 存储到 LocalStorage + + // 在后续请求中获取令牌并附加到请求头 + const storedToken = localStorage.getItem('token'); + fetch("https://your-api.com/protected-endpoint", { + method: "GET", + headers: { + "token": storedToken // 添加 token 到请求头 + } + }) + .then(response => response.json()) + .then(data => console.log(data)) + .catch(error => console.log('Error:', error)); + ``` + +3. 请求带上令牌 + 后续的每次请求,前端将 JWT 令牌携带上。 + +4. 服务端校验令牌 + 服务端接收到请求后,拦截请求并检查是否携带令牌。若没有令牌,拒绝访问;若令牌存在,校验令牌的**有效性**(包括有效期),若有效则放行,进行请求处理。 + + + ### 拦截器(Interceptor) 在拦截器当中,我们通常也是做一些通用性的操作,比如:**我们可以通过拦截器来拦截前端发起的请求**,将登录校验的逻辑全部编写在拦截器当中。在校验的过程当中,如发现用户登录了(携带JWT令牌且是合法令牌),就可以直接放行,去访问spring当中的资源。如果校验时发现并没有登录或是非法令牌,就可以直接给前端响应未登录的错误信息。 -1.定义工具类:生成、解析JWT令牌 - -2.登录时生成JWT令牌 - -3.定义拦截器,要实现怎样的功能 - -4.注册配置拦截器,哪些方法前要加拦截器=》校验JWT - #### 快速入门 -1. **定义拦截器,实现HandlerInterceptor接口,并重写其所有方法** +1. 定义拦截器,**实现HandlerInterceptor接口**,并重写其所有方法 ```java //自定义拦截器 @Component -public class LoginCheckInterceptor implements HandlerInterceptor { +public class JwtTokenUserInterceptor implements HandlerInterceptor { //目标资源方法执行前执行。 返回true:放行 返回false:不放行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { @@ -2264,13 +2435,15 @@ public class LoginCheckInterceptor implements HandlerInterceptor { 注意: -​ preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行 +​ `preHandle`方法:目标资源方法执行前执行。 **返回true:放行 返回false:不放行** -​ postHandle方法:目标资源方法执行后执行 +​ `postHandle`方法:目标资源方法执行后执行 -​ afterCompletion方法:视图渲染完毕后执行,最后执行 +​ `afterCompletion`方法:视图渲染完毕后执行,最后执行 -2. **注册配置拦截器,实现WebMvcConfigurer接口,并重写addInterceptors方法** + + +2. **注册配置拦截器**,实现WebMvcConfigurer接口,并重写addInterceptors方法 ```java @Configuration @@ -2278,22 +2451,24 @@ public class WebConfig implements WebMvcConfigurer { //自定义的拦截器对象 @Autowired - private LoginCheckInterceptor loginCheckInterceptor; - + private JwtTokenUserInterceptor jwtTokenUserInterceptor; @Override - public void addInterceptors(InterceptorRegistry registry) { - //注册自定义拦截器对象 - registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求) + protected void addInterceptors(InterceptorRegistry registry) { + log.info("开始注册自定义拦截器..."); + registry.addInterceptor(jwtTokenUserInterceptor) + .addPathPatterns("/user/**") + .excludePathPatterns("/user/user/login") + .excludePathPatterns("/user/shop/status"); } } ``` #### 拦截路径 -addPathPatterns指定拦截路径; +`addPathPatterns`指定拦截路径; -调用`excludePathPatterns("不拦截路径")`方法,指定哪些资源不需要拦截。 +调用`excludePathPatterns("不拦截的路径")`方法,指定哪些资源不需要拦截。 | 拦截路径 | 含义 | 举例 | | --------- | -------------------- | --------------------------------------------------- | @@ -2307,74 +2482,81 @@ addPathPatterns指定拦截路径; 主要在preHandle中写逻辑 ```java -@Override //目标资源方法执行前执行。 返回true:放行 返回false:不放行 - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - System.out.println("preHandle .... "); - //1.获取请求url - //2.判断请求url中是否包含login,如果包含,说明是登录操作,放行 - - //3.获取请求头中的令牌(token) - String token = request.getHeader("token"); - log.info("从请求头中获取的令牌:{}",token); - - //4.判断令牌是否存在,如果不存在,返回错误结果(未登录) - if(!StringUtils.hasLength(token)){ - log.info("Token不存在"); - - //创建响应结果对象 - Result responseResult = Result.error("NOT_LOGIN"); - //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类) - String json = JSONObject.toJSONString(responseResult); - //设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8) - response.setContentType("application/json;charset=utf-8"); - //响应 - response.getWriter().write(json); - - return false;//不放行 +public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + //判断当前拦截到的是Controller的方法还是其他资源 + if (!(handler instanceof HandlerMethod)) { + //当前拦截到的不是动态方法,直接放行 + return true; } - //5.解析token,如果解析失败,返回错误结果(未登录) + //1、从请求头中获取令牌 + String token = request.getHeader(jwtProperties.getUserTokenName()); + + //2、校验令牌 try { - JwtUtils.parseJWT(token); - }catch (Exception e){ - log.info("令牌解析失败!"); - - //创建响应结果对象 - Result responseResult = Result.error("NOT_LOGIN"); - //把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类) - String json = JSONObject.toJSONString(responseResult); - //设置响应头 - response.setContentType("application/json;charset=utf-8"); - //响应 - response.getWriter().write(json); - + log.info("jwt校验:{}", token); + Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); + Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString()); + log.info("当前用户id:", userId); + BaseContext.setCurrentId(userId); + //3、通过,放行 + return true; + } catch (Exception ex) { + //4、不通过,响应401状态码 + response.setStatus(401); return false; } - - //6.放行 - return true; } ``` -### 拦截器&&全局异常处理 - -**执行时机不同**:拦截器 (`HandlerInterceptor`) 主要在请求处理的前后进行拦截和处理,而全局异常处理器在控制器方法抛出异常后进行捕获和处理。 - -**作用不同**:拦截器用于拦截请求,可以进行权限验证、日志记录等预处理操作;全局异常处理器专注于异常的统一处理和返回错误信息,确保异常不会导致程序崩溃或未处理的异常信息泄露给客户端。 - - - ## 全局异常处理 +**当前问题:**如果程序因不知名原因报错,响应回来的数据是一个JSON格式的数据,但这种JSON格式的数据不符合开发规范当中所提到的统一响应结果Result吗,导致前端不能解析出响应的JSON数据。 + +![image-20230112130253486](https://pic.bitday.top/i/2025/04/08/soju68-0.png) + +当我们没有做任何的异常处理时,我们三层架构处理异常的方案: + +- Mapper接口在操作数据库的时候出错了,此时异常会往上抛(谁调用Mapper就抛给谁),会抛给service。 +- service 中也存在异常了,会抛给controller。 +- 而在controller当中,我们也没有做任何的异常处理,所以最终异常会再往上抛。最终抛给框架之后,框架就会返回一个JSON格式的数据,里面封装的就是错误的信息,但是框架返回的JSON格式的数据并不符合我们的开发规范。 + + + +**如何解决:** + +- 方案一:在所有Controller的所有方法中进行try…catch处理 + + - 缺点:代码臃肿(不推荐) + +- 方案二:全局异常处理器 + + - 好处:简单、优雅(推荐) + + + +**全局异常处理** + - 定义全局异常处理器非常简单,就是定义一个类,在类上加上一个注解**@RestControllerAdvice**,加上这个注解就代表我们定义了一个全局异常处理器。 - 在全局异常处理器当中,需要定义一个方法来捕获异常,在这个方法上需要加上注解**@ExceptionHandler**。通过@ExceptionHandler注解当中的value属性来指定我们要捕获的是哪一类型的异常。 ```java @RestControllerAdvice public class GlobalExceptionHandler { - + //处理 RuntimeException 异常 + @ExceptionHandler(RuntimeException.class) + public Result handleRuntimeException(RuntimeException e) { + e.printStackTrace(); + return Result.error("系统错误,请稍后再试"); + } + // 处理 NullPointerException 异常 + @ExceptionHandler(NullPointerException.class) + public Result handleNullPointerException(NullPointerException e) { + e.printStackTrace(); + return Result.error("空指针异常,请检查代码逻辑"); + } //处理异常 @ExceptionHandler(Exception.class) //指定能够处理的异常类型,Exception.class捕获所有异常 public Result ex(Exception e){ @@ -2385,30 +2567,73 @@ public class GlobalExceptionHandler { } ``` +模拟NullPointerException + +```java +String str = null; +// 调用 null 对象的方法会抛出 NullPointerException +System.out.println(str.length()); // 这里会抛出 NullPointerException +``` + +模拟RuntimeException + +```java +int res=10/0; +``` + ## 事务 -### Spring事务日志开关 +### 问题分析: ```java -logging: - level: - org.springframework.jdbc.support.JdbcTransactionManager: debug +@Slf4j +@Service +public class DeptServiceImpl implements DeptService { + @Autowired + private DeptMapper deptMapper; + + @Autowired + private EmpMapper empMapper; + + //根据部门id,删除部门信息及部门下的所有员工 + @Override + public void delete(Integer id){ + //根据部门id删除部门信息 + deptMapper.deleteById(id); + + //模拟:异常发生 + int i = 1/0; + + //删除部门下的所有员工信息 + empMapper.deleteByDeptId(id); + } +} ``` +即使程序运行抛出了异常,部门依然删除了,但是部门下的员工却没有删除,造成了数据的不一致。 + +因此,需要事务来控制这组操作,让这组操作同时成功或同时失败。 + + + ### Transactional注解 -@Transactional作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。一般会在**业务层(Service)**当中来控制事务 +`@Transactional`作用:就是在当前这个方法执行开始之前来开启事务,**方法执行完毕之后提交事务**。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。一般会在**业务层(Service)**当中来控制事务。 -@Transactional注解书写位置: +`@Transactional`注解书写位置: -- 方法 - - 当前方法交给spring进行事务管理 -- 类 - - 当前类中所有的方法都交由spring进行事务管理 -- 接口 - - 接口下所有的实现类当中所有的方法都交给spring 进行事务管理 +- 方法:当前方法交给spring进行事务管理 +- 类:当前类中所有的方法都交由spring进行事务管理 +- 接口:接口下所有的实现类当中所有的方法都交给spring 进行事务管理 + + + +`@Transactional`注解当中的两个常见的属性: + +1. 异常回滚的属性:rollbackFor +2. 事务传播行为:propagation 默认情况下,只有出现**RuntimeException(运行时异常)**才会回滚事务。假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的**rollbackFor**属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。 @@ -2428,28 +2653,65 @@ logging: -在@Transactional注解的后面指定一个属性**propagation**,通过 propagation 属性来指定传播行为。可以在嵌套的子事务上加入。 +在`@Transactional`注解的后面指定一个属性**propagation**,通过 propagation 属性来指定事务的传播行为。 -```java -@Transactional(propagation = Propagation.REQUIRES_NEW) -``` +什么是事务的传播行为呢? + +- 就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。A方法运行的时候,首先会开启一个事务,在A方法当中又调用了B方法, B方法自身也具有事务,那么B方法在运行的时候,到底是加入到A方法的事务当中来,还是B方法在运行的时候新建一个事务? | **属性值** | **含义** | | ------------ | ------------------------------------------------------------ | -| REQUIRED | 【默认值】有父事务则加入,**若父事务报异常则一起回滚**;无父事务则创建新事务 | +| REQUIRED | 【默认值】有父事务则加入,**父子有异常则一起回滚**;无父事务则创建新事务 | | REQUIRES_NEW | 需要新事务,无论有无,**总是创建新事务** | - REQUIRED :大部分情况下都是用该传播行为即可。 - REQUIRES_NEW :当我们**不希望事务之间相互影响**时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。 +```java +@Transactional(propagation = Propagation.REQUIRES_NEW) +``` + + + +### Spring事务日志开关 + +```yml +logging: + level: + org.springframework.jdbc.support.JdbcTransactionManager: debug +``` + +当你设置 `debug` 级别日志时,Spring 会打印出关于事务的详细信息,例如事务的开启、提交、回滚以及数据库操作。 + +![image-20230107144312892](https://pic.bitday.top/i/2025/04/08/t15yw1-0.png) + + + +### **总结** + +当 **Service 层发生异常** 时,Spring 会按照以下顺序处理: + +1. **事务的回滚**:如果 Service 层抛出了一个异常(如 `RuntimeException`),并且这个方法是 `@Transactional` 注解标注的,Spring 会在方法抛出异常时 **回滚事务**。Spring 事务管理器会自动触发回滚操作。 +2. **异常传播到 Controller 层**:如果异常在 Service 层处理后未被捕获,它会传播到 Controller 层(即调用 `Service` 方法的地方)。 +3. **全局异常处理器**:当异常传播到 Controller 层时,全局异常处理器(`@RestControllerAdvice` 或 `@ControllerAdvice`)会捕获并处理该异常,返回给前端一个标准的错误响应。 + ## AOP -AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程。 +**AOP**(Aspect-Oriented Programming,面向切面编程)是一种编程思想,旨在将横切关注点(如日志、性能监控等)从核心业务逻辑中分离出来。简单来说,AOP 是通过对特定方法的增强(如统计方法执行耗时)来实现**代码复用**和关注点分离。 + +**实现业务方法执行耗时统计的步骤** + +1. 定义模板方法:将记录方法执行耗时的公共逻辑提取到**模板方法**中。 +2. 记录开始时间:在方法执行前记录开始时间。 +3. 执行原始业务方法:中间部分执行实际的业务方法。 +4. 记录结束时间:在方法执行后记录结束时间,计算并输出执行时间。 + +通过 AOP,我们可以在不修改原有业务代码的情况下,完成对方法执行耗时的统计。 + -我们要想完成统计各个业务方法执行耗时的需求,我们只需要定义一个**模板方法**,将记录方法执行耗时这一部分公共的逻辑代码,定义在模板方法当中,在这个方法开始运行之前,来记录这个方法运行的开始时间,在方法结束运行的时候,再来记录方法运行的结束时间,**中间就来运行原始的业务方法**。 ### 快速入门 @@ -2457,7 +2719,7 @@ AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方 1. 导入依赖:在pom.xml中导入AOP的依赖 -```java +```xml org.springframework.boot spring-boot-starter-aop @@ -2472,7 +2734,7 @@ AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方 @Slf4j public class TimeAspect { ////第一个星号表示任意返回值,第二个星号表示类/接口,第三个星号表示所有方法。 - @Around("execution(* com.itheima.service.*.*(..))") + @Around("execution(* edu.whut.zy123.service.*.*(..))") public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { //记录方法执行开始时间 long begin = System.currentTimeMillis(); @@ -2501,62 +2763,102 @@ public class TimeAspect { ### 核心概念 -**1. 连接点:JoinPoint**,可以被AOP控制的方法 +**1. 连接点:JoinPoint**,可以被AOP控制的**方法**,代表方法的执行位置 +**2. 通知:Advice**,指对目标方法的“增强”操作 (体现为额外的**代码**) +**3. 切入点:PointCut**,是一个表达式,匹配连接点的条件,它指定了 **在目标方法的哪些位置插入通知**,比如在哪些方法调用之前、之后、或者哪些方法抛出异常时进行增强。 -**2. 通知:Advice**,指哪些重复的逻辑,也就是共性功能 (必须的) +**4. 切面:Aspect**,通知与切入点的结合 -**3. 切入点表达式:PointCut**,匹配连接点的条件,通知仅会在切入点方法执行时被应用 (必须的) +**5.目标对象:Target**,被 AOP 代理的对象,通知会作用到目标对象的对应方法上。 +示例: +```java +@Slf4j +@Component +@Aspect +public class MyAspect { + @Before("execution(* edu.whut.zy123.service.MyService.doSomething(..))") + public void beforeMethod(JoinPoint joinPoint) { + // 连接点:目标方法执行位置 + System.out.println("Before method: " + joinPoint.getSignature().getName()); + } +} +``` -**4. 切面:Aspect**,描述通知与切入点的对应关系(通知+切入点表达式) +`joinPoint` 代表的是 `doSomething()` 方法执行的**连接点**。 -**5.目标对象:Target**,通知所应用的对象 +`beforeMethod()` 方法就是一个**前置通知** + +`"execution(* com.example.service.MyService.doSomething(..))"`是**切入点** + +`MyAspect`是**切面**。 + +`com.example.service.MyService` 类的实例是**目标对象** ### 通知类型 -Spring中AOP的通知类型: - -- **@Around**:环绕通知,此注解标注的通知方法在目标方法**前、后都被执行** -- @Before:前置通知,此注解标注的**通知方法在目标方法前被执行** -- @After :后置通知,此注解标注的通知方法在目标方法后被执行,**无论是否有异常都会执行** -- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,**有异常不会执行** -- @AfterThrowing : 异常后通知,此注解标注的通知方法**发生异常后执行** +- **@Around**:环绕通知。此通知会在目标方法**前后都执行**。 +- @Before:前置通知。此通知在目标方法**执行之前**执行。 +- @After :后置通知。此通知在目标方法**执行后**执行,无论方法是否抛出异常。 +- @AfterReturning : 返回后通知。此通知在目标方法**正常返回**后执行,**发生异常时不会执行**。 +- @AfterThrowing : 异常后通知。此通知在目标方法**抛出异常后**执行。 在使用通知时的注意事项: -- @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行 -- @Around环绕通知方法的返回值,**必须指定为Object**,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。 +- **@Around** 通知必须调用 `ProceedingJoinPoint.proceed()` 才能执行目标方法,其他通知不需要。 +- **@Around** 通知的返回值必须是 `Object` 类型,用于接收原始方法的返回值。 +**通知执行顺序** +1. 默认情况下,不同切面类的通知执行顺序由**类名的字母顺序**决定。 -通知的执行顺序大家主要知道两点即可: +2. 可以通过 `@Order` 注解指定切面类的执行顺序,数字越小,优先级越高。 -1. 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的 -2. 可以在**切面类上**面加上**@Order注解**,来控制不同的切面类通知的执行顺序。切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行) + 例如:`@Order(1)` 表示该切面类的通知优先执行。 -eg:@Order(1) +```java +@Aspect +@Order(1) // 优先级1 +@Component +public class AspectOne { + @Before("execution(* edu.whut.zy123.service.MyService.*(..))") + public void beforeMethod() { + System.out.println("AspectOne: Before method"); + } +} +``` + +```java +@Aspect +@Order(2) // 优先级2 +@Component +public class AspectTwo { + @Before("execution(* edu.whut.zy123.service.MyService.*(..))") + public void beforeMethod() { + System.out.println("AspectTwo: Before method"); + } +} +``` + +如果调用 `MyService` 中的某个方法,AspectOne切面类中的通知会先执行。 + +结论:目标方法前的通知方法,**Order小**的或者**类名的字母顺序在前**的先执行。 + +目标方法后的通知方法,**Order小**的或者**类名的字母顺序在前**的后执行。 + +相对于显式设置(Order)的通知,默认通知的优先级最低。 ### 切入点表达式 -#### 公共表示 - -先定义一个公共的pt(),然后可以直接引用。 - -![image-20240322105617334](https://pic.bitday.top/i/2025/03/19/u6r4y1-2.png) - -切入点表达式: - -- 描述切入点方法的一种表达式 - - 作用:主要用来**决定项目中的哪些方法需要加入通知** - 常见形式: @@ -2564,6 +2866,31 @@ eg:@Order(1) 1. execution(……):根据方法的签名来匹配 2. @annotation(……) :根据注解匹配 +#### 公共表示@Pointcut + +使用 `@Pointcut` 注解可以将切点表达式提取到一个独立的方法中,提高代码复用性和可维护性。 + +```java +@Aspect +@Component +public class LoggingAspect { + + // 定义一个切点,匹配com.example.service包下 UserService 类的所有方法 + @Pointcut("execution(public * com.example.service.UserService.*(..))") + public void userServiceMethods() { + // 该方法仅用来作为切点标识,无需实现任何内容 + } + + // 在目标方法执行前执行通知,引用上面的切点 + @Before("userServiceMethods()") + public void beforeUserServiceMethods() { + System.out.println("【日志】即将执行 UserService 中的方法"); + } +} +``` + + + #### execution execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为: @@ -2576,21 +2903,27 @@ execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) th - 访问修饰符:可省略(比如: public、protected) -- 包名.类名: 可省略,**但不建议** +- 包名.类名.: 可省略,**但不建议** - throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常) -eg: +示例: ```java -@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))") +//如果希望匹配 public void delete(Integer id) +@Before("execution(void edu.whut.zy123.service.impl.DeptServiceImpl.delete(java.lang.Integer))") + +//如果希望匹配 public void delete(int id) +@Before("execution(void edu.whut.zy123.service.impl.DeptServiceImpl.delete(int))") ``` +在 Pointcut 表达式中,为了确保匹配准确,通常建议对非基本数据类型使用**全限定名**。这意味着,对于像 Integer 这样的类,最好写成 `java.lang.Integer` -可以**使用通配符描述切入点** -- `*` :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、**任意类型的一个参数**,也可以通配包、类、方法名的一部分 +可以使用**通配符**描述切入点 + +- `*` :**单个**独立的任意符号,可以通配任意返回值、包名、类名、方法名、**任意类型的一个参数**,也可以通配包、类、方法名的一部分 ```java execution(* edu.*.service.*.update*(*)) @@ -2598,13 +2931,29 @@ execution(* edu.*.service.*.update*(*)) 这里update后面的'星'即通配方法名的一部分,()中的'星'表示有且仅有一个任意参数 -- `..` :**多个连续的任意符号**,可以通配任意层级的包,或任意类型、任意个数的参数 +可以匹配: + +```java +package edu.zju.service; + +public class UserService { + public void updateUser(String username) { + // 方法实现 + } +} +``` + +- `..` :**多个**连续的任意符号,可以通配任意层级的包,或**任意类型、任意个数**的参数 + +```java +execution(* com.example.service.UserService.*(..)) +``` #### annotation -那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这两个方法。我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。 +那么如果我们要匹配多个无规则的方法,比如:list()和 delete()这**两个**方法。我们可以借助于另一种切入点表达式annotation来描述这一类的切入点,从而来简化切入点表达式的书写。 实现步骤: @@ -2630,16 +2979,14 @@ public @interface MyLog { 2. 在业务类要做为连接点的**方法上添加**自定义注解 ```java - @MyLog //自定义注解(表示:当前方法属于目标方法) - public void delete(Integer id) { - //1. 删除部门 - deptMapper.delete(id); - } +@MyLog //自定义注解(表示:当前方法属于目标方法) +public void delete(Integer id) { + //1. 删除部门 + deptMapper.delete(id); +} ``` - - -3. aop切面类上使用类似如下的切面表达式: +3. AOP切面类上使用类似如下的切面表达式: ```java @Before("@annotation(edu.whut.anno.MyLog)") @@ -2647,10 +2994,26 @@ public @interface MyLog { -### 连接点 +### 连接点JoinPoint ![image-20240322131745283](https://pic.bitday.top/i/2025/03/19/u6og3o-2.png) + + +**执行**: ProceedingJoinPoint和 JoinPoint 都是调用 `proceed()` 就会执行被代理的方法 + +```java +Object result = joinPoint.proceed(); +``` + +**获取调用方法时传递的参数**,即使只有一个参数,也以数组形式返回: + +```java +Object[] args = joinPoint.getArgs(); +``` + + + **`getSignature()`**: 返回一个`Signature`类型的对象,这个对象包含了被拦截点的签名信息。在方法调用的上下文中,这包括了方法的名称、声明类型等信息。 - **方法名称**:可以通过调用`getName()`方法获得。 @@ -2659,7 +3022,7 @@ public @interface MyLog { -Object[] args = joinPoint.**getArgs()**; 可以获取调用方法时传递的参数数组 + diff --git a/自学/Java笔记本.md b/自学/Java笔记本.md index f7b0b45..a8d8972 100644 --- a/自学/Java笔记本.md +++ b/自学/Java笔记本.md @@ -79,128 +79,90 @@ IDEA快捷键: 1. 二进制:0b 八进制:0 十六进制:0x + + 2. 在 `System.out.println()` 方法中,"ln" 代表 "line",表示换行。因此,`println` 实际上是 "print line" 的缩写。这个方法会在输出文本后自动换行. - ```java - System.out.println("nihao "+1.3331); #Java 会自动将数值转换为字符串 - ``` +```java +System.out.println("nihao "+1.3331); #Java 会自动将数值转换为字符串 +``` - 当直接打印一个没有重写 `toString()` 方法的对象时,Java 默认会调用 `Object` 类的 `toString()` 方法,其输出格式通常为: - ```java - java.lang.Object@15db9742 - ``` - - - - 当打印重写`toString()` 方法的对象时: - - ```java - class Person { - private String name; - private int age; - - public Person(String name, int age) { - this.name = name; - this.age = age; - } - - @Override - public String toString() { - return "Person{name='" + name + "', age=" + age + "}"; - } - } - - public class Main { - public static void main(String[] args) { - Person person = new Person("Alice", 30); - System.out.println(person); //会自动调用对象的 toString() 方法 - } - } - - ``` - - ```java - Person{name='Alice', age=30} - ``` - - 3. 一维数组创建: - ```java - // 方式1:先声明,再指定长度(默认值为0、null等) - int[] arr1 = new int[10]; // 创建一个长度为10的int数组 - - // 方式2:使用初始化列表直接创建数组 - int[] arr2 = {1, 2, 3, 4, 5}; // 创建并初始化一个包含5个元素的int数组 - - String[] strs = {"eat", "tea", "tan", "ate", "nat", "bat"}; - - // 方式3:结合new关键字和初始化列表创建数组(常用于明确指定类型时) - int[] arr3 = new int[]{1, 2, 3, 4, 5}; // 与方式2效果相同 - ``` +```java +// 方式1:先声明,再指定长度(默认值为0、null等) +int[] arr1 = new int[10]; // 创建一个长度为10的int数组 + +// 方式2:使用初始化列表直接创建数组 +int[] arr2 = {1, 2, 3, 4, 5}; // 创建并初始化一个包含5个元素的int数组 + +String[] strs = {"eat", "tea", "tan", "ate", "nat", "bat"}; + +// 方式3:结合new关键字和初始化列表创建数组(常用于明确指定类型时) +int[] arr3 = new int[]{1, 2, 3, 4, 5}; // 与方式2效果相同 +``` + - 4. 字符串创建 - ```java - String str = "Hello, World!"; //(1)直接赋值 - - String str = new String("Hello, World!"); //使用 new 关键字 - - char[] charArray = {'H', 'e', 'l', 'l', 'o'}; - String str = new String(charArray); //通过字符数组创建 - ``` +```java +String str = "Hello, World!"; //(1)直接赋值 + +String str = new String("Hello, World!"); //使用 new 关键字 + +char[] charArray = {'H', 'e', 'l', 'l', 'o'}; +String str = new String(charArray); //通过字符数组创建 +``` 5. switch-case - ```java - public class SwitchCaseExample { - public static void main(String[] args) { - // 定义一个 int 类型变量,作为 switch 的表达式 - int day = 3; - String dayName; - - // 根据 day 的值执行相应的分支 - switch(day) { - case 1: - dayName = "Monday"; // 当 day 为 1 时 - break; // 结束当前 case - case 2: - dayName = "Tuesday"; // 当 day 为 2 时 - break; - case 3: - dayName = "Wednesday"; // 当 day 为 3 时 - break; - case 4: - dayName = "Thursday"; // 当 day 为 4 时 - break; - case 5: - dayName = "Friday"; // 当 day 为 5 时 - break; - case 6: - dayName = "Saturday"; // 当 day 为 6 时 - break; - case 7: - dayName = "Sunday"; // 当 day 为 7 时 - break; - default: - // 如果 day 不在 1 到 7 之间 - dayName = "Invalid day"; - } - - // 输出最终结果 - System.out.println("The day is: " + dayName); - } - } - - ``` +```java +public class SwitchCaseExample { + public static void main(String[] args) { + // 定义一个 int 类型变量,作为 switch 的表达式 + int day = 3; + String dayName; + + // 根据 day 的值执行相应的分支 + switch(day) { + case 1: + dayName = "Monday"; // 当 day 为 1 时 + break; // 结束当前 case + case 2: + dayName = "Tuesday"; // 当 day 为 2 时 + break; + case 3: + dayName = "Wednesday"; // 当 day 为 3 时 + break; + case 4: + dayName = "Thursday"; // 当 day 为 4 时 + break; + case 5: + dayName = "Friday"; // 当 day 为 5 时 + break; + case 6: + dayName = "Saturday"; // 当 day 为 6 时 + break; + case 7: + dayName = "Sunday"; // 当 day 为 7 时 + break; + default: + // 如果 day 不在 1 到 7 之间 + dayName = "Invalid day"; + } + + // 输出最终结果 + System.out.println("The day is: " + dayName); + } +} +``` + - #### Java传参方式 @@ -897,6 +859,23 @@ protected static volatile int counter; #定义成员变量 +#### 全限定名 + +全限定名(Fully Qualified Name,简称 FQN)指的是一个类或接口在 Java 中的完整名称,包括它所在的包名。例如: + +- 对于类 `Integer`,其全限定名是 `java.lang.Integer`。 +- 对于自定义的类 `DeptServiceImpl`,如果它位于包 `edu.zju.zy123.service.impl` 中,那么它的全限定名就是 `edu.zju.zy123.service.impl.DeptServiceImpl`。 + +使用全限定名可以消除歧义,确保指定的类型在整个项目中唯一无误。 + +使用场景: + +Spring AOP 的 Pointcut 表达式 + +MyBatis的XML映射文件的**namespace属性** + + + ### JAVA面向对象 #### **JAVA**三大特性 @@ -1046,7 +1025,8 @@ class Dog extends Animal { ``` - + + #### 接口 @@ -1291,13 +1271,15 @@ public class SomeException extends Exception { ``` + + ## 好用的方法 ### toString() -Arrays.toString() +**Arrays.toString()**转一维数组 -作用:方便地输出数组。 +**Arrays.deepToString()**转二维数组 这个方法是是用来将数组转换成String类型输出的,入参可以是long,float,double,int,boolean,byte,object 型的数组。 ```java @@ -1319,6 +1301,45 @@ public class Main { System.out.println("二维数组输出: " + Arrays.deepToString(twoD)); } } +``` + + + +**自定义对象的`toString()` 方法** + +每个 Java 对象默认都有 `toString()` 方法(可以根据需要覆盖) + +当直接打印一个没有重写 `toString()` 方法的对象时,其输出格式通常为: + +```java +java.lang.Object@15db9742 +``` + +当打印重写`toString()` 方法的对象时: + +```java +class Person { + private String name; + private int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public String toString() { + return "Person{name='" + name + "', age=" + age + "}"; + } +} + +public class Main { + public static void main(String[] args) { + Person person = new Person("Alice", 30); + System.out.println(person); //会自动调用对象的 toString() 方法 + //Person{name='Alice', age=30} + } +} ``` @@ -1330,7 +1351,7 @@ public class Main { **类路径**是JVM在运行时用来查找类文件和资源文件的一组目录或JAR包。在许多项目(例如Maven或Gradle项目)中,`src/main/resources`目录下的内容在编译时会被复制到输出目录(如`target/classes`),`src/main/java` 下编译后的 class 文件也会放到这里。 -```java +```text MyProject/ ├── src/ │ └── main/ @@ -1338,16 +1359,26 @@ MyProject/ │ └── com/ │ └── example/ │ └── Main.java -└── resources/ - ├── emp.xml - └── static/ - └── tt.img +├── resources/ +│ ├── emp.xml +│ └── static/ +│ └── tt.img +└── target/ + └── classes/ + ├── com/ + │ └── example/ + │ └── Main.class + ├── emp.xml + └── static/ + └── tt.img ``` +------------ + ```java // 获取 resources 根目录下的 emp.xml 文件路径 -String empFile = this.getClass().getClassLoader().getResource("emp.xml").getFile(); +String empFileUrl = this.getClass().getClassLoader().getResource("emp.xml").getFile(); // 获取 resources/static 目录下的 tt.img 文件路径 URL resourceUrl = getClass().getClassLoader().getResource("static/tt.img"); @@ -1401,6 +1432,8 @@ public class Test1Class{ **2.获取类的构造器** +- 定义类 + ```java public class Cat{ private String name; @@ -1593,21 +1626,188 @@ public class FieldReflectionTest { -### Junit 单元测试 +### 注解 -![image-20240307172717512](https://pic.bitday.top/i/2025/03/19/u75u7l-2.png) +在 Java 中,注解用于给程序元素(类、方法、字段等)**添加元数据**,这些元数据可被编译器、工具或运行时**反射读取**,以实现配置、检查、代码生成以及框架支持(如依赖注入、AOP 等)功能,而不直接影响代码的业务逻辑。 + +比如:Junit框架的@Test注解可以用在方法上,用来标记这个方法是测试方法,被`@Test`标记的方法能够被Junit框架执行。 + +再比如:@Override注解可以用在方法上,用来标记这个方法是重写方法,被`@Override`注解标记的方法能够被IDEA识别进行语法检查。 + +#### 使用注解 + +**定义注解** + +使用 `@interface` 定义注解 ```java - @Test - public void testListUser(){ - Listlist=userMapper.list(); - for(User user:list){ - System.out.println(user); - } - } +// 定义注解 +@Retention(RetentionPolicy.RUNTIME) // 定义注解的生命周期 +@Target(ElementType.METHOD) // 定义注解可以应用的Java元素类型 +public @interface MyAnnotation { + // 定义注解的元素(属性) + String description() default "This is a default description"; + int value() default 0; +} ``` -写了@Test注解,那么该测试函数就可以直接运行!若一个测试类中写了多个测试方法,可以全部执行! +**元注解** + +**是修饰注解的注解**。 + +@Retention(RetentionPolicy.RUNTIME) //指定注解的生命周期,即在运行时有效,可用于反射等用途。 + +@Target(ElementType.METHOD) //方法上的注解 + +@Target(ElementType.TYPE) //类上的注解(包含类、接口、枚举等类型) + + + +**简化使用**:如果注解中只有一个属性需要设置,而且该属性名为 `value`,则在使用时可以省略属性名 + +```java +@MyAnnotation(5) // 等同于 @MyAnnotation(value = 5) +public void someMethod() { + // 方法实现 +} +``` + + + +当需要为注解的多个属性赋值时,传参必须指明属性名称: + +```java +@MyAnnotation(value = 5, description = "Specific description") +public void anotherMethod() { + // 方法实现 +} +``` + + + +如果所有属性都使用默认值,可以直接使用注解而不传入任何参数: + +```java +@MyAnnotation +public void anotherMethod() { + // 方法实现 +} +``` + + + +#### 解析注解 + +```java +public class MyClass { + @MyAnnotation(value = "specific value") + public void myMethod() { + // 方法实现 + } +} +``` + + + +```java +import java.lang.reflect.Method; + +public class AnnotationReader { + + public static void main(String[] args) throws NoSuchMethodException { + // 获取MyClass的Class对象 + Class obj = MyClass.class; + + // 获取myMethod方法的Method对象 + Method method = obj.getMethod("myMethod"); + + // 获取方法上的MyAnnotation注解实例 + MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); + + if (annotation != null) { + // 输出注解的value值 + System.out.println("注解的value: " + annotation.value()); + } + } +} + +``` + +快速检查某个注解是否存在于`method` 上 + +```java +if (method.isAnnotationPresent(MyAnnotation.class)) { + // 如果存在MyAnnotation注解,则执行相应逻辑 +} +``` + +检查方法 `method` 上是否存在 `MyAnnotation` 注解。如果存在,就返回该注解的实例,否则返回 `null` + +```java +MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); +``` + + + +### Junit 单元测试 + +**步骤** + +**1.导入依赖** +将 JUnit 框架的 jar 包添加到项目中(注意:IntelliJ IDEA 默认集成了 JUnit,无需手动导入)。 + +**2.编写测试类** + +- 为待测业务方法创建对应的测试类。 +- 测试类中定义测试方法,要求方法必须为 `public` 且返回类型为 `void`。 + +**3.添加测试注解** +在测试方法上添加 `@Test` 注解,确保 JUnit 能自动识别并执行该方法。 + +**4.运行测试** +在测试方法上右键选择“JUnit运行”。 + +- 测试通过显示绿色标志; +- 测试失败显示红色标志。 + +```java +public class UserMapperTest { + @Test + public void testListUser() { + UserMapper userMapper = new UserMapper(); + List list = userMapper.list(); + Assert.assertNotNull("User list should not be null", list); + list.forEach(System.out::println); + } +} +``` + + + +**注意**,如果需要使用**依赖注入**,需要在测试类上加`@SpringBootTest`注解 + +它会启动 Spring 应用程序上下文,并在测试期间模拟运行整个 Spring Boot 应用程序。这意味着你可以在集成测试中使用 Spring 的各种功能,例如**自动装配、依赖注入、配置加载**等 + +```java +@RunWith(SpringRunner.class) +@SpringBootTest +public class UserMapperTest { + + @Autowired + private UserMapper userMapper; + + @Test + public void testListUser() { + List list = userMapper.list(); + Assert.assertNotNull("User list should not be null", list); + list.forEach(System.out::println); + } +} +``` + + + +写了`@Test`注解,那么该测试函数就可以直接运行!若一个测试类中写了多个测试方法,可以全部执行! ![image-20240307173454288](https://pic.bitday.top/i/2025/03/19/u6um98-2.png) @@ -1654,98 +1854,6 @@ public class AnnotationTest4 { -### 注解 - -定义: - -```java -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -// 定义注解 -@Retention(RetentionPolicy.RUNTIME) // 定义注解的生命周期 -@Target(ElementType.METHOD) // 定义注解可以应用的Java元素类型 -public @interface MyAnnotation { - // 定义注解的元素(属性) - String description() default "This is a default description"; - int value() default 0; -} - -``` - -@Target(ElementType.METHOD) //方法上的注解 - -@Target(ElementType.CLASS) //类上的注解 - - - -**简化使用**:当注解只有一个元素需要设置时,且该元素的名字是`value`,在使用该注解时可以不用显式地指定元素名 - -```java -@MyAnnotation(5) // 等同于 @MyAnnotation(value = 5) -public void someMethod() { - // 方法实现 -} - -``` - - - -如果要同时设置`description`,则不能省略元素名: - -```java -@MyAnnotation(value = 5, description = "Specific description") -public void anotherMethod() { - // 方法实现 -} - -``` - - - -**获得注解上的value:**反射 - -```java -public class MyClass { - - @MyAnnotation(value = "specific value") - public void myMethod() { - // 方法实现 - } -} - -``` - - - -```java -import java.lang.reflect.Method; - -public class AnnotationReader { - - public static void main(String[] args) throws NoSuchMethodException { - // 获取MyClass的Class对象 - Class obj = MyClass.class; - - // 获取myMethod方法的Method对象 - Method method = obj.getMethod("myMethod"); - - // 获取方法上的MyAnnotation注解实例 - MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); - - if (annotation != null) { - // 输出注解的value值 - System.out.println("注解的value: " + annotation.value()); - } - } -} - -``` - - - ### 对象拷贝属性 ```java diff --git a/自学/linux服务器.md b/自学/linux服务器.md index d093eeb..24977f6 100644 --- a/自学/linux服务器.md +++ b/自学/linux服务器.md @@ -995,7 +995,7 @@ Joe主题:https://github.com/HaoOuBa/Joe [Joe再续前缘主题 - 搭建本站同款网站 - 易航博客](https://blog.yihang.info/archives/18.html) -修改文章详情页的上方信息: +自定义文章详情页的上方信息(如更新日期/文章字数第): `typecho/usr/themes/Joe/module/single/batten.php` ```php @@ -1044,6 +1044,32 @@ if (!defined('__TYPECHO_ROOT_DIR__')) { ``` + + +修改代码块背景色: + +`typecho/usr/themes/Joe/assets/css/joe.global.css` + +```css +.joe_detail__article code:not([class]) { + border-radius: var(--radius-inner, 4px); /* 可以设置一个默认值 */ + background: #f5f5f5; /* 稍微偏灰的背景色 */ + color: #000000; /* 黑色字体 */ + padding: 2px 6px; /* 内边距可以适当增大 */ + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + word-break: break-word; + font-weight: normal; + -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + white-space: pre-wrap; /* 保持代码换行 */ + font-size: 0.875em; + margin-inline-start: 0.25em; + margin-inline-end: 0.25em; +} +``` + + + 大坑:{x}会显示为勾选框,无法正常进行latex公式解析,因为`typecho/usr/themes/Joe/public/short.php`中设置了短代码替换,**在文章输出前**对 `$content` 中的特定标记或短代码进行搜索和替换,从而实现一系列自定义功能。现已全部注释。 diff --git a/自学/力扣Hot 100题.md b/自学/力扣Hot 100题.md index 2f8e1ef..e15df6d 100644 --- a/自学/力扣Hot 100题.md +++ b/自学/力扣Hot 100题.md @@ -68,11 +68,21 @@ if (flag == false) { //更常用! `Integer.toString(int i)`:将 `int` 转换为字符串。 +`Integer.compare(int a,int b)` 比较a和b的大小,内部实现: + +``` +public static int compare(int x, int y) { + return (x < y) ? -1 : ((x == y) ? 0 : 1); +} +``` + +避免了 **整数溢出** 的风险,在排序中建议使用`Integer.compare(int a,int b)`代替 `a-b` + ### 常用数据结构 -#### `String` +#### String 子串:字符串中**连续的一段字符**。 @@ -107,7 +117,7 @@ String sortedStr = new String(charArray); -#### `StringBuffer` +#### StringBuffer `StringBuffer` 是 Java 中用于操作可变字符串的类 @@ -162,7 +172,7 @@ sb.setLength(0); -#### `HashMap` +#### HashMap - 基于哈希表实现,查找、插入和删除的平均时间复杂度为 O(1)。 - 不保证元素的顺序。 @@ -220,7 +230,7 @@ visited[i][j] = true; -#### `HashSet` +#### HashSet - 基于哈希表实现,查找、插入和删除的平均时间复杂度为 O(1)。 @@ -263,7 +273,7 @@ visited[i][j] = true; -#### `PriorityQueue` +#### PriorityQueue - 基于优先堆(最小堆或最大堆)实现,元素按优先级排序。 - **默认是最小堆**,即队首元素是最小的。 `new PriorityQueue<>(Comparator.reverseOrder());`定义最大堆 @@ -501,7 +511,7 @@ class MinHeap { -#### **`ArrayList`** +#### **ArrayList** - 基于数组实现,支持动态扩展。 - 访问元素的时间复杂度为 O(1),在末尾插入和删除的时间复杂度为 O(1)。 @@ -597,7 +607,7 @@ for (int i = 0; i < list.size(); i++) { -#### **`数组(Array)`** +#### **数组(Array)** 数组是一种固定长度的数据结构,用于存储相同类型的元素。数组的特点包括: @@ -688,7 +698,7 @@ Arrays.fill(memo, -1); -#### `二维数组` +#### 二维数组 ```java int rows = 3; @@ -724,7 +734,7 @@ public void setZeroes(int[][] matrix) { -#### `Queue` +#### Queue 队尾插入,队头取! @@ -761,7 +771,7 @@ public class QueueExample { ``` -#### `Deque`(双端队列+栈) +#### Deque(双端队列+栈) 支持在队列的两端(头和尾)进行元素的插入和删除。这使得 Deque 既能作为队列(FIFO)又能作为栈(LIFO)使用。 @@ -847,7 +857,7 @@ public class DequeExample { -#### `Iterator` +#### Iterator - **`HashMap`、`HashSet`、`ArrayList` 和 `PriorityQueue`** 都实现了 `Iterable` 接口,支持 `iterator()` 方法。 @@ -1135,9 +1145,9 @@ public class IntervalSort { // 自定义比较器,先比较第一个元素,如果相等再比较第二个元素 Arrays.sort(intervals, (a, b) -> { if (a[0] != b[0]) { - return a[0] - b[0]; + return Integer.compare(a[0], b[0]); } else { - return a[1] - b[1]; + return Integer.compare(a[1], b[1]); } }); diff --git a/自学/苍穹外卖.md b/自学/苍穹外卖.md index 5aef4c4..1298073 100644 --- a/自学/苍穹外卖.md +++ b/自学/苍穹外卖.md @@ -1123,8 +1123,6 @@ public class OrderTask { - - ## Websocket WebSocket 是基于 TCP 的一种新的**网络协议**。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建**持久性**的连接, 并进行**双向**数据传输。 diff --git a/自学/草稿.md b/自学/草稿.md index 93f7d7f..66ac3a8 100644 --- a/自学/草稿.md +++ b/自学/草稿.md @@ -1,53 +1,144 @@ -- 当然可以!下面是你可以直接记到笔记里的内容: +下面从架构、扩展性、安全性、管理成本等几个维度,对 **Session** 和 **JWT** 进行对比,帮助你根据场景选择合适的方案。 - ------ +------ - ### 🧠 题型:**Top K 高频元素**(LeetCode 347) +## 一、基本原理 - **题目描述**:给定一个整数数组 `nums` 和一个整数 `k`,返回出现频率最高的前 `k` 个元素,返回顺序可以任意。 +| 特性 | Session | JWT(JSON Web Token) | +| -------- | ------------------------------------ | ----------------------------------------------- | +| 存储方式 | 服务端存储会话数据(如内存、Redis) | 客户端存储完整的令牌(通常在 Header 或 Cookie) | +| 标识方式 | 客户端持有一个 Session ID | 客户端持有一个自包含的 Token | +| 状态管理 | 有状态(Stateful),服务器要维护会话 | 无状态(Stateless),服务器不存会话 | - ------ +------ - ### 📌 解法一:大根堆(最大堆) +## 二、对比分析 - **思路**: +### 1. 架构与扩展性 - 1. 使用 `HashMap` 统计每个元素的出现频率。 - 2. 构建一个**大根堆**(`PriorityQueue` + 自定义比较器),根据频率降序排列。 - 3. 将所有元素加入堆中,**弹出前 `k` 个元素**即为答案。 +- **Session** + - 单体应用:内存中维护 Map,简单易用。 + - 分布式/集群:需要共享 Session(如 Redis、数据库、Sticky Session),增加运维成本。 +- **JWT** + - 无状态:令牌自带用户信息及签名,服务器只需校验签名即可,无需存储。 + - 分布式友好:各节点只要共享签名密钥(或公钥),即可校验,无需集中存储。 - **适合场景**: +### 2. 性能 - - 实现简单,适用于对全部元素排序后取前 `k` 个。 - - 时间复杂度:**O(n log n)**,因为需要将所有 `n` 个元素都加入堆。 +- **Session** + - 每次请求都要从存储(内存/Redis/DB)读取会话数据,IO 成本或网络开销。 + - 并发高时,集中式 Session 存储可能成为瓶颈。 +- **JWT** + - 校验签名(HMAC 或 RSA)为 CPU 操作,无网络开销,性能开销较小。 + - 但 Token 通常更大(包含多段 Base64),每次请求都要传输,带宽略增。 - ------ +### 3. 安全性 - ### 📌 解法二:小根堆(最小堆) +- **Session** + - 会话数据保存在服务器端,客户端只能拿到 Session ID,敏感数据不暴露。 + - 可在服务器端随时销毁或更新 Session(强制登出、权限变更即时生效)。 +- **JWT** + - 令牌自包含所有声明(claims),如果存敏感数据需加密(JWE),否则仅签名(JWS)也可能泄露信息。 + - 无法主动撤销(除非做黑名单),需要控制有效期并结合“刷新令牌”机制。 - **思路**: +### 4. 可控性与管理 - 1. 使用 `HashMap` 统计频率。 - 2. 构建一个**小根堆**,堆中仅保存前 `k` 个高频元素。 - 3. 遍历每个元素: - - 如果堆未满,直接加入。 - - 如果当前元素频率大于堆顶(最小频率),则弹出堆顶,加入当前元素。 - 4. 最终堆中保存的就是前 `k` 个高频元素。 +- **Session** + - **可控性强**:服务器可随时作废 Session,适合需要即时注销、权限动态调整的场景。 + - 过期策略灵活:可按用户、按应用统一配置。 +- **JWT** + - **可控性弱**:Token 一旦签发,在到期前无法从服务器强制失效(除非额外维护黑名单)。 + - 需要设计“短生命周期 + 刷新令牌”模式,增加实现复杂度。 - **适合场景**: +### 5. 跨域与移动端 - - 当 `k ≪ n` 时效率更高。 - - 时间复杂度:**O(n log k)**,因为堆中最多维护 `k` 个元素。 +- **Session** + - 依赖 Cookie(同源策略),跨域或移动端(原生 App)使用受限。 + - 跨域时需配合 CORS + `withCredentials`,且浏览器必须支持并开启 Cookie。 +- **JWT** + - 与 HTTP 协议无关,既可放在 Authorization 头,也可放在 URL、LocalStorage,移动端/第三方客户端更友好。 + - 只要客户端能发送 HTTP Header,就能携带 Token。 - ------ +### 6. 实现复杂度 - ### ✅ 总结对比: +- **Session** + - 框架通常开箱即用(如 Spring Session),开发者只需开启即可。 + - 自动处理过期、失效,管理简单。 +- **JWT** + - 需要设计签名算法、密钥管理、过期策略、刷新机制、黑名单等。 + - 容易因配置不当造成安全漏洞(算法降级、密钥泄露、Token 劫持等)。 - | 方法 | 适合场景 | 时间复杂度 | 空间复杂度 | - | ------ | --------------- | ---------- | ---------- | - | 大根堆 | k ≈ n,简单易写 | O(n log n) | O(n) | - | 小根堆 | k ≪ n,更高效 | O(n log k) | O(n) | +------ - ------ +## 三、何时选用 - 需要我再写成代码模板笔记也可以,随时说! \ No newline at end of file +| 场景类型 | 推荐方案 | 原因 | +| --------------------------- | -------- | ---------------------------------------------------------- | +| 单体 Web 应用、后台管理系统 | Session | 简单、可控、安全性高,框架支持完善。 | +| 分布式微服务、无状态 API | JWT | 无需集中存储,易扩展;移动端/第三方客户端友好。 | +| 高度安全、需即时失效场景 | Session | 可随时在服务器端销毁会话,确保强制登出或权限变更即时生效。 | +| 跨域或多端(Web + App) | JWT | Token 可在多种客户端轻松传递,无需依赖浏览器 Cookie。 | + +------ + +## 四、示例代码 + +### Session 示例(Spring Boot) + +```java +// 登录时创建 Session +@PostMapping("/login") +public String login(HttpServletRequest req, @RequestParam String user, @RequestParam String pass) { + if (authService.verify(user, pass)) { + req.getSession().setAttribute("userId", user); + return "登录成功"; + } + return "登录失败"; +} + +// 受保护资源 +@GetMapping("/profile") +public User profile(HttpServletRequest req) { + String userId = (String) req.getSession().getAttribute("userId"); + return userService.findById(userId); +} +``` + +### JWT 示例(Spring Boot + jjwt) + +```java +// 生成 Token +@PostMapping("/login") +public String login(@RequestParam String user, @RequestParam String pass) { + if (authService.verify(user, pass)) { + return Jwts.builder() + .setSubject(user) + .setExpiration(new Date(System.currentTimeMillis() + 3600_000)) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + throw new UnauthorizedException(); +} + +// 过滤器中校验 +@Override +protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) { + String token = req.getHeader("Authorization"); + if (token != null && token.startsWith("Bearer ")) { + String user = Jwts.parser().setSigningKey(secretKey) + .parseClaimsJws(token.substring(7)) + .getBody().getSubject(); + // 将 user 存入 SecurityContext … + } + chain.doFilter(req, res); +} +``` + +------ + +## 五、结论 + +- **Session**:上手简单、安全可控,适合绝大多数传统 Web 应用。 +- **JWT**:更灵活、易扩展,适合分布式架构、多端场景,但需要更复杂的设计与安全防护。 + +根据你的项目架构、团队经验和安全需求,选择最合适的方案即可。 \ No newline at end of file