**代理模式**是一种结构型设计模式让你能够提供对象的替代品或其占位符代理控制着对于原对象的访问并允许在将请求提交给对象前后进行一些处理

代理设计模式

问题

为什么要控制对于某个对象的访问呢举个例子有这样一个消耗大量系统资源的巨型对象你只是偶尔需要使用它并非总是需要

代理模式解决的问题

数据库查询有可能会非常缓慢

你可以实现延迟初始化在实际有需要时再创建该对象对象的所有客户端都要执行延迟初始代码不幸的是这很可能会带来很多重复代码

在理想情况下我们希望将代码直接放入对象的类中但这并非总是能实现比如类可能是第三方封闭库的一部分

解决方案

代理模式建议新建一个与原服务对象接口相同的代理类然后更新应用以将代理对象传递给所有原始对象客户端代理类接收到客户端请求后会创建实际的服务对象并将所有工作委派给它

代理模式的解决方案

代理将自己伪装成数据库对象可在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作

这有什么好处呢如果需要在类的主要业务逻辑前后执行一些工作你无需修改类就能完成这项工作由于代理实现的接口与原类相同因此你可将其传递给任何一个使用实际服务对象的客户端

真实世界类比

信用卡是一大捆现金的代理

信用卡和现金在支付过程中的用处相同

信用卡是银行账户的代理银行账户则是一大捆现金的代理它们都实现了同样的接口均可用于进行支付消费者会非常满意因为不必随身携带大量现金商店老板同样会十分高兴因为交易收入能以电子化的方式进入商店的银行账户中无需担心存款时出现现金丢失或被抢劫的情况

代理模式结构

代理设计模式的结构
  1. 服务接口Service Interface声明了服务接口代理必须遵循该接口才能伪装成服务对象

  2. 服务Service类提供了一些实用的业务逻辑

  3. 代理Proxy类包含一个指向服务对象的引用成员变量代理完成其任务例如延迟初始化记录日志访问控制和缓存等后会将请求传递给服务对象

    通常情况下代理会对其服务对象的整个生命周期进行管理

  4. 客户端Client能通过同一接口与服务或代理进行交互所以你可在一切需要服务对象的代码中使用代理

伪代码

尽管代理模式在绝大多数 Java 程序中并不常见但它在一些特殊情况下仍然非常方便当你希望在无需修改客户代码的前提下于已有类的对象上增加额外行为时该模式是无可替代的

Java 标准程序库中的一些代理模式的示例

  • java.lang.reflect.Proxy

  • java.rmi.*

  • javax.ejb.EJB查看评论

  • javax.inject.Inject查看评论

  • javax.persistence.PersistenceContext

**识别方法**代理模式会将所有实际工作委派给一些其他对象除非代理是某个服务的子类否则每个代理方法最后都应该引用一个服务对象

缓存代理

在本例中代理模式有助于实现延迟初始化并对低效的第三方 YouTube 集成程序库进行缓存

当你需要在无法修改代码的类上新增一些额外行为时代理模式的价值无可估量

some_cool_media_library

some_cool_media_library/ThirdPartyYouTubeLib.java: 远程服务接口

1
2
3
4
5
6
7
8
9
package refactoring_guru.proxy.example.some_cool_media_library;

import java.util.HashMap;

public interface ThirdPartyYouTubeLib {
HashMap<String, Video> popularVideos();

Video getVideo(String videoId);
}

some_cool_media_library/ThirdPartyYouTubeClass.java: 远程服务实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package refactoring_guru.proxy.example.some_cool_media_library;

import java.util.HashMap;

public class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib {

@Override
public HashMap<String, Video> popularVideos() {
connectToServer("http://www.youtube.com");
return getRandomVideos();
}

@Override
public Video getVideo(String videoId) {
connectToServer("http://www.youtube.com/" + videoId);
return getSomeVideo(videoId);
}

// -----------------------------------------------------------------------
// Fake methods to simulate network activity. They as slow as a real life.

private int random(int min, int max) {
return min + (int) (Math.random() * ((max - min) + 1));
}

private void experienceNetworkLatency() {
int randomLatency = random(5, 10);
for (int i = 0; i < randomLatency; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}

private void connectToServer(String server) {
System.out.print("Connecting to " + server + "... ");
experienceNetworkLatency();
System.out.print("Connected!" + "\n");
}

private HashMap<String, Video> getRandomVideos() {
System.out.print("Downloading populars... ");

experienceNetworkLatency();
HashMap<String, Video> hmap = new HashMap<String, Video>();
hmap.put("catzzzzzzzzz", new Video("sadgahasgdas", "Catzzzz.avi"));
hmap.put("mkafksangasj", new Video("mkafksangasj", "Dog play with ball.mp4"));
hmap.put("dancesvideoo", new Video("asdfas3ffasd", "Dancing video.mpq"));
hmap.put("dlsdk5jfslaf", new Video("dlsdk5jfslaf", "Barcelona vs RealM.mov"));
hmap.put("3sdfgsd1j333", new Video("3sdfgsd1j333", "Programing lesson#1.avi"));

System.out.print("Done!" + "\n");
return hmap;
}

private Video getSomeVideo(String videoId) {
System.out.print("Downloading video... ");

experienceNetworkLatency();
Video video = new Video(videoId, "Some video title");

System.out.print("Done!" + "\n");
return video;
}

}

some_cool_media_library/Video.java: 视频文件

1
2
3
4
5
6
7
8
9
10
11
12
13
package refactoring_guru.proxy.example.some_cool_media_library;

public class Video {
public String id;
public String title;
public String data;

Video(String id, String title) {
this.id = id;
this.title = title;
this.data = "Random video.";
}
}

proxy

proxy/YouTubeCacheProxy.java: 缓存代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package refactoring_guru.proxy.example.proxy;

import refactoring_guru.proxy.example.some_cool_media_library.ThirdPartyYouTubeClass;
import refactoring_guru.proxy.example.some_cool_media_library.ThirdPartyYouTubeLib;
import refactoring_guru.proxy.example.some_cool_media_library.Video;

import java.util.HashMap;

public class YouTubeCacheProxy implements ThirdPartyYouTubeLib {
private ThirdPartyYouTubeLib youtubeService;
private HashMap<String, Video> cachePopular = new HashMap<String, Video>();
private HashMap<String, Video> cacheAll = new HashMap<String, Video>();

public YouTubeCacheProxy() {
this.youtubeService = new ThirdPartyYouTubeClass();
}

@Override
public HashMap<String, Video> popularVideos() {
if (cachePopular.isEmpty()) {
cachePopular = youtubeService.popularVideos();
} else {
System.out.println("Retrieved list from cache.");
}
return cachePopular;
}

@Override
public Video getVideo(String videoId) {
Video video = cacheAll.get(videoId);
if (video == null) {
video = youtubeService.getVideo(videoId);
cacheAll.put(videoId, video);
} else {
System.out.println("Retrieved video '" + videoId + "' from cache.");
}
return video;
}

public void reset() {
cachePopular.clear();
cacheAll.clear();
}
}

downloader

downloader/YouTubeDownloader.java: 媒体下载应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package refactoring_guru.proxy.example.downloader;

import refactoring_guru.proxy.example.some_cool_media_library.ThirdPartyYouTubeLib;
import refactoring_guru.proxy.example.some_cool_media_library.Video;

import java.util.HashMap;

public class YouTubeDownloader {
private ThirdPartyYouTubeLib api;

public YouTubeDownloader(ThirdPartyYouTubeLib api) {
this.api = api;
}

public void renderVideoPage(String videoId) {
Video video = api.getVideo(videoId);
System.out.println("\n-------------------------------");
System.out.println("Video page (imagine fancy HTML)");
System.out.println("ID: " + video.id);
System.out.println("Title: " + video.title);
System.out.println("Video: " + video.data);
System.out.println("-------------------------------\n");
}

public void renderPopularVideos() {
HashMap<String, Video> list = api.popularVideos();
System.out.println("\n-------------------------------");
System.out.println("Most popular videos on YouTube (imagine fancy HTML)");
for (Video video : list.values()) {
System.out.println("ID: " + video.id + " / Title: " + video.title);
}
System.out.println("-------------------------------\n");
}
}

Demo.java: 初始化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package refactoring_guru.proxy.example;

import refactoring_guru.proxy.example.downloader.YouTubeDownloader;
import refactoring_guru.proxy.example.proxy.YouTubeCacheProxy;
import refactoring_guru.proxy.example.some_cool_media_library.ThirdPartyYouTubeClass;

public class Demo {

public static void main(String[] args) {
YouTubeDownloader naiveDownloader = new YouTubeDownloader(new ThirdPartyYouTubeClass());
YouTubeDownloader smartDownloader = new YouTubeDownloader(new YouTubeCacheProxy());

long naive = test(naiveDownloader);
long smart = test(smartDownloader);
System.out.print("Time saved by caching proxy: " + (naive - smart) + "ms");

}

private static long test(YouTubeDownloader downloader) {
long startTime = System.currentTimeMillis();

// User behavior in our app:
downloader.renderPopularVideos();
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderPopularVideos();
downloader.renderVideoPage("dancesvideoo");
// Users might visit the same page quite often.
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderVideoPage("someothervid");

long estimatedTime = System.currentTimeMillis() - startTime;
System.out.print("Time elapsed: " + estimatedTime + "ms\n");
return estimatedTime;
}
}

代理模式适合应用场景

使用代理模式的方式多种多样我们来看看最常见的几种

延迟初始化虚拟代理)。 如果你有一个偶尔使用的重量级服务对象一直保持该对象运行会消耗系统资源时可使用代理模式

你无需在程序启动时就创建该对象可将对象的初始化延迟到真正有需要的时候

访问控制保护代理)。 如果你只希望特定客户端使用服务对象这里的对象可以是操作系统中非常重要的部分而客户端则是各种已启动的程序包括恶意程序), 此时可使用代理模式

代理可仅在客户端凭据满足要求时将请求传递给服务对象

本地执行远程服务远程代理)。 适用于服务对象位于远程服务器上的情形

在这种情形中代理通过网络传递客户端请求负责处理所有与网络相关的复杂细节

记录日志请求日志记录代理)。 适用于当你需要保存对于服务对象的请求历史记录时

代理可以在向服务传递请求前进行记录

缓存请求结果缓存代理)。 适用于需要缓存客户请求结果并对缓存生命周期进行管理时特别是当返回结果的体积非常大时

代理可对重复请求所需的相同结果进行缓存还可使用请求参数作为索引缓存的键值

智能引用可在没有客户端使用某个重量级对象时立即销毁该对象

代理会将所有获取了指向服务对象或其结果的客户端记录在案代理会时不时地遍历各个客户端检查它们是否仍在运行如果相应的客户端列表为空代理就会销毁该服务对象释放底层系统资源

代理还可以记录客户端是否修改了服务对象其他客户端还可以复用未修改的对象

实现方式

  1. 如果没有现成的服务接口你就需要创建一个接口来实现代理和服务对象的可交换性从服务类中抽取接口并非总是可行的因为你需要对服务的所有客户端进行修改让它们使用接口备选计划是将代理作为服务类的子类这样代理就能继承服务的所有接口了

  2. 创建代理类其中必须包含一个存储指向服务的引用的成员变量通常情况下代理负责创建服务并对其整个生命周期进行管理在一些特殊情况下客户端会通过构造函数将服务传递给代理

  3. 根据需求实现代理方法在大部分情况下代理在完成一些任务后应将工作委派给服务对象

  4. 可以考虑新建一个构建方法来判断客户端可获取的是代理还是实际服务你可以在代理类中创建一个简单的静态方法也可以创建一个完整的工厂方法

  5. 可以考虑为服务对象实现延迟初始化

代理模式优缺点

  • 你可以在客户端毫无察觉的情况下控制服务对象

  • 如果客户端对服务对象的生命周期没有特殊要求你可以对生命周期进行管理

  • 即使服务对象还未准备好或不存在代理也可以正常工作

  • 开闭原则你可以在不对服务或客户端做出修改的情况下创建新代理

  • 代码可能会变得复杂因为需要新建许多类

  • 服务响应可能会延迟

与其他模式的关系

  • 适配器模式能为被封装对象提供不同的接口代理模式能为对象提供相同的接口装饰模式则能为对象提供加强的接口

  • 外观模式代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化*代理与其服务对象遵循同一接口使得自己和服务对象可以互换在这一点上它与外观*不同

  • 装饰代理有着相似的结构但是其意图却非常不同这两个模式的构建都基于组合原则也就是说一个对象应该将部分工作委派给另一个对象两者之间的不同之处在于*代理通常自行管理其服务对象的生命周期装饰*的生成则总是由客户端进行控制

SpringBoot优雅的对参数进行校验

什么是不优雅的参数校验

后端对前端传过来的参数也是需要进行校验的,如果在controller中直接校验需要用大量的if else做判断,以添加用户的接口为例,需要对前端传过来的参数进行校验, 如下的校验就是不优雅的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/user")
public class UserController {

@PostMapping("add")
public ResponseEntity<String> add(User user) {
if(user.getName()==null) {
return ResponseResult.fail("user name should not be empty");
} else if(user.getName().length()<5 || user.getName().length()>50){
return ResponseResult.fail("user name length should between 5-50");
}
if(user.getAge()< 1 || user.getAge()> 150) {
return ResponseResult.fail("invalid age");
}
// ...
return ResponseEntity.ok("success");
}
}

针对这个普遍的问题,Java开发者在Java API规范 (JSR303) 定义了Bean校验的标准validation-api,但没有提供实现。

hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。

Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。

接下来,我们以springboot项目为例,介绍Spring Validation的使用

实现案列

加入依赖
1
2
3
4
5
6
7
8
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
请求参数封装

对每个参数字段添加validation注解约束和message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.springbootvalidation.entity;


import com.example.springbootvalidation.group.AddUserGroup;
import com.example.springbootvalidation.group.EditUserGroup;
import jakarta.validation.constraints.*;
import lombok.Builder;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;

@Data
@Builder
public class User {
private static final long serialVersionUID = 1L;

@NotEmpty(message = "could not be empty"controller中的接口使用校验时使用分组)
private String userId;

@NotEmpty(message = "could not be empty")
@Email(message = "invalid email")
private String email;

@NotEmpty(message = "could not be empty")
@Pattern(regexp = "^(\\d{6})(\\d{4})(\\d{2})(\\d{2})(\\d{3})([0-9]|X)$", message = "invalid ID")
private String cardNo;

@NotEmpty(message = "could not be empty")
@Length(min = 1, max = 10, message = "nick name should be 1-10")
private String nickName;

@NotNull(message = "could not be empty")
@Range(min = 0, max = 1, message = "sex should be 0-1")
private int sex;

@Max(value = 100, message = "Please input valid age")
private int age;
}

Controller中获取参数绑定结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.springbootvalidation.controller;

import com.example.springbootvalidation.entity.ResponseResult;
import com.example.springbootvalidation.entity.User;
import com.example.springbootvalidation.group.AddUserGroup;
import com.example.springbootvalidation.group.EditUserGroup;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Slf4j
@RestController
public class UserController {
@PostMapping("/addUser")
public ResponseResult<String> add(@Validated @RequestBody User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<ObjectError> errors = bindingResult.getAllErrors();
errors.forEach(p -> {
FieldError fieldError = (FieldError) p;
log.error("Invalid Parameter : object - {},field - {},errorMessage - {}", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
});
return ResponseResult.fail("invalid parameter");
}
return ResponseResult.success();
}

校验结果

POST访问添加User的请求

输出结果,后台输出参数绑定错误信息:(包含哪个对象,哪个字段,什么样的错误描述)

1
2
3
4
2024-05-07T21:51:28.371+08:00 ERROR 34844 --- [spring-boot-validation] [nio-8080-exec-2] c.e.s.controller.UserController          : Invalid Parameter : object - user,field - cardNo,errorMessage - could not be empty
2024-05-07T21:51:28.371+08:00 ERROR 34844 --- [spring-boot-validation] [nio-8080-exec-2] c.e.s.controller.UserController : Invalid Parameter : object - user,field - userId,errorMessage - could not be empty
2024-05-07T21:51:28.371+08:00 ERROR 34844 --- [spring-boot-validation] [nio-8080-exec-2] c.e.s.controller.UserController : Invalid Parameter : object - user,field - nickName,errorMessage - could not be empty
2024-05-07T21:51:28.371+08:00 ERROR 34844 --- [spring-boot-validation] [nio-8080-exec-2] c.e.s.controller.UserController : Invalid Parameter : object - user,field - email,errorMessage - invalid email

分组校验

上面的例子中,其实存在一个问题,User既可以作为addUser的参数(id为空),又可以作为editUser的参数(id不能为空),这时候怎么办呢?分组校验登场

先定义分组(无需实现接口)
1
2
3
4
public interface EditUserGroup {
}
public interface AddUserGroup {
}
在User的userId字段添加分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class User implements Serializable {
private static final long serialVersionUID = 1L;

@NotEmpty(message = "could not be empty",groups = {EditUserGroup.class})
private String userId;

@NotEmpty(message = "could not be empty")
@Email(message = "invalid email")
private String email;

@NotEmpty(message = "could not be empty")
@Pattern(regexp = "^(\\d{6})(\\d{4})(\\d{2})(\\d{2})(\\d{3})([0-9]|X)$", message = "invalid ID")
private String cardNo;

@NotEmpty(message = "could not be empty")
@Length(min = 1, max = 10, message = "nick name should be 1-10")
private String nickName;

@NotNull(message = "could not be empty")
@Range(min = 0, max = 1, message = "sex should be 0-1")
private int sex;

@Max(value = 100, message = "Please input valid age")
private int age;
}
controller中的接口使用校验时使用分组,注意:需要使用@Validated注解
1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping("/editUser")
public ResponseResult<String> edit(@Validated({EditUserGroup.class}) @RequestBody User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<ObjectError> errors = bindingResult.getAllErrors();
errors.forEach(p -> {
FieldError fieldError = (FieldError) p;
log.error("Invalid Parameter : object - {},field - {},errorMessage - {}", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
});
return ResponseResult.fail("invalid parameter");
}
return ResponseResult.success();
}
测试
1
2024-05-07T22:05:32.148+08:00 ERROR 35269 --- [spring-boot-validation] [nio-8080-exec-2] c.e.s.controller.UserController          : Invalid Parameter : object - user,field - userId,errorMessage - could not be empty

@Validated和@Valid什么区别

在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:

分组

@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。

注解地方

@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上

@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上

嵌套类型

如果address是user的一个嵌套对象属性, 只能用@Valid

有哪些常用的校验

JSR303/JSR-349: JSR303是一项标准,只提供规范不提供实现,规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,位于javax.validation.constraints包下。JSR-349是其的升级版本,添加了一些新特性

@AssertFalse            被注释的元素只能为false
@AssertTrue             被注释的元素只能为true
@DecimalMax             被注释的元素必须小于或等于{value}
@DecimalMin             被注释的元素必须大于或等于{value}
@Digits                 被注释的元素数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
@Email                  被注释的元素不是一个合法的电子邮件地址
@Future                 被注释的元素需要是一个将来的时间
@FutureOrPresent        被注释的元素需要是一个将来或现在的时间
@Max                    被注释的元素最大不能超过{value}
@Min                    被注释的元素最小不能小于{value}
@Negative               被注释的元素必须是负数
@NegativeOrZero         被注释的元素必须是负数或零
@NotBlank               被注释的元素不能为空
@NotEmpty               被注释的元素不能为空
@NotNull                被注释的元素不能为null
@Null                   被注释的元素必须为null
@Past                   被注释的元素需要是一个过去的时间
@PastOrPresent          被注释的元素需要是一个过去或现在的时间
@Pattern                被注释的元素需要匹配正则表达式"{regexp}"
@Positive               被注释的元素必须是正数
@PositiveOrZero         被注释的元素必须是正数或零
@Size                   被注释的元素个数必须在{min}和{max}之间

hibernate validation:hibernate validation是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等

@CreditCardNumber       被注释的元素不合法的信用卡号码
@Currency               被注释的元素不合法的货币 (必须是{value}其中之一)
@EAN                    被注释的元素不合法的{type}条形码
@Email                  被注释的元素不是一个合法的电子邮件地址  (已过期)
@Length                 被注释的元素长度需要在{min}和{max}之间
@CodePointLength        被注释的元素长度需要在{min}和{max}之间
@LuhnCheck              被注释的元素${validatedValue}的校验码不合法, Luhn模10校验和不匹配
@Mod10Check             被注释的元素${validatedValue}的校验码不合法, 模10校验和不匹配
@Mod11Check             被注释的元素${validatedValue}的校验码不合法, 模11校验和不匹配
@ModCheck               被注释的元素${validatedValue}的校验码不合法, ${modType}校验和不匹配  (已过期)
@NotBlank               被注释的元素不能为空  (已过期)
@NotEmpty               被注释的元素不能为空  (已过期)
@ParametersScriptAssert 被注释的元素执行脚本表达式"{script}"没有返回期望结果
@Range                  被注释的元素需要在{min}和{max}之间
@SafeHtml               被注释的元素可能有不安全的HTML内容
@ScriptAssert           被注释的元素执行脚本表达式"{script}"没有返回期望结果
@URL                    被注释的元素需要是一个合法的URL
@DurationMax            被注释的元素必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
@DurationMin            被注释的元素必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}

spring validation:spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中

自定义validation

如果上面的注解不能满足我们检验参数的要求,我们能不能自定义校验规则呢? 可以

定义注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.springbootvalidation.annotation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {TelephoneNumberValidator.class}) // 指定校验器
public @interface TelephoneNumber {
String message() default "Invalid telephone number";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
定义校验器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.springbootvalidation.annotation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.regex.Pattern;

public class TelephoneNumberValidator implements ConstraintValidator<TelephoneNumber, String> {
private static final String REGEX_TEL = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|13[0-9]\\d{8}|15[1089]\\d{8}";

@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
try {
return Pattern.matches(REGEX_TEL,s);
} catch (Exception e) {
return false;
}
}
}

使用

1
2
3
   @TelephoneNumber(message = "invalid telephone number") // 这里
private String telephone;
}

源码地址https://github.com/Breeze1203/JavaAdvanced/tree/main/springboot-demo/spring-boot-validation

Spring Security 异常处理

认证【登录】失败

1、用户名找不到

当我们登录的时候,如果用户名找不到抛出出:UsernameNotFoundException,可以被拦截LoginFailureHandler因为UsernameNotFoundException继承自:AuthenticationException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.boot.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.boot.entity.Perm;
import com.boot.entity.User;
import com.boot.mapper.PermMapper;
import com.boot.mapper.UserMapper;
import com.boot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

@Autowired
UserMapper userMapper;

@Autowired
PermMapper permMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("username",username);
User user = userMapper.selectOne(queryWrapper);

if(user == null){
throw new UsernameNotFoundException("用户未找到");
}

//根据用户名查找权限
QueryWrapper<Perm> permQueryWrapper = new QueryWrapper();
permQueryWrapper.eq("user_id",user.getId());

List<Perm> perms = permMapper.selectList(permQueryWrapper);

//权限标识
List<String> permTags = perms.stream().map(Perm::getTag).collect(Collectors.toList());

user.setAuthorities(AuthorityUtils.createAuthorityList(permTags));

return user;
}
}

2、密码错误异常
this.getAuthenticationManager().authenticate(authRequest)中抛出org.springframework.security.authentication.BadCredentialsException: 用户名或密码错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.boot.security;

import com.boot.entity.User;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import lombok.SneakyThrows;

import org.springframework.security.authentication.AuthenticationServiceException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.io.BufferedReader;

import java.io.IOException;

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

@SneakyThrows

@Override

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

if (!request.getMethod().equals("POST")) {

throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());

}

String username = request.getParameter("username");

String password = request.getParameter("password");

UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,password);

return this.getAuthenticationManager().authenticate(authRequest);

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.boot.security;

import jakarta.servlet.ServletException;

import jakarta.servlet.http.HttpServletRequest;

import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import java.io.IOException;

public class LoginFailureHandler implements AuthenticationFailureHandler {

@Override

public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

System.out.println("登录异常信息:");

System.out.println("exception = " + exception);

}

}

授权失败【没有操作权限

引言

数据库索引,绝对是MySQL的核心功能之一,如果没有索引机制的数据库,那数据的检索效率绝对是令人无法接受的,毕竟没有索引的表数据,就如同一个普通的文本文件存储在磁盘中。在《索引上篇》中,我们对于MySQL提供的索引机制,从引入,到创建、使用、分类、管理….等进行了全面阐述,相信经过上一篇的讲解后,大家对MySQL索引机制建立了系统化的认知,而本篇则会以上篇为基础,对索引机制进一步加深掌握。

不过在上篇中虽对数据库索引机制有了完善认知,但还不够,因为上篇仅是单纯的认知阶段,能否真正的在实际项目中运用好索引机制,还需要具备丰富的经验以及一些原则与方法论,比如下述一些关于索引的问题:

  • 索引虽然能给MySQL检索数据的效率带来质的飞跃,但加入索引没有带来新问题吗?

  • 既然索引能够提升查询性能,那是不是为表中每个字段建立索引,性能会更好?

  • •一张数据表中,哪些类型的字段不适合建立索引呢?又是因为什么原因呢?

  • 表中会存在大量的字段,但其中哪些字段建立索引才能够最大的性能收益呢?

  • MySQL提供的索引种类也不少,一个字段上建立什么类型的索引才最好呢?

  • 当表中存在多个索引时,一条查询SQL有多条路径可走,此时走哪条索引最好?

对于这些问题,如果仅靠上篇索引的知识,相信是很难回答具体的,那在本篇中,则重点讲解索引应用相关的方式方法,例如各索引优劣分析、建立索引的原则、使用索引的指南以及索引失效与索引优化等内容。

一、MySQL各索引的优劣分析

首先来聊聊索引机制带来的利害关系,有句古话曾说过:“凡事有利必有弊”,而MySQL的索引机制也不例外,引入索引机制后,能够给数据库带来的优势很明显:

  • 整个数据库中,数据表的查询速度直线提升,数据量越大时效果越明显。

  • 通过创建唯一索引,可以确保数据表中的数据唯一性,无需额外建立唯一约束。

  • 在使用分组和排序时,同样可以显著减少SQL查询的分组和排序的时间。

  • 连表查询时,基于主外键字段上建立索引,可以带来十分明显的性能提升。

  • 索引默认是B+Tree有序结构,基于索引字段做范围查询时,效率会明显提高。

  • 从MySQL整体架构而言,减少了查询SQL的执行时间,提高了数据库整体吞吐量。

看着上面一条又一条的好处,似乎感觉索引好处很大啊,对于这点确实毋庸置疑,但只有好处吗?No,同时也会带来一系列弊端,如:

  • 建立索引会生成本地磁盘文件,需要额外的空间存储索引数据,磁盘占用率会变高。

  • 写入数据时,需要额外维护索引结构,增、删、改数据时,都需要额外操作索引。

  • 写入数据时维护索引需要额外的时间开销,执行写SQL时效率会降低,性能会下降。

当然,但对数据库整体来说,索引带来的优势会大于劣势。不过也正由于索引存在弊端,因此索引不是越多越好,合理建立索引才是最佳选择。

1.1、主键索引存在的陷阱

相信大家数据库的表中,主键一般都是使用自增ID,但这是为什么呢?有人可能会回答自增ID不会重复,确保了主键唯一性。这样也确实没错,但不会重复的又不仅仅只有自增ID,比如我使用随机的UUID也不会重复,为何不使用UUID呢?这是由于索引存在一个陷阱!

众所周知,一张表中大多数情况下,会将主键索引以聚簇的形式存在磁盘中,上篇文章也聊到过,聚簇索引在存储数据时,表数据和索引数据是一起存放的。同时,MySQL默认的索引结构是B+Tree,也就代表着索引节点的数据是有序的。

此时结合上面给出的一些信息,主键索引是聚簇索引,表数据和索引数据在一块、索引结构是有序的,那再反推前面给出的疑惑,为何不使用UUID呢?因为UUID是无序的,如果使用UUID作为主键,那么每当插入一条新数据,都有可能破坏原本的树结构,如下:

* 索引维护

比如上图中的灰色节点,是一条新插入的数据,此时经过计算后,应该排第二个位置,那就代表着后面的三个节点需要移动,然后给灰色节点挪出一个位置存储,从而确保索引的有序性。

由于主键索引是聚簇索引,因此上述案例中,当后续节点需要挪动时,也就代表着还需要挪动表数据,如果是偶尔需要移动还行,但如果主键字段值无序,那代表着几乎每次插入都有可能导致树结构要调整。但使用自增ID就不会有这个问题,所有新插入的数据都会放到最后。

因此大家数据表的主键,最好选用带顺序性的值,否则有可能掉入主键索引的“陷阱”中。

1.2、联合索引存在的矛盾

为了多条件查询时的效率更高,一般都会同时对多个字段建立联合索引,但之前也聊到过,联合索引存在一个致命的问题,比如在用户表中,通过id、name、age三个字段建立一个联合索引,此时来了一条查询SQL,如下:

1
SELECT * FROM zz_user WHERE name = "竹子" AND age = "18";

而这条SQL语句是无法使用联合索引的,为什么呢?因为查询条件中,未包含联合索引的第一个字段,想要使用联合索引,那么查询条件中必须包含索引的第一个字段,如下:

1
SELECT * FROM zz_user WHERE name = "竹子" AND id = 6;

上面这条SQL才是能命中多列索引的语句,因此在建立索引时也需要考虑这个问题,确保建立出的联合索引能够命中率够高。

1.3、前缀索引存在的弊端

前缀索引的特点是短小精悍,我们可以利用一个字段的前N个字符创建索引,以这种形式创建的索引也被称之为前缀索引,相较于使用一个完整字段创建索引,前缀索引能够更加节省存储空间,当数据越多时,带来的优势越明显。

不过前缀索引虽然带来了节省空间的好处,但也正由于其索引节点中,未存储一个字段的完整值,所以MySQL也无法通过前缀索引来完成ORDER BY、GROUP BY等分组排序工作,同时也无法完成覆盖扫描等操作。

1.4、全文索引存在的硬伤

之前做模糊查询时,通常都会使用like%语法,不过这种方式虽然能够实现效果,但随着表越来越大,数据越来越多时,其性能会出现明显下降,而全文索引的推出则能够完美解决该问题,可以利用全文索引代替like%语法实现模糊查询,它的性能会比like%快上N倍。

全文索引虽然可以实现模糊查询,但也存在一系列硬伤,一起来看看。

①由于全文索引是基于分词实现的,所以对一个字段建立全文索引后,MySQL会对该字段做分词处理,这些分词结果也会被存储在全文索引中,因此全文索引的文件会额外的大!

②由于全文索引对每个字段值都会做分词,因此当修改字段值后,分词是需要时间的,所以修改字段数据后不会立马自动更新全文索引,此时需要咱们写存储过程,并调用它手动更新全文索引中的数据。

③除开上述两点外,全文索引最大的硬伤在于对中文支持不够友好,类似于英文可以直接通过符号、空格来分词,但中文呢?一个词语来形容就是博大精深,无法精准的对一段文字做分词,因此全文索引在检索中文时,存在些许精准度问题。

因此如果你项目规模较大,通常再引入ElasticSearch、Solr、MeiliSearch等搜索引擎是一个更佳的选择。

1.5、唯一索引存在的快慢问题

唯一索引有个很大的好处,就是查询数据时会比普通索引效率更高,因为基于普通索引的字段查询数据,例如:

1
SELECT * FROM TABLE_XX WHERE COLUMN_XX = "XX";

假设COLUMN_XX字段上建立了一个普通索引,此时基于这个字段查询数据时,当查询到一条COLUMN_XX = "XX"的数据后,此时会继续走完整个索引树,因为可能会存在多条字段值相同的数据。

因此唯一索引查询数据时,会比普通索引快上一截,但插入数据时就不同了,因为要确保数据不重复,所以插入前会检查一遍表中是否存在相同的数据。但普通索引则不需要考虑这个问题,因此普通索引的数据插入会快一些。

1.6、哈希索引的致命问题

哈希索引,也就是数据结构为Hash类型的索引,不过估计大家接触的比较少,毕竟创建索引时都默认用的B+树结构。但要比起查询速度,哈希索引绝对是MySQL中当之无愧的魁首!因为采用哈希结构的索引,会以哈希表的形式存储索引字段值,当基于该字段查询数据时,只需要经过一次哈希计算就可获取到数据。

但哈希结构的致命问题在于无序,也就是无法基于哈希索引的字段做排序、分组等工作。

因此如果你确定一个表中,不会做排序这类的工作,那可以适当选用哈希结构作为索引的数据结构,它会给你带来意想不到的性能收益~

二、建立索引的正确姿势

经过上述一系列分析后,简单讲明了每种索引类型存在的缺陷问题,但这跟我们本篇有啥关系呢?其实关系很大,因为只有当你了解了每种索引存在的劣势,才能更好的考虑并设计出合理的索引,而不是一股脑的盲目创建索引。

在实际项目场景中,当SQL查询性能较慢时,我们常常会有一个疑惑:表中哪个字段建立一个索引能带来最大的性能收益呢?一般来说,判断字段是否要添加的索引的依据,是看这个字段是否被经常当做查询条件使用,但也不能光依靠这一个依据来判断,比如用户表中的性别字段,就会经常被用做查询条件,但如果对性别字段建立一个索引,那对查询的性能提升并不大,因为性别就两个值:男/女(不包含泰国在内),那对其建立索引,索引文件中就只会有两个索引节点,大致情况如下:

* 性别索引

这种情况下,为性别建立一个索引,带来的性能收益显然不是太大。同时,上图中给出的案例,也不是索引真正的样子,如果表中存在主键索引或聚簇索引,对其他字段建立的索引,都是次级索引,也被称为辅助索引,其节点上的值,存储的并非一条完整的行数据,而是指向聚簇索引的索引字段值。

如果基于辅助索引查询数据,最终数据会以何种方式被检索出来,这里就牵扯到MySQL中的一个新概念,也就是SQL执行时的回表问题。

2.1、索引查询时的回表问题

什么叫做回表呢?意思就是指一条SQL语句在MySQL内部,要经过两次查询过程才能获取到数据。这是跟索引机制有关的,先来看看索引在MySQL内部真正的面貌:

* 表中索引结构

在上图用户表中,基于ID字段先建立了一个主键索引,然后又基于name字段建立了一个普通索引,此时MySQL默认会选用主键索引作为聚簇索引,将表数据和主键索引存在同一个文件中,也就是主键索引的每个索引节点,都直接对应着行数据。而基于name字段建立的索引,其索引节点存放的则是指向聚簇索引的ID值。

在这种情况下,假设有一条下述SQL,其内部查询过程是啥样的呢?

1
SELECT * FROM zz_user WHERE name = "子竹";

首先会走name字段的索引,然后找到对应的ID值,然后再基于查询到的ID值,再走ID字段的主键索引,最终得到一整条行数据并返回。

在这个案例中,一条查询SQL经历了两次查询才获取到数据,这个过程则被称之为回表。

回表动作会导致额外的查询开销,因此尽量可以基于主键做查询,如果实在需要使用非主键字段查询,那么尽量要写明查询的结果字段,而并非使用*

当然,实际情况中建立联合索引,利用索引覆盖特性,从而避免使用辅助索引,这样也能够消除回表动作,但关于这点后面再聊,先来说说建立索引需要遵循的一些原则。

2.2、建立索引时需要遵守的原则

前面说过一点,当建立索引仅考虑一个字段是否被经常用于查询是不够的,往往一个合适的索引需要更为细致与长远的思考,例如使用多个字段建立是否会更好?创建其他类型的索引性能是否会更佳?下面我们就一起来看看建立索引时,需要遵守的一些原则:

  • 经常频繁用作查询条件的字段应酌情考虑为其创建索引。

  • 表的主外键或连表字段,必须建立索引,因为能很大程度提升连表查询的性能。

  • 建立索引的字段,一般值的区分性要足够高,这样才能提高索引的检索效率。

  • 建立索引的字段,值不应该过长,如果较长的字段要建立索引,可以选择前缀索引。

  • 建立联合索引,应当遵循最左前缀原则,将多个字段之间按优先级顺序组合。

  • 经常根据范围取值、排序、分组的字段应建立索引,因为索引有序,能加快排序时间。

  • 对于唯一索引,如果确认不会利用该字段排序,那可以将结构改为Hash结构。

  • 尽量使用联合索引代替单值索引,联合索引比多个单值索引查询效率要高。

同时,除开上述一些建立索引的原则外,在建立索引时还需有些注意点:

  • 值经常会增删改的字段,不合适建立索引,因为每次改变后需维护索引结构。

  • 一个字段存在大量的重复值时,不适合建立索引,比如之前举例的性别字段。

  • 索引不能参与计算,因此经常带函数查询的字段,并不适合建立索引。

  • 一张表中的索引数量并不是越多越好,一般控制在3,最多不能超过5。

  • 建立联合索引时,一定要考虑优先级,查询频率最高的字段应当放首位。

  • 当表的数据较少,不应当建立索引,因为数据量不大时,维护索引反而开销更大。

  • 索引的字段值无序时,不推荐建立索引,因为会造成页分裂,尤其是主键索引。

对于索引机制,在建立时应当参考上述给出的意见,这每一条原则都是从实际经验中总结出来的,前面八条不一定要全面思考,但后面七条注意点,一定要牢记,如若你的索引符合后面七条中的描述,那一定要更改索引。

对于每一条建议是为什么,在后面的《索引原理篇》讲完之后大家就会彻底理解,这里就不展开叙述了,接下来重点聊一下联合索引,以及它的最左前缀原则。

2.3、联合索引的最左前缀原则

首先在讲最左前缀原则之前,先看看上述给出的一条原则:

  • 尽量使用联合索引代替单值索引,联合索引比多个单值索引查询效率要高。

对于这一点是为什么呢?举个栗子理解,比如此时基于X、Y、Z字段建立了一个联合索引,实际上也相当于建立了三个索引:XX、YX、Y、Z,因此只要查询中使用了这三组字段,都可以让联合索引生效。

但如若查询中这三个字段不以AND形式出现,而是单独作为查询条件出现,那单值索引性能会好一些,但三个不同的索引,维护的代价也会高一些。

其实联合索引的最左前缀原则,道理很简单的,就是组成联合索引的多个列,越靠左边优先级越高,同时也只有SQL查询条件中,包含了最左的字段,才能使用联合索引,例如:

1
SELECT * FROM tb WHERE Y = "..." AND Z = "...";

上面这条SQL就显然并不会使用联合索引,因为不符合最左前缀原则,最左侧的X字段未曾被使用。也正由于MySQL在使用联合索引时会遵循最左前缀原则,所以才在前面建立索引的建议中给出了一条:

  • 建立联合索引时,一定要考虑优先级,查询频率最高的字段应当放首位。

因为将查询频率越高的字段放首位,就代表着查询时命中索引的几率越大。同时,MySQL的最左前缀原则,在匹配到范围查询时会停止匹配,比如>、<、between、like这类范围条件,并不会继续使用联合索引,举个栗子:

1
SELECT * FROM tb WHERE X = "..." AND Y > "..." AND Z = "...";

当执行时,虽然上述SQL使用到X、Y、Z作为查询条件,但由于Y字段是>范围查询,因此这里只能使用X索引,而不能使用X、YX、Y、Z索引。

最后再来一个简单的栗子,加深一下对于联合索引的认知:

1
2
3
4
5
6
7
8
9
10
11
-- 查询名字为'竹子'的用户
SELECT * FROM user WHERE name = '竹子';

-- 查询名字为'竹子'且年龄为18的用户
SELECT * FROM user WHERE name = '竹子' AND age = 18;

-- 创建索引,仅针对name列
CREATE INDEX index_name ON user(name);

-- 创建索引,针对name和age两列
CREATE INDEX index_name ON user(name, age);

比如上述这个案例中,对于这两条SQL选第一种方式创建索引,还是第二种呢?答案是B,因为两条sql完全能够利用到第二个创建的联合索引。

1
2
3
4
5
-- 查询名字为'竹子'且年龄为18的用户
SELECT * FROM user WHERE name = '竹子' AND age = 18;

-- 查询年龄为18且名字为'竹子'的用户
SELECT * FROM user WHERE age = 18 AND name = '竹子';

同时选B建立联合索引后,如上两条SQL都会利用到上面创建的联合索引,SQL是否走索引查询跟where后的条件顺序无关,因为MySQL优化器会优化,对SQL查询条件进行重排序。

三、索引失效与使用索引的正确姿势

相信这一点大家看了有些懵,啥叫使用索引的正确姿势?索引不是MySQL执行SQL时自动选择的吗?我们只能建立索引,怎么使用啊?其实这里是指我们编写SQL时,要注意的点,毕竟MySQL查询时到底使不使用索引,这完全取决于你编写的SQL

但很多小伙伴在平时写SQL的时候,一般只追求实现业务功能,只要能够查询出相应的数据即可,压根不会过度考虑这条SQL应用到索引,那么这里就是给出一些经验之谈,讲清楚几点写SQL时的方法论。

其实索引本身是一把双刃剑,用的好能够给我们带来异乎寻常的查询效率,用的不好则反而会带来额外的磁盘占用及写入操作时的维护开销。因此大家一定要切记,既然选择建了索引,那一定要利用它,否则还不如干脆别建,既能节省磁盘空间,又能提升写入效率。

3.1、索引失效的那些事儿

想要用好索引,那一定要先搞清楚那些情况会导致索引失效,弄明白这些事项之后,在写SQL的时候刻意避开,那你写出来的SQL十有八九是会用到索引的,那么在数据库中那些情况下会导致索引失效呢?下面一起来聊一聊,但单纯的讲概念会有种纸上谈兵的感觉,因此下面简单的举个案例,然后来说明索引失效的一些情况。

1
2
3
4
5
6
7
8
9
10
11
SELECT***FROM*zz_users;
+---------+-----------+----------+----------+---------------------+
|*user_id*|*user_name*|*user_sex*|*password*|*register_time*******|
+---------+-----------+----------+----------+---------------------+
|*******1*|*熊猫******|********|*6666*****|*2022-08-14*15:22:01*|
|*******2*|*竹子******|********|*1234*****|*2022-09-14*16:17:44*|
|*******3*|*子竹******|********|*4321*****|*2022-09-16*07:42:21*|
+---------+-----------+----------+----------+---------------------+

ALTER TABLE zz_users ADD PRIMARY KEY p_user_id(user_id);
ALTER TABLE zz_users ADD KEY unite_index(user_name, user_sex, password);

此时对这张用户表,分别创建两个索引,第一个是基于user_id创建的主键索引,第二个是使用user_name、user_sex、password三个字段创建的联合索引。

3.1.1、查询中带有OR会导致索引失效

1
EXPLAIN SELECT * FROM zz_users WHERE user_id = 1 OR user_name = "熊猫";

例如上述这条SQL,其中既包含了主键索引的字段,又包含了联合索引的第一个字段,按理来说是会走索引查询的对吗?但看看执行结果:

* or导致索引失效

从结果中可看到type=ALL,显然并未使用索引来查询,也就代表着,虽然所有查询条件都包含了索引字段,但由于使用了OR,最终导致索引失效。

3.1.3、模糊查询中like以%开头导致索引失效

众所周知,使用like关键字做模糊查询时,是可以使用索引的,那来看看下述这条SQL

1
EXPLAIN SELECT * FROM zz_users WHERE user_name LIKE "%熊";

在这条SQL中以联合索引中的第一个字段作为了查询条件,此时会使用索引吗?看看结果:

* like %*导致索引失效

结果中显示依旧走了全表扫描,并未使用索引,但like不以%开头,实际上是不会导致索引失效的,例如:

* %结尾

在这里以%结尾,其实可以使用联合索引来检索数据,并不会导致索引失效。

3.1.4、字符类型查询时不带引号导致索引失效

1
2
3
INSERT INTO zz_users VALUES(4, "1111", "男", "4321", "2022-09-17 23:48:29");

EXPLAIN SELECT * FROM zz_users WHERE user_name = '111';

上述这条SQL按理来说是没有半点问题的,目前是符合联合索引的最左匹配原则的,但来看看结果:

* 不带引号对比

从结果中很明显的可以看出,由于user_name是字符串类型的,因此查询时没带引号,竟然直接未使用索引,导致了索引失效(上面也放了对比图,大家可以仔细看看区别)。

3.1.5、索引字段参与计算导致索引失效

1
EXPLAIN SELECT * FROM zz_users WHERE user_id - 1 = 1;

上面这条SQL看着估计有些懵,但实际上很简单,就是查询ID=2的数据,理论上因为查询条件中使用了主键字段,应该会使用主键索引,但结果呢?

* 索引字段参与计算

由于索引字段参与了计算,所以此时又导致了索引失效,因此大家要切记,千万不要让索引字段在SQL中参与计算,也包括使用一些聚合函数时也会导致索引失效,其根本原因就在于索引字段参与了计算导致的。

这里的运算也包括+、-、*、/、!.....等一系列涉及字段计算的逻辑。

3.1.6、字段被用于函数计算导致索引失效

1
EXPLAIN SELECT * FROM zz_users WHERE SUBSTRING(user_name, 0, 1) = "竹子";

上述中,我们使用SUBSTRING函数对user_name字段进行了截取,然后再用于条件查询,此时看看执行结果:

* 函数计算

很显然,并未使用索引查询,这也是意料之中的事情,毕竟这一条和3.1.5的原因大致相同,索引字段参与计算导致失效。

3.1.7、违背最左前缀原则导致索引失效

1
EXPLAIN SELECT * FROM zz_users WHERE user_sex = "男" AND password = "1234";

上述这条SQL中,显然用到了联合索引中的性别和密码字段,此时再看看结果:

* 违背最左匹配

由于违背了联合索引的最左前缀原则,因为没使用最左边的user_name字段,因此也导致索引失效,从而走了全表查询。

3.1.8、不同字段值对比导致索引失效

从一张表中查询出一些值,然后根据这些值去其他表中筛选数据,这个业务也是实际项目中较为常见的场景,下面为了简单实现,就简单用姓名和性别模拟一下字段对比的场景:

1
EXPLAIN SELECT * FROM zz_users WHERE user_name = user_sex;

按理来说,因为user_name属于联合索引的第一个字段,所以上述这条SQL中规中矩,理论上会走索引的,但看看结果:

* 字段对比

显然,这个场景也会导致索引无法使用,因此之后也要切记这点。

3.1.9、反向范围操作导致索引失效

一般来说,如果SQL属于正向范围查询,例如>、<、between、like、in...等操作时,索引是可以正常生效的,但如果SQL执行的是反向范围操作,例如NOT IN、NOT LIKE、IS NOT NULL、!=、<>...等操作时,就会出现问题,例如:

1
EXPLAIN SELECT * FROM zz_users WHERE user_id NOT IN (1, 2, 3);

上述SQL的意思很简单,也就是查询user_id不是1,2,3的数据,这里是基于主键索引字段user_id查询的,但会走索引吗?来看看结果:

* 范围查询对比

结果也很明显,使用NOT关键字做反向范围查询时,并不会走索引,索引此时失效了,但是做正向范围查询时,索引依旧有效。

对于这一点,其实大家可以慢慢实验,并非所有的正向范围操作都会走索引,例如IS NULL就不会走,它的反向操作:IS NOT NULL同样不会走。

3.1.10、索引失效小结

MySQL中还有一种特殊情况会导致索引失效,也就是当走索引扫描的行数超过表行数的30%时,MySQL会默认放弃索引查询,转而使用全表扫描的方式检索数据,因此这种情况下走索引的顺序磁盘IO,反而不一定有全表的随机磁盘IO快。

还有一点要牢记:关于索引是否会失效,实际上也跟索引的数据结构、MySQL的版本、存储引擎的不同有关,例如一条SQL语句在B+Tree索引中会导致索引失效,但在哈希索引中却不会(好比IS NULL/IS NOT NULL),这种情况在不同版本、不同引擎中都有可能会体现出来。

但到目前为止,大致上已经将MySQL中会导致索引失效的几种情况罗列说明了,接下来一起看看使用索引的正确姿势!

3.2、使用索引的正确姿势

其实到这里,对于如何使用索引才是正确的呢?总结如下:

  • 查询SQL中尽量不要使用OR关键字,可以使用多SQL或子查询代替。

  • 模糊查询尽量不要以%开头,如果实在要实现这个功能可以建立全文索引。

  • 编写SQL时一定要注意字段的数据类型,否则MySQL的隐式转换会导致索引失效。

  • 一定不要在编写SQL时让索引字段执行计算工作,尽量将计算工作放在客户端中完成。

  • 对于索引字段尽量不要使用计算类函数,一定要使用时请记得将函数计算放在=后面。

  • 多条件的查询SQL一定要使用联合索引中的第一个字段,否则会打破最左匹配原则。

  • 对于需要对比多个字段的查询业务时,可以拆分为连表查询,使用临时表代替。

  • 在SQL中不要使用反范围性的查询条件,大部分反范围性、不等性查询都会让索引失效。

实际上无非就是根据前面给出的索引失效情况,尽量让自己编写的SQL不会导致索引失效即可,写出来的SQL能走索引查询,那就能在很大程度上提升数据检索的效率。

接下来再重点讲几个较重要的内容,既索引覆盖、索引下推、Multi-Range Read机制、索引跳跃式扫描机制。

3.2.1、索引覆盖

在之前聊到过,由于表中只能存在一个聚簇索引,一般都为主键索引,而建立的其他索引都为辅助索引,包括联合索引也例外,最终索引节点上存储的都是指向主键索引的值,拿前面的用户表为例:

1
SELECT * FROM zz_users WHERE user_name="竹子" AND user_sex="男";

虽然这条SQL会走联合索引查询,但是基于联合索引查询出来的值仅是一个指向主键索引的ID,然后会拿着这个ID再去主键索引中查一遍,这个过程之前聊过,被称为回表过程。

那么回表问题无法解决吗?必须得经过两次查询才能得到数据吗?答案并非如此。

比如假设此时只需要user_name、user_sex、password这三个字段的信息,此时SQL语句可以更改为如下情况:

1
SELECT user_name, user_sex, password FROM zz_users WHERE user_name = "竹子" AND user_sex = "男";

此时将SQL更改为查询所需的列后,就不会发生回表现象,Why?再这里很多小伙伴可能会疑惑,这是什么道理啊?因为此时所需的user_name、user_sex、password三个字段数据,在联合索引中完全包含,因此可以直接通过联合索引获取到数据。

但如果查询时用*,因为联合索引中不具备完整的一行数据,只能再次转向聚簇索引中获取完整的行数据,因此到这里大家应该也明白了为什么查询数据时,不能用*的原因,这是因为会导致索引覆盖失效,造成回表问题。

当然,再来提一点比较有意思的事情,先看SQL

1
EXPLAIN SELECT user_name, user_sex FROM zz_users WHERE password = "1234" AND user_sex = "男";

比如上述这条SQL,显然是不符合联合索引的最左前缀匹配原则的,但来看看执行结果:

* 索引覆盖

这个结果是不是很令你惊讶,通过EXPLAIN分析的结果显示,这条SQL竟然使用了索引,这是什么原因呢?也是因为索引覆盖。

一句话概述:就是要查询的列,在使用的索引中已经包含,被所使用的索引覆盖,这种情况称之为索引覆盖。

3.2.2、索引下推

索引下推是MySQL5.6版本以后引入的一种优化机制,还是以之前的用户表为例,先来看一条SQL语句:

1
2
3
INSERT INTO zz_users VALUES(5, "竹竹", "女", "8888", "2022-09-20 22:17:21");

SELECT * FROM zz_users WHERE user_name LIKE "竹%" AND user_sex = "男";

首先为了更加直观的讲清楚索引下推,因此先再向用户表中增加一条数据。然后再来看看后面的查询SQL,这条SQL会使用联合索引吗?答案是会的,但只能部分使用,因为联合索引的每个节点信息大致如下:

1
2
3
4
5
6
7
{
****["熊猫","女","6666"]*:*1,
****["竹子","男","1234"]*:*2,
****["子竹","男","4321"]*:*3,
****["1111","男","4321"]*:*4,
****["竹竹","女","8888"]*:*5
}

由于前面使用的是模糊查询,但%在结尾,因此可以使用这个字作为条件在联合索引中查询,整个查询过程如下:

  • 利用联合索引中的user_name字段找出「竹子、竹竹」两个索引节点。

  • 返回索引节点存储的值「2、5」给Server层,然后去逐一做回表扫描。

  • 在Server层中根据user_sex=”男”这个条件逐条判断,最终筛选到「竹子」这条数据。

有人或许会疑惑,为什么user_sex="男"这个条件不在联合索引中处理呢?因为前面是模糊查询,所以拼接起来是这样的:竹x男,由于这个x是未知的,因此无法根据最左前缀原则去匹配数据,最终这里只能使用联合索引中user_name字段的一部分,后续的user_sex="男"还需要回到Server层处理。

那什么又叫做索引下推呢?也就是将Server层筛选数据的工作,下推到引擎层处理。

以前面的案例来讲解,MySQL5.6加入索引下推机制后,其执行过程是什么样子的呢?

  • 利用联合索引中的user_name字段找出「竹子、竹竹」两个索引节点。

  • 根据user_sex=”男”这个条件在索引节点中逐个判断,从而得到「竹子」这个节点。

  • 最终将「竹子」这个节点对应的「2」返回给Server层,然后聚簇索引中回表拿数据。

相较于没有索引下推之前,原本需要做「2、5」两次回表查询,但在拥有索引下推之后,仅需做「2」一次回表查询。

索引下推在MySQL5.6版本之后是默认开启的,可以通过命令set optimizer_switch='index_condition_pushdown=off|on';命令来手动管理。

3.2.3、MRR(Multi-Range Read)机制

Multi-Range Read简称为MRR机制,这也是和索引下推一同在MySQL5.6版本中引入的性能优化措施,那什么叫做MRR优化呢?

一般来说,在实际业务中我们应当尽量通过索引覆盖的特性,减少回表操作以降低IO次数,但在很多时候往往又不得不做回表才能查询到数据,但回表显然会导致产生大量磁盘IO,同时更严重的一点是:还会产生大量的离散IO,下面举个例子来理解。

1
SELECT * FROM zz_student_score WHERE score BETWEEN 0 AND 59;

上述这条SQL所做的工作很简单,就是在学生成绩表中查询所有成绩未及格的学生信息,假设成绩字段上存在一个普通索引,那思考一下,这条SQL的执行流程是什么样的呢?

  • 先在成绩字段的索引上找到0分的节点,然后拿着ID去回表得到成绩零分的学生信息。

  • 再次回到成绩索引,继续找到所有1分的节点,继续回表得到1分的学生信息。

  • 再次回到成绩索引,继续找到所有2分的节点……

  • 周而复始,不断重复这个过程,直到将0~59分的所有学生信息全部拿到为止。

那此时假设此时成绩0~5分的表数据,位于磁盘空间的page_01页上,而成绩为5~10分的数据,位于磁盘空间的page_02页上,成绩为10~15分的数据,又位于磁盘空间的page_01页上。此时回表查询时就会导致在page_01、page_02两页空间上来回切换,但0~5、10~15分的数据完全可以合并,然后读一次page_01就可以了,既能减少IO次数,同时还避免了离散IO

MRR机制就主要是解决这个问题的,针对于辅助索引的回表查询,减少离散IO,并且将随机IO转换为顺序IO,从而提高查询效率。

MRR机制具体是怎么做的呢?MRR机制中,对于辅助索引中查询出的ID,会将其放到缓冲区的read_rnd_buffer中,然后等全部的索引检索工作完成后,或者缓冲区中的数据达到read_rnd_buffer_size大小时,此时MySQL会对缓冲区中的数据排序,从而得到一个有序的ID集合:rest_sort,最终再根据顺序IO去聚簇/主键索引中回表查询数据。

可以通过上述这条命令开启或关闭MRR机制,MySQL5.6及以后的版本是默认开启的。

3.2.4、Index Skip Scan索引跳跃式扫描

在讲联合索引时,咱们提到过最左前缀匹配原则,也就是SQL的查询条件中必须要包含联合索引的第一个字段,这样才能命中联合索引查询,但实际上这条规则也并不是100%遵循的。因为在MySQL8.x版本中加入了一个新的优化机制,也就是索引跳跃式扫描,这种机制使得咱们即使查询条件中,没有使用联合索引的第一个字段,也依旧可以使用联合索引,看起来就像跳过了联合索引中的第一个字段一样,这也是跳跃扫描的名称由来。

但跳跃扫描究竟是怎么实现的呢?上个栗子快速理解一下。

比如此时通过(A、B、C)三个列建立了一个联合索引,此时有如下一条SQL

1
SELECT * FROM tb_xx WHERE B = xxx AND C = xxx;

按理来说,这条SQL既不符合最左前缀原则,也不具备使用索引覆盖的条件,因此绝对是不会走联合索引查询的,但思考一个问题,这条SQL中都已经使用了联合索引中的两个字段,结果还不能使用索引,这似乎有点亏啊对不?因此MySQL8.x推出了跳跃扫描机制,但跳跃扫描并不是真正的“跳过了”第一个字段,而是优化器为你重构了SQL,比如上述这条SQL则会重构成如下情况:

1
2
3
4
5
SELECT * FROM tb_xx WHERE B = xxx AND C = xxx
UNION ALL
SELECT * FROM tb_xx WHERE B = xxx AND C = xxx AND A = "yyy"
...
SELECT * FROM tb_xx WHERE B = xxx AND C = xxx AND A = "zzz";

其实也就是MySQL优化器会自动对联合索引中的第一个字段的值去重,然后基于去重后的值全部拼接起来查一遍,一句话来概述就是:虽然你没用第一个字段,但我给你加上去,今天这个联合索引你就得用,不用也得给我用。当然,如果熟悉Oracle数据库的小伙伴应该知道,跳跃扫描机制在Oracle中早就有了,但为什么MySQL8.0版本才推出这个机制呢?还记得咱们在《MySQL架构篇》中的闲谈嘛?MySQL几经转手后,最终归到了Oracle旗下,因此跳跃扫描机制仅是Oracle公司:从Oracle搬到了“自己的MySQL”上而已。

但是跳跃扫描机制也有很多限制,比如多表联查时无法触发、SQL条件中有分组操作也无法触发、SQL中用了DISTINCT去重也无法触发…..,总之有很多限制条件,具体的可以参考《MySQL官网8.0-跳跃扫描》。

其实这个跳跃性扫描机制,只有在唯一性较差的情况下,才能发挥出不错的效果,如果你联合索引的第一个字段,是一个值具备唯一性的字段,那去重一次再拼接,几乎就等价于走一次全表。

最后,可以通过通过set @@optimizer_switch = 'skip_scan=off|on';命令来选择开启或关闭跳跃式扫描机制。当然,该参数仅限MySQL8.0以上的版本,如果在此之下的版本暂时就不用考虑了。

四、索引应用篇总结

至此,MySQL索引应用篇,也就是索引中篇就结束了,相信大家认真看完本篇之后,对于索引的掌握性、熟练程度绝对会更上一层楼,因为本章中从索引的优劣分析,到建立索引的原则、索引失效的情景、使用索引的正确姿势、MySQL对于索引的优化机制等各方面,对索引进行了进一步阐述。

经历中、上两篇的阐述后,对于MySQL索引这个大体系已经建立出了完整的认知,下一篇就是《索引原理篇》啦,在中、上两篇中抛出了很多疑惑,都留在了索引原理篇中去分析,因为只有当你真正搞懂了索引的底层实现,才能更好的理解一些前面给出的建议、定论及概念。

当然,如果你认为我的文章对你有帮助,那可以动动发财的小手,点上一个免费的小赞赞~,点赞量足够多可加快《索引原理篇》的解锁进度,更文速度完全取决于诸位的点赞数量!当然,就算不给赞,《索引原理篇》也不会缺席噢!最后再给出两条关于索引的查询命令:

  • • show status like ‘%Handler_read%’;查看当前会话的索引使用情况。

  • • show global status like ‘Handler_read%’;:查询全局索引使用情况。

正向代理与反向代理

正向代理

正向代理(forward proxy):是一个位于客户端和目标服务器之间的服务器(代理服务器),为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端;

这种代理在生活中其实是比较常见的,比如访问国外网站技术,其用到的就是代理技术,有时候,为们想访问某国外网站,该网站无法在国内直接访问,但我们可以访问一个代理服务器,这个代理服务器可以访问到国外的网站。这样呢,国外对国外网站的访问就需要通过代理服务器转发请求,并且改代理服务器也会将请求的响应在返回给用户;

这个过程我们可以做一个比喻,租房子:租房子的时候,一般情况下,我们很难联系到房东,因为有些房东为了图方便,只把自己的房屋信息和钥匙交给中介了。而房客想要租房子,只能通过中介才能联系到房东。而对于房东来说,他可能根本不知道真正要租他的房子的人是谁,他只知道是中介在联系他

所以正向代理其实是冒充了客户端,去和目标服务器进行交互通过正向代理服务器访问目标服务器,目标服务器是不知道真正的客户端是谁的,甚至不知道访问自己的是一个代理(有时候中介也直接冒充租客)

反向代理

反向代理(reverse proxy):是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

我们在租房子的过程中,除了有些房源需要通过中介以外,还有一些是可以直接通过房东来租的。用户直接找到房东租房的这种情况就是我们不使用代理直接访问国内的网站的情况。

还有一种情况,就是我们以为我们接触的是房东,其实有时候也有可能并非房主本人,有可能是他的亲戚、朋友,甚至是二房东。但是我们并不知道和我们沟通的并不是真正的房东。这种帮助真正的房主租房的二房东其实就是反向代理服务器。这个过程就是反向代理。

1. 线程组

可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式,如图所示:

*

线程组的作用是:可以批量管理线程或线程组对象,有效地对线程或线程组对象进行组织。线程组的activeCount()可以获得活动线程的总数,但由于线程是动态的,因此这个值只是一个估计值,无法确定精确,list()方法可以打印这个线程组中所有的线程信息,对调试有一定帮助。使用Thread的构造函数,指定线程所属的线程组,将线程和线程组关联起来。

线程组还有一个值得注意的方法stop(),它会停止线程组中所有的线程。这看起来是一个很方便的功能,但是它会遇到和Thread.stop()相同的问题,因此使用时也需要格外谨慎。

2. 守护线程

  • 在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程就可以理解为守护线程。

  • 当一个Java应用内,所有非守护进程都结束时,Java虚拟机就会自然退出。

而Java中变成守护线程就相对简单了。

1
2
3
Thread t=new DaemonT(); 
t.setDaemon(true);
t.start();

这样就开启了一个守护线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package test;

public class Test
{
public static class DaemonThread extends Thread
{
@Override
public void run()
{
for (int i = 0; i < 10000000; i++)
{
System.out.println("hi");
}
}
}

public static void main(String[] args) throws InterruptedException
{
DaemonThread dt = new DaemonThread();
dt.start();
}
}

当线程dt不是一个守护线程时,在运行后,我们能看到控制台输出hi

当在start之前加入

1
dt.setDaemon(true);

控制台就直接退出了,并没有输出。

3. 线程优先级

Thread类中有3个变量定义了线程优先级。

1
2
3
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package test;

public class Test
{
public static class High extends Thread
{
static int count = 0;
@Override
public void run()
{
while (true)
{
synchronized (Test.class)
{
count++;
if (count > 10000000)
{
System.out.println("High");
break;
}
}
}
}
}
public static class Low extends Thread
{
static int count = 0;
@Override
public void run()
{
while (true)
{
synchronized (Test.class)
{
count++;
if (count > 10000000)
{
System.out.println("Low");
break;
}
}
}
}
}

public static void main(String[] args) throws InterruptedException
{
High high = new High();
Low low = new Low();
high.setPriority(Thread.MAX_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);
low.start();
high.start();
}
}

让一个高优先级的线程和低优先级的线程同时争夺一个锁,看看哪个最先完成。

当然并不一定是高优先级一定先完成。再多次运行后发现,高优先级完成的概率比较大,但是低优先级还是有可能先完成的。

4. 基本的线程同步操作

synchronized 和 Object.wait() Obejct.notify()

synchronized有三种加锁方式:

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。

  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。

  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

作用于实例方法,则不要new两个不同的实例

作用于静态方法,只要类一样就可以了,因为加的锁是类.class,可以new两个不同实例。

wait和notify的用法:用什么锁住,就用什么调用wait和notify

**观察者模式**是一种行为设计模式允许你定义一种订阅机制可在对象事件发生时通知多个观察该对象的其他对象

观察者设计模式

问题

假如你有两种类型的对象 顾客 商店 。 顾客对某个特定品牌的产品非常感兴趣例如最新型号的 iPhone 手机), 而该产品很快将会在商店里出售

顾客可以每天来商店看看产品是否到货但如果商品尚未到货时绝大多数来到商店的顾客都会空手而归

访问商店或发送垃圾邮件

前往商店和发送垃圾邮件

另一方面每次新产品到货时商店可以向所有顾客发送邮件可能会被视为垃圾邮件)。 这样部分顾客就无需反复前往商店了但也可能会惹恼对新产品没有兴趣的其他顾客

我们似乎遇到了一个矛盾要么让顾客浪费时间检查产品是否到货要么让商店浪费资源去通知没有需求的顾客

解决方案

拥有一些值得关注的状态的对象通常被称为*目标由于它要将自身的状态改变通知给其他对象我们也将其称为发布者publisher)。 所有希望关注发布者状态变化的其他对象被称为订阅者*subscribers)。

观察者模式建议你为发布者类添加订阅机制让每个对象都能订阅或取消订阅发布者事件流不要害怕这并不像听上去那么复杂实际上该机制包括 1一个用于存储订阅者对象引用的列表成员变量2几个用于添加或删除该列表中订阅者的公有方法

订阅机制

订阅机制允许对象订阅事件通知

现在无论何时发生了重要的发布者事件它都要遍历订阅者并调用其对象的特定通知方法

实际应用中可能会有十几个不同的订阅者类跟踪着同一个发布者类的事件你不会希望发布者与所有这些类相耦合的此外如果他人会使用发布者类那么你甚至可能会对其中的一些类一无所知

因此所有订阅者都必须实现同样的接口发布者仅通过该接口与订阅者交互接口中必须声明通知方法及其参数这样发布者在发出通知时还能传递一些上下文数据

通知方法

发布者调用订阅者对象中的特定通知方法来通知订阅者

如果你的应用中有多个不同类型的发布者且希望订阅者可兼容所有发布者那么你甚至可以进一步让所有发布者遵循同样的接口该接口仅需描述几个订阅方法即可这样订阅者就能在不与具体发布者类耦合的情况下通过接口观察发布者的状态

真实世界类比

杂志和报纸订阅

杂志和报纸订阅

如果你订阅了一份杂志或报纸那就不需要再去报摊查询新出版的刊物了出版社即应用中的发布者”) 会在刊物出版后甚至提前直接将最新一期寄送至你的邮箱中

出版社负责维护订阅者列表了解订阅者对哪些刊物感兴趣当订阅者希望出版社停止寄送新一期的杂志时他们可随时从该列表中退出

一个气象站(WeatherData)负责收集温度、湿度、气压,当这些数据有变化时,需要通知所有注册的显示屏(比如“当前天气显示屏”和“统计显示屏”),让他们更新数据

“大喇叭”:被观察者(Subject)接口

首先,气象站需要有个“大喇叭”功能,能让大家来“订阅”或“取消订阅”,并且能在有新消息时“广播”出去。这就是 Subject 接口

1
2
3
4
5
public interface Subject {
void registerObserver(Observer o); // 注册:你过来听我广播
void removeObserver(Observer o); // 移除:你不想听了就走
void notifyObservers(); // 通知:我广播啦,大家听着!
}

2. “听众”:观察者(Observer)接口

然后,每个想接收气象数据更新的“显示屏”都得是个“听众”。它们需要知道,一旦收到通知,该怎么“更新”自己的显示。这就是 Observer 接口

1
2
3
4
public interface Observer {
// update:气象站通知我了,把最新数据告诉我,我来更新
void update(float temperature, float humidity, float pressure);
}

3. “真气象站”:具体被观察者(WeatherData)

WeatherData 就是我们实际的气象站,它会保存所有“听众”的列表,并且在数据变化时,挨个通知它们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {
private List<Observer> observers; // 听众名单
private float temperature; // 温度
private float humidity; // 湿度
private float pressure; // 气压

public WeatherData() {
observers = new ArrayList<>(); // 初始化听众名单
}

@Override
public void registerObserver(Observer o) {
observers.add(o); // 听众来了,加入名单
System.out.println(" [气象站] " + o.getClass().getSimpleName() + " 注册成功。");
}

@Override
public void removeObserver(Observer o) {
observers.remove(o); // 听众走了,从名单移除
System.out.println(" [气象站] " + o.getClass().getSimpleName() + " 已取消注册。");
}

@Override
public void notifyObservers() {
System.out.println("\n[气象站] 数据变化!开始通知所有听众...");
for (Observer observer : observers) {
// 遍历名单,挨个通知每个听众最新的数据
observer.update(temperature, humidity, pressure);
}
}

// 气象数据发生变化了!
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
// 数据一变,马上通知所有听众
measurementsChanged();
}

private void measurementsChanged() {
notifyObservers(); // 内部方法,直接调用通知
}
}

“真显示屏”:具体观察者(CurrentConditionsDisplay / StatisticsDisplay)

这些就是具体的“听众”,它们会向气象站“注册”自己,一旦收到通知,就按自己的方式显示数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;

public CurrentConditionsDisplay(Subject weatherData) {
weatherData.registerObserver(this); // 创建时就向气象站注册自己
}

@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display(); // 收到数据了,赶紧显示出来
}

public void display() {
System.out.println(" [当前天气] 温度: " + temperature + "F, 湿度: " + humidity + "%");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class StatisticsDisplay implements Observer {
private float maxTemp = 0.0f;
private float minTemp = 200f;
private float tempSum = 0.0f;
private int numReadings;

public StatisticsDisplay(Subject weatherData) {
weatherData.registerObserver(this); // 创建时向气象站注册自己
}

@Override
public void update(float temperature, float humidity, float pressure) {
// 收到数据了,计算统计信息
tempSum += temperature;
numReadings++;
if (temperature > maxTemp) maxTemp = temperature;
if (temperature < minTemp) minTemp = temperature;
display(); // 显示统计结果
}

public void display() {
System.out.println(" [统计信息] 平均: " + (tempSum / numReadings)
+ "F, 最高: " + maxTemp + "F, 最低: " + minTemp + "F");
}
}

Demo.java: 初始化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class WeatherStation {
public static void main(String[] args) {
// 1. 建立气象站
WeatherData weatherData = new WeatherData();

// 2. 准备两个显示屏,并让它们“订阅”气象站
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);

// 3. 气象站发布第一次数据
System.out.println("\n--- 第一次气象数据发布 ---");
weatherData.setMeasurements(80, 65, 30.4f);

// 4. 气象站发布第二次数据
System.out.println("\n--- 第二次气象数据发布 ---");
weatherData.setMeasurements(82, 70, 29.2f);

// 5. 统计显示屏不干了,取消订阅
System.out.println("\n--- 统计显示屏取消订阅 ---");
weatherData.removeObserver(statisticsDisplay);

// 6. 气象站发布第三次数据,这时统计显示屏就不会收到通知了
System.out.println("\n--- 第三次气象数据发布 ---");
weatherData.setMeasurements(78, 90, 29.0f);
}
}

观察者模式适合应用场景

当一个对象状态的改变需要改变其他对象或实际对象是事先未知的或动态变化的时可使用观察者模式

当你使用图形用户界面类时通常会遇到一个问题比如你创建了自定义按钮类并允许客户端在按钮中注入自定义代码这样当用户按下按钮时就会触发这些代码

观察者模式允许任何实现了订阅者接口的对象订阅发布者对象的事件通知你可在按钮中添加订阅机制允许客户端通过自定义订阅类注入自定义代码

当应用中的一些对象必须观察其他对象时可使用该模式但仅能在有限时间内或特定情况下使用

订阅列表是动态的因此订阅者可随时加入或离开该列表

实现方式

  1. 仔细检查你的业务逻辑试着将其拆分为两个部分独立于其他代码的核心功能将作为发布者其他代码则将转化为一组订阅类

  2. 声明订阅者接口该接口至少应声明一个 update方法

  3. 声明发布者接口并定义一些接口来在列表中添加和删除订阅对象记住发布者必须仅通过订阅者接口与它们进行交互

  4. 确定存放实际订阅列表的位置并实现订阅方法通常所有类型的发布者代码看上去都一样因此将列表放置在直接扩展自发布者接口的抽象类中是显而易见的具体发布者会扩展该类从而继承所有的订阅行为

    但是如果你需要在现有的类层次结构中应用该模式则可以考虑使用组合的方式将订阅逻辑放入一个独立的对象然后让所有实际订阅者使用该对象

  5. 创建具体发布者类每次发布者发生了重要事件时都必须通知所有的订阅者

  6. 在具体订阅者类中实现通知更新的方法绝大部分订阅者需要一些与事件相关的上下文数据这些数据可作为通知方法的参数来传递

    但还有另一种选择订阅者接收到通知后直接从通知中获取所有数据在这种情况下发布者必须通过更新方法将自身传递出去另一种不太灵活的方式是通过构造函数将发布者与订阅者永久性地连接起来

  7. 客户端必须生成所需的全部订阅者并在相应的发布者处完成注册工作

观察者模式优缺点

  • 开闭原则你无需修改发布者代码就能引入新的订阅者类如果是发布者接口则可轻松引入发布者类)。

  • 你可以在运行时建立对象之间的联系

  • 订阅者的通知顺序是随机的

与其他模式的关系

  • 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:

    • 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。

    • 命令在发送者和请求者之间建立单向连接。

    • 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。

    • 观察者允许接收者动态地订阅或取消接收请求。

  • 中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式而有时可以同时使用让我们来看看如何做到这一点

    *中介者的主要目标是消除一系列系统组件之间的相互依赖这些组件将依赖于同一个中介者对象观察者*的目标是在对象之间建立动态的单向连接使得部分对象可作为其他对象的附属发挥作用

    有一种流行的中介者模式实现方式依赖于*观察者中介者对象担当发布者的角色其他组件则作为订阅者可以订阅中介者的事件或取消订阅中介者以这种方式实现时它可能看上去与观察者*非常相似

    当你感到疑惑时记住可以采用其他方式来实现中介者例如你可永久性地将所有组件链接到同一个中介者对象这种实现方式和*观察者*并不相同但这仍是一种中介者模式

    假设有一个程序其所有的组件都变成了发布者它们之间可以相互建立动态连接这样程序中就没有中心化的中介者对象而只有一些分布式的观察者

通用解析文件转为对象集合插入

在实际工作中,将Excel文件解析成对象集合然后导入数据库是非常常见的需求,面对不同的对象,我们往往需要实现不同的功能,于是萌生了写一个通用方法,来实现不同类的解析,具体实现思路,通过泛型,注解,加反射来实现

导入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>

定义注解

1
2
3
public @interface ExcelColumn {
String name();
}

具体实现工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package org.pt;

import org.apache.poi.ss.usermodel.*;

import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class ExcelReader<T> {
private final Class<T> type;

public ExcelReader(Class<T> type) {
this.type = type;
}

public List<T> readExcel(String filePath) {
List<T> dataList = new ArrayList<>();
try {
FileInputStream file = new FileInputStream(new File(filePath));
// Create Workbook instance holding reference to .xlsx file
Workbook workbook = WorkbookFactory.create(file);
// Get the first sheet
Sheet sheet = workbook.getSheetAt(0);
// Get the header row
Row headerRow = sheet.getRow(0);
// 根据第一行列数来初始化header数组
String[] headers = new String[headerRow.getLastCellNum()];
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
// 将第一行对应列的标识符存到对应数组下标
headers[i] = headerRow.getCell(i).getStringCellValue();
}
// Iterate through each row
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
// 如果这行为空直接跳过
if (row == null) continue;
//通过反射创建了type表示的类的一个新实例,并且假定这个类有一个无参数的构造函数
T obj = type.getDeclaredConstructor().newInstance();
// Iterate through each cell
for (int j = 0; j < row.getLastCellNum(); j++) {
// 开始遍历每一行每一列
Cell cell = row.getCell(j);
// 获取对应列的标识符号
String header = headers[j];
Object value = getCellValue(cell);
// Find corresponding field using reflection
Field field = getFieldByName(header);
if (field != null) {
field.setAccessible(true);
field.set(obj, convertValueToType(value, type.getTypeName().getClass()));
}
}

dataList.add(obj);
}
workbook.close();
file.close();
} catch (Exception e) {
e.printStackTrace();
}

return dataList;
}

private Field getFieldByName(String name) {
for (Field field : type.getDeclaredFields()) {
ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
if (annotation != null && annotation.name().equals(name)) {
return field;
}
}
return null;
}

private Object convertValueToType(Object value, Class<?> type) {
if (value == null) {
return null;
}

if (type == String.class) {
return value.toString();
} else if (type == int.class || type == Integer.class) {
if (value instanceof String) {
return Integer.parseInt((String) value);
} else if (value instanceof Number) {
return ((Number) value).intValue();
}
} else if (type == double.class || type == Double.class) {
if (value instanceof String) {
return Double.parseDouble((String) value);
} else if (value instanceof Number) {
return ((Number) value).doubleValue();
}
} else if (type == boolean.class || type == Boolean.class) {
if (value instanceof String) {
return Boolean.parseBoolean((String) value);
} else if (value instanceof Boolean) {
return value;
}
} else if (type == Date.class) {
if (value instanceof Date) {
return value;
} else if (value instanceof String) {
// 根据需要进行日期格式转换
// 此处假设日期字符串为标准格式
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
return dateFormat.parse((String) value);
} catch (ParseException e) {
e.printStackTrace();
}
}
}

// 如果没有匹配的类型,则返回原始值
return value;
}


private Object getCellValue(Cell cell) {
// 初始化一个空值
if (cell == null) {
return null;
}

// 根据单元格类型进行不同的处理
switch (cell.getCellType()) {
case STRING:
// 如果是字符串类型,直接返回字符串值
return cell.getStringCellValue();
case NUMERIC:
// 如果是数字类型,判断是否是日期格式
if (DateUtil.isCellDateFormatted(cell)) {
// 如果是日期格式,返回日期对象
return cell.getDateCellValue();
} else {
// 否则返回数字值
return cell.getNumericCellValue();
}
case BOOLEAN:
// 如果是布尔类型,返回布尔值
return cell.getBooleanCellValue();
case FORMULA:
// 如果是公式类型,返回公式字符串
return cell.getCellFormula();
default:
// 其他类型返回空值
return null;
}
}


public static void main(String[] args) {
ExcelReader<Employee> excelReader = new ExcelReader<>(Employee.class);
List<Employee> dataList = excelReader.readExcel("your_excel_file.xlsx");

// Print the data
for (Employee employee : dataList) {
System.out.println(employee);
}
}

}

结论:用好反射

Google搜索技巧

文章目录

  1. 一、31个谷歌搜索技巧(google搜索质量大全)

    1. 1. 完全匹配搜索法

    2. 2. 排除搜索法

    3. 3.X或Y搜索法

    4. 4.文本中的单词搜索法

    5. 5.文本+Tile,URL等中的单词搜索法

    6. 6.Title中的单词搜索法

    7. 7.Tile+文本,URL等中的单词搜索法

    8. 8.URL中的单词搜索法

    9. 9.网站内内容搜索法

    10. 10.相关搜索法

    11. 11.一个页面链接到另一个页面的查询法

    12. 12.类似单词或 同义词搜索法

    13. 13.单词定义搜索法

    14. 14.缺失单词搜索法

    15. 15.特定地址的消息搜索法

    16. 16.特定文件类型搜索法

    17. 17.翻译内容搜索法

    18. 18.电话搜索法

    19. 19.电话区号搜索法

    20. 20.邮编搜索法

    21. 21.数字区间搜索法

    22. 22.股票(股票代码)搜索法

    23. 23.计算器搜索法

    24. 24.计时器搜索法

    25. 25.食物对比搜索法

    26. 26.体育比赛得分及时间表搜索法

    27. 27.航班状态搜索法

    28. 28.日出日落时间搜索法

    29. 29.天气搜索法

    30. 30.括号搜索法 ()

    31. 31.site:命令搜索法

  2. 二、谷歌高级搜索(谷歌搜索指令2.0)

    1. 1 thought on “如何在谷歌上搜索: 31个 Google 高级搜索技巧”

    2. Leave a Comment Cancel Reply

如果你也和我一样是一个开发者,应该每天都使用谷歌进行搜索。但是,除非你是一个技术客,否则你很可能仍然在使用谷歌最简单的搜索形式。不仅效率低,还很可能找不到理想的搜索结果。

如果你使用本文中的搜索技巧,就可以在谷歌中仅输入几个单词,就可以找到你想要的结果。本篇文章董哥将告诉大家一些搜索技巧—-而且这些搜索指令学起来非常简单。

谷歌搜索 下面是一些最有用的 Google 搜索技巧。有了它你很快就会成为谷歌搜索专家。

一、31个谷歌搜索技巧(google搜索质量大全)

1. 完全匹配搜索法

假设你正在谷歌上搜索关于页面SEO终极指南的内容。如果谷歌搜索框中输入:页面SEO终极指南,会得到一些模棱两可的搜索结果。那如何只搜索那些跟页面SEO终极指南相关的结果呢?要做到这一点,只需将搜索短语包含在双引号中。
例如:“页面SEO终极指南”

完全匹配搜索-例子

2. 排除搜索法

假设你想搜索关于lose weight的内容,但是你想排除任何包含这个词的广告结果。要做到这一点,只需在要排除的单词前面使用-符号。
例如:lose weight -advertising

3.X或Y搜索法

默认情况下,当你进行搜索时,谷歌将包含在搜索中指定的所有术语。如果你正在寻找与一个或多个术语中的任何一个相关的搜索结果,那么可以使用 OR 运算符。(注: OR必须大写)。
例如:鱼OR熊掌 或者 鱼 | 熊掌(反馈的结果也是一样的)

4.文本中的单词搜索法

如果你想找到一个网页,所有你要搜索的词都出现在该网页的文本中(但不一定要挨在一起) ,输入—allintext: 后面紧接着是要查找的单词或短语。
例如:allintext:外贸网站感谢页

allintext搜索命令-例子

5.文本+Tile,URL等中的单词搜索法

如果你想找到一个网页,其中一个术语出现在该页面的文本中,另一个术语出现在该页面的其他地方,如标题或 URL。可以键入第一个术语后面跟着 intext: 紧接着是另一个术语。
例如:seo终极指南 intext:搜索意图

intext搜索命令例子

6.Title中的单词搜索法

想要找到一个标题中包含某些单词的网页(但不一定在彼此的旁边) ?输入 allintitle: 紧接着是单词或短语。
例如:allintitle:跳出率案例

allintitle搜索命令案例

7.Tile+文本,URL等中的单词搜索法

想要找到一个网页,其中一个术语出现在页面的标题,另一个术语出现在页面的其他地方,如在文本或网址?输入第一个词,然后是 intitle: 紧接着是另一个词。

例如:首次输入延迟 intitle:核心网页指标提升

一个术语出现在页面的标题,另一个术语出现在页面的其他地方的搜索案例

8.URL中的单词搜索法

如果你希望查找 URL 中提到的搜索查询的页面,请键入 allinurl: 然后是搜索查询。
例如:allinurl:埃克森数字营销

allinurl搜索命令例子

9.网站内内容搜索法

有时,你需要在一个特定的网站上搜索与某个短语相匹配的内容。即使网站不支持内置的搜索功能,你也可以使用谷歌搜索网站中的词条。搜索格式: xxxx.com “内容”
例如,你要在埃克森数字营销网站搜索关于用户体验的内容,你可以在谷歌中搜索: site:www.xnbeast.com “用户体验”

网站内内容搜索法例子

10.相关搜索法

如果你想找到与你已知网站内容相似的新网站,可以使用 related: xxxx.com
例如:related:amazon.com
再例如,大家知道ahref是一个SEO工具,他们的博客是ahrefs.com/blog.如果你要找跟他们类似的博客内容,你可以搜:related:ahrefs.com/blog

查询相关性。我们还可以使用SITE:xxx.com 得到一个结果数,然后使用site:xxx.com+内容在得到一个结果数。将第二个数除以第一个数,如果数值大于0.5说明是内容很相关的网站。

例如,利用这个命令查看一下埃克森数字营销跟外贸网站和SEO这两个内容的相关性。
首先搜索site:xnbeast.com,得到的结果数为69
然后搜索site:xnbeast.com 外贸网站,结果数为55.
55/69=0.79 这说明埃克森数字营销网站与内容:外贸网站非常相关。

再看一下埃克森数字营销网站与SEO的相关性。
首先搜索site:xnbeast.com, 得到的结果数为69
然后搜索site:xnbeast.com seo, 得到的结果数为55
55/69=0.79,这说明埃克森数字营销网站与内容:SEO也非常相关。
(这个命令可以用于相关话题链接建设)

11.一个页面链接到另一个页面的查询法

假设你想搜索每一个引用 BuzzFeed 文章的网站。要做到这一点,请使用 link: 后面紧跟一个页面的名称。谷歌会给你链接到 BuzzFeed 官方网站的所有页面。网址越具体,你得到的结果就越少,越有针对性。
例如:link:buzzfeed

12.类似单词或 同义词搜索法

假设你要在搜索中包含一个单词,但是也希望包含相似单词或同义词的结果。要做到这一点,在单词前输入~ 。
例如: “页面SEO优化” ~指南

相关内容搜索例子

13.单词定义搜索法

如果您需要快速查找单词或短语的定义,只需使用 define: 命令。你可以通过按扩音器图标来听单词的发音。
例如:define:plethora

14.缺失单词搜索法

有时人们经常忘记一两个特定的词组,歌词,电影名言,或者其他什么?你可以使用星号 作为通配符,这可以帮助你找到短语中缺少的单词。
例如:much
about nothing 或者 埃克森*营销 又或者 偏偏*你

15.特定地址的消息搜索法

如果你正在寻找与某个特定地点相关的消息,你可以使用 location: 命令来搜索来自该地点的新闻。
例如:star wars location:London, world war 2 location: China

16.特定文件类型搜索法

如果要查找特定文件类型的结果,可以使用修饰符 filetype: 。
例如,你可能只想找到与快速减肥有关的 PowerPoint 演示文稿。那可以搜索-“Quickly lose weight”filetype:ppt

17.翻译内容搜索法

想要将一个简单的单词或短语从一种语言翻译成另一种语言吗?不需要去翻译网站。只要搜索:translate[单词]to[语言]即可。
例如:translate: pencil to Spanish. translate: search engine optimization to Russia

18.电话搜索法

假设有人打电话到你的手机上,但你不知道是谁。如果你只有一个电话号码,你可以使用电话簿功能在谷歌上查找。
例如:phonebook:546-88-488
(注意: 本例中的数字是虚假好吗。你必须使用真是电话号码才能结果。)

19.电话区号搜索法

如果你只需要查一个电话号码的区号,只要输入三位数的区号,谷歌就会告诉你它的来源。
例如搜索:617

20.邮编搜索法

如果你需要查找某地址的邮政编码,只需要搜索地址的部分,包括城镇或城市名称和州、省或国家。它将返回带有邮编的结果(如果适用的话) 。
例如:Fuqian street, dongying, shandong

21.数字区间搜索法

这是一个很少使用但非常有用的搜索方法。假设你要查找包含任意一个数字范围的结果。你可以使用 X..Y 修饰语。这种类型的搜索对于年份(如下所示)、价格或任何您想要提供一系列数字的地方都是有用的。
例如:president 1930..2021 Iphone $500..$3000

22.股票(股票代码)搜索法

只要输入一个有效的股票代码作为你的搜索词,谷歌会给你当前的财务状况和一个快速的股票缩略图。

我们拿盐田港股票代码000088为例,搜索:000088 即可得到该股票的信息。

23.计算器搜索法

你要做一个快速的计算,不想打开计算器小程序,这时可以只是输入你的算式到谷歌。
例如:586648*345

24.计时器搜索法

手边没有计时器?谷歌帮你做好了。只要输入一个时间量 + “timer”,倒计时将自动开始

例如,我要一个20分钟的计时器。
那么可以搜索:20 min timer

25.食物对比搜索法

如果你想知道两种(相当普通的)食物是如何相互比较的,你可以在谷歌上快速搜索,看看它们在卡路里、脂肪、蛋白质、胆固醇、钠、钾和其他营养素上有何不同。

例如:pizza vs broccoli

26.体育比赛得分及时间表搜索法

想知道你最喜欢的球队或比赛的最新比分和未来的时间表吗?搜索一个球队名称或者两个球队名称,Google 会在第一个搜索结果出来之前,使用 Google Sports 来显示比分和时间表。
例如: manchester united

27.航班状态搜索法

如果你在 Google 中输入航空公司和航班号,它会告诉你航班信息、状态和其他有用的信息。
例如:BA 181

28.日出日落时间搜索法

如果你好奇太阳什么时候升起,什么时候落下,你可以用谷歌搜索日出或日落这两个词以及地名。
例如:sunrise beijing

29.天气搜索法

下一次你要查找某个地区的天气统计数据或天气预报时,搜索天气+地点。谷歌会在第一个搜索结果出现之前给你两个选项。
例如:weather qingdao

30.括号搜索法 ()

用()来将多个术语或搜索运算符进行分组来控制搜索的执行方式。
比如: (red OR white) shoe

31.site:命令搜索法

使用site: +域名,查看该域名被谷歌收录的所有页面。
例如:site:xnbeast.com

site搜索命令案例

site:+网站的具体页面URL,可以查看该网站中的某一个页面是否被谷歌收录。
例如:site:xnbeast.com/core-web-vitals/

site搜索命令具体页面谷歌收录查询例子 site搜索法可以查询网站上是否有http元素。可以使用搜索命令site:xnbeast.com -inurl:https

site搜索命令查询http元素

site搜索法可以查询网站上的重复内容。可以使用搜索命令-site:xnbeast.com+内容。就可以查到你的网站内容有没有被分享或者剽窃。 例如:-site:xnbeast.com “埃克森数字营销”

site命令查询内容抄袭分享的案例

site搜索法查看某人/某品牌(或者公司)的社交档案。可以使用命令name (site:twitter.com | site:facebook.com | site: linkedln.com | youtube.com)

查询某人或公司社交账号的搜索命令案例

二、谷歌高级搜索(谷歌搜索指令2.0)

在进行复杂搜索时,你还可以使用高级搜索来缩小搜索结果范围。

按照搜索内容的类别,谷歌提供了不同的高级搜索页面:

在这些高级搜索中可以尝试过滤条件:

  • 语言

  • 地区

  • 最后更新时间

  • 网站或域名

  • 字词出现位置

  • 安全搜索

  • 文件类型

  • 使用权限

主要内容

  • 运行时数据区域

  • 对象访问

  • OutOfMemoryError异常

运行时数据区域

虚拟机会将java程序过程中所管理的内存划分为不同数据区域,这些区域都有各自的用途,以及创建和销毁时间,注意分为以下几个部分:

程序计数器

当前线程所执行的字节码的行号指示器,字节码指示器通过改变计数器的值来选去需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等。java虚拟机通过线程轮流切换并分配处理器时间来实现,因此,为了线程能恢复到正确执行位置,每条线程都都有一块独立的程序计数器,保证每个线程之间计数器互不影响,独立存储,线程私有

如果线程执行的是一个java方法,计数器记录的是字节码指令地址,如果是native,则计数器值为空(Undefined),此区域是**唯一**一个在虚拟机规范中没有规定任何内存溢(OutOfMemoryError)的区域

java虚拟机栈

与程序计数器一样,java虚拟机栈也是线程私有的,生命周期与线程相同。描述的是java方法执行的内存模型,每个方法执行的时候会创建一个栈桢,栈桢用来存储局部变量表,操作栈,动态链接,方法出口等信息,当一个方法被调用直至完成的过程,标志着一个栈桢从虚拟机栈入栈到出栈的过程

局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,他可能是一个执行对象起始位置的引用指针,也可能指向一个代表对象的句柄或其它与此对象相关的位置)和returnAddress类型(指向一条字节码指令的地址)

虚拟机规范中,此区域有两种异常情况,线程请求的栈深度大于虚拟机所允许深度,将抛出Stac kOverflowError异常;虚拟机动态扩展是无法申请到足够内存抛出OutOfMemoryError

内存溢出(Memory Overflow):内存溢出通常指的是程序在运行过程中,由于某种原因(如无限循环、递归调用过深等)导致其占用的内存超过了系统分配给它的内存空间

内存泄漏(Memory Leak):内存泄漏是指在计算机程序中,由于疏忽或错误导致的一种内存分配后无法释放的现象,即分配出去的内存没有被正确回收

本地方法栈

本地方法栈与java虚拟机栈类似,虚拟机栈为执行java方法服务,本地方法栈为执行Native方法服务

堆(Heap)

堆是所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域唯一目的存放对象实例,在虚拟机规范中,所有对象实例以及数组都要在堆上分配,java堆也是垃圾收集器管理的主要区域(GC堆),如果从内存回收的角度看,由于现在垃圾收集器采用的分代收集,所以java堆分为新生代,老年代,在细致一点有Eden、From Survivor、to Survivor,在java虚拟机规范中,java堆可以处于不连续的内存空间中,只要在逻辑上连续即可,堆的大小也是可以扩展的(通过-Xmx和-Xms控制),如果堆中没有内存完成实例分配且无法扩展,则抛出OutOfMemoryError

方法区

与堆一样,是各个线程共享的内存区域,存放的是已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据,在虚拟机规范中将方法区描述为堆的一个逻辑部分,但它有一个别名叫做Non-Heap(非堆),目的应该是与java堆区分开来,对于习惯在HotSpot虚拟机上开发的开发者来说,很多人又愿意将方法区称为“永久代”,对于其它虚拟机是没有这个概念的,java虚拟机对这个区域的限制是非常宽松的,不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集,相对而言垃圾收集行为在这个区域还是比较少见的,这个内存区域回收目标为常量池的回收和对类型的卸载,当方法区无法满足内存分配需求,将抛出OutOfMemoryError异常

运行时常量池

是方法区的一部分,Class文件中除了类版本,字段,方法,接口等描述信息外,还有常量池,常量池用来存放编译器生成的各种字面量和符号应用,这部分内容在类加载后存放到方法区的运行时常量池,运行时常量池属于方法区,会受到方法区的限制,也会抛出OutOfMemoryError异常

对象访问

Object obj=new Object(),假设这句代码出现在方法体里面,那Object obj会反映到java栈中的本地方法表中,作为一个引用(reference)类型数据出现,而new Object()这部分语句反映到java堆中,形成一块存储了Object类型所有实力数据值,对象中的各个实例子段的结构化内存,这块内存长度是不固定的,在java中还必须包含能查找到此对象的类型数据(如对象类型,父类,实现的接口,方法等)的地址信息,这些类型数据则存在方法区中

引用类型定义这个引用应该通过那种方式去定位,主流的访问方式有两种:使用句柄直接指针

句柄访问

java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了实例数据和类型数据各种各自具体地址信息

指针访问

堆对象的布局中要考虑如何放置访问类型数据的相关的信息,reference中直接存储的就是对象地址

两种各有优势,句柄好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改,指针访问最大的好处是访问速度更快,节省了一次指针定位时间开销

实战:OutOfMemoryError异常

堆溢出

只要我们不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制来清除这些对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常 ,设置堆的最小值-Xms,最大值-Xms,另外通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照一以便事后分析

MV Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

1
2
3
4
5
6
7
8
9
10
public class HeapooM (

static class OOMObject {
}

public static void main (Stringl) args) {
List<00MObject> l i s t = new ArrayList<OOMObject>() ; while (true) (
list. ad (new OOMObject ()) :

}
虚拟机栈和本地方法栈溢出

-Xoss参数(设置本地方法栈大小,虽然存在,但实际上无效),栈容量只由-Xss参数设定,关于虚拟机栈和本地方法栈,虚拟机规范中描述了两种异常

  • 如果线程请求的栈深度大虚拟机所允许的最大深度 ,将抛出StackOverflowError异常。

  • 又如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError 异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* MV Args: -Xss128k * @author zzm
*/
public class JavaVMStackSOF {
 
private int stackLength= 1;
 
public void stackLeak (){
 stackLength++;
 s t a c k L e a k ();
}
 
 
public s t a t i c void main(String() args) throws
Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF () ;
try{
    oom.stackLeak () ;
catch(Throwable e) {
    System.out.println("stack length:"+ oom.stackLength) ;
throw e;
}
 
 
运行结果:
stack length:2402
Exception ni thread "main" java. lang.StackOverflowError
at org.fenixsoft.oom.VMStackSOF.leak (VMStackSOF.java:20)

实验结果表明,无论是由于栈桢太大,还是虚拟机栈容量太小,当内存无法分配的时候,都是抛出StackOverflowError

运行时常量池溢出

如果要向运行时常量池中添加内容,最简单的做法是使用Strinf.intern()这个native方法,这个方法的作用是如果池中包含一个等一此String对象的字符串,则返回代表池中这个字符串的String对象,否则,将此String对象包含的字符串添加到常量池中,并返回此s tring对象的引用,由于常量池分配在方法区,可以通过-XX:PermSize和-XX:MaxPermSiza限制方法区的大小,从而限制常量池的容量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
*MV Args: X-X: PermSize=10M -XX:MaxPermSize=10M * @author zm
*/
public class RuntimeConstant Pool00M (
public static void main(String() args) (
// 使用List 保持着常量池引用,避免FU11 GC回收常量池行为
 List<String> list = new ArrayList<String>();
// 10MB的Permsize在integer 范園內足够产生0OM了
 int i = 0;
while (true) {
list.add (String.valueof (i++) .intern());
}

运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: Permen space
at java.lang.String. intern (Native Method)
at org.fenixsoft.oom. RuntimeConstantPool00M.main (RuntimeConstantPoolOOM.java:18)

从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提 示 信 息 是 “ PermGenspace ” , 说 明 运 行 时 常 量 池 属 于 方 法 区 (HotSpot 虚 拟 机 中 的 永 久 代)的 一部分。

方法区溢出

方 法 区 用 于存 放 Class 的 相 关 信 息 , 如 类 名 、 访 问 修 饰 符 、 常 量 池 、 字 段 描 述、方法描述等。对 于这个区域的测试,基本的思路是运行时产生大量的类去填 满方法区,直到溢出。虽然直接使用Java SE API 也可以动态产生类 (如反射时的 GeneratedConstructorAccessor 和动态代理等),但在本次实验中操作起来比较麻 烦。下面,笔者借助CGLib“直接操作字节码运行时,生成了大量的 动态类。 值得特别注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应 用经常会出现在实际应用中:当前的很多主流框架,如Spring和Hibernat e对类进行增 强时,都会使用到CGLib 这类字节码技术,增强的类越多,就需要越大的方法区来保证 动态生成的Class 可以加载人内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* MV Args: -XX: Permsize=10M -XX:MaxPermSize=10M * @author zzm
*/
public class JavaMethodAreaOoM (
 
public static void main(String() args) {
 while(true) {
    Enhancer enhancer = new Enhancer ();
    enhancer. setSuperclass (00MObject.class);
    enhancer. setUseCache (false);
    enhancer. setCallback (new MethodInterceptor () (
        public Object intercept (Object obj, Method method, Object () args, MethodProxy proxy) throws Throwable {
          return proxy.invokeSuper (obj, args) ;
        }
});
enhancer.create();
                          }
                        }
static class OOMObject{}
}

                           
运行结果:
Caused by: java.lang.OutOfMemoryError: PermGen space at java. lang.ClassLoader.defineClass1 (Native Method)
at java.lang.ClassLoader.defineClassCond(Classloader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
... 8more

方法区溢出也是一种常见的内存溢出异常, 一个类如果要被垃圾收集器回收掉,判 定条件是非常苛刻的。在经常动态生成大量Class 的应用中,需要特别注意类的回收状 况。这类场景除了上面提到的程序使用了GCLib 字节码增强外,常见的还有:大量JSP 或 动 态 产 生 JSP 文 件 的 应 用 (JSP 第 一次 运 行 时 需 要 编 译 为 Java 类 )、 基 于 OSGi 的 应 用 (即使是同 一个类文件,被不同的加载器加载也会视为不同的类)等

本地直接内存溢出

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则 默 认 与 Java 堆 的 最 大 值 (- X m x 指 定 ) 一 样 。 代 码 清 单 2 - 6 越 过 了 DirectByteBuffer 类 , 直 接 通 过 反 射 获 取 Unsafe 实 例 并 进 行 内 存 分 配 (Unsafe类 的getUnsafe ()方 法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar 中的类 才能使用Unsafe的功能)。因为,虽然使用DirectByteBuffer 分配内存也会抛出 内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过 计 算 得 知 内 存 无 法 分 配 , 于是 手动 抛 出 异 常 , 真 正 申 请 分 配 内 存 的 方 法 是 unsafeallocateMemory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MV Args: -Xmx20M -XX:MaxDirectMemorySize=10M * @author zzm
*/
public class DirectMemoryOOM {
 
    private static final int M_IB = 1024 * 1024;
 
    public static void main(String() args) throws Exception {
      Field unsafeField = Unsafe.class.getDeclaredFields() [0);        unsafeField. setAccessible (true);
      Unsafe unsafe = (Unsafe) unsafeField.get (null) ;
      while (true){
      unsafe.allocateMemory (_1MB);
      }
  }
}
0%