技术栈java面试JAVA设计面试题目hot
mengnankkzhouController
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("密码错误"); }
String token = JwtUtil.createToken(user.getId().toString(), 10 * 60 * 1000);
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.你在工作中是如何进行参数非空校验的?
在工作中,我会根据具体场景,在 三个层次 对参数进行非空校验:
- 前端校验:前端通过 JS 或组件校验框架(如 Element Plus 表单校验、AntD Rule)来做第一层参数合法性校验,提升用户体验。
- 后端控制层校验(推荐):使用 Spring 的
javax.validation
注解(如 @NotBlank
、@NotNull
、@Size
)结合 @Valid
实现自动参数校验,统一异常处理返回提示。
- 业务逻辑校验(服务层):对关键参数进行业务校验(如账号是否存在、密码是否为空、两个字段不能同时为空等),保证逻辑正确性。
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;
@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();
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; } }
|
java扩展
1.同步和异步的区别是什么?
同步(Synchronous)和异步(Asynchronous) 是两种不同的任务执行方式,主要区别在于任务的执行是否需要等待其他任务完成。
同步的话是需要等待任务完成之后,收到返回的确认请求的时候再进行其他的
异步的话,就是不需要等待,直接进行下一步任务