java常见bugs与安全漏洞

欢迎你来读这篇博客,这篇博客主要是关于java常见bug与安全漏洞及避免方法的介绍
其中包括了关于我的见解和收集的知识分享。

序言

Bug

NPE 场景

  1. 调用了空对象的实例方法
  2. 访问空对象的属性
  3. 数组为空的时候,取长度
  4. 不能抛出空异常对象
  5. 方法的返回值是null,调用方法直接使用
  6. 自动拆箱导致NPE
    • 空包装对象赋值给基本数据类型时
    • 方法传参时
    • 空对象进行比较大小

避免方法

  • 使用之前初始化
  • 避免返回null
  • 外部传值,一定要及时判断
  • 基本数据类型优于包装类型,优先使用基本数据类型
  • 不确定的包装类型,先校验后使用
  • 对于Null值的包装类型,赋值为0

字符串、数组、集合场景

  • null字符串调用equals方法
  • 对象数组new出来,但元素没有初始化
  • list的addAll方法,传递null

Optional

  • 不能作为类字段使用,没有实现序列化接口
  • 容器类,代表存在和不存在
  • orElse(new Object()) 存在返回,空提供默认值
  • orElseGet() 存在返回,空由函数去产生 // 配合单例
  • orElseThrow 存在返回,空抛出异常
  • ifPresent 存在处理
  • map 执行操作后返回一个optional对象
  • 解决了什么问题
    • 使代码更加优雅简洁
    • 减少npe
  • 解决不了什么问题
    • 不具备传导性
  • 何时使用
    • Optional的预期用途主要是作为返回类型。
  • 何时不要使用
    • 不要将其用作类中的字段,因为它不可序列化。如果确实需要序列化包含Optional值的对象,则Jackson库提供了将Optionals视为普通对象的支持。这意味着Jackson将空对象视为空,将具有值的对象视为包含该值的字段。
    • 不要将其用作构造函数和方法的参数,因为这会导致不必要的复杂代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Optional 常用
// 返回一个空的Optional对象 private static final Optional<?> EMPTY = new Optional<>();
Optional<String> empty = Optional.empty();
// 返回特定的非空值Optional。
Optional<String> opt = Optional.of("name");
// 返回描述指定值的Optional,如果非空,则返回空值。
Optional<String> optional = Optional.ofNullable("name");
// 根据value是否为空返回bool值 如果存在值,则返回true;反之,返回false。如果所包含的对象不为null,则返回true,反之返回false。
optional.isPresent()
// 如果此Optional中存在值,则返回该值,否则抛出 NoSuchElementException。
String value = optional.get();
// 返回包装的值(如果存在)及其参数
String value = optional.orElse("password");
// 返回值(如果存在);否则,调用other并返回该调用的结果。 该orElseGet() 方法类似于 orElse()。但是,如果没有Optional值,则不采用返回值,而是采用供应商功能接口,该接口将被调用并返回调用的值:
String name = Optional.ofNullable(nullName).orElseGet(() -> "john");
// 如果value,根据exceptionSupplier抛出异常
String value = optional.orElseThrow(() -> new IllegalArgumentException("Value cannot be null"));
// 根据传入的的predicate条件,处理value,如果条件满足,返回当前Optional,否则,返回空的Optional
Optional<String> filteredName = optional.filter(name -> name.startsWith("n"));
// 对value进行转换,如果为空,返回空的Option
Optional<String> mappedName = optionalName.map(String::toUpperCase);
// 与map的区别在于,要求用户自己将转换结果转为Optional对象。
Optional<Integer> nameLength = optionalName.flatMap(name -> Optional.of(name.length()));

Optional 源码与注释

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
package java.util;

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;

/**
一个可能包含也可能不包含非null值的容器对象。 如果存在值,则isPresent()返回true 。 如果不存在任何值,则该对象被视为空,并且isPresent()返回false 。
提供了其他取决于所包含值是否存在的方法,例如orElse() (如果不存在值,则返回默认值)和ifPresent() (如果存在值,则执行操作)。
这是一个基于值的类; 在Optional实例上使用标识敏感的操作(包括引用等于( == ),标识哈希码或同步)可能会产生不可预测的结果,应避免使用
*/
public final class Optional<T> {
/**
* empty()通用实例
*/
private static final Optional<?> EMPTY = new Optional<>();

/**
* 如果不为空,则为该值;否则为false。 如果为null,则表示不存在任何值
*/
private final T value;

/**
构造一个空实例。
impl注意:
通常,每个VM仅应存在一个空实例EMPTY
*/
private Optional() {
this.value = null;
}

/**
返回一个空的Optional实例。 此Optional没有值。
类型参数:<T> –不存在的值的类型
返回值:一个空的Optional
api注意:
尽管这样做可能很诱人,但应通过将==与Optional.empty()返回的实例进行比较来避免测试对象是否为空。 不能保证它是一个单例。 而是使用isPresent()
*/
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}

/**
使用描述的值构造一个实例。
参数:值–要描述的非null值
抛出:NullPointerException如果值为null
*/
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}

/**
返回一个Optional描述给定的非null值。
参数:value –要描述的值,必须为非null
类型参数:<T> –值的类型
返回值:存在值的Optiona
*/
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}

/**
返回一个描述给定值的Optional ,如果不为null ,则返回一个空的Optional 。
参数:值–描述的可能为null值
类型参数:<T> –值的类型
返回值:一个Optional与如果指定值是非当前值null ,否则一个空Optional
*/
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}

/**
如果存在值,则返回该值,否则抛出NoSuchElementException 。
返回值:此Optional描述的非null值
抛出:NoSuchElementException如果不存在任何值
api注意:此方法的首选替代方法是orElseThrow() 。
*/
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}

/**
如果存在值,则返回true ,否则返回false 。

返回值:如果存在值,则为true ,否则为false
*/
public boolean isPresent() {
return value != null;
}

/**
如果不存在值,则返回true ,否则返回false 。

返回值:如果不存在值,则为true ,否则为false
*/
public boolean isEmpty() {
return value == null;
}

/**
如果存在值,则使用该值执行给定的操作,否则不执行任何操作。
参数:动作–要执行的动作(如果存在值)
*/
public void ifPresent(Consumer<? super T> action) {
if (value != null) {
action.accept(value);
}
}

/**
如果存在值,则使用该值执行给定的操作,否则执行给定的基于空的操作。
参数:动作–要执行的动作(如果存在值)emptyAction –要执行的基于空的操作(如果不存在任何值)
抛出:NullPointerException如果存在一个值并且给定的操作为null ,或者不存在任何值并且给定的基于空的操作为null
@since 9
*/
public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
if (value != null) {
action.accept(value);
} else {
emptyAction.run();
}
}

/**
如果存在一个值,并且该值与给定的谓词匹配,则返回描述该值的Optional ,否则返回一个空的Optional 。

参数:谓词–应用于值的谓词(如果存在)
返回值:一个Optional描述此的值Optional ,如果一个值存在并且该值给定的谓词相匹配,否则一个空Optional
抛出:NullPointerException如果谓词为null
*/
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent()) {
return this;
} else {
return predicate.test(value) ? this : empty();
}
}

/**
如果存在值,则返回一个Optional描述(就像by ofNullable ),将给定映射函数应用于该值的结果,否则返回一个空的Optional 。
如果映射函数返回null结果,则此方法返回空的Optional
*/
public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent()) {
return empty();
} else {
return Optional.ofNullable(mapper.apply(value));
}
}

/**
如果存在一个值,则返回将给定Optional -bearing映射函数应用于该值的结果,否则返回一个空的Optional 。
此方法类似于map(Function) ,但是映射函数是其结果已经是Optional函数,如果调用该函数,则flatMap不会将其包装在其他Optional 。

参数:mapper –应用于值的映射函数(如果存在)
类型参数:<U> –映射函数返回的Optional值的类型
返回值:施加的结果Optional荷瘤映射函数此的值Optional ,如果一个值存在,否则一个空Optional
抛出:NullPointerException如果映射函数为null或返回null结果

*/
public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent()) {
return empty();
} else {
@SuppressWarnings("unchecked")
Optional<U> r = (Optional<U>) mapper.apply(value);
return Objects.requireNonNull(r);
}
}

/**
如果值存在时,返回一个Optional描述的值,否则将返回一个Optional产生通过供给功能。

参数:供应商–产生要返回的Optional的供应功能
返回值:返回一个Optional描述此的值Optional ,如果一个值存在,否则Optional所生产的供应功能。
抛出:NullPointerException如果提供的函数为null或产生null结果
* @since 9
*/
public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier) {
Objects.requireNonNull(supplier);
if (isPresent()) {
return this;
} else {
@SuppressWarnings("unchecked")
Optional<T> r = (Optional<T>) supplier.get();
return Objects.requireNonNull(r);
}
}

/**
如果存在值,则返回仅包含该值的顺序Stream ,否则返回空Stream 。

返回值:作为Stream的可选值
* @since 9
*/
public Stream<T> stream() {
if (!isPresent()) {
return Stream.empty();
} else {
return Stream.of(value);
}
}

/**
如果存在值,则返回该值,否则返回other 。

参数:其他–要返回的值(如果不存在任何值)。 可以为null 。
返回值:值(如果存在),否则other
*/
public T orElse(T other) {
return value != null ? value : other;
}

/**
如果存在值,则返回该值,否则返回由供应函数产生的结果。

参数:供应商–产生要返回的值的供应函数
返回值:值(如果存在),否则提供功能产生的结果
*/
public T orElseGet(Supplier<? extends T> supplier) {
return value != null ? value : supplier.get();
}

/**
* If a value is present, returns the value, otherwise throws
* {@code NoSuchElementException}.
*
* @return the non-{@code null} value described by this {@code Optional}
* @throws NoSuchElementException if no value is present
* @since 10
*/
public T orElseThrow() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}

/**
如果存在值,则返回该值,否则抛出由异常提供函数产生的异常。

参数:exceptionSupplier –产生要抛出的异常的提供函数
类型参数:<X> –引发的异常类型
返回值:值(如果存在)
抛出:X –如果不存在任何值NullPointerException如果不存在任何值并且异常提供函数为null
api注意:带有空参数列表的对异常构造函数的方法引用可用作提供者
*/
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
}

正确处理异常

  • 使用异常,而不是返回码(或类似),因为异常会更加的详细
  • 主动捕获检查性异常,并对异常信息进行反馈(日志或标记)
  • 保持代码整洁,一个方法中不要有多个 try catch 或者嵌套的 try catch
  • 捕获更加具体的异常,而不是通用的 Exception
  • 合理的设计自定义的异常类

常见异常

  • 并发修改:可迭代对象在遍历的同时做修改,则会报并发修改异常
  • 类型转换:类型转换不符合 Java 的继承关系,则会报类型转换异常
  • 枚举查找:枚举在查找时,如果枚举值不存在,不会返回空,而是直接抛出异常

资源管理

try-with-resources

java计算、接口、集合

BigDecimal精度问题

  • 初始化设置精度需要匹配
    • bigDecimal.setScale(2) 可大不能小
    • bigDecimal.setScale(2,BigDecimal.ROUND_HALF_UP)
  • 除法结果需要精度
  • 数值比较需要精度匹配

日期

SimpleDataFormat

  • 可以解析大于或等于它定义的时间精度,但不能解析小于它定义的精度
  • 线程不安全,多线程会抛出异常
    • 原因:内部有一个calendar
    • 解决方法:定义为局部变量、使用ThreadLoacl、Synchronize

迭代问题

  • for-each优于for

集合判等问题

  • equals和hashcode与其可能带来的潜在问题
  • 类实现了 compareTo 方法,就需要实现 equals 方法
  • compareTo 与 equals 的实现过程需要同步

Lombok

  • @EqualsAndHashCode(callSuper=true)
  • 命名问题

抽象类和接口

抽象类、接口的含义和特性

  • 抽象类是子类的通用特性,包含了属性和行为;接口是定义行为,并不关心谁去实现
  • 抽象类是对类本质的抽象,表达的是 is a 的关系;接口是对行为的抽象,表达的是like a 的关系

抽象类、接口的相同点

  • 接口中的方法(java8 改变了这一语法)和抽象类中的抽象方法都不能有方法体,并且必须在子类中实现
  • 都可以被继承,但是不能被实例化

抽象类、接口的不同点

  • 使用时语法不同,抽象类使用 extends,接口则使用implements
  • 接口中只能定义常量,所以,不能表达对象状态,而抽象类可以
  • 接口中的方法必须是 public类型的,而抽象类则没有限制
  • 类可以同时实现多个接口(间接解决了 Java 不支持多继承的we难题),但是只能继承一个抽象类

默认方法与静态方法

lambda与函数式接口

Stream和lambda真的高效吗

迭代过多可能会导致计算低效

序列化

  • 父类不可序列化,子类实现了序列化接口也不行
    • 需要提供父类的默认无参构造器
  • 类中存在引用对象
    • 所有属性都是可序列化的才可以序列化
  • 同一个对象多次序列化,直接有更新
    • 不会重复序列化,会影响结果

泛型、反射、编译优化

泛型

玩转泛型的前提是理解泛型擦除。

泛型的特性

  • 先检查再编译
  • 泛型不支持继承
  • 泛型的类型变量不能是基本类型
  • 泛型的类型参数只能是类类型,不能是简单类型

使用原始类型,可能会带来灾难性后果 不声明泛型类型。主要是为了兼容旧代码。

反射

  • 应用场景
    • 开发通用框架
    • 动态代理
    • 注解
    • 可扩展性功能
  • 缺点
    • 性能开销
    • 破坏封装性
    • 内部曝光

什么情况下反射获取不到Method

  • 当方法是基本类型时,反射获取 Method 的参数类型也必须一致
  • 如果调用的方法属于当前对象的父类,那么 getDetlaredMethod 获取不到Method

字符串

可以看我的Java日志Java·奇技淫巧这两篇文章,有关于字符串拼接的讨论。

深拷贝与浅拷贝

Object类的clone()是浅拷贝

线程安全

synchronized

  • 不会被继承,子类需要重新指定
  • 标记位置:方法声明、方法体
  • JDK实现:对象头标记,字节码标记 monitor
  • JDK优化:偏向锁-轻量级锁-重量级锁

多线程原子更新变量值

  • countDownLatch
  • Atomic系列

阻塞队列

阻塞队列:支持两个附加操作的队列。队列空则等待、队列满则等待。

方法 抛出异常 返回特殊值 一直阻塞 超时退出
插入 add offer put offer(time)
移除 remove poll take poll(time)
检查 element peek - -

copy-on-write容器

主要目的是解决并发读写List异常

优点:并发读不需要加锁,提高进程的并发度

缺点:内存占用问题,一致性问题

应用场景:读多写少,如黑名单,白名单。

使用的时候,需要注意减少扩容开销,使用批量添加

线程池

TODO:自定义线程池,相关文章太多了,不再这里写了,有时间了,补充。

ThreadLocal

threadlocal并不是来解决并发或者共享问题的。

使用问题:

  • 不支持继承
  • 如果不及时清理现场,会造成数据混乱。

Spring 常见的坑

Spring Bean 默认名称生成策略导致空指针

Spring 对 Class前两个字母都是大写的,进行了特殊处理,把大写变成小写。但是默认策略是第一个字母会变成小写。

如果类名是QWERService。GetBean(qWERService) 就找不到。

解决方案:

  • 使用类型获取
  • getBean传正确
  • 类名规避这种情况
  • 根据类型获取Bean getBean(QWERService.class)

使用@Autowired依然空指针

  • 属性对象注入了,但是对象没被注册成bean
  • 虽然标记成SpringBean,属性也注入了,但是使用new去获取类对象。
    • 正确做法应该是,Bean的整个生命周期都应该被Spring容器管理
  • Spring的包扫描机制,没扫描到

不使用自动注入,如何获取上下文

  • ApplicationContextInitializer
  • ApplicationListener
  • SpringApplication.run()的返回值

什么场景需要使用获取并使用上下文?

多线程下SpringBean的数据不符合预期怎么办

  • 默认单例Bean
    • 如果有全局属性,造成线程间通信,如果不做线程安全,就会有问题。
      • 优势
        • 减少新生成实例的小号
        • 减少JVM垃圾回收
        • 快速获取Bean
      • 劣势
        • 线程不安全
      • 默认单例的理由
        • 少创建实例
        • 垃圾回收便捷
        • 使用缓存快速获取
  • 原型

报错:存在多个Bean异常

  • @Autowired:属于 Spring框架,默认使用类型(byType)进行注入
  • @Qualifier:结合 @Autowired 一起使用,自动注入策略由 byType 变成 byName
  • @Resource:JavaEE 自带的注解,默认按 byName 自动注入。JDK9-11某个版本已经移除。目前遵循JakartaEE标准,而非JavaEE标准。
  • @Primary:存在多个相同类型的 Bean m @Primary 用于定义首选项

Bean注入常见异常

  • 只定义了接口,没有具体实现
    • 解决方法 require 参数设置为false
  • 定义了接口的多个实现类,只使用@Autowire注入

循环依赖怎么办

解决方式,三级缓存。只能解决单例模式下的循环依赖。

  • field的方式
  • set方式

循环依赖问题最终解决方案,还是要通过避免循环依赖进行解决。出现循环依赖是设计的问题。

循环依赖通常意味着类之间的耦合过强,可能需要重新审视和调整设计。例如:

  • 单一职责原则:确保每个类只负责一个职责,避免一个类同时依赖多个功能,这样可以降低类之间的相互依赖。
  • 依赖反转:如果类 A 依赖类 B,而类 B 又依赖类 A,考虑引入接口或抽象类,反转依赖方向。通过接口使类之间的依赖关系更加灵活。
  • 抽象工厂模式:引入工厂类来管理对象的创建,而不是直接在类之间相互依赖。这样可以让依赖关系更加清晰。
  • 事件驱动:如果两个组件之间的依赖是为了处理某些事件或操作,可以考虑使用事件驱动的机制,解耦它们之间的关系。

如何利用Bean生命周期

@Transactional

  • 捕获异常,标记回滚(编程式事务)
  • 捕获异常,抛出Check异常,不能回滚
  • 捕获异常,抛出UnCheck异常,可以回滚
  • 捕获特定异常,根据异常,可以选择性回滚
  • 一个没标注事务的方法,调用一个标注事务的方法,事务失效

Spring MVC 的坑

自定义返回

  • 使用 ResponseEntity 类:标识整个 HTTP 响应(状态码、头部信息、响应体
  • 异常类或 Controller 方法上标识 @ResponseStatus 注解
  • 使用 @ControllerAdvice(@RestControllerAdvice)和 @ExceptionHandler 注解

时间序列化和反序列化的问题

  • @DateTimeFormat
    • FE2BE
    • Get 可以正常,Post不行
  • @JsonFormat
    • 自定义实现Json序列化格式转换

日志放在拦截器还是过滤器

  • Filter
  • Interceptor

读取Request输入流,请求数据就不见了

SpringBoot 中的坑

配置出错

  • 配置文件加载顺序
  • 配置文件优先级
  • 推荐使用同一种格式yml

定时任务

  • @EnableScheduling
  • @Scheduled
    • fixedDelay
    • fixedRate
    • initialDelay
    • cron
  • spring.task.scheduling
  • ScheduleConfig

线程池

  • @EnableAsync
  • @Async
  • 有没有返回值

异步任务如果抛出异常,spring框架只打印了一行日志,并没有做处理。开发中需要捕获异常,并处理

异步任务如果超时了。一直阻塞不太好。选择超时时间,超时不做处理。

自定义线程池,配置自定义异常处理。

Jackson

  • ObjectMapper线程安全
  • 尽量注入使用,尽可能重用。
  • 注解
    • JsonIgnore
    • JsonProperty
    • JsonFormat
    • JsonInclude
    • JsonIgnoreProperties
    • JsonSerialize

Security

xss

主要思路

  • 严格校验长度
  • 严格根据需求校验DTO
  • 长文本必须转义
1
2
3
// Apache Commons Lang 库中的方法,用于将 HTML 特殊字符(如 <, >, &, " 等)转义成 HTML 实体字符,从而防止 HTML 注入攻击。
StringEscapeUtils.escapeHtml(value);
// 写一个filter拦截所有http请求,转换一下

SQL Inject

目前此漏洞很少了,如果代码中有涉及到硬编码sql的情况,使用#{},而非${}。

防盗链

目前云厂商的OSS基本上都送防盗链防护或者卖这个服务。接入就行了,便宜又方便。

底层实现原理:根据黑白名单,判断HTTP请求头中的Referer字段。

CSRF

  • Spring Security CSRF Token

幂等

  • Token令牌
    • 获取token,判断是否缓存中有,没有直接报错,有则执行,执行完业务逻辑后删除。
  • MVCC
  • 去重表
  • 悲观锁

防止伪造Token请求

核心接口做多因子认证

  • 短信验证码
  • 人脸识别

防止机器人

  • 图形验证码,限流,黑白名单

忘记密码漏洞

  • 多因子验证
  • 多因子验证码接口调用前,图形验证码拦截,防止机器模拟
  • 重试次数限制
  • 防止DDos
  • 黑白名单

隐藏域漏洞

  • 值传递不要用隐藏域

任意文件上传漏洞

  • 文件格式限制
  • 文件格式校验
    • 前端校验扩展名
    • 服务器校验扩展名
    • content-type校验
  • 文件上传目录限制,权限限制
  • 文件重命名

其他漏洞

  • 直接异常信息,会给攻击者以提示。要做隔离
  • 上线去除所有注释

网站安全漏洞扫描

信息加密与密钥管理

接口幂等

通过token机制进行幂等和防重。

参考资料

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


java常见bugs与安全漏洞
https://allendericdalexander.github.io/2025/03/31/java_bugs/
作者
AtLuoFu
发布于
2025年3月31日
许可协议