JAVA设计面试题目hot

Controller

1.写一个简单的登录接口的Controller,包含用户ID和密码字段。

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
@RestController
@RequestMapping("/api/auth")
public class AuthController {

@Autowired
private UserService userService;

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
User user = userService.findByUserId(request.getUserId());
if (user == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户不存在");
}

if (!user.getPassword().equals(request.getPassword())) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("密码错误");
}

// 模拟生成 JWT(可替换为真实生成)
String token = JwtUtil.createToken(user.getId().toString(), 10 * 60 * 1000); // 10分钟有效

Map<String, Object> response = new HashMap<>();
response.put("accessToken", token);
response.put("userId", user.getId());
response.put("username", user.getUsername());

return ResponseEntity.ok(response);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class UserService {
public User findByUserId(String userId) {
// 模拟查库
if ("admin".equals(userId)) {
User user = new User();
user.setId(1L);
user.setUserId("admin");
user.setUsername("管理员");
user.setPassword("123456"); // 明文密码仅用于演示
return user;
}
return null;
}
}

2.对于POST请求,你是直接用参数接收,还是封装成对象接收?

封装成对象来接受。

在开发中,对于 POST 请求,我更倾向于使用 @RequestBody 将参数封装成对象。这种方式更清晰、可维护,也方便进行参数校验和自动生成 Swagger 文档。如果参数较少、是简单的表单提交,偶尔也可以用 @RequestParam

1
2
3
4
5
6
7
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
String userId = request.getUserId();
String password = request.getPassword();
// 登录逻辑...
}

1
2
3
4
5
6
@Data
public class LoginRequest {
private String userId;
private String password;
}

封装成类,参数多的时候,不宜混乱,方便扩展,新增字段的时候,不用改方法签名,支持验证注解,如 @NotBlank@Size 直接加在类字段上。更加适合Swagger自动文档,自动识别请求结构。

不封装的话,参数签名越来越长,而且无法处理 application/json 请求。不能队单个字段加验证规则。

3.你在工作中是如何进行参数非空校验的?

在工作中,我会根据具体场景,在 三个层次 对参数进行非空校验:

  1. 前端校验:前端通过 JS 或组件校验框架(如 Element Plus 表单校验、AntD Rule)来做第一层参数合法性校验,提升用户体验。
  2. 后端控制层校验(推荐):使用 Spring 的 javax.validation 注解(如 @NotBlank@NotNull@Size)结合 @Valid 实现自动参数校验,统一异常处理返回提示。
  3. 业务逻辑校验(服务层):对关键参数进行业务校验(如账号是否存在、密码是否为空、两个字段不能同时为空等),保证逻辑正确性。

controller层

1
2
3
4
5
6
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
// 如果参数校验失败,会自动抛出异常
return authService.login(request);
}

DTO+注解

1
2
3
4
5
6
7
8
9
10
@Data
public class LoginRequest {

@NotBlank(message = "用户ID不能为空")
private String userId;

@NotBlank(message = "密码不能为空")
private String password;
}

全局异常处理:

1
2
3
4
5
6
7
8
9
10
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleValidationException(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldError().getDefaultMessage();
return ResponseEntity.badRequest().body(Map.of("error", msg));
}
}

还封装了统一的 BaseRequest 类 + 参数校验通用注解,配合全局异常返回标准化的错误结构,做到接口友好、开发高效。

BaseRequest

1
2
3
4
5
6
7
8
9
10
11
12
@Data
public class BaseRequest {
@NotNull(message = "请求时间戳不能为空")
private Long timestamp;

@NotBlank(message = "请求来源不能为空")
private String source; // web / app / admin 等

@NotBlank(message = "签名不能为空")
private String sign; // 用于简单签名校验
}

实际请求

1
2
3
4
5
6
7
8
9
10
@Data
public class LoginRequest extends BaseRequest {

@NotBlank(message = "用户ID不能为空")
private String userId;

@NotBlank(message = "密码不能为空")
private String password;
}

Controller 中使用 @Valid 接收参数

1
2
3
4
5
6
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
// 校验通过,执行业务逻辑
return authService.login(request);
}

全局异常处理返回统一格式(如 Result<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;

public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}

public static Result<?> error(String message) {
return new Result<>(400, message, null);
}
}

参数校验统一异常处理(@ControllerAdvice

1
2
3
4
5
6
7
8
9
10
11
12
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Result<?>> handleValidation(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldError().getDefaultMessage();
return ResponseEntity.badRequest().body(Result.error(msg));
}

// 其他通用异常处理
}

我们项目里统一封装了请求基类 BaseRequest,每个接口请求都包含 timestamp、source、sign 等字段,同时结合 Spring 的 @Valid 和自定义的通用注解进行字段校验,配合统一异常处理类,返回格式统一成 Result<T>。这样接口文档清晰、调试方便、前后端联调效率高,开发体验提升很多。

设计模式

分类 模式 作用 示例关键点
创建型 单例模式 保证全局只有一个实例 Spring 中的 Bean 默认就是单例
创建型 工厂模式 统一创建对象 数据库连接、策略类
结构型 代理模式 给目标对象增加额外功能 Spring AOP、事务、日志
结构型 装饰器模式 动态扩展对象功能 IO 流、过滤器链
行为型 策略模式 可切换的行为算法 支付、消息推送
行为型 观察者模式 发布/订阅事件通知 MQ、事件监听
行为型 模板方法 固定流程 + 可变步骤 抽象 Controller、登录流程
行为型 责任链模式 多个处理器依次处理 过滤器链、权限认证
行为型 状态模式 对象状态驱动行为 订单、任务状态
行为型 命令模式 把操作封装成对象 任务撤销、限流操作命令

单例模式

确保一个类在整个应用中 只有一个实例,并提供一个全局访问点。

  • Redis 工具类、线程池、连接池、配置类等
  • Spring 默认是单例 Bean(IOC 控制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
private static volatile Singleton instance;

private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 防止指令重排
}
}
}
return instance;
}
}

基于volatile和synchronized

volatile:防止 JVM 指令重排序,确保实例初始化完成后再赋值。

双重校验:第一次 if 避免每次加锁,第二次 if 避免并发重复创建。

比如Calendar:

1
Calendar calendar = Calendar.getInstance();

比如 Logger:

1
Logger logger = Logger.getLogger(MyClass.class.getName());

工厂模式

根据不同类型 生成不同实现类实例,解耦对象创建过程。

  • 根据类型创建支付处理器、登录方式、导出类型等

  • 创建支付方式(支付宝、微信)

    创建导出服务(Excel、PDF)

    登录策略、文件解析等类型多变组件

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
package com.mengnankk.designpattern;

// 工厂类
public class ExportFactory {
public static ExportService getExport(String type) {
if ("excel".equalsIgnoreCase(type)) {
return new ExcelExportService();
} else {
throw new RuntimeException("不支持的类型");
}
}
}


interface ExportService {
void export();
}


class ExcelExportService implements ExportService {
public void export() {
System.out.println("导出Excel");
}
}

结合接口隔离 + 枚举注册

通过工厂类封装创建逻辑

返回的是接口类型,利于扩展和解耦

结合枚举或配置可实现注册式工厂

策略模式

定义一系列算法(策略),并使它们可以相互替换。将行为与选择解耦

多种支付方式、登录方式、审核策略等

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 com.mengnankk.designpattern;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class PayContext {
@Autowired
private Map<String,PayStrategy> paymap;

public void dopay(String type){
PayStrategy strategy = paymap.get(type);
if (strategy!=null) strategy.pay();
}
}
interface PayStrategy{
void pay();
}
@Component("aliPay")
class AliPay implements PayStrategy {
public void pay() {
System.out.println("支付宝支付");
}
}
@Component("wechatPay")
class WeChatPay implements PayStrategy {
public void pay() { System.out.println("微信支付"); }
}



利用 Spring 容器自动注入所有策略 Bean

根据 key 获取策略执行对应逻辑,避免写死 if-else 或 switch

和工厂模式的最大区别是:策略封装行为,工厂封装创建

面试题目:设计模式场景题:比如现在有支付宝、微信、各大银行卡等支付方式,最坏的情况是写了20多个if-else判断是用户选择哪种支付方式,耦合度很高,如果叫你优化,你怎么想?

我们可以使用策略模式,先封装一个支付的接口,然后把每个支付的方式分装成类,然后用一个map集合或者是工厂类来让用户选择具体的是哪个支付方式,这样就避免了大量使用if-else。

需要一个接口,然后具体的实现类,然后一个工厂类,使用map简化策略选择。还有一个策略的上下文类

然后客户端调用上下文类(里面是工厂实例),然后选择支付金额。这里的支付金额要使用BigDecimal

因为 double二进制浮点数,在进行运算时会出现精度误差

使用BigDecimal 基于字符串表示的小数类,可以实现精确计算,避免金额误差,防止“丢钱”或“多扣”的风险。

代理模式

在不修改目标类的情况下,增强其功能(如日志、权限、事务等)

日志、权限、事务控制、接口限流

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.mengnankk.designpattern;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.lang.annotation.*;

@Aspect
@Component
public class LogAspect {

@Before("@annotation(com.mengnankk.designpattern.Log)") // 指定注解全路径
public void logBefore(JoinPoint point) {
System.out.println("调用方法:" + point.getSignature().getName());
}
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface Log {
String value() default "";
}


AOP 本质是动态代理(JDK 或 CGLIB)

利用切点表达式 @annotation 定位目标方法

增强功能通过切面类编织

修饰器模式

在不修改原始对象的情况下,动态增强其功能(与代理很像,区别是包装对象)

请求/响应增强处理、日志追加处理、加密/解密

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
package com.mengnankk.designpattern;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class RequestLogFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) {
// 初始化逻辑(可选)
}

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
System.out.println("记录请求日志");
chain.doFilter(req, res);
}

@Override
public void destroy() {
// 资源释放逻辑(可选)
}
}

类似代理,但更偏向“嵌套增强”

典型用法是 Filter、Interceptor,形成处理链

可以组合多个修饰器链式增强

适配器模式

配器模式像是“插头转换器”,让两个原本不兼容的接口连接上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface MediaPlayer {
void play(String type, String fileName);
}

class OldPlayer {
public void playAudio(String fileName) {
System.out.println("播放音频:" + fileName);
}
}

class AudioAdapter implements MediaPlayer {
private OldPlayer oldPlayer = new OldPlayer();

@Override
public void play(String type, String fileName) {
if ("mp3".equalsIgnoreCase(type)) {
oldPlayer.playAudio(fileName);
}
}
}

维度 代理模式 适配器模式
目的 控制访问目标对象,添加附加逻辑 接口不兼容时进行适配
原始类 与代理类实现同一接口 与目标接口无关,通过包装“适配”
使用时机 不想或不能直接访问目标对象,增加功能(如远程调用、权限) 复用已有类但其接口不符合当前系统要求
是否增强 是,可以添加前置/后置逻辑 否,仅做接口适配

模板方法模式

模板方法,比如说有三个模板方法A、B、C,第一个子类我想让他实现ABC这么执行,第二个子类我想让他实现ACB,这个怎么实现?(使用“钩子方法”,抽离一个方法返回true、false,true就ABC,false就ACB)

模板方法模式 + 钩子方法(Hook)自定义流程控制

在抽象类中定义整体流程 A → B → C,然后用钩子方法来控制流程顺序

  • 抽象类中定义 template() 模板方法
  • A()C() 是固定流程
  • B() 是可选的
  • 使用 shouldDoB() 钩子方法(返回 true/false)来决定执行顺序,是 ABC 还是 ACB

定义抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Template {

// 模板方法
public final void execute() {
A();
if (shouldDoBFirst()) {
B();
C();
} else {
C();
B();
}
}

protected abstract void A();
protected abstract void B();
protected abstract void C();

// 钩子方法,默认返回 true,即执行 ABC
protected boolean shouldDoBFirst() {
return true;
}
}

子类去继承这个抽象类,然后重写父类的方法shouldDoBFirst,按照需求重写。

1
2
3
4
5
6
7
8
9
public class FirstChild extends Template {
protected void A() { System.out.println("A"); }
protected void B() { System.out.println("B"); }
protected void C() { System.out.println("C"); }
protected boolean shouldDoBFirst() {
return true; // 默认就是 ABC
}
}

java扩展

1.同步和异步的区别是什么?

同步(Synchronous)和异步(Asynchronous) 是两种不同的任务执行方式,主要区别在于任务的执行是否需要等待其他任务完成。

同步的话是需要等待任务完成之后,收到返回的确认请求的时候再进行其他的

异步的话,就是不需要等待,直接进行下一步任务