Java基础

finally语句块什么时候不会执行

finally语句块在两种情况下不会执行:

  1. 程序没有进入到try语句块因为异常导致程序终止,这个问题主要是开发人员在编写代码的时候,异常捕获的范围不够

  2. 在try或者cache语句块中,执行了System.exit(0)语句,导致JVM直接退出

  3. 程序所在的线程死亡。

  4. 关闭 CPU。

⭐⭐⭐动态代理

  • JDK动态代理:只适用于接口,代理类实现了目标对象的接口,并在方法调用时添加额外的功能(例如事务管理)。
  • CGLIB代理:基于子类的动态代理,适用于类。CGLIB通过创建目标类的子类并覆盖其中的方法来添加额外的功能。

代理模式在软件开发中是一种常见的设计模式,用于在不改变目标对象的情况下,通过代理对象来控制对目标对象的访问。代理模式可以分为静态代理和动态代理,它们之间有一些显著的区别。以下是详细的解释:

1. 静态代理

定义:静态代理是在编译时由程序员手动编写或者通过工具生成代理类。代理类在编译时已经确定,不会在运行时变化。

实现方式:在静态代理中,代理类和目标类都实现相同的接口,代理类持有一个目标对象的引用,并在代理类的方法中调用目标对象的方法。

优点

  • 简单明了,易于理解和实现。
  • 可以在编译时检查类型安全。

缺点

  • 代码量大,目标类每新增一个方法,代理类都需要进行相应的修改。
  • 不够灵活,代理类和目标类之间的耦合度较高。

示例代码

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
// 接口
public interface MyService {
void doSomething();
}

// 目标类
public class MyServiceImpl implements MyService {
@Override
public void doSomething() {
System.out.println("Doing something...");
}
}

// 代理类
public class MyServiceProxy implements MyService {
private MyServiceImpl target;

public MyServiceProxy(MyServiceImpl target) {
this.target = target;
}

@Override
public void doSomething() {
System.out.println("Before doing something...");
target.doSomething();
System.out.println("After doing something...");
}

public static void main(String[] args) {
MyServiceImpl target = new MyServiceImpl();
MyService proxy = new MyServiceProxy(target);
proxy.doSomething();
}
}

2. 动态代理

定义:动态代理是在运行时通过反射机制动态生成代理类。Java中主要有两种实现方式:JDK动态代理和CGLIB动态代理。

实现方式

  • JDK动态代理:通过java.lang.reflect.Proxy类和InvocationHandler接口实现。只能代理实现了接口的类。
  • CGLIB动态代理:通过继承目标类来生成子类进行代理。可以代理没有实现接口的类。

优点

  • 灵活性高,可以在运行时动态生成代理类。
  • 代码复用性高,可以通过一个通用的代理类来代理多个目标类。

缺点

  • 性能稍低于静态代理,因为涉及到反射机制。
  • 调试和排错相对困难。

JDK动态代理示例

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 接口
public interface MyService {
void doSomething();
}

// 目标类
public class MyServiceImpl implements MyService {
@Override
public void doSomething() {
System.out.println("Doing something...");
}
}

// 动态代理类
public class LoggingInvocationHandler implements InvocationHandler {
private final Object target;

public LoggingInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before doing something...");
Object result = method.invoke(target, args);
System.out.println("After doing something...");
return result;
}

public static void main(String[] args) {
MyService target = new MyServiceImpl();
MyService proxy = (MyService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LoggingInvocationHandler(target)
);
proxy.doSomething();
}
}

CGLIB动态代理示例

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
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

// 目标类
public class MyService {
public void doSomething() {
System.out.println("Doing something...");
}
}

// 动态代理类
public class LoggingMethodInterceptor implements MethodInterceptor {
private final Object target;

public LoggingMethodInterceptor(Object target) {
this.target = target;
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before doing something...");
Object result = proxy.invoke(target, args);
System.out.println("After doing something...");
return result;
}

public static void main(String[] args) {
MyService target = new MyService();

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class);
enhancer.setCallback(new LoggingMethodInterceptor(target));

MyService proxy = (MyService) enhancer.create();
proxy.doSomething();
}
}

总结

  • 静态代理:在编译时创建代理类,优点是简单易懂,缺点是灵活性差、代码量大。
  • 动态代理:在运行时创建代理类,优点是灵活性高、代码复用性好,缺点是性能稍差、调试困难。

根据具体的应用场景选择合适的代理模式,可以在保证代码简洁性的同时,实现灵活的功能扩展。

接口和抽象类有什么共同点和区别?

共同点

  • 都不能被实例化。
  • 都可以包含抽象方法。
  • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

区别

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  • 一个类只能继承一个类,但是可以实现多个接口。
  • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

深拷贝,浅拷贝,引用拷贝

浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。

深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

引用拷贝: 引用拷贝就是两个不同的引用指向同一个对象。

浅拷贝、深拷贝、引用拷贝示意图

为什么需要同时重写hashCode和equals

当你在集合如HashMapHashSetHashtable中存储对象时,这些集合使用哈希表来存储元素。哈希表基于哈希码来快速存储和检索元素。如果只重写了equals方法而没有重写hashCode方法,会导致以下问题:

  1. 违反hashCodeequals的合同
    • 根据Java规范,如果两个对象根据equals方法比较是相等的,那么它们的hashCode也必须相等。
    • 如果只重写equals方法但不重写hashCode方法,则两个对象即使在逻辑上是相等的(equals返回true),它们的哈希码也可能不同。这会违反合同,导致集合行为异常。
  2. 集合操作的正确性
    • 插入操作:当你往HashSetHashMap中插入一个对象时,集合会先调用对象的hashCode方法来找到存储桶位置,然后在该位置用equals方法检查对象是否已经存在。如果hashCode不一致,相等的对象可能会被放到不同的存储桶,导致集合中包含重复的对象。
    • 查找操作:类似地,查找操作(如containsget)也依赖于哈希码来快速定位对象。如果hashCode不一致,即使两个对象通过equals方法比较是相等的,查找操作也可能失败,因为它们可能在不同的存储桶中。

String 为什么是不可变的?

  1. String 类中使用 final 关键字修饰字符数组来保存字符串,保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。

  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

String s1 = new String(“abc”);这句话创建了几个字符串对象?

会创建 1 或 2 个字符串对象。

  1. 如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。

  2. 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。

Java值传递

Java 中将实参传递给方法(或函数)的方式是 值传递

  • 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
  • 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。

Unsafe类

待补充。。。。。

SPI

https://www.bilibili.com/video/BV1E44y1N7Nk/

Java的SPI(Service Provider Interface)机制是一种服务发现机制,用于为某个接口寻找实现类,使得接口的实现可以在运行时动态加载。这一机制通过在META-INF/services目录下配置文件来实现,配置文件中指定了服务接口的实现类。这在Java生态系统中非常常见,尤其是在实现可插拔架构和模块化设计时。以下是对Java SPI机制的详细讲解:

SPI机制的组成部分

  1. 服务接口(Service Interface):这是一个Java接口,定义了服务的功能规范。所有的服务提供者都需要实现这个接口。

  2. 服务提供者(Service Provider):这是实现服务接口的具体类。在SPI机制中,服务提供者的类需要在一个特定的配置文件中注册。

  3. 配置文件:这是一个位于META-INF/services目录下的文件,文件名是服务接口的全限定名(包括包名)。文件内容是具体的服务提供者类的全限定名。

SPI机制的工作流程

  1. 定义服务接口:定义一个服务接口,描述所提供的服务。
  2. 实现服务接口:创建一个或多个实现类,实现服务接口。
  3. 创建配置文件:在META-INF/services目录下创建一个文件,文件名是服务接口的全限定名,文件内容是服务提供者实现类的全限定名。
  4. 加载服务提供者:使用ServiceLoader类动态加载并实例化服务提供者。

实际例子

假设我们有一个服务接口com.example.MyService和一个实现类com.example.impl.MyServiceImpl

步骤1:定义服务接口

1
2
3
4
5
package com.example;

public interface MyService {
void execute();
}

步骤2:实现服务接口

1
2
3
4
5
6
7
8
9
10
package com.example.impl;

import com.example.MyService;

public class MyServiceImpl implements MyService {
@Override
public void execute() {
System.out.println("Executing MyServiceImpl");
}
}

步骤3:创建配置文件

META-INF/services目录下创建一个文件,文件名为com.example.MyService,文件内容为:

1
com.example.impl.MyServiceImpl

步骤4:加载服务提供者

使用ServiceLoader动态加载服务提供者:

1
2
3
4
5
6
7
8
9
10
11
12
package com.example;

import java.util.ServiceLoader;

public class Main {
public static void main(String[] args) {
ServiceLoader<MyService> serviceLoader = ServiceLoader.load(MyService.class);
for (MyService service : serviceLoader) {
service.execute();
}
}
}

运行结果

执行Main类的main方法时,会输出:

1
Executing MyServiceImpl

SPI机制的优势

  1. 松耦合:通过SPI机制,接口和实现类之间可以实现松耦合,不需要在编译时确定具体实现类。
  2. 可扩展性:可以在运行时动态加载新的实现类,方便系统扩展。
  3. 模块化设计:有利于实现模块化设计,服务提供者可以独立于服务接口开发和发布。

使用场景

SPI机制广泛应用于各种Java框架和库中,如Java的JDBCJAX-WSJAX-RS等。这些框架和库通过SPI机制动态加载具体的驱动或实现类,增强了系统的灵活性和可扩展性。

通过SPI机制,可以很方便地实现服务的动态加载和管理,提供了良好的扩展能力和模块化支持。在实际开发中,可以利用这一机制构建灵活、可扩展的系统架构。

场景题

单点登录这块怎么实现的

使用jwt解决单点登录的流程如下:

image-20230521113941467

回答要点:

1,先解释什么是单点登录

单点登录的英文名叫做:Single Sign On(简称SSO

2,介绍自己项目中涉及到的单点登录

3,介绍单点登录的解决方案,以JWT为例

​ I. 用户访问其他系统,会在网关判断token是否有效

​ II. 如果token无效则会返回401(认证失败)前端跳转到登录页面

​ III. 用户发送登录请求,返回浏览器一个token,浏览器把token保存到cookie

​ IV. 再去访问其他服务的时候,都需要携带token,由网关统一验证后路由到目标服务

权限认证是如何实现的

Spring security

image-20230521114432028

上传数据的安全性你们怎么控制

使用非对称加密(或对称加密),给前端一个公钥让他把数据加密后传到后台,后台解密后处理数据

  • 传输的数据很大建议使用对称加密,不过不能保存敏感信息
  • 传输的数据较小,要求安全性高,建议采用非对称加密

你负责项目的时候遇到了哪些比较棘手的问题

(1)设计模式

  • 工厂模式+策略
  • 责任链模式

回答思路

1,什么背景(技术问题)

2,过程(解决问题的过程)

3,最终落地方案

举例:

①:介绍登录业务(一开始没有用设计模式,所有的登录方式都柔和在一个业务类中,不过,发现需求经常改)

②:登录方式经常会增加或更换,每次都要修改业务层代码,所以,经过我的设计,使用了工厂设计模式和策略模式,解决了,经常修改业务层代码的问题

③:详细介绍一下工厂模式和策略模式(参考前面设计模式的课程)

(2)线上BUG

  • CPU飙高
  • 内存泄漏
  • 线程死锁

(3)调优

  • 慢接口
  • 慢SQL
  • 缓存方案

(4)组件封装

  • 分布式锁
  • 接口幂等
  • 分布式事务
  • 支付通用

你们项目中日志怎么采集的

采用的ELK

image-20230521232913086

介绍ELK的三个组件:

  • Elasticsearch是全文搜索分析引擎,可以对数据存储、搜索、分析
  • Logstash是一个数据收集引擎,可以动态收集数据,可以对数据进行过滤、分析,将数据存储到指定的位置
  • Kibana是一个数据分析和可视化平台,配合Elasticsearch对数据进行搜索,分析,图表化展示

扫码登录流程

image-20240705212918697

  1. 存在临时token

  2. 二维码状态由PC端轮询等方式监控

  3. 生成二维码,二维码待确认,二维码已确认三种状态

Maven

https://mp.weixin.qq.com/s/K0Q-kwP0HFwZ8qD7awxkFQ

pom文件里的的区别和作用