【Java面试题01】Spring经典面试题
文章目录
- 一、前言🚀🚀🚀
- 二、Spring面试题:☀️☀️☀️
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 生命周期
- 4.Spring 循环依赖
- 5.Spring boot自动配置原理
- 6.Spring MVC 执行流程
- 7.Spring 事务失效的场景
- 三、总结:🍓🍓🍓
一、前言🚀🚀🚀
☀️
你每一天的努力会在未来的某一个点交汇成宏伟的画面。
本文简介:本人是大三软件工程专业,java后端方向,学习路线:java基础->JDBC->Maven->MyBatis->SSM,通过做笔记分享到博客上的形式,激励自己学习,同时方便复习,欢迎大佬们评论或私信斧正 Thanks♪(・ω・))ノ
二、Spring面试题:☀️☀️☀️
1.Spring IOC
IOC(Inversion of Control,控制反转)是Spring框架的核心概念之一,它是一种设计原则,用于实现松耦合。
IOC是指将对象的创建、依赖注入和生命周期的管理权从应用程序代码转移到外部容器(在Spring中就是IOC容器)的过程。 传统编程中,对象自己控制直接创建依赖对象;而在IOC模式下,这个控制权被反转了,由容器来管理对象的依赖关系。
DI(Dependency Injection,依赖注入):对象被动接受依赖,而不是自己创建依赖
●构造器注入
●Setter方法注入
●字段注入(通过注解)
Spring IOC容器的核心接口:
● BeanFactory:基础容器,提供基本的DI功能
● ApplicationContext:扩展了BeanFactory,提供更多企业级功能(如AOP、事件传播等)
IOC的优势
- 降低耦合度:对象间依赖关系由容器管理,减少硬编码
- 提高可测试性:便于单元测试,可以轻松替换依赖
- 提高可维护性:配置集中管理,修改方便
- 促进面向接口编程:依赖注入通常基于接口而非具体实现
// 传统方式 - 直接创建依赖
public class UserService {private UserRepository userRepository = new UserRepositoryImpl();
}// IOC方式 - 依赖注入
public class UserService {private UserRepository userRepository;// 构造器注入public UserService(UserRepository userRepository) {this.userRepository = userRepository;}
}// Spring配置
@Configuration
public class AppConfig {@Beanpublic UserRepository userRepository() {return new UserRepositoryImpl();}@Beanpublic UserService userService() {return new UserService(userRepository());}
}
Spring IOC通过这种机制,实现了组件间的松耦合,使得应用程序更易于维护和扩展。
2.Spring AOP
AOP 是面向切面编程,是一种编程思想,Spring 的AOP是将与业务无关但影响多个对象的公共行为和逻辑的代码,抽取并封装为一个可重用的模块 减少代码冗余。
作用: 降低了模块间的耦合度,同时提高了系统的可维护性。
应用场景: 最常用的就是日志,权限,事务。
比如说权限
- 自定义一个注解
/*** 权限校验*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthCheck {/*** 必须有某个角色** @return*/String mustRole() default "";}
- 切面
@Aspect
@Component
public class AuthInterceptor {@Resourceprivate UserService userService;/*** 执行拦截** @param joinPoint* @param authCheck* @return*/@Around("@annotation(authCheck)")public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {String mustRole = authCheck.mustRole();RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();// 当前登录用户User loginUser = userService.getLoginUser(request);UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);// 不需要权限,放行if (mustRoleEnum == null) {return joinPoint.proceed();}// 必须有该权限才通过UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());if (userRoleEnum == null) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 如果被封号,直接拒绝if (UserRoleEnum.BAN.equals(userRoleEnum)) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 必须有管理员权限if (UserRoleEnum.ADMIN.equals(mustRoleEnum)) {// 用户没有管理员权限,拒绝if (!UserRoleEnum.ADMIN.equals(userRoleEnum)) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}}// 通过权限校验,放行return joinPoint.proceed();}
}
- 使用实例
/*** 创建题目** @param questionAddRequest* @param request* @return*/@PostMapping("/add")@AuthCheck(mustRole=UserConstant.ADMIN_ROLE)public BaseResponse<Long> addQuestion(@RequestBody QuestionAddRequest questionAddRequest, HttpServletRequest request) {ThrowUtils.throwIf(questionAddRequest == null, ErrorCode.PARAMS_ERROR);// todo 在此处将实体类和 DTO 进行转换Question question = new Question();BeanUtils.copyProperties(questionAddRequest, question);//标签是 json串,java中是集合List<String> tags = questionAddRequest.getTags();if (tags != null) {question.setTags(JSONUtil.toJsonStr(tags));}// 数据校验questionService.validQuestion(question, true);// todo 填充默认值User loginUser = userService.getLoginUser(request);question.setUserId(loginUser.getId());// 写入数据库boolean result = questionService.save(question);ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);// 返回新写入的数据 idlong newQuestionId = question.getId();return ResultUtils.success(newQuestionId);}
再来说说日志的实现,日志可以AOP
@Component
@Aspect
@Slf4j
public class LogInterceptor {@Around("execution(* com.ls.supstar.controller.*.*(..))")public Object doInterceptor(ProceedingJoinPoint point) throws Throwable {// 记时StopWatch stopWatch = new StopWatch();stopWatch.start();// 获取请求的路径RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();// 生成请求日志唯一idString requestId = UUID.randomUUID().toString();// 请求的ipString requestIp = request.getRemoteAddr();// // 请求的主机名称String clientHostName = getClientHostName(requestIp);// 请求的mac地址String macAddress = getMacAddress(requestIp);// 发起请求的用户Object user = request.getSession().getAttribute("user");// 请求urlString url = request.getRequestURL().toString();// todo 请求参数, 注意隐私,特别是密码要进行加密Object[] args = point.getArgs();
//String reqParam = "[" + StringUtils.join(args, ",") + "]";// 输出请求日志log.info("request start: requestId:{}, userId:{},requestIp:{},url:{}, reqParam:{}", requestId,user,requestIp, url, reqParam);
// log.info("request start: requestId:{}, userId:{},requestIp:{},clientHostName:{},macAddress:{},url:{}", requestId,user,requestIp,clientHostName,macAddress, url);Object result = null;try {// 执行目标方法并获取响应结果result = point.proceed();// 将响应结果转换为字符串String respParam = result != null ? result.toString() : "error";// 输出响应日志stopWatch.stop();long totalTimeMillis = stopWatch.getTotalTimeMillis();log.info("request end: requestId: {}, cost: {}ms, respParam: {}", requestId, totalTimeMillis, respParam);} catch (BusinessException e) {stopWatch.stop();long totalTimeMillis = stopWatch.getTotalTimeMillis();log.error("request end Business exception caught: requestId: {}, cost: {}ms, code: {}, message: {}",requestId, totalTimeMillis, e.getCode(), e.getMessage());throw e; // 保持 BusinessException 类型不变} catch (Exception e) {stopWatch.stop();long totalTimeMillis = stopWatch.getTotalTimeMillis();log.error("request end System exception caught: requestId: {}, cost: {}ms, message: {}",requestId, totalTimeMillis, e.getMessage());throw e; // 保持 Exception 类型不变}return result;}/*** 获取客户端主机名* @param clientIp* @return*/public static String getClientHostName(String clientIp) {try {// 如果是IPv6的本地回环地址,转换为IPv4if ("0:0:0:0:0:0:0:1".equals(clientIp) || "127.0.0.1".equals(clientIp)) {// 获取本地主机名 这里不要通过ip 去解析 主机名,可能会有误,因为通过ip解析主机名称,是查dns系统 ip 对应的主机名,可能会有误。InetAddress localHost = InetAddress.getLocalHost();return localHost.getHostName();}// 不是本地的请求 就根据ip 获得IntAddress, 然后在获取主机名称InetAddress inetAddress = InetAddress.getByName(clientIp);return inetAddress.getHostName();} catch (UnknownHostException e) {return "unknown";}}/*** 获取mac 地址* @param ipAddress* @return*/public static String getMacAddress(String ipAddress) {try {// 如果是本地回环地址,直接返回本机MAC地址if ("0:0:0:0:0:0:0:1".equals(ipAddress) || "127.0.0.1".equals(ipAddress)) {return getLocalMacAddress();}Process process = Runtime.getRuntime().exec("arp -a " + ipAddress);BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));String line;while ((line = reader.readLine()) != null) {if (line.contains(ipAddress)) {String[] parts = line.split("\\s+");return parts[2]; // MAC地址通常是第三列, 打断点就知道}}} catch (Exception e) {e.printStackTrace();}return "无法获取MAC地址";}/*** 获取本机的mac 地址* @return*/public static String getLocalMacAddress() {try {InetAddress ip = InetAddress.getLocalHost();NetworkInterface network = NetworkInterface.getByInetAddress(ip);byte[] mac = network.getHardwareAddress();StringBuilder sb = new StringBuilder();for (int i = 0; i < mac.length; i++) {sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? "-" : ""));}return sb.toString();} catch (Exception e) {e.printStackTrace();return "无法获取MAC地址";}}
}
3.Spring 生命周期
嗯,这个步骤还是挺多的,它大概流程是这样的。
① 实例化: Spring 容器根据配置文件或注解 实例化 Bean 对象。
② 属性注入: Spring 将依赖(通过构造器、setter方法或字段注入)注入到 Bean 实例中。
③ 初始化前的扩展机制: 如果 Bean 实现了 BeanNameAware 等 aware 接口,则执行 aware 注入。
④ 初始化前(BeanPostProcessor): 在 Bean 初始化之前,可以 通过 BeanPostProcessor 接囗对 Bean 进行一些额外的处理。
⑤ 初始化: 调用 InitializingBean 接囗的 afterPropertiesSet() 方法 或通过 init-method 属性指定的初始化方法
⑥ 初始化后(BeanPostProcessor): 在 Bean 初始化后,可以通过 BeanPostProcessor 进行进一步的处理使用 Bean:Bean 已经初始化完成,可以被容器中的其他 Bean 使用。
⑦ 销毁: 当容器关闭时,Spring 调用 DisposableBean 接口的 destroy() 方法 或通过 destroy-method 属性指定的销毁方法。
4.Spring 循环依赖
循环依赖(Circular Dependency) 是指两个或多个模块、类、组件之间相互依赖,形成一个闭环。简而言之,模块A依赖于模块B,而模块B又依赖于模块A,这会导致依赖链的循环,无法确定加载或初始化的顺序。
在 Spring 中主要是使用三级缓存来解决了循环依赖:
一级缓存(Singleton Objects Map): 用于存储完全初始化的单例Bean。
二级缓存(Early Singleton Objects Map): 用于存储尚未完全初始化,但已实例化的Bean,用于提前暴露对象,避免循环依赖问题。
三级缓存(Singleton Factories Map): 用于存储对象工厂,当需要时,可以通过工厂创建早期Bean(特别是为了支持AOP代理对象的创建)。
解决步骤:
Spring 首先创建 Bean 实例,并将其加入三级缓存中(Factory)。当一个 Bean 依赖另一个未初始化的 Bean 时,Spring 会从三级缓存中获取 Bean 的工厂,并生成该 Bean 的对象(若有代理则是代理对象)
代理对象存入二级缓存,解决循环依赖,
一旦所有依赖 Bean 被完全初始化,Bean 将转移到一级缓存中。
5.Spring boot自动配置原理
Spring Boot 的自动配置是通过 @EnableAutoConfiguration 注解实现,这个注解包含@Import({AutoConfigurationImportSelector.class}) 注解。
导入的这个类会去扫描 classpath 下所有的 META-INF/spring.factories 中的文件,
根据文件中指定的配置类加载相应的 Bean 的自动配置,
这些 Bean通常会使用 @ConditionalOnclass、@ConditionalOnMissingBean@ConditionalOnProperty 等条件注解。来控制自动配置的加载条件,
例如仅在类路径中存在某个类时,才加载某些配置。
6.Spring MVC 执行流程
客户端发出请求。
- DispatcherServlet 拦截(再次之前是Filter拦截,我们可以在Filter过滤不好的请求,比如说我们假如黑名单的用户),处理请求到HandlerMapping。
- HandlerMapping 找到具体的处理器,生成处理器对象及处理器拦截器(如果有)(处理器执行链),再一起返回给DispatcherServlet。
- DispatcherServlet 根据 handlerMapping 的返回结果(处理器执行链),找到HanderAptor执行处理器执行链的方法(处理请求参数),然后找到对应的handler然后执行方法,然后再将结果响应给 HandlerAdaptor(处理返回值)。
- HandlerAdaptor 处理后将 ModeAndView 返回给 DispatcherServlet.
- DispatcherServlet再将MoveAndView,返回给ViewResolver 视图解析器,视图解析器返回给 DispatcherServlet ,真正的View对象,经过渲染展示视图。
- DispatcherServlet返回结果给请求。
但是现在大部分都是前后端分离开发,流程有些变化。
在 HandlerApator 处理handler时,加上 @RequestBody。通过 HttpMessage 来处理返回结果,转化为 json 对象.返回给前端。
7.Spring 事务失效的场景
Spring 的支持的事务,包括编程式事务和声明式事务。但是一般都是使用声明式事务。
在需要使用事务的方法上添加@Transactional。
事务失效的情况
- 异常信息被捕获。
原因 :事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉。
解决:在catch块添加throw new RuntimeException(e)抛出。 - 抛出检查异常
原因:Spring 默认只会回滚非检查异常。
解决: 添加属性@Transactional(rollbackFor=Exception.class) - 非pubulic方法。
原因:spring事务的实现底层是代理, Spring 为方法创建代理、添加事务通知、前提条件都是该方法是 public 的。
解决:改为 public 方法 - 方法内调用。
原因:spring事务底层是代理实现。方法内调用不走代理。
解决方法:不直接调用,而是获取当前的代理对象,通过代理调用方法。
三、总结:🍓🍓🍓
以上就是今天要讲的内容