java面试题01

Java 后端开发面试题

[TOC]

说明:

这份 Java 后端开发面试题是 ChatGPT 根据我的校招简历自动生成的有针对性的高频面试题,分为项目经验考察和专业技能考察两部分。

第一章 项目经验

一、智慧星球——在线视频学习平台——微服务项目

1、简要介绍一下你参与的智慧星球项目的技术架构和主要功能。

答案: 智慧星球是一个在线视频学习平台的微服务项目。它使用了 Spring Boot 和 Spring Cloud 作为基础框架,数据库采用 MySQL,ORM 框架使用 MyBatis Plus。主要功能包括后台管理系统的教师管理、课程分类管理、点播课程管理、订单管理、优惠券管理、公众号菜单管理、直播管理等功能。微信公众号实现了授权登录、课程浏览、购买、观看和分享、观看直播、消息自动回复等功能。

2、请解释一下微服务架构,并说明为什么选择微服务架构作为该项目的架构方式。

答案: 微服务架构是一种将应用程序拆分为一组小型、独立的服务的架构风格。在该项目中,采用微服务架构可以将不同的功能模块独立开发、部署和扩展,实现了高内聚、低耦合的目标。通过使用 Nacos 进行服务注册和发现,以及 OpenFeign 实现模块间的远程调用,可以更好地管理和扩展整个系统,提高系统的可维护性和可扩展性。

3、在微服务架构中,如何处理服务之间的通信和数据传递?

答案: 在微服务架构中,可以使用多种方式处理服务之间的通信和数据传递。在智慧星球项目中,采用了 Spring Cloud 提供的 OpenFeign 来实现模块间的远程调用,它基于 HTTP 协议,通过定义接口的方式来实现服务之间的通信。通过在接口上使用注解,可以指定远程服务的 URL 和参数,Spring Cloud 会自动处理远程调用和数据传递的细节,简化了开发和集成的过程。

4、请解释一下 JWT 和 Token 鉴权的工作原理。

答案: JWT(JSON Web Token)是一种用于在网络应用间传递信息的安全方法。它由三部分组成,分别是头部(Header)、载荷(Payload)和签名(Signature)。

工作原理如下:

  1. 用户在登录成功后,服务端生成一个包含用户信息的 Token,并将其发送给客户端。

  2. 客户端在后续的请求中将该 Token 携带在请求头中。

  3. 服务端在接收到请求时,将从 Token 中解析出的用户信息用于权限验证和业务操作。

  4. 服务端使用秘钥对 Token 进行签名,确保 Token 的完整性和安全性。

5、请解释一下微信授权登录的流程。

答案: 微信授权登录是通过使用微信开放平台的 OAuth2.0 协议实现的。

流程如下:

  1. 用户在微信客户端点击授权登录按钮。

  2. 微信客户端跳转到开发者配置的授权页面,并向用户展示授权请求。

  3. 用户同意授权后,微信客户端将用户重定向到开发者指定的回调 URL,并附带授权临时票据 code。

  4. 开发者通过后端服务器接收到 code 后,使用 code 和 AppID、AppSecret 等参数向微信服务器发送请求,获取访问令牌(access_token)和用户唯一标识(openid)等信息。

  5. 开发者可以使用 access_token 和 openid 进行用户认证和授权操作。

6、请说明项目中使用的腾讯云对象存储、视频点播和欢拓云直播的作用和实现方式。

答案: 腾讯云对象存储用于实现图片上传,视频点播用于实现视频的存储和播放,欢拓云直播用于实现直播功能。在项目中,通过集成相应的 SDK 或 API,可以使用腾讯云对象存储的接口实现图片的上传和访问,使用腾讯云视频点播的接口实现视频的上传和播放,使用欢拓云直播的接口实现直播的观看和管理。

7、请介绍一下项目中使用的 EasyExcel 和 ECharts 的作用以及实现方式。

答案: EasyExcel 是一个 Java 处理 Excel 文件的开源库,用于读写 Excel 数据。在项目中,通过使用 EasyExcel,可以方便地读取和写入 Excel 文件,实现课程分类管理中的数据导入和导出功能。ECharts 是一个用于绘制图表的 JavaScript 库,用于展示视频的播放量。通过使用 ECharts,可以将视频播放量的数据进行可视化展示,例如绘制折线图。

8、请说明项目中使用的 Swagger 的作用和实现方式。

答案: Swagger 是一个用于生成接口文档和测试接口的工具。在该项目中,通过集成 Swagger,可以自动生成项目的接口文档,并提供了一个可视化的界面供开发人员查看和测试接口。开发人员可以通过配置注解来描述接口的信息和参数,并使用 Swagger UI 来展示接口文档,并提供接口测试的功能。

二、简易的 IOC 和 DispatcherServlet Web 应用程序

1、请简要介绍一下你在这个项目中实现的 IOC 容器的原理和工作流程。

答案: 在这个项目中,我实现的 IOC(Inversion of Control)容器的原理和工作流程如下:

  1. 加载配置文件:首先,IOC 容器会读取指定的 XML 配置文件,其中包含了 Bean 的定义信息,包括类名、属性、依赖等。

  2. 创建对象实例:IOC 容器通过反射机制根据配置文件中的类名,动态地创建对象实例。它会调用类的构造函数来实例化对象。

  3. 处理对象依赖:一旦对象实例化完成,IOC 容器会检查对象的依赖关系。它会根据配置文件中的依赖信息,自动解析对象之间的依赖关系。

  4. 注入依赖:IOC 容器将会自动将依赖对象注入到相应的属性中。它通过调用对象的 setter 方法来完成属性的注入。

  5. 提供对象实例:一旦所有的对象都被创建和注入完成,IOC 容器会将这些对象保存起来,并且可以根据需要提供对象的实例。其他组件可以通过容器来获取所需的对象实例,实现了对象的解耦和灵活的组装。

总结起来,IOC 容器的核心思想是通过控制反转的方式,将对象的创建和依赖管理交给容器来完成。它通过读取配置文件、反射机制和依赖注入等技术,实现了对象的动态创建和组装。这样可以大大降低组件之间的耦合度,提高代码的可维护性和灵活性。

2、你是如何设计和实现 DispatcherServlet 中央控制器的?请谈谈你的思路和关键步骤。

答案: 在设计和实现 DispatcherServlet 中央控制器时,我采用了以下思路和关键步骤:

  1. 配置 URL 映射规则:在项目的配置文件中,我定义了 URL 与 Controller 方法之间的映射规则。这可以通过配置文件、注解或编程方式完成。例如,可以使用 XML 配置文件或注解来指定 URL 与 Controller 方法的对应关系。

  2. 请求的处理流程:当收到一个请求时,DispatcherServlet 作为中央控制器,接收并处理该请求。它首先根据请求的 URL 查找对应的 Controller 类和方法。

  3. 动态加载 Controller 类:利用 Java 的反射机制,我动态加载对应的 Controller 类。这样可以根据配置的类路径创建 Controller 类的实例。

  4. 调用 Controller 方法:通过反射,我调用 Controller 类中与 URL 对应的方法来处理请求。这些方法通常包含了业务逻辑和数据处理操作。传递给 Controller 方法的参数可以是请求参数、表单数据或其他需要的参数。

  5. 处理结果返回:Controller 方法执行完后,会返回一个表示处理结果的对象。DispatcherServlet 将该结果转换为适当的响应格式(如 HTML、JSON 等),并将其返回给客户端。

总结起来,设计和实现 DispatcherServlet 中央控制器的关键步骤包括 URL 映射规则的配置、根据 URL 查找对应的 Controller 类和方法、动态加载 Controller 类、通过反射调用 Controller 方法处理请求,并将处理结果返回给客户端。这种设计模式可以实现一种灵活的、可扩展的请求处理方式,使得开发者能够更好地组织和管理 Web 应用的请求处理逻辑。

3、你在项目中实现的 Filter 和 Listener 组件的作用是什么?请谈谈你是如何应用它们的。

答案: Filter 和 Listener 组件在项目中起到了全局预处理和后处理的作用。Filter 组件可以用于拦截请求,进行一些通用的预处理操作,如解决跨域和设置编码等。Listener 组件可以监听 Web 应用的生命周期事件,如应用启动和关闭等,进行一些特定的操作。在这个项目中,我应用了 Filter 组件来处理请求的全局预处理,例如解决跨域和设置编码。同时,我也应用了 Listener 组件来监听应用的启动事件,进行一些初始化操作。

4、你在项目中应用了哪些设计模式?请列举并解释一下你为什么选择这些设计模式。

答案: 在这个项目中,我应用了以下设计模式:

  • 单例模式:用于确保 IOC 容器和 DispatcherServlet 中央控制器的单一实例,避免重复创建和资源浪费。

  • 工厂模式:用于创建对象实例,将对象的创建过程封装起来,使得代码更具可读性和可维护性。

  • 代理模式:用于实现 AOP(面向切面编程),通过代理对象对目标对象进行包装,实现横切关注点的统一处理。

  • 前端控制器模式:用于将请求的分发和处理集中到一个中央控制器,提高代码的可维护性和灵活性。

  • 策略模式:用于实现不同的请求处理策略,根据请求的不同类型选择相应的处理逻辑。

  • 模板视图模式:用于将视图的渲染和展示逻辑与业务逻辑分离,实现解耦和重用。

第二章 专业技能

一、熟悉 Java 基本语法和面向对象思想,熟悉 Java 集合框架,理解多线程编程,了解 JDK 21 虚拟线程新特性。

1、Java 中的继承和多态有什么区别?

答案: 继承是一种机制,它允许一个类继承另一个类的属性和方法。子类可以继承父类的非私有成员,并且可以通过重写方法来改变其行为。多态是指同一类型的对象调用同一方法时,可能会产生不同的行为。它可以通过方法的重写和方法的重载来实现。

2、Java 中的接口和抽象类有什么区别?

答案: 接口是一种完全抽象的类,其中只定义了方法的签名而没有方法的实现。它提供了一种规范,用于定义类应该实现的方法。抽象类是一个可以包含抽象方法和具体方法的类,它不能被实例化,只能被继承。区别在于,一个类可以实现多个接口,但只能继承一个抽象类。

3、Java 中的 ArrayList 和 LinkedList 有什么区别?

答案: ArrayList 和 LinkedList 都是 Java 集合框架中的实现类。ArrayList 基于动态数组实现,它支持随机访问和快速的插入/删除操作。LinkedList 基于链表实现,它支持高效的插入/删除操作,但对于随机访问的效率较低。

4、什么是 Java 中的线程?如何创建和启动一个线程?

答案: 线程是执行单元,用于实现并发执行。在 Java 中,可以通过两种方式创建线程:继承 Thread 类,重写 run()方法,并调用 start()方法;或者实现 Runnable 接口,实现 run()方法,并创建 Thread 对象来包装 Runnable 实例。通过调用 Thread 的 start()方法来启动线程。

5、如何实现线程同步?请举例说明。

答案: 可以使用 Java 中的关键字 synchronized 来实现线程同步。它可以修饰方法或代码块,确保在同一时间只有一个线程可以访问被修饰的代码。例如,可以使用 synchronized 关键字修饰一个共享资源的访问方法,以避免多个线程同时修改该资源。

6、Java 中的 Lock 和 synchronized 的区别是什么?

答案: Lock 是 Java 并发包提供的一种机制,用于实现线程同步。与 synchronized 不同,Lock 是显式地获取和释放锁,可以实现更细粒度的线程控制,可以通过 lock()和 unlock()方法手动控制锁的获取和释放。相对而言,synchronized 是隐式地获取和释放锁,简单易用,但对控制粒度较低。

7、什么是 Java 中的线程池?它有什么好处?

答案: 线程池是一组预先创建的线程,用于执行提交的任务。它可以避免为每个任务创建新线程的开销,并提供对线程的管理和复用。线程池的好处包括提高性能和资源利用率、控制并发线程数、提供任务排队和调度等。

8、Java 中的并发容器有哪些?

答案: Java 中的并发容器包括 ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentSkipListSet 等。这些容器提供了线程安全的操作,并且能够高效地支持并发访问。

9、如何在 Java 中处理线程间的通信?

答案: 在 Java 中,可以使用以下方法来处理线程间的通信:

  • 使用共享变量:多个线程共享一个变量,并通过 synchronized 关键字或其他同步机制确保线程之间的可见性和一致性。

  • 使用 wait()和 notify()/notifyAll()方法:通过 Object 类提供的 wait()方法使线程进入等待状态,然后使用 notify()或 notifyAll()方法唤醒等待的线程。

  • 使用线程安全的队列:例如,BlockingQueue 可以用于在生产者和消费者之间进行安全的数据交换。

10、请解释一下 JDK 21 中的虚拟线程(Virtual Threads)新特性,并提供一个示例代码来说明其用法。

答案: JDK 21 引入了虚拟线程(Virtual Threads)作为一项新特性,旨在提高 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
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

public class VirtualThreadExample
{
public static void main(String[] args)
{
try (var executor = Executors.newVirtualThreadPerTaskExecutor())
{
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
try
{
Thread.sleep(Duration.ofSeconds(1).toMillis());
System.out.println("任务 " + i + " 被提交");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
});
});
}
}
}

在上面的示例代码中,使用了 Executors.newVirtualThreadPerTaskExecutor() 方法创建了一个虚拟线程池,并使用 executor.submit() 方法提交了一些异步任务。每个任务会休眠 1 秒钟,然后打印出任务完成的消息。通过使用虚拟线程,我们可以以更高效的方式处理并发任务。

二、熟悉常见数据结构和算法,如快速排序、二分查找等,熟悉常用的设计模式,如单例、工厂、代理等。

1、描述快速排序算法的原理,并给出相应的 Java 代码示例。

答案: 快速排序是一种常见的排序算法,基本思想是通过分治法将一个数组分成两个子数组,然后对这两个子数组进行递归排序。

具体步骤如下:

  1. 从数组中选择一个元素作为基准(通常选择第一个或最后一个元素)。

  2. 将数组划分为两个子数组,小于基准的元素放在左侧,大于基准的元素放在右侧。

  3. 对左右子数组递归应用快速排序算法。

  4. 合并左子数组、基准元素和右子数组,得到最终排序结果。

下面是 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
public class QuickSort
{
public static void quickSort(int[] arr, int low, int high)
{
if (low < high)
{
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}

private static int partition(int[] arr, int low, int high)
{
int pivot = arr[high];
int i = low - 1;

for (int j = low; j < high; j++)
{
if (arr[j] < pivot)
{
i++;
swap(arr, i, j);
}
}

swap(arr, i + 1, high);

return i + 1;
}

private static void swap(int[] arr, int i, int j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

// 使用示例
int[] arr = {9, 5, 1, 8, 2, 7};
QuickSort.quickSort(arr, 0, arr.length - 1);

// 输出:[1, 2, 5, 7, 8, 9]
System.out.println(Arrays.toString(arr));

2、二分查找是一种高效的查找算法,请描述其原理,并给出相应的 Java 代码示例。

答案:二分查找是一种在有序数组中查找特定元素的算法,基本思想是通过比较中间元素与目标元素的大小关系,不断缩小查找范围。

具体步骤如下:

  1. 初始化左指针 left 和右指针 right,分别指向数组的第一个元素和最后一个元素。

  2. 计算中间元素的索引 mid,即 mid = (left + right) / 2

  3. 比较中间元素与目标元素的大小关系:

    • 如果中间元素等于目标元素,则找到目标元素,返回索引。

    • 如果中间元素大于目标元素,则目标元素可能在左半部分,将右指针 right 更新为 mid - 1

    • 如果中间元素小于目标元素,则目标元素可能在右半部分,将左指针 left 更新为 mid + 1

  4. 重复步骤 2 和步骤 3,直到找到目标元素或左指针大于右指针。

下面是 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
public class BinarySearch
{
public static int binarySearch(int[] arr, int target)
{
int left = 0;
int right = arr.length - 1;

while (left <= right)
{
int mid = left + (right - left) / 2;

if (arr[mid] == target)
{
return mid;
}
else if (arr[mid] > target)
{
right = mid - 1;
}
else
{
left = mid + 1;
}
}

// 目标元素不存在
return -1;
}
}

// 使用示例
int[] arr = {1, 2, 5, 7, 8, 9};
int target = 7;
int index = BinarySearch.binarySearch(arr, target);

// 输出:目标元素的索引:3
System.out.println("目标元素的索引:" + index);

3、单例设计模式是一种常见的设计模式,请给出一个线程安全的单例模式的 Java 代码示例,并解释其原理。

答案: 单例设计模式旨在保证一个类只有一个实例,并提供一个全局访问点。

下面是一个线程安全的单例模式的 Java 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton
{
private static Singleton instance;

private Singleton()
{
// 私有构造函数,防止外部实例化
}

public static synchronized Singleton getInstance()
{
if (instance == null)
{
instance = new Singleton();
}

return instance;
}
}

该实现使用了懒加载的方式,在第一次调用 getInstance() 方法时创建实例。通过将 getInstance() 方法设为 synchronized,可以保证线程安全,即每次只有一个线程可以进入该方法,避免了并发创建实例的问题。

4、工厂模式是一种常见的设计模式,请给出一个工厂模式的 Java 代码示例,并解释其原理。

答案: 工厂模式旨在通过工厂类创建对象,而不是直接使用 new 关键字实例化对象。

下面是一个简单的工厂模式的 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
public interface Shape
{
void draw();
}

public class Circle implements Shape
{
@Override
public void draw()
{
System.out.println("绘制圆形");
}
}

public class Rectangle implements Shape
{
@Override
public void draw()
{
System.out.println("绘制矩形");
}
}

public class ShapeFactory
{
public Shape createShape(String type)
{
if (type.equalsIgnoreCase("circle"))
{
return new Circle();
}
else if (type.equalsIgnoreCase("rectangle"))
{
return new Rectangle();
}
else
{
throw new IllegalArgumentException("Unsupported shape type.");
}
}
}

在上面的示例中,Shape 接口定义了绘制形状的方法,CircleRectangle 是实现了 Shape 接口的具体形状类。ShapeFactory 是工厂类,根据传入的参数 type 创建相应的形状对象。通过使用工厂模式,客户端代码可以通过工厂类创建对象,而无需直接与具体的形状类耦合。

5、代理模式是一种常见的设计模式,请给出一个静态代理模式的 Java 代码示例,并解释其原理。

答案: 代理模式旨在为其他对象提供一种代理,以控制对该对象的访问。

下面是一个静态代理模式的 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
public interface Image
{
void display();
}

public class RealImage implements Image
{
private String filename;

public RealImage(String filename)
{
this.filename = filename;
loadFromDisk();
}

private void loadFromDisk()
{
System.out.println("从磁盘加载图片:" + filename);
}

@Override
public void display()
{
System.out.println("显示图片:" + filename);
}
}

public class ImageProxy implements Image
{
private RealImage realImage;
private String filename;

public ImageProxy(String filename)
{
this.filename = filename;
}

@Override
public void display()
{
if (realImage == null)
{
realImage = new RealImage(filename);
}

realImage.display();
}
}

在上面的示例中,Image 接口定义了显示图片的方法,RealImage 是实现了 Image 接口的具体图片类,ImageProxy 是代理类。当调用 display() 方法时,ImageProxy 会先检查 realImage是否已经创建了真实图片对象。如果已经创建,则直接调用真实图片对象的 display() 方法显示图片;如果尚未创建,则先创建真实图片对象,然后调用其 display() 方法显示图片。通过使用代理模式,可以在访问真实图片对象之前或之后执行一些额外的操作,例如加载图片、权限验证等。

6、请解释什么是链表(LinkedList)数据结构,并给出一个 Java 代码示例。

答案: 链表是一种常见的动态数据结构,由一系列节点组成,每个节点包含数据和指向下一个节点的引用。链表中的节点不一定是连续存储的,而是通过指针或引用链接在一起。链表分为单向链表和双向链表两种形式。

下面是一个单向链表的 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
public class ListNode
{
int val;
ListNode next;

public ListNode(int val)
{
this.val = val;
}
}

public class LinkedList
{
private ListNode head;

public void insert(int val)
{
ListNode newNode = new ListNode(val);

if (head == null)
{
head = newNode;
}
else
{
ListNode curr = head;

while (curr.next != null)
{
curr = curr.next;
}

curr.next = newNode;
}
}

public void display()
{
ListNode curr = head;

while (curr != null)
{
System.out.print(curr.val + " ");
curr = curr.next;
}

System.out.println();
}
}

// 使用示例
LinkedList list = new LinkedList();
list.insert(1);
list.insert(2);
list.insert(3);

// 输出:1 2 3
list.display();

在上面的示例中,ListNode 是链表的节点类,包含一个值 val 和一个指向下一个节点的引用 nextLinkedList 是链表类,具有插入节点和显示链表的功能。通过调用 insert() 方法插入节点,并通过调用 display() 方法显示链表的内容。

7、请解释什么是栈(Stack)数据结构,并给出一个 Java 代码示例。

答案: 栈是一种常见的线性数据结构,遵循后进先出(Last-In-First-Out,LIFO)的原则。栈中的元素只能在栈顶进行插入和删除操作。栈的插入操作称为入栈(push),删除操作称为出栈(pop)。

下面是一个栈的 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
import java.util.EmptyStackException;

public class Stack
{
private int[] data;
private int top;

public Stack(int capacity)
{
data = new int[capacity];
top = -1;
}

public boolean isEmpty()
{
return top == -1;
}

public boolean isFull()
{
return top == data.length - 1;
}

public void push(int value)
{
if (isFull())
{
throw new StackOverflowError("Stack is full");
}

data[++top] = value;
}

public int pop()
{
if (isEmpty())
{
throw new EmptyStackException();
}

return data[top--];
}

public int peek()
{
if (isEmpty())
{
throw new EmptyStackException();
}

return data[top];
}
}

// 使用示例
Stack stack = new Stack(5);
stack.push(1);
stack.push(2);
stack.push(3);

// 输出:3
System.out.println(stack.pop());

// 输出:2
System.out.println(stack.peek());

在上面的示例中,Stack 类使用数组实现了栈数据结构。data 数组用于存储栈中的元素,top 表示栈顶的索引。通过调用 push() 方法将元素入栈,调用 pop() 方法将元素出栈,调用 peek() 方法获取栈顶元素而不删除它。通过 isEmpty()isFull() 方法可以判断栈是否为空或已满。

8、请解释什么是队列(Queue)数据结构,并给出一个 Java 代码示例

答案: 队列是一种常见的线性数据结构,遵循先进先出(First-In-First-Out,FIFO)的原则。队列中的元素只能在队尾插入(入队)和在队头删除(出队)。新元素插入的一端称为队尾,已有元素删除的一端称为队头。

下面是一个队列的 Java 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.LinkedList;
import java.util.Queue;

public class QueueExample
{
public static void main(String[] args)
{
Queue<Integer> queue = new LinkedList<>();

// 入队
queue.offer(1);
queue.offer(2);
queue.offer(3);

// 出队
while (!queue.isEmpty())
{
System.out.println(queue.poll());
}
}
}

在上面的示例中,使用 Java 标准库中的 Queue 接口和 LinkedList 类实现了队列。通过调用 offer() 方法将元素入队,调用 poll() 方法将元素出队,并使用 isEmpty() 方法检查队列是否为空。

请注意,Java 标准库中的 Queue 接口还提供了其他操作,例如 peek() 方法用于获取队头元素但不删除它,size() 方法用于获取队列中元素的数量等。具体使用哪些操作取决于需求。

9、什么是哈希表(HashTable)数据结构,并给出一个 Java 代码示例。

答案: 哈希表(HashTable)是一种常见的数据结构,用于存储键值对(Key-Value)。它通过哈希函数将键映射到数组中的特定位置,以实现高效的插入、删除和查找操作。

下面是一个使用 Java 中的HashMap类实现的哈希表示例:

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
import java.util.HashMap;

public class HashTableExample
{
public static void main(String[] args)
{
HashMap<String, Integer> hashMap = new HashMap<>();

// 添加键值对
hashMap.put("Alice", 25);
hashMap.put("Bob", 30);
hashMap.put("Charlie", 35);

// 获取值
// 输出:30
System.out.println(hashMap.get("Bob"));

// 检查键是否存在
// 输出:true
System.out.println(hashMap.containsKey("Alice"));

// 删除键值对
hashMap.remove("Charlie");

// 迭代哈希表
for (String key : hashMap.keySet())
{
int value = hashMap.get(key);
System.out.println(key + ": " + value);
}
}
}

在上面的示例中,使用 Java 标准库中的HashMap类实现了哈希表。通过调用put(key, value)方法可以添加键值对,通过调用get(key)方法可以获取键对应的值,通过调用containsKey(key)方法可以检查键是否存在,通过调用remove(key)方法可以删除键值对。此外,可以使用keySet()方法获取哈希表中所有键的集合,并通过迭代集合获取键和对应的值。

需要注意的是,哈希表的具体实现可能不同,但主要思想是使用哈希函数将键映射到数组中的位置,以提高插入、删除和查找操作的效率。

10、什么是二叉树(Binary Tree)数据结构,并给出一个 Java 代码示例。

答案: 二叉树(Binary Tree)是一种常见的树形数据结构,由节点组成,每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树具有递归性质,每个节点都可以看作是根节点,其左子树和右子树也是二叉树。

下面是一个二叉树的 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
class TreeNode
{
int val;
TreeNode left;
TreeNode right;

public TreeNode(int val)
{
this.val = val;
}
}

public class BinaryTreeExample
{
public static void main(String[] args)
{
// 创建二叉树
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);

// 遍历二叉树
System.out.println("前序遍历:");
preOrderTraversal(root);
System.out.println();

System.out.println("中序遍历:");
inOrderTraversal(root);
System.out.println();

System.out.println("后序遍历:");
postOrderTraversal(root);
System.out.println();
}

// 前序遍历
public static void preOrderTraversal(TreeNode root)
{
if (root != null) {
System.out.print(root.val + " ");
preOrderTraversal(root.left);
preOrderTraversal(root.right);
}
}

// 中序遍历
public static void inOrderTraversal(TreeNode root)
{
if (root != null) {
inOrderTraversal(root.left);
System.out.print(root.val + " ");
inOrderTraversal(root.right);
}
}

// 后序遍历
public static void postOrderTraversal(TreeNode root)
{
if (root != null) {
postOrderTraversal(root.left);
postOrderTraversal(root.right);
System.out.print(root.val + " ");
}
}
}

在上面的示例中,TreeNode 是二叉树的节点类,包含一个值 val,以及左子节点 left 和右子节点 rightBinaryTreeExample 类创建了一个二叉树,并实现了三种常见的遍历方法:前序遍历、中序遍历和后序遍历。

前序遍历按照根节点、左子树、右子树的顺序遍历节点;中序遍历按照左子树、根节点、右子树的顺序遍历节点;后序遍历按照左子树、右子树、根节点的顺序遍历节点。

在示例中,通过调用 preOrderTraversal()inOrderTraversal()postOrderTraversal() 方法可以遍历二叉树,并按照不同的顺序输出节点的值。

三、熟悉 MySQL 的基本操作,如数据库设计、SQL 查询优化、事务处理、分库分表等。

1、什么是索引?为什么使用索引?请举例说明如何创建索引。

答案: 索引是一种数据结构,用于加快数据库查询的速度。它类似于书籍的目录,可以根据关键字快速定位到对应的数据。

创建索引的语法如下:

1
CREATE INDEX index_name ON table_name (column_name);

例如,创建一个名为”idx_username”的索引,用于提高对”user”表中的”username”列的查询速度:

1
CREATE INDEX idx_username ON user (username);

2、什么是 SQL 查询优化?请举例说明如何优化查询性能。

答案: SQL 查询优化是通过调整查询语句和数据库结构来提高查询性能的过程。

优化查询性能的方法包括:

  • 避免使用 SELECT *,只选择需要的列。

  • 使用合适的索引:为经常用于查询的列创建索引,避免全表扫描。

  • 缓存重复查询结果:使用缓存技术,避免重复执行相同的查询。

  • 优化查询语句:避免使用不必要的 JOIN 操作,合理使用 WHERE 子句和 LIMIT 限制结果集大小等。

  • 对查询进行分析,使用 EXPLAIN 关键字查看查询执行计划,并进行必要的优化。

3、什么是数据库的事务?如何确保事务的原子性?

答案: 事务是一组被视为单个逻辑单元的操作,要么全部执行成功,要么全部回滚。事务的原子性是通过将操作封装在 BEGIN、COMMIT 和 ROLLBACK 语句中来实现的。BEGIN 表示事务的开始,COMMIT 表示事务的提交,而 ROLLBACK 表示事务的回滚。

4、什么是数据库事务的隔离级别?请列举不同的隔离级别。

答案: 数据库事务的隔离级别定义了多个事务之间的可见性和并发性。

常见的隔离级别包括:

  • 读未提交(Read Uncommitted):一个事务可以读取另一个事务未提交的数据。

  • 读已提交(Read Committed):一个事务只能读取另一个事务已提交的数据。

  • 可重复读(Repeatable Read):在同一个事务中,多次读取同一数据的结果是一致的,即使其他事务对该数据进行了修改。

  • 串行化(Serializable):事务之间完全隔离,每个事务按顺序执行。

5、请解释什么是数据库连接池,以及其作用是什么?

答案: 数据库连接池是一个管理数据库连接的缓冲池。它的作用是在应用程序和数据库之间建立和复用数据库连接,以提高性能和可伸缩性。连接池会预先创建一定数量的数据库连接,并将它们保存在池中,当应用程序需要连接数据库时,从池中获取一个连接,并在使用完毕后将连接返回到池中,以便其他应用程序可以复用。

6、什么是数据库事务的并发控制?请解释并发控制中的锁和事务隔离的关系。

答案: 并发控制是指在多个事务并发执行时,保证数据一致性和事务隔离的机制。锁是并发控制的一种常见手段,用于对数据库中的数据进行访问控制。事务隔离级别定义了事务之间的可见性和并发性,通过锁机制可以实现不同隔离级别的并发控制。

7、请解释什么是数据库的锁,以及常见的锁类型。

答案: 数据库的锁是用于控制并发访问的机制,以保证事务的隔离性和数据的完整性。

常见的锁类型包括:

  • 共享锁(Shared Lock):多个事务可以同时获取共享锁,用于读取数据,但不允许其他事务修改数据。

  • 排他锁(Exclusive Lock):只允许一个事务获取排他锁,用于修改数据,其他事务无法同时获取共享锁或排他锁。

  • 行锁(Row Lock):锁定数据库表中的某行数据,用于控制对特定行的访问。

  • 表锁(Table Lock):锁定整个数据库表,用于控制对整个表的访问。

8、如何处理并发事务冲突?请介绍一下悲观锁和乐观锁的原理和应用场景。

答案: 并发事务冲突是指多个事务同时访问和修改相同的数据时可能导致的数据一致性问题。处理并发事务冲突常用的方式包括悲观锁和乐观锁。

  • 悲观锁:在执行读写操作之前,悲观锁会对数据加锁,阻止其他事务对数据的修改。常见的悲观锁实现方式是通过数据库的行级锁或表级锁来实现。悲观锁适用于并发写入较多的场景,但可能导致性能下降和锁竞争问题。

  • 乐观锁:乐观锁假设事务之间不会发生冲突,不会主动加锁,而是在提交事务时检查数据是否被其他事务修改过。常见的乐观锁实现方式是使用版本号或时间戳来检测数据的并发修改。乐观锁适用于并发读取较多、冲突较少的场景,可以减少锁竞争和性能开销。

9、请解释什么是分库分表,并说明其优缺点。

答案: 分库分表是将一个大型数据库拆分成多个小型数据库或表,以提高数据库的扩展性和性能。

优点包括:

  • 提高读写性能:分库分表可以将负载分散到多个数据库或表上,提高并发处理能力。

  • 提高可用性:当部分数据库或表发生故障时,其他数据库或表仍然可用。

缺点包括:

  • 数据一致性:跨库事务管理和数据同步变得更加复杂。

  • 查询复杂性:涉及多个数据库或表的查询需要进行联合查询或分布式查询。

10、请解释什么是聚集索引和非聚集索引,并描述它们之间的区别。

答案: 聚集索引(Clustered Index)和非聚集索引(Non-clustered Index)是 MySQL 数据库中常见的两种索引类型,它们在物理存储和数据访问方面有所不同。

聚集索引:

  • 聚集索引定义了表的物理排序顺序,决定了数据行在磁盘上的存储位置。
  • 一个表只能拥有一个聚集索引,通常是主键索引。
  • 聚集索引的叶子节点包含了完整的数据行,因此可以满足覆盖索引的查询需求。
  • 由于数据行按聚集索引的顺序进行物理存储,所以聚集索引对于范围查询和排序操作的性能影响较大。

非聚集索引:

  • 非聚集索引是基于表中的某个列或多个列创建的索引,与实际数据行的物理存储顺序无关。
  • 一个表可以拥有多个非聚集索引。
  • 非聚集索引的叶子节点包含了索引列的值以及指向对应数据行的指针。
  • 当使用非聚集索引进行查询时,需要通过索引找到对应的行指针,然后再根据指针找到实际的数据行,因此需要进行两次查找操作。
  • 非聚集索引适合于快速定位数据行的查询,但在范围查询和排序操作上的性能可能相对较差。

总结:

聚集索引决定了表中数据行的物理存储顺序,可以满足覆盖索引的查询需求,适合范围查询和排序操作;而非聚集索引是基于表中列的值创建的索引,需要进行两次查找操作,适合快速定位数据行的查询。

四、熟悉常用的 Java Web 开发框架的使用,如 Spring、Spring MVC、MyBatis、Spring Boot 等,理解 IOC、AOP 原理,了解 Spring MVC 的工作流程和 Spring Boot 的启动过程、自动装配原理。

1、什么是 IOC(控制反转)和 DI(依赖注入)?它们在 Spring 框架中有何作用?

答案: IOC 是一种设计模式,它将对象的创建和对象之间的依赖关系的管理交给了容器。DI 是 IOC 的一种实现方式,通过注入依赖对象来实现对象之间的解耦。在 Spring 框架中,IOC 和 DI 使得开发者可以更好地管理对象的创建和依赖关系,提高了代码的可维护性和可测试性。

2、请简要介绍一下 Spring 框架中的 AOP(面向切面编程)。

答案: AOP 是一种编程范式,它通过将横切逻辑(如日志记录、事务管理等)与业务逻辑分离,使得代码的重用性和可维护性得到提高。在 Spring 框架中,AOP 通过使用代理对象对目标对象进行包装,实现了在目标对象的方法执行前、执行后或异常抛出时插入横切逻辑的功能。

3、Spring 中的 Bean 作用域有哪些?它们的区别是什么?

答案: Spring 框架中的 Bean 作用域包括单例(Singleton)、原型(Prototype)、会话(Session)、请求(Request)和全局会话(Global Session)等。

它们的区别如下:

  • 单例:在整个应用程序中只创建一个 Bean 实例。

  • 原型:每次请求时创建一个新的 Bean 实例。

  • 会话:在 Web 应用中,为每个会话创建一个 Bean 实例。

  • 请求:在 Web 应用中,为每个请求创建一个 Bean 实例。

  • 全局会话:在基于 Portlet 的 Web 应用中,为每个全局会话创建一个 Bean 实例。

4、Spring 框架中的 Bean 生命周期是怎样的?

答案: Spring 框架中的 Bean 生命周期包括以下阶段:

  1. 实例化:根据 Bean 的定义,创建 Bean 的实例。

  2. 属性赋值:将配置的属性值或引用注入到 Bean 实例中。

  3. 初始化:执行自定义的初始化逻辑,可以实现 InitializingBean 接口或添加自定义的初始化方法。

  4. 使用:Bean 实例可供其他组件使用。

  5. 销毁:执行自定义的销毁逻辑,可以实现 DisposableBean 接口或添加自定义的销毁方法。

5、如何解决 Spring 框架中的循环依赖问题?

答案: Spring 框架通过三级缓存解决循环依赖问题。当创建 Bean 时,Spring 会将正在创建的 Bean 放入“当前创建 Bean”缓存中,然后继续创建 Bean 的属性依赖。如果遇到循环依赖,Spring 会将正在创建 Bean 的 ObjectFactory 放入“早期暴露 Bean”缓存中,以供循环依赖的 Bean 使用。最后,当 Bean 创建完成后,Spring 会将其放入“已完成 Bean”缓存中,以供后续的 Bean 依赖使用。

6、Spring 框架中的事务管理是如何实现的?

答案: Spring 框架中的事务管理通过 AOP 实现。在 Spring 中,可以通过声明式事务管理和编程式事务管理两种方式来管理事务。声明式事务管理通过在方法上添加事务注解(如@Transactional)来定义事务的边界,Spring 会在方法执行前后自动管理事务的开启、提交和回滚。编程式事务管理则通过编写代码来手动管理事务的开启、提交和回滚。

7、Spring MVC 的工作流程是怎样的?

答案: Spring MVC 的工作流程如下:

  1. 客户端发送请求到 DispatcherServlet。

  2. DispatcherServlet 根据请求的 URL 找到对应的 HandlerMapping,确定请求对应的 Controller。

  3. Controller 处理请求,并返回一个 ModelAndView 对象。

  4. DispatcherServlet 通过 ViewResolver 解析 ModelAndView 中的 View 名字,得到具体的 View 对象。

  5. View 对象负责渲染 Model 数据,并生成最终的响应结果。

  6. DispatcherServlet 将响应结果返回给客户端。

8、MyBatis 的工作原理是什么?MyBatis 中 #{} 和 ${} 的区别?

答案: MyBatis 是一种 Java 持久化框架,用于简化数据库访问的过程。它提供了一个基于映射的方式来执行 SQL 查询、插入、更新和删除等操作。

MyBatis 的工作原理:

  1. 配置文件(mybatis-config.xml)来描述如何连接数据库,如何获得 SqlSession 实例等。

  2. 定义 SQL 映射文件,将 SQL 语句与 bean 类或某个接口进行映射。

  3. 根据配置文件获取 SqlSession 实例,执行映射文件中定义的 SQL 语句。

  4. 使用动态代理实现接口返回结果集

#{} 和 ${} 的区别:

  • #{} 表示一个参数,MyBatis 会根据 PreparedStatement 设置该参数,有防 SQL 注入的好处。

  • ${} 直接将参数填充在 SQL 语句中,可能存在 SQL 注入风险。${} 一般用在空值或其他非输入值的字段上,如写 SQL 时使用分表等。

总之:#{}可以有效预防 SQL 注入,应该尽量使用#{}。${}不可以有效预防 SQL 注入,只适合某些非输入值的特殊场景使用。

9、Spring Boot 的启动过程是怎样的?

答案: Spring Boot 的启动过程如下:

  1. 加载 Spring Boot 的核心配置文件,创建并初始化 Spring 应用上下文。

  2. 扫描应用程序中的类,识别和注册容器管理的 Bean。

  3. 执行各种自动配置,包括加载外部配置文件、创建数据库连接池等。

  4. 启动嵌入式的 Web 服务器(如 Tomcat、Jetty 等)。

  5. 注册 Servlet、Filter、Listener 等 Web 组件。

  6. 执行应用程序的初始化逻辑。

  7. 应用程序启动完成,等待处理请求。

10、什么是自动装配?Spring Boot 框架中有哪些自动装配的方式?

答案: 自动装配(Autosiring)是 Spring 框架中的一个核心功能,它能够根据特定的规则,自动将应用程序中的各个组件(Bean)进行连接和配置,减少了手动配置的工作量,提高了开发效率。

在 Spring Boot 框架中,有以下几种自动装配的方式:

  • 组件扫描(Component Scanning):Spring Boot 通过组件扫描机制自动发现应用程序中的组件。可以使用@ComponentScan注解指定要扫描的包路径,Spring Boot 会自动将带有@Component及其派生注解的类注册为 Bean。

  • 条件装配(Conditional Configuration):Spring Boot 提供了条件装配的功能,可以根据特定的条件决定是否配置某个 Bean。可以使用@Conditional注解及其派生注解在配置类或 Bean 上设置条件,当条件满足时,相应的 Bean 会被自动装配。

  • 自动配置(Auto-Configuration):Spring Boot 提供了大量的自动配置类,根据应用程序的依赖和配置,自动配置框架中的各种组件。自动配置类通常使用@Configuration注解进行标记,通过条件装配来决定是否生效。

  • 自动装配(Autowiring):Spring Boot 支持自动装配,即根据类型和名称自动将依赖注入到 Bean 中。可以使用@Autowired注解将需要的依赖注入到 Bean 中,Spring Boot 会自动寻找合适的候选 Bean 进行注入。

  • 属性注入(Property Injection):Spring Boot 可以自动将配置文件中的属性值注入到 Bean 的属性中。可以使用@Value注解将属性与配置文件中的属性值进行绑定,Spring Boot 会自动加载配置文件,并将属性值注入到相应的 Bean 中。

这些自动装配的方式使得 Spring Boot 应用程序的开发更加便捷,可以减少冗余的配置代码,提高开发效率。

五、了解微服务架构和分布式系统的基本概念,如 Nacos 注册与配置、Gateway 网关、OpenFeign 服务调用等。

1、什么是微服务架构?它的优势和劣势是什么?

答案: 微服务架构是一种将应用程序拆分为一组小型、松耦合的服务的方法。每个服务都可以独立部署、扩展和管理。优势包括提高系统的可扩展性、可维护性和灵活性,允许团队使用不同的技术栈和开发周期。劣势包括增加了系统的复杂性、部署和监控的挑战,以及在跨服务的通信和一致性处理方面的复杂性。

2、Nacos 是什么?它在微服务架构中的作用是什么?

答案: Nacos 是一个用于动态服务发现、配置管理和服务治理的开源平台。它提供了服务注册与发现、动态配置管理、服务健康监测等功能。在微服务架构中,Nacos 可以用作服务注册中心,让服务实例能够注册自己的信息并让其他服务发现并调用它们。

3、什么是 Gateway 网关?它的作用是什么?

答案: Gateway 网关是微服务架构中的一种设计模式,它充当了服务的入口,负责将外部请求路由到相应的微服务实例。它可以处理认证、授权、限流、负载均衡等功能,同时也可以提供统一的 API 接口给客户端使用。Gateway 网关可以起到保护微服务免受恶意请求和异常流量的作用。

4、请解释一下 OpenFeign 在微服务架构中的作用。

答案: OpenFeign 是一个用于声明式、模板化的 HTTP 客户端工具,它简化了在微服务架构中进行服务间通信的开发过程。通过使用注解和接口定义,开发人员可以轻松地声明需要调用的远程服务,并使用类似于本地方法调用的方式进行调用,而无需手动处理底层的 HTTP 请求和序列化。

5、你了解哪些微服务架构中的负载均衡策略?

答案: 在微服务架构中,有几种常见的负载均衡策略。

以下是其中一些常见的策略:

  • 轮询(Round Robin):将请求依次分发给可用的服务实例。每个请求按顺序选择下一个服务实例,直到循环到第一个实例。

  • 随机(Random):随机选择可用的服务实例来处理请求。每个请求都会随机选择一个服务实例,没有特定的顺序。

  • 最少连接(Least Connection):选择当前连接数最少的服务实例来处理请求。这种策略考虑了每个服务实例的负载情况,将请求分发给连接数最少的实例,以实现负载均衡。

  • IP 哈希(IP Hash):根据客户端的 IP 地址将请求路由到特定的服务实例。使用客户端的 IP 地址计算哈希值,然后将请求发送到对应哈希值的服务实例。

  • 加权轮询(Weighted Round Robin):为每个服务实例分配一个权重值,权重值越高的实例,接收到的请求比例就越大。这种策略可以根据服务实例的处理能力和性能分配不同的权重。

  • 加权随机(Weighted Random):类似于加权轮询,但是选择服务实例的方式是随机的。每个实例的选择概率与其权重成比例。

  • 一致性哈希(Consistent Hashing):通过哈希算法将请求映射到服务实例。这种策略可以在添加或删除服务实例时最小化请求的重新分配,因为只有少量的请求会受到影响。

这些负载均衡策略可以根据具体的需求和环境选择适合的策略,以实现在微服务架构中的负载均衡和性能优化。

6、在微服务架构中,如何保证数据的一致性?

答案: 在微服务架构中,确保数据的一致性是一个具有挑战性的任务,因为数据可能分布在不同的服务中,并且每个服务都有自己的数据库或数据存储。

下面是两个常用的方法来保证数据的一致性:

  • 事件驱动架构(Event-Driven Architecture):事件驱动架构是一种常见的解决方案,它通过在服务之间发送和接收事件来实现数据的一致性。当一个服务的数据发生变化时,它会发布一个事件,其他服务可以订阅这个事件并采取相应的操作来保持数据的一致性。例如,当一个订单服务接收到一个新订单时,它可以发布一个“订单创建”事件,其他服务,如库存服务和支付服务可以通过订阅这个事件来更新库存和执行支付操作。通过事件驱动架构,各个服务之间的数据变更可以异步地进行,从而提高了系统的可伸缩性和灵活性。

  • 分布式事务(Distributed Transactions):分布式事务是另一种保证数据一致性的常见方法。它通过将多个操作组合为一个原子操作来确保数据的一致性。当一个跨多个服务的操作需要保证一致性时,可以使用分布式事务来协调这些服务的操作。分布式事务使用了一致性协议,如两阶段提交(Two-Phase Commit)或三阶段提交(Three-Phase Commit),来确保所有参与者在提交或回滚操作时达成一致的状态。这种方法可以保证在跨多个服务的操作中,要么所有的操作都成功提交,要么所有的操作都回滚,从而保持数据的一致性。

这些方法都有各自的优势和限制,选择合适的方法取决于具体的业务需求和系统架构。在实践中,通常会根据具体情况综合使用多种方法来保证数据的一致性。

7、你知道什么是服务降级吗?它在微服务架构中的作用是什么?

答案: 服务降级是一种应对系统故障或高负载情况的策略,当某个微服务出现问题时,可以通过降低其功能或性能来保证整个系统的可用性。服务降级可以通过返回缓存数据、返回默认值、限制请求频率等方式来实现,从而减少对故障服务的依赖。

8、如何处理微服务架构中的服务间通信失败的情况?

答案: 在微服务架构中,服务间通信失败是一个常见的情况。

处理这种情况需要考虑以下几个方面:

  • 重试机制:当服务间通信失败时,可以通过实施重试机制来尝试重新发送请求。重试可以在服务内部实现,也可以通过使用消息队列来处理。在实施重试机制时,可以设置最大重试次数和重试间隔,以避免无限制地进行重试。

  • 超时处理:为了防止服务间通信失败导致的长时间阻塞,可以设置超时机制。在发送请求后,如果没有及时收到响应,可以选择中断请求并进行相应的处理,例如记录日志、发送警报或返回适当的错误信息。

  • 熔断机制:为了防止服务间通信失败引发的级联故障,可以实施熔断机制。熔断机制会监控服务间通信的错误率或响应时间,并在达到一定阈值时中断对该服务的请求。中断后,可以选择返回一个预定义的错误响应,而不是继续请求该服务。

  • 降级处理:当服务间通信失败时,可以选择降级处理。降级意味着在服务不可用或响应时间过长的情况下,提供一个备用的简化功能。例如,返回缓存数据、使用默认值或提供有限功能的替代服务。

  • 监控和告警:建立监控系统来实时监测服务间通信的状态和性能,以便及时发现故障并采取措施。同时,设置适当的告警机制,以便在服务间通信失败时及时通知相关团队进行处理。

  • 日志和故障排查:记录服务间通信的日志,并建立适当的故障排查机制。通过分析日志,可以定位问题的根本原因,并采取措施进行修复或优化。

综上所述,处理微服务架构中的服务间通信失败需要综合考虑重试机制、超时处理、熔断机制、降级处理、监控和告警以及日志和故障排查等策略,以提高系统的可靠性和容错性。

9、你了解微服务架构中的消息队列吗?它有什么作用?

答案: 在微服务架构中,消息队列是一种常见的通信模式,用于实现微服务之间的异步通信。它允许一个微服务将消息发送到队列,而不需要直接与接收方进行通信。接收方则可以按照自己的节奏从队列中获取消息并进行处理。

消息队列在微服务架构中具有以下几个主要作用:

  • 异步通信:消息队列提供了一种异步的通信机制。发送方将消息发送到队列后,可以立即继续处理其他任务,而无需等待接收方的响应。这样可以提高系统的整体性能和吞吐量。

  • 服务解耦:通过使用消息队列,微服务之间的通信变得松耦合。发送方只需要将消息发送到队列中,而不需要知道消息的具体接收方。接收方可以根据自己的需求从队列中订阅感兴趣的消息。这种解耦使得微服务的开发和维护更加灵活和可扩展。

  • 削峰填谷:消息队列可以用于平衡系统中不同微服务之间的负载。当一个微服务处理能力不足以应对突发的高负载时,可以将消息发送到队列中,以便稍后处理。这样可以有效地平衡系统的负载,防止系统崩溃或性能下降。

  • 可靠性保证:消息队列通常提供持久化机制,确保即使在系统故障或重启后,消息也不会丢失。消息可以持久化到磁盘上,以便在系统恢复后重新加载和处理。这提高了系统的可靠性和健壮性。

  • 系统解耦:通过引入消息队列,可以将复杂的系统拆分为多个独立的微服务,每个微服务负责处理特定的业务逻辑。这种解耦使得系统更容易理解、开发和维护。

综上所述,消息队列在微服务架构中发挥着重要的作用,包括实现异步通信、服务解耦、削峰填谷和提供可靠性保证。它是构建可靠、可扩展和高性能微服务系统的重要组成部分。

10、在微服务架构中,如何进行服务监控和日志记录?

答案: 微服务架构中的服务监控和日志记录非常重要。可以使用监控工具(如 Prometheus、Grafana)来收集和展示服务的运行指标,以便及时发现和解决问题。同时,使用日志记录框架(如 ELK Stack、Sleuth+Zipkin)可以帮助收集、存储和分析微服务的日志信息,以便进行故障排查和性能优化。

六、了解 Redis 缓存的使用,如数据存储、缓存策略、哨兵机制、发布订阅功能以及应对缓存雪崩等。

1、什么是 Redis?它的主要特点是什么?Redis 与传统关系型数据库的区别是什么?

答案: Redis(Remote Dictionary Server)是一个开源的内存数据存储系统,它提供了键值对的存储,并支持多种数据结构。

Redis 具有以下主要特点:

  • 数据存储在内存中,因此读写速度非常快。

  • 支持多种数据结构,包括字符串、哈希、列表、集合和有序集合等。

  • 提供了丰富的功能,如事务、持久化、发布订阅等。

  • 可以通过主从复制和哨兵机制实现高可用性。

Redis 和传统关系型数据库有以下几个主要区别:

  • 存储方式:Redis 将数据存储在内存中,而传统关系型数据库通常将数据存储在磁盘上。

  • 数据结构:Redis 支持多种数据结构,如字符串、哈希、列表等,而关系型数据库使用表和行来组织数据。

  • 查询语言:Redis 使用类似于键值对的 API 进行数据访问,而关系型数据库使用 SQL 查询语言。

  • 持久化:Redis 可以选择将数据持久化到磁盘上,但默认情况下只将数据存储在内存中,而关系型数据库通常将数据持久化到磁盘上。

2、Redis 的持久化机制有哪些?它们有什么区别?

答案: Redis 提供了两种持久化机制:

  • RDB(Redis Database):将 Redis 在内存中的数据定期保存到磁盘上的二进制文件。RDB 是一个快照(snapshot)的形式,保存了某个时间点上的数据快照。它适用于数据比较稳定,可以容忍一定数据丢失的场景。

  • AOF(Append-Only File):将 Redis 的操作日志以追加的方式写入磁盘文件。AOF 记录了 Redis 的所有写操作指令,通过重放这些指令可以恢复数据。AOF 适用于需要高数据安全性和可靠性的场景。

RDB 持久化方式相对于 AOF 方式更加高效,因为它只需要保存一个快照文件,而 AOF 方式需要记录每条写操作指令。但是 AOF 方式更加安全,因为可以通过重放操作日志来恢复数据,并且可以配置不同级别的同步策略来控制数据的安全性和性能。

3、Redis 的主从复制是什么?它的作用是什么?

答案: Redis 的主从复制是指将一个 Redis 节点(主节点)的数据复制到其他 Redis 节点(从节点)的过程,从节点会持续地复制主节点上的数据更新操作,以保持数据的一致性。

主从复制的作用包括:

  • 提高读性能:主从复制可以使得读操作分摊到多个节点上,从而提高整体的读性能。

  • 数据备份:从节点可以作为主节点数据的备份,当主节点发生故障时,可以快速切换到从节点继续提供服务。

  • 扩展性:通过添加多个从节点,可以扩展系统的读能力,满足高并发读取的需求。

4、Redis 的缓存策略有哪些?请分别说明它们的特点。

答案: Redis 的常见缓存策略包括:

  • LRU(Least Recently Used):最近最少使用策略,淘汰最近使用次数最少的数据。

  • LFU(Least Frequently Used):最不经常使用策略,淘汰使用频率最低的数据。

  • TTL(Time To Live):设置数据的过期时间,过期后自动删除。

  • Random(随机):随机选择要淘汰的数据。

LRU 和 LFU 是基于数据的访问频率来确定淘汰策略的,TTL 是基于数据的过期时间来淘汰数据的,而随机策略则是随机选择要淘汰的数据,没有特定的规则。

5、Redis 的哨兵机制是什么?它的作用是什么?

答案: Redis 的哨兵机制是一种用于监控和管理 Redis 主从复制和高可用性的解决方案。哨兵是一个独立的进程,负责监控 Redis 实例的状态,并在主节点下线时自动将一个从节点升级为新的主节点。

哨兵的主要作用包括:

  • 监控:哨兵定期检查 Redis 实例的状态,包括主节点和从节点是否正常运行。

  • 自动故障转移:当主节点宕机时,哨兵会自动将一个从节点升级为新的主节点,确保系统的高可用性。

  • 配置提供:哨兵负责维护 Redis 实例的配置信息,如果配置发生变化,哨兵会通知客户端进行更新。

6、Redis 的发布订阅功能是什么?如何使用它?

答案: Redis 的发布订阅功能允许客户端订阅一个或多个频道,并接收发布到这些频道的消息。发布者发布消息到指定的频道,订阅者则可以接收到相应的消息。

使用发布订阅功能的步骤如下:

  • 订阅频道:客户端使用SUBSCRIBE命令来订阅一个或多个频道,例如SUBSCRIBE channel1

  • 发布消息:发布者使用PUBLISH命令将消息发布到指定的频道,例如PUBLISH channel1 message1

  • 接收消息:订阅者通过订阅的频道接收到发布者发布的消息。

通过发布订阅功能,可以实现实时的消息传递和事件通知,适用于实时聊天、消息队列等场景。

7、如何应对 Redis 缓存穿透问题?

答案: Redis 缓存穿透是指恶意请求或者非法请求查询一个不存在的 Key,导致请求直接访问数据库,造成数据库压力过大。

解决 Redis 缓存穿透问题的方法包括:

  • 布隆过滤器:使用布隆过滤器判断请求的 Key 是否存在于缓存中或者数据库中,如果不存在,则直接拦截请求,避免对数据库的查询压力。

  • 空值缓存:对于查询数据库结果为空的情况,也将空结果缓存起来,设置一个较短的过期时间,避免重复的查询请求。

  • 热点数据预加载:将热点数据提前加载到缓存中,确保缓存中存在大部分常用的数据,降低缓存穿透的概率。

8、如何应对 Redis 缓存雪崩问题?

答案: Redis 缓存雪崩是指在某个时间点,大量的缓存数据同时失效或过期,导致大量的请求直接访问数据库,造成数据库压力过大,甚至崩溃。

应对 Redis 缓存雪崩问题的常见方法包括:

  • 设置合理的缓存过期时间:通过合理设置缓存数据的过期时间,避免大量缓存同时失效。

  • 使用分布式锁:在缓存失效时,通过分布式锁来控制只有一个请求去重新加载缓存,其他请求等待并使用旧的缓存数据。

  • 增加缓存层:引入多级缓存架构,如本地缓存和分布式缓存的组合,提高系统的容错性和稳定性。

  • 随机过期时间:可以在设置缓存过期时间时引入一个随机值,使得缓存的过期时间分散,避免大量缓存同时失效。

9、如何应对 Redis 缓存击穿问题?

答案: Redis 缓存击穿是指在高并发情况下,一个热点数据的缓存过期或失效,导致大量请求直接访问数据库,造成数据库压力过大的情况。

为了应对缓存击穿问题,可以采取以下措施:

  • 设置热点数据的永不过期:对于一些非常热门的数据,可以将其缓存设置为永不过期,确保热点数据始终存在于缓存中,避免缓存失效导致的击穿问题。

  • 加互斥锁(Mutex Lock):在缓存失效的时候,通过加互斥锁的方式,保证只有一个线程能够访问数据库,其他线程等待获取锁。当第一个线程从数据库中加载数据后,其他线程可以从缓存中获取数据,避免了对数据库的重复访问。

  • 使用短暂的自动过期时间:在缓存失效后,第一个请求可以触发一个异步任务去更新缓存,而其他请求可以先返回旧的缓存数据。这样可以避免大量请求同时访问数据库,减轻数据库的压力。

  • 布隆过滤器(Bloom Filter):布隆过滤器是一种高效的数据结构,用来判断一个元素是否存在于集合中。可以将热点数据的键存储在布隆过滤器中,当请求到来时,先通过布隆过滤器快速判断请求的数据是否存在于缓存中,如果不存在,直接返回缓存未命中,避免了对数据库的访问。

  • 缓存预热:在系统启动或低峰期,可以通过预热的方式将一些热点数据提前加载到缓存中,以减少缓存失效时的冷启动问题,降低缓存击穿的概率。

  • 分布式锁:如果系统是分布式部署的,可以使用分布式锁(如基于 Redis 实现的分布式锁)来保证只有一个节点能够更新缓存,其他节点等待获取锁。这样可以避免多个节点同时访问数据库,减少数据库压力。

以上是常见的应对 Redis 缓存击穿问题的策略。根据具体的业务场景和需求,选择合适的策略或者结合多种策略进行综合应对,以提高系统的性能和可靠性。

10、Redis 缓存穿透、缓存雪崩、缓存击穿有什么区别?

Redis 缓存穿透、缓存雪崩和缓存击穿是三种与缓存相关的常见问题,它们之间有以下区别:

  1. 缓存穿透(Cache Penetration):
    缓存穿透指的是在缓存中无法找到所需数据,并且该数据也不存在于后端数据存储(例如数据库)中。这种情况下,每次请求都会穿透缓存层,直接访问后端存储系统,导致缓存无效,增加了后端负载。通常是由于恶意请求或者查询不存在的数据引起的。

    解决方案:

    • 布隆过滤器(Bloom Filter):在缓存层之前使用布隆过滤器过滤掉不存在的数据。

    • 缓存空对象(Cache Null Object):对于查询结果为空的请求,也将空对象缓存起来,避免多次访问后端存储系统。

  2. 缓存雪崩(Cache Avalanche):
    缓存雪崩指的是在某个时间点,大量的缓存失效,导致大量的请求直接访问后端存储系统,给后端系统带来极大的压力。通常是由于缓存项同时失效,或者在同一时间段内集中大量请求导致的。

    解决方案:

    • 设置合理的过期时间:将缓存的过期时间分散开,避免大量缓存同时失效。

    • 使用多级缓存:将请求分散到不同的缓存层,减少单一缓存层的负载压力。

    • 限流和熔断:控制请求的并发量,避免系统超负荷运行。

  3. 缓存击穿(Cache Breakdown):
    缓存击穿指的是一个热点数据的缓存失效,导致大量的请求同时访问后端存储系统,增加了后端负载。与缓存雪崩不同的是,缓存击穿只有少数几个缓存项失效,而不是全部。

    解决方案:

    • 加锁和并发控制:使用互斥锁(如分布式锁)来保证只有一个线程去加载数据到缓存,其他线程等待。

    • 提前加载热点数据:针对热点数据,可以提前进行预加载,避免在缓存失效时大量请求同时访问后端存储系统。

    • 热点数据永不过期:对于热点数据,可以将其缓存设置为永不过期,确保始终可用。

总结:

缓存穿透是指查询不存在的数据,导致每次请求都穿透缓存访问后端存储系统;缓存雪崩是指大量缓存同时失效,导致请求直接访问后端存储系统;缓存击穿是指一个热点数据的缓存失效,导致大量请求同时访问后端存储系统。针对这些问题,可以采取不同的解决方案来提高系统的稳定性和性能。

'SpringIOC'

Spring IOC

IOC 原理

代码耦合

实际开发中,我们如果在对象 A 内部去创建、修改或者注销另一个对象 B,这会导致对象之间非常复杂的依赖关系,不利于代码的维护更新。

比如我们直接在上层类内调用了底层类的构造方法,一旦底层类的构造方法发生改变,就必须修改所有上层类的代码。

依赖注入

( Dependency Injection ) 我们用依赖注入的方式来降低耦合度。所谓依赖注入,就是把底层对象作为参数传入上层对象。避免底层类被修改后上层类代码也要随之改动。我们一般通过构造方法或者 setter 方法注入底层对象。

  1. 设值注入:依赖的对象通过 setter 方法传入的,对象已经实例化,发生属性填充和依赖注入的时候。

  2. 构造注入:依赖的对象是通过构造器传入,发生在实例化 Bean 的时候。

主要采用设值注入,性能更好更易读。但对于依赖关系无需变化的 Bean 采用构造注入。所有的依赖关系全部在构造器内设定。

优势:使用依赖注入后,即使底层类发生变化,上层类代码也不必改动,大大降低了代码的耦合度。

劣势:但这也导致了我们在初始化对象的过程中要书写复杂的代码。

控制反转

( Inversion of Control ) 控制反转,将对象的管理权交给 IOC 容器。

Spring 框架内会定义一个 IOC 容器类,通过其来统一管理对象的生命周期:创建、资源存取、注销;并自动维护对象间的依赖关系。用户只需要配置 XML 文件或者添加注解标明类之间的映射关系,初始化过程中的代码将由 IOC 容器自动完成。

IOC 容器底层通过工厂模式和 Java 反射机制来实现:

  1. IOC 容器根据 XML 配置文件或者注解读取 Bean 中保存的对象信息。
  2. IOC 容器充当工厂类,利用 Java 反射机制读取需要生成哪些对象,来自动生成相应的对象。

基础概念

IOC 容器

在 Spring 框架中已经定义了 ApplicationContext 和 BeanFactory 作为 IOC 容器类。其中 ApplicationContext是 BeanFactory 的子类,提供了事件发布、国际化信息支持等其他高级特性。

我们可以通过 IOC 容器类的 setBean 方法创建 Bean ,也可以通过 getBean 方法把 Bean 实例化并使用。

1
2
3
4
5
6
7
8
public void testUser(){
// 加载配置文件,创建 IOC 容器对象
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
// IOC 容器根据 Bean 创建对象实例
Person newUser = (Person) context.getBean("person");
// 调用对象方法
System.out.print(newUser.toString());
}

Java Bean

Java Bean 就是程序中被管理的对象在 IOC 容器中的代理,记录了对象信息。

Bean 包含以下参数:Bean 名称(name) 、所代理的类(class) 、以及作用域(scope)。

Bean 的作用域

  • singleton 单例模式(默认): 在整个 IoC 容器中,Bean 只有一个对象实例。
  • prototype 原型模式: 每次调用 Ioc 容器的 getBean 方法,都将产生一个新的对象实例。
  • request: 对于每次 HTTP 请求,Bean 都将产生一个新的对象实例。
  • session: 对于每个 HTTP Session,Bean 都将产生一个新的对象实例。
  • global session: 对于每个全局的 HTTP Session,Bean 都将产生一个新的对象实例。

Bean 的生命周期

Spring 对 Bean 方法进行了抽象和封装,开发者只需要进行配置和调用简单接口,具体实现都交付给 Spring 工厂来管理。

在调用 getBean 方法时,Spring 的工作流大致可分为以下两步:

  1. 解析:读 xml 配置,扫描类文件,从配置或者注解中获取 Bean 的定义信息,注册一些扩展功能。
  2. 加载:通过解析完的定义信息获取 Bean 实例。

获取 BeanName,对传入的 name 进行解析,转化为可以从 Map 中获取到 BeanDefinition 的 bean name。
合并 Bean 定义,对父类的定义进行合并和覆盖,如果父类还有父类,会进行递归合并,以获取完整的 Bean 定义信息。
实例化,使用构造或者工厂方法创建 Bean 实例。
属性填充,寻找并且注入依赖,依赖的 Bean 还会递归调用 getBean 方法获取。
初始化,调用自定义的初始化方法。
获取最终的 Bean,如果是 FactoryBean 需要调用 getObject 方法,如果需要类型转换调用 TypeConverter 进行转化。

循环依赖

三个类 A、B、C,然后 A 关联 B,B 关联 C,C 又关联 A,这就形成了一个循环依赖。如果是方法调用是不算循环依赖的,循环依赖必须要持有引用。

  1. 构造器循环依赖。依赖的对象是通过构造器传入的,发生在实例化 Bean 的时候。

无法解决

  1. 设值循环依赖。依赖的对象是通过 setter 方法传入的,对象已经实例化,发生属性填充和依赖注入的时候。

Spring 框架只支持单例下的设值循环依赖。原型模式检测到循环依赖会直接抛出 BeanCurrentlyInCreationException 异常。


IOC 注解详解

配置

为类添加 @Configuration 注解,表示该类为配置类。起到类似 XML 文件的作用,配置 IOC 容器用来管理 Bean。

组件扫描

为配置类添加 @ComponentScan 注解,启用组件扫描。配置类将根据注解向 IOC 容器添加 Bean,默认扫描本类中的 @Bean 方法。

可以指定需要扫描的包,这会扫描包内的所有组件。如 @ComponentScan(value="com.company.project")

注册 (setBean)

  • 为类添加 @Component 注解

表示该类型被注册为 Bean 。Bean 的名称默认为类名的首字母小写,作用域默认为单例模式。

  1. 可以为注册的 Bean 指定名称,等同于 @Component("car")

  2. 可以为注册的 Bean 指定作用域,如 @Component("prototype")

在 Spring MVC 中,我们可以把 @Component 细化为:

  • @Controller 注解:表示展示层的 Bean
  • @Service 注解:表示业务层的 Bean
  • @Repository 注解:表示数据访问层的 Bean
1
2
3
4
5
6
@Component
@Scope("prototype")
class Car implements Vehicle{
@AutoWired
private FrameWork frameWork;
}
  • 为方法添加 @Bean 注解

方法返回类型将被注册为 Bean。Bean 的名称默认为方法名,作用域默认为单例模式。

  • 可以为注册的 Bean 指定名称,等同于 @Bean(name = "myFoo")

  • 主要用在 @Configuration 注解的类里,也可以用在 @Component 注解的类里。

装配 (getBean)

  • 为对象添加 @Autowired 注解

表示自动装配。在使用对象时 Spring 将根据类型自动查找 Bean 去创建对象,无法找到 Bean 则抛出异常。

  1. 如果想要在无法找到 Bean 时返回 null 值,则将注解改为 @Autowired(required=false)

  2. 如果自动装配对象的类型是接口,而配置的实现类 Bean 有多个。则必须用 @Qualifier 注解来指定 Bean 的名称。

1
2
3
@Autowired
@Qualifier("car")
private Vehicle vehicle;
  • 为对象添加 @Resource 注解

表示自动装配。默认按对象名称去查找 Bean,找不到再按类型去查找 Bean。

  1. 注解可以指定按名称或者类型去查找 Bean,如 @Resource(name="car") 或者 @Resource(type=Car.class)

  2. 也可以同时按名称和类型查找 Bean,任何一个不匹配都将报错。

1
2
@Resource(name="car")
private Vehicle vehicle;

@Autowired 是 Spring 的注解,@Resource 是 J2EE 的注解。

'SpringAOP'

Spring AOP


AOP 原理

面向切面

( Aspect Orient Programming ) 面向切面编程,是面向对象编程(OOP) 的一种补充。

在 Java 程序自上而下处理主业务时,也会经常处理一些和主业务逻辑无关的问题(比如在接收用户访问请求时,计算程序响应该请求的运行时间)。这些代码如果和主逻辑代码混淆,会导致后期难以维护。

AOP 就是将这些横切性问题和主逻辑解耦。保证开发者不修改主逻辑代码的前提下,能为系统中的业务组件添加删除、或复用某种功能。

代理模式

AOP 的本质是修改业务组件实际执行方法的源代码。即代理类 A 封装了目标类 B ,外部调用 B 的目标方法时会被代理类 A 拦截,代理类 A 一方面执行切面逻辑,一方面把调用转发给目标类 B ,执行目标方法。

该过程是代理模式的实现,代理方式有以下两种:

  • 静态 AOP :在编译阶段对程序源代码进行修改,生成静态的 AOP 代理类(字节码文件已被修改)。性能更好。

  • 动态 AOP :在运行阶段动态生成代理对象。灵活性更好。

动态代理

Spring 中的 AOP 是通过动态代理实现的,有以下两种方式:

  • JDK 动态代理

利用反射机制生成一个实现代理接口的类,在调用具体方法前调用 InvokeHandler 来处理。

JDK 代理只能对实现接口的类生成代理。代理生成的是一个接口对象,因此代理类必须实现了接口,否则会抛出异常。

  • CGlib 动态代理

直接操作字节码对代理对象类的字节码文件加载并修改,生成子类来处理。

CGlib 代理针对类实现代理,对指定的类生成一个子类并覆盖其中的方法,因此不能代理 final 类。


AOP 注解详解

配置

对负责扫描组件的配置文件类(@Configuration) 添加 @EnableAspectJAutoProxy 注解,启用 AOP 功能。

默认通过 JDK 动态代理方式进行织入。但必须代理一个实现接口的类,否则会抛出异常。

注解改为 @EnableAspectJAutoProxy(proxyTargetClass = true)

通过 cglib 的动态代理方式进行织入。但如果拓展类的方法被 final 修饰,则织入无效。

1
2
3
4
5
@Configuration
@ComponentScan(basePackageClasses = {com.company.project.service.Meal.class})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}

切面

对组件类(@component) 添加 @Aspect 注解,表示该类为切面类。

增强类型

前置通知

切面方法注解 @Before 表示目标方法调用前,执行该切面方法。

1
2
3
4
@Before("execution(* com.company.project.service.Meal.eat(..))")
public void cook() {
System.out.println("cook");
}

后置通知

  • 切面方法注解 @After 表示目标方法返回或抛出异常后,执行该切面方法。
  • 切面方法注解 @AfterReturning 只在目标方法返回后,执行该切面方法。
  • 切面方法注解 @AfterThrowing 只在目标方法抛出异常后,执行该切面方法。
1
2
3
4
@AfterReturning("execution(* com.company.project.service.Meal.eat(..))")
public void clean() {
System.out.println("clean");
}

环绕通知

切面方法注解 @Around 表示切面方法执行过程中,执行目标方法。

传入参数为 ProceedingJoinPoint 类对象,表示目标方法。在切面方法中调用其 proceed 方法来执行。

1
2
3
4
5
6
7
8
9
10
@Around("execution(* com.company.project.service.Meal.eat(..))")
public void party(ProceedingJoinPoint pj) {
try {
System.out.println("cook");
pj.proceed();
System.out.println("clean");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}

切点声明

在切面方法中需要声明切面方法要切入的目标方法,execution 指示器是我们定义切点时最主要使用的指示器。

格式为: execution(返回数据类型 路径.类.方法(传入参数类型))

参数 功能
execution(* com.company.project.service.Meal.eat(..)) 执行 Meal 类的 eat 方法时切入
execution(* com.company.project.service.Meal.eat(int,String)) 执行 Meal 类的 eat(int,String) 方法时切入
execution(* com.company.project.service.Meal.*(..)) 执行 Meal 类的所有方法时切入
execution(* com.company.project.service.*.*(..)) 执行 service 包内的任意方法时切入(不包含子包)
execution(* com.company.project.service..*.*(..)) 执行 service 包内的任意方法时切入(包含子包)
execution(public * *(..)) 执行所有目标类的所有 public 方法时切入
execution(* pre*(...)) 执行所有目标类所有以 pre 为前缀的方法时切入

还有一些其他指示器:

参数 功能
within(com.company.project.service.*) 执行 service 包内的任意方法时切入
this(com.company.project.service.AccountService) 执行实现 AccountService 接口的代理对象的任意方法时切入
target(com.company.project.service.AccountService) 执行实现 AccountService 接口的目标对象的任意方法时切入
args(java.io.Serializable) 任何一个只接受一个参数,并且运行时所传入的参数是 Serializable 接口的方法
  • 多个匹配条件之间使用链接符连接: &&||!
  • within 指示器表示可以选择的包,bean 指示器可以在切点中选择 bean 。

如参数 execution(String com.company.project.service.test1.IBuy.buy(double)) && args(price) && bean(girl)

要求返回类型为 String ;参数类型为 double ;参数名为 price ;调用目标方法的 bean 名称为 girl 。

简化代码

对于类中要频繁要切入的目标方法,我们可以使用 @Pointcut 注解声明切点表达式,简化代码。

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
@Aspect
@Component
public class EatPlus {

@Pointcut("execution(* com.company.project.service.Meal.eat(..))")
public void point(){}

@Before("point()")
public void cook() {
System.out.println("cook");
}

@Around("point()")
public void party(ProceedingJoinPoint pj) {
try {
System.out.println("cook");
pj.proceed();
System.out.println("clean");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}

@Pointcut("execution(String com.company.project.service.Meal.eat(double)) && args(price) && bean(people)")
public void point2(double price) {
}

@Around("point2(price)")
public String pay(ProceedingJoinPoint pj, double price){
try {
pj.proceed();
if (price > 100) {
System.out.println("can not afford");
return "没有购买";
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return "购买";
}
}

常用 AOP

异常处理

  • @ControllerAdvice / @RestControllerAdvice: 标注当前类为所有 Controller 类服务

  • @ExceptionHandler: 标注当前方法处理异常(默认处理 RuntimeException)
    @ExceptionHandler(value = Exception.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
27
28
29
30
31
@RestControllerAdvice
public class ControllerExceptionHandler {

@ExceptionHandler(Throwable.class)
public ResultBean handleOtherException(Throwable e) {
String message = String.format("错误=%s,位置=%s", e.toString(), e.getStackTrace()[0].toString());
return ResultBean.error(ErrorCode.UNKNOWN_ERROR.getErrorCode(), message);
}

@ExceptionHandler(StreamPlatformException.class)
public ResultBean handleVenusException(StreamPlatformException e) {
return ResultBean.error(e.getErrorCode(), e.getMessageToUser());
}

@ExceptionHandler(FormValidationException.class)
public ResultBean handleFormValidationException(FormValidationException e) {
StringBuilder message = new StringBuilder();
e.getResult().getAllErrors().forEach(objectError -> {
if (objectError instanceof FieldError) {
FieldError fieldError = (FieldError) objectError;
message.append("参数").append(fieldError.getField())
.append("错误值为").append(fieldError.getRejectedValue())
.append(fieldError.getDefaultMessage());
} else {
message.append(objectError.getDefaultMessage());
}
});
return ResultBean.error(ErrorCode.PARAMETER_VALIDATION_ERROR.getErrorCode(),
String.format(ErrorCode.PARAMETER_VALIDATION_ERROR.getMessage(), message));
}
}

拦截器

  • 拦截器(Interceptor)

Java Web 中,在执行 Controller 方法前后对 Controller 请求进行拦截和处理。依赖于 web 框架,在 Spring 配置。在实现上基于 Java 的反射机制。

  • 过滤器(Filter)

Java Web 中,在 request/response 传入 Servlet 前,过滤信息或设置参数。依赖于 servlet 容器,在 web.xml 配置。在实现上基于函数回调。

两者常用于修改字符编码、删除无用参数、登录校验等。Spring 框架中优先使用拦截器:功能接近、使用更加灵活。

拦截器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在配置中引入拦截器对象(单独编写拦截器类)

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 导入拦截器对象,默认拦截全部
InterceptorRegistration addInterceptor = registry.addInterceptor(new myInterceptor());

// 排除配置
addInterceptor.excludePathPatterns("/error","/login","/user/login");
addInterceptor.excludePathPatterns("/asserts/**");
addInterceptor.excludePathPatterns("/webjars/**");
addInterceptor.excludePathPatterns("/public/**");
// 拦截配置
addInterceptor.addPathPatterns("/**");
}

拦截器类通过实现 HandlerInterceptor 接口或者继承 HandlerInterceptorAdapter 类。

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 myInterceptor extends HandlerInterceptorAdapter {

// Session key
public final static String SESSION_KEY = "user";

// preHandle 预处理
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 检查 session
HttpSession session = request.getSession();
if (session.getAttribute(SESSION_KEY) != null) return true;
// 重定向到登录页面
request.setAttribute("message","登录失败,请先输入用户名和密码。");
request.getRequestDispatcher("login").forward(request,response);
return false;
}

// postHandle 善后处理
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
System.out.println("INTERCEPTOR POSTHANDLE CALLED");
}

}

过滤器类通过继承 Filter 类实现,直接添加注解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component                                                                // 作为组件,交给容器处理
@ServletComponentScan // 扫描组件
@WebFilter(urlPatterns = "/login/*",filterName = "loginFilter") // 设定过滤路径和名称
@Order(1) // 设定优先级(值小会优先执行)
public class LoginFilter implements Filter{

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 过滤器前执行
System.out.println("before");
// 执行内部逻辑
filterChain.doFilter(servletRequest,servletResponse);
// 过滤器后执行
System.out.println("after");
}

@Override
public void destroy() {
}
}

Spring

Spring


基本概念

Spring

Spring 是用于开发 Java 应用程序的开源框架,为解决企业应用开发的复杂性而创建。

  1. Spring 的基本设计思想是利用 IOC(依赖注入)和 AOP (面向切面)解耦应用组件,降低应用程序各组件之间的耦合度。
  2. 在这两者的基础上,Spring 逐渐衍生出了其他的高级功能:如 Security,JPA 等。

Spring MVC

Spring MVC 是 Spring 的子功能模块,专用于 Web 开发。

Spring MVC 基于 Servlet 实现,将 Web 应用中的数据业务、显示逻辑和控制逻辑进行分层设计。开发者可以直接调用 Spring MVC 框架中 Spring 解耦的组件,快速构建 Web 应用。

Spring Boot

Spring Boot 是用于简化创建 Spring 项目配置流程,快速构建 Spring 应用程序的辅助工具。Spring Boot 本身并不提供 Spring 框架的核心特性以及扩展功能。但 在创建 Spring 项目时,Spring Boot 可以:

  1. 自动添加 Maven 依赖,不需要在 pom.xml 中手动添加配置依赖。
  2. 不需要配置 XML 文件,将全部配置浓缩在一个 appliaction.yml 配置文件中。
  3. 自动创建启动类,代表着本工程项目和服务器的启动加载。
  4. 内嵌 Tomcat 、Jetty 等容器,无需手动部署 war 文件。

Spring Boot 配置

依赖

在Spring Boot中,引入的所有包都是 starter 形式:

spring-boot-starter-web-services,针对 SOAP Web Services
spring-boot-starter-web,针对 Web 应用与网络接口
spring-boot-starter-jdbc,针对 JDBC
spring-boot-starter-data-jpa,基于 Hibernate 的持久层框架
spring-boot-starter-cache,针对缓存支持

默认映射路径

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/

优先级顺序:META-INF/resources > resources > static > public

全局配置

位于 resources 文件夹下,支持以下两种格式。由 Spring Boot 自动加载。

  1. application.properties
  2. application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#端口号
server.port=8080
#访问前缀
server.servlet.context-path=/demo

#数据库驱动
jdbc.driver=com.mysql.jc.jdbc.Driver
#数据库链接
jdbc.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
#数据库用户名
jdbc.username=root
#数据库密码
jdbc.password=wdh19970506

#Mybatis
#配置文件路径
mybatis_config_file=mybatis-config.xml
#SQL语句配置路径
mapper_path=/mapper/**.xml
#实体类所在包
type_alias_package=com.example.demo.entity
  • JDBC 连接 Mysql5 驱动: com.mysql.jdbc.Driver
  • JDBC 连接 Mysql6 驱动: com.mysql.cj.jdbc.Driver , URL 必须要指定时区 serverTimezone !

多重配置

在 Spring Boot 中,我们往往需要配置多个不同的配置文件去适应不同的环境:

  • application-dev.properties 开发环境
  • application-test.properties 测试环境
  • application-prod.properties 生产环境

只需要在程序默认配置文件 application.properties 中设置环境,就可以使用指定的配置。

1
spring.profiles.active=dev

启动类

@SpringBootApplication 类:作为程序入口,在创建 Spring Boot 项目时自动创建。

等同于 @Configuration + @EnableAutoConfiguration + @ComponentScan ,会自动完成配置并扫描路径下所有包。

1
2
3
4
5
6
7
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

Spring 需要定义调度程序 servlet ,映射和其他支持配置。我们可以使用 web.xml 文件或 Initializer 类来完成此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyWebAppInitializer implements WebApplicationInitializer {

@Override
public void onStartup(ServletContext container) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.setConfigLocation("com.pingfangushi");
container.addListener(new ContextLoaderListener(context));
ServletRegistration.Dynamic dispatcher = container
.addServlet("dispatcher", new DispatcherServlet(context));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}

还需要将 @EnableWebMvc 注释添加到 @Configuration 类,并定义一个视图解析器来解析从控制器返回的视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableWebMvc
@Configuration
public class ClientWebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver bean
= new InternalResourceViewResolver();
bean.setViewClass(JstlView.class);
bean.setPrefix("/WEB-INF/view/");
bean.setSuffix(".jsp");
return bean;
}
}

JDBC

JDBC


JDBC 简介

JDBC 是 Java EE 提供的数据库接口,负责连接 java 程序和后台数据库。安装数据库驱动程序后,开发者可以按照 JDBC 规范直接在 Java 程序上对数据库进行操作,由数据库厂商负责具体实现。

驱动安装

  1. 下载 MySQL 驱动包,解压后得到 jar 库文件:http://dev.mysql.com/downloads/connector/j/

  2. 打开 IDE,在对应项目中 configure build path 导入 jar 库文件。


JDBC 编程

JDBC 常用工具类位于 sql 包内,使用时需导入:import java.sql.* 。使用时可能 抛出 SQLException 异常。

加载驱动

JDBC 首先要使用反射机制加载驱动类,并创建其对象。

1
2
Class.forName("com.mysql.cj.jdbc.Driver");          // MySQL 数据库驱动
Class.forName("oracle.jdbc.driver.OracleDriver"); // Oracle 数据库驱动

连接数据库 Connection

JDBC 由 Connection 类负责连接数据库,参数中输入数据库 URL、账号、密码。

1
2
3
4
5
6
7
8
// 连接本地 RUNOOB 数据库,需设置时区
static final String DB_URL = "jdbc:mysql://localhost:3306/RUNOOB?useSSL=false&serverTimezone=UTC";

static final String USER = "root"; // 数据库账号
static final String PASS = "123456"; // 数据库密码

Connection conn = DriverManager.getConnection(DB_URL,USER,PASS); // 建立连接
conn.close(); // 关闭连接

执行语句 Statement

JDBC 由 Statement 类负责发送 SQL 语句。

1
2
3
4
5
6
7
8
Statement stmt = = conn.createStatement();         // 创建 Statement 对象

// executeQuery 执行查询操作,返回 ResultSet 结果集
ResultSet rs = stmt.executeQuery("SELECT * FROM websites");
// executeUpdate 执行更新操作,返回 int 数据表示受影响行数
int len = stmt.executeUpdate("DELETE * FROM websites");

stmt.close(); // 关闭 Statement 对象

返回查询结果 ResultSet

JDBC 由 ResultSet 类返回 select 语句执行结果,读取 executeQuery 方法返回的数据。

1
2
3
4
5
ResultSet rs = stmt.executeQuery(sql);             // 获取返回结果

while(rs.next()){ // 输出返回结果
System.out.println(rs.getString("area_id"));
}

JDBC 进阶

预编译 PreparedStatement

PreparedStatement 类继承自 Statement 类,在 JDBC 开发中用来取代前者。有以下两个优势:

  1. 可对 SQL 语句进行预编译,可以灵活地修改 SQL 语句,提高开发效率。
  2. 把用户输入单引号转义,防止恶意注入,保护数据库安全。
1
2
3
4
5
6
Connection connection = DriverManager.getConnection();
String sql = "INSERT INTO test(id,name) VALUES (?,?)";
PreparedStatement stmt = connection.preparedStatement(sql); // 创建对象并预编译
stmt.setInt(1, 755); // 在第一个占位符(?)位置插入数字
stmt.setString(2, "MrJoker"); // 在第二个占位符(?)位置插入字符串
stmt.executeUpdate(); // 更新并执行

批处理 executeBath

PreparedStatement 类可以通过 executeBath 方法批量处理 SQL 语句,进一步提高效率。其返回值为一个 int[] 数组。

1
2
3
4
5
6
7
8
9
10
Connection connection = DriverManager.getConnection();
String sql = "INSERT INTO test(id,name) VALUES (?,?)";
PreparedStatement stmt = connection.prepareStatement(sql);
for (int i = 1; i <= 1000; i++) {
stmt.setInt(1, i);
stmt.setString(2, (i + "号士兵"));
stmt.addBatch(); // 语句添加到批处理序列中
}
preparedStatement.executeBatch(); // 语句发送给数据库批量处理
preparedStatement.clearBatch(); // 清空批处理序列

大文本和二进制数据

  • clob 用于存储大文本

  • blob用于存储二进制数据


JDBC 示例

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
// 适用于 JDK 1.8 以后版本

import java.sql.*;

public class MySQLTest{

static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
static final String DB_URL = "jdbc:mysql://localhost:3306/RUNOOB?useSSL=false&serverTimezone=UTC";
static final String USER = "root";
static final String PASS = "123456";

public static void useMethod(){
Connection conn = null;
PreparedStatement stmt = null;
try{
Class.forName(JDBC_DRIVER);
conn = DriverManager.getConnection(DB_URL,USER,PASS);
stmt = conn.preparedStatement("SELECT id, name, url FROM websites");
ResultSet rs = stmt.executeQuery();
while(rs.next()){
System.out.println(rs.getString("area_id"));
}
rs.close();
stmt.close();
conn.close();
}catch(SQLException se){ // 处理 JDBC 错误
se.printStackTrace();
}catch(Exception e){ // 处理 Class.forName 错误
e.printStackTrace();
}finally{
try{
if(stmt != null) stmt.close();
}catch(SQLException se2){}
try{
if(conn != null) conn.close();
}catch(SQLException se){}
}
}
}

IO流

#IO流

##总览

流的分类:

类别 分类1 分类2
单位 字节流 字符流
流向 输入流 输出流
功能 节点流 处理流
流名称 字节输入 字节输出 字符输入 字符输出
4大基类 InputStream OutputStream Reader Writer
文件流 FileInputStream FileOutputStream FileReader FileWriter
转换流 - - InputStreamReader OutputStreamWriter
System System.in System.out - -
打印流 - PrintStream - PrintWriter
缓存流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
数据流 DataInputStream DataOutputStream - -
内存流 ByteArrayInputStream ByteArrayOutputStream - -
对象流 ObjectInputStream ObjectOutputStream - -
随机访问流 - RandomAccessFile - -

##四大抽象类

InputStream

说明:继承自InputStream的流都是用于向程序中输入数据的,且数据的单位为字节(8位)

核心方法:

public abstract int read()	//从输入流中读取数据的下一个字节, 返回读到的字节值.若遇到流的末尾,返回-1
public int read(byte[] b)	//从输入流中读取 b.length 个字节的数据并存储到缓冲区数组b中.返回的是实际读到的字节总数
public int read(byte[] b, int off, int len)	//读取 len 个字节的数据,并从数组b的off位置开始写入到这个数组中
public void close()	//关闭此输入流并释放与此流关联的所有系统资源
public int available()	//返回此输入流下一个方法调用可以不受阻塞地从此输入流读取(或跳过)的估计字节数
public long skip(long n)	//跳过和丢弃此输入流中数据的 n 个字节,返回实现路过的字节数。

OutputStream

说明:继承自OutputStream的流是程序用于向外输出数据的,且数据的单位为字节(8位)

核心方法:

public abstract void write(int b)	//将指定的字节写入此输出流
public void write(byte[] b)	//将 b.length 个字节从指定的 byte 数组写入此输出流
public void write(byte[] b, int off, int len)	//将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流
public void flush()	//刷新此输出流并强制写出所有缓冲的输出字节
pulbic void close()	//关闭此输出流并释放与此流有关的所有系统资源

Reader

说明:继承自Reader的流都是用于向程序中输入数据的,且数据的单位为字符(16位)

核心方法:

public int read()	//读取单个字符的编码,返回作为整数读取的字符,如果已到达流的末尾返回-1
public int read(char[] cbuf)		//将字符读入数组,返回读取的字符数
public abstract int read(char[] cbuf, int off, int len)	//读取 len 个字符的数据,并从数组cbuf的off位置开始写入到这个数组中
public abstract void close()	//关闭该流并释放与之关联的所有资源
public long skip(long n) 	//跳过n个字符
int available() 	//还可以有多少能读到的字节数

Writer

说明:继承自Writer的流是程序用于向外输出数据的,且数据的单位为字符(16位)

核心方法:

public void write(int c)		//写入单个字符
public void write(char[] cbuf)	//写入字符数组
public abstract void write(char[] cbuf, int off, int len)	//写入字符数组的某一部分
public void write(String str)	//写入字符串
public void write(String str, int off, int len)	//写字符串的某一部分
public abstract void close()	//关闭此流,但要先刷新它
public abstract void flush()	//刷新该流的缓冲,将缓冲的数据全写到目的地

##节点流
###文件流

说明:文件流主要用来操作文件

  • FileInputStream:继承自InputStream

  • FileOutputStream继承自OutputStream
    FileOutputStream(String name, boolean append) 指定文件名和是否以追回方式写入

  • FileReader继承自Reader
    核心方法:

    1、构造方法
        FileReader(File file)
        FileReader(String fileName)
    2、成员方法
        int read()	//每次读取一个字符,末尾-1返回值就是读入的内容
        int read(char[] cbuf)	//每次读取一组字符,最多读数组长度个,末尾-1返回值实际读取的个数
        int read(char[] cbuf,int off, int len)	//每次读取一组字符,最多len个,数据存入数组从off开始,末尾-1返回值实际读取的个数
        void close()
    
  • FileWriter继承自Writer
    核心方法:

    1、构造方法
        FileWriter(File file)
        FileWriter(File file,  boolean append)
        FileWriter(String fileName)
        FileWriter(String fileName, boolean append)	//构造方法来指定是否使用追加模式
    2、成员方法
        void write(int c) 
        void write(char[] cbuf,int off, int len)
        void write(String str, int off,  int len)
        void flush()
        void close()
    

###内存流

说明:内存流主要用来操作内存,输入和输出可以从文件中来,也可以将设置在内存之上(内存:相当于长度可变的字节数组)

分类:
ByteArrayInputStream类:主要完成将内容从内存读入程序之中

数据<<<---------ByteArrayInputStream<<<---------内存

构造方法:

ByteArrayInputStream(byte[] b)
ByteArrayInputStream(byte[] b,int off,int len)

常用方法:

read()
skip()
available()

ByteArrayOutputStream类:主要是将数据写入到内存中

数据--------->>>ByteArrayOutputStream--------->>>内存

构造方法:

ByteArrayOutputStream()
ByteArrayOutputStream(int size) :指定缓冲区大小(byte)

常用方法:

byte[] toByteArray():将内存流转换为字节数组
toString()
write(int)
write(byte[] bytes)
write(byte[] bytes ,int off,int len) 
writeTo(OutputStream)

注意:内存流不需要关闭

##处理流(过滤流)
###缓冲流

说明:缓冲流是处理流的一种,建立在相应的节点流之上,对读写的数据提供了缓冲的功能,提高了读写的效率,还增加了一些新的方法

注意

  • 1、对于缓冲输出流,写出的数据会先缓存在内存缓冲区中,关闭此流前要用flush()方法将缓存区的数据立刻写出
  • 2、关闭过滤流时,会自动关闭过滤流所包装的所有底层流

BufferedInputStream 可以对任何的InputStream流进行包装

BufferedOutputStream 可以对任何的OutputStream流进行包装

BufferedReader 可以对任何的Reader流进行包装

新增了readLine()方法用于一次读取一行字符串(以‘\r’或‘\n’认为一行结束)返回一行 如果没有返回null

BufferedWriter 可以对任何的Writer流进行包装

新增了newLine()方法,用于跨平台的写入换行符

###Object流

说明:JDK提供的ObjectOutputStream和ObjectInputStream类是用于存储和读取基本数据类型或对象的过滤流

序列化:用ObjectOutputStream类保存基本数据类型或对象的机制叫序列化

反序列化:用ObjectInputStream类读取基本数据类型或对象的机制叫反序列化

Serializable接口

作用:能被序列化的对象所对应的类必须实现java.io.Serializable这个标识性接口
注意:实现此接口的类,需要提供一个静态long类型的常量serialVersionUID,保证序列化与反序列化的一致性

构造方法:

public ObjectOutputStream(OutputStream out)
public ObjectInputStream(InputStream in)

transient关键字:

transient关键字修饰成员变量时,表示这个成员变量是不需要序列化的
static修饰的成员变量也不会被序列化

###打印流

说明:向控制台输出数据

PrintStream类:字节输出流

PrintWriter类:字符输出流

打印流示例(注意:write写入的是字节):

PrintStream ps = new PrintStream("src/print.txt");
    ps.write(355);// 字节 00000000 00000000 00000001 01100011
                    // 舍弃前三位---》01100011--》c
    ps.println(355);
    ps.flush();
    ps.close();

注意

System.out就是PrintStream的一个实例
PrintStream和PrintWriter的输出操作不会抛出异常

构造方法:

PrintStream(OutputStream out)
PrintStream(OutputStream out, boolean autoFlush)
PrintWriter(Writer out)
PrintWriter(Writer out, boolean autoFlush)
PrintWriter(OutputStream out)
PrintWriter(OutputStream out, boolean autoFlush)

###转换流

作用:转换流用于在字节流和字符流之间转换。

分类:

  • InputStreamReader

    1)是Reader的子类,将输入的字节流变为字符流,即将一个字节流的输入对象变为字符流的输入对象
    2)InputStreamReader需要和InputStream“套接”,它可以将字节流中读入的字节解码成字符
    
  • OutputStreamWriter

    1)是Writer的子类,将输出的字符流变为字节流,即将一个字符流的输出对象变为字节流的输出对象
    2)OutputStreamWriter需要和OutputStream“套接”,它可以将要写入字节流的字符编码成字节
    

转换过程:

  • 写入数据

    程序--->>字符数据--->>字符流--->>OutputStreamWriter--->>字节流--->>文件
    
  • 读出数据

      程序<<---字符数据<<---字符流<<----InputStreamReader<<---字节流<<---文件
    

###数据流

DataInputStream

作用:读取简单数据类型和字符串

核心方法:

readInt() 读取一个基本数据类型数据
readInt() 读取一个基本数据类型数据

DataOutputStream

作用:写出简单数据类型和字符串

核心方法:

writeInt(int i)
writeUTF(String s) 写入UTF-8编码的字符串

##RandomAccessFile类(随机访问文件)

作用:完成随机读取功能,可以读取指定位置的内容

构造方法:

public RandomAccessFile(File file,  String mode) 
public RandomAccessFile(File file,  String mode) 

文件的打开模式

“r” 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。  
“rw” 打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。  

常用方法:

getFilePointer():返回子文件中当前的偏移量	
seek(long l):设置到此文件开头测量到的文件的偏移量 在该位置的下一个发生读、写操作

注意:

RandomAccessFile raf = new RandomAccessFile("src/per.txt", "rw");
//这里遍历的时候需注意要用getFilePointer()读取光标的位置
for (int i = 0; i < raf.length(); i = (int) raf.getFilePointer()) {
    //do ...			
}

异常处理

异常


异常类型

Throwable 类

Java 程序中的异常是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。为了能够及时有效地处理程序中的运行错误,必须使用异常类。

java 程序中所有的异常都继承自 Throwable 类,Throwable 类有两个子类 Error 类和 Exception 类:

  • Error 类:【错误】表示 java 程序在运行时产生的无法处理的故障(如堆栈溢出),错误出现时会导致程序无法正常执行并强制退出。

  • Exception 类:【异常】表示 java 程序中产生的可以被处理的故障,异常出现时可以由程序进行处理。

RuntimeException 类

【运行时异常】 Exception 类的子类。

表示 java 程序运行状态中发生的异常,在编译时无法被检测。在 java 程序运行时会由系统自动抛出,允许应用程序不进行处理。

异常类型 介绍
ArithmeticException 算术异常,以零做除数
ArrayIndexOutOfBoundException 数组越界异常
NullPointerException 空指针异常,对象不存在

Checked Exception 类

【可检查异常】Exception 类除 RuntimeException 以外其他子类的统称。

表示 java 程序编译时检测到的异常。出现时必须在程序中进行捕获或抛出,否则编译不会通过。

异常类型 介绍
IOException IO 异常
FileNotFoundException 找不到文件异常,继承自 IO 异常
ClassNotFoundException 找不到类异常

Exception 类

源码解析

状态信息

Throwable / Exception 类是有状态的(因此 Throwable 是接口而不能是类),记录了四个信息:

1
2
3
4
private transient Object backtrace;                          // 栈的回溯点
private String detailMessage; // 异常的信息:在创建异常时备注
private Throwable cause = this; // 异常的原因:导致该异常的异常,默认为自身
private StackTraceElement[] stackTrace = UNASSIGNED_STACK; // 异常的发生顺序:以栈的形式存储

构造方法

Throwable / Exception 类含有四个构造方法,在创建时可以记录异常信息:

1
2
3
4
throw new Exception();                           // 默认
throw new Exception("message"); // 记录异常信息
throw new Exception(e); // 记录异常原因
throw new Exception("message", e); // 记录详细信息和异常原因

常用方法

Throwable / Exception 类定义了多种常用方法用于获取异常数据,常用的有:

  • getMessage 方法:获取异常的信息。
  • getStackTrace 方法:获取的异常发生顺序。
  • printStackTrace 方法:获取异常的发生顺序并打印(开发和调试阶段用来显示异常信息,帮助开发者找出错误)。
1
2
3
4
catch(Exception e){
System.out.println(e.getMessage());
e.printStacTrace();
}

自定义异常

我们也可以通过继承并重写 Exception / RuntimeException 类的方式,自定义异常类并使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 自定义异常,重写方法可任选
class MyException extends Exception {
@Override
public MyException() {
super();
}
@Override
public MyException(String message) {
super(message);
}
@Override
public MyException(String message, Throwable cause){
super(message,cause);
}
@Override
public MyException(Throwable cause) {
super(cause);
}
}

异常转译

在项目开发过程中,当 Sevice/DAO 层出现如 SQLException 异常时,程序一般不会把底层的异常传到 controller 层。程序可以捕获原始异常,然后再抛出一个新的业务异常。

1
2
3
4
catch(SQLException e){
throw new MyException("SQL Error", e);
}


异常处理

抛出异常 throw

当方法执行出现问题时,方法就会创建异常对象并抛出。开发者可以在程序中自行抛出异常;JVM 在执行程序时发现问题也会自动抛出异常。

  • throw 语句:开发者自行创建异常对象并抛出,等待程序进行异常处理。

  • throws 语句:声明方法可能抛出某种异常且未经处理,调用该方法的上级需要进行异常处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

class TestException{
// 把方法中的抛出异常交给上层处理
public void writeList(int size) throws IndexOutOfBoundsException, IOException{
PrintWriter out = null;
// 用户自定义异常并抛出
if(size < 1) throw new IndexOutOfBoundsException("至少要输出1个字符");
try{
// 虚拟机自动发现异常也会抛出,必须出现在 try 代码块中
out = new PrintWriter(new FileWriter(txt));
for (int i = 0; i < size; i++)
System.out.println("Value at: " + i + " = " + list.get(i));
}finally{
if (out != null) out.close();
}
}
}

捕获异常 catch

当方法执行抛出异常时,必须由专门的代码块对异常进行处理。

  • try 语句:可能出现异常的代码块。

  • catch 语句:捕获相应异常后停止执行 try 代码,转而执行对应 catch 代码。如果没有异常 catch 代码不会执行。

  • finally 语句:无论是否发生异常,finally 代码总会被执行。一般用于释放资源。

注意事项

  1. 如果 try 语句中出现的异常未被 catch,默认将异常 throw 给上层调用者处理。但必须在方法中声明 throws。

  2. try/catch 代码中的 return 语句会在执行完 finally 后再返回,但 finally 中对返回变量的改变不会影响最终的返回结果。

  3. finally 代码中应避免含有 return 语句或抛出异常,否则只会执行 finally 中的 return 语句,且不会向上级抛出异常。

Java 7 后在 try 语句中打开 IO 流,会在跳出后自动关闭流。不必再用 finally 语句关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TestException{               
public void writeList(int size) {
PrintWriter out = null;
try {
if(size < 1) throw new IndexOutOfBoundsException("至少要输出1个字符");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < size; i++)
System.out.println("Value at: " + i + " = " + list.get(i));
} catch (IndexOutOfBoundsException e) {
System.err.println("Caught IndexOutOfBoundsException: " + e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) out.close();
}
}
}