11. AOP 的底层实现,动态代理是如何动态,假如有 100 个对象,如何动态的为这 100 个对象代理
深度解析
核心考点
考察 AOP 的核心实现原理(动态代理)、「动态」的本质,以及批量代理的实战思路,是 Spring 核心原理的高频题。
核心拆解
- AOP 的本质:横切逻辑与业务逻辑解耦,底层依赖动态代理实现;
- 动态代理的「动态」:运行时生成代理类,而非编译期硬编码,无需为每个目标类编写代理类;
- 批量代理的关键:统一的代理规则(如基于接口 / 类的匹配规则)+ 工厂模式 / 容器管理。
实战易错点
- 混淆 JDK 动态代理和 CGLIB 的适用场景:JDK 代理要求目标类实现接口,CGLIB 可代理无接口类;
- 批量代理时忽略性能:100 个对象代理需控制代理创建频率,避免重复生成代理类。
标准答案
一、AOP 的底层实现(动态代理)
AOP(面向切面编程)的核心是「在不修改业务代码的前提下,为方法增加横切逻辑(如日志、事务、权限)」,底层依赖两种动态代理方式:
表格
| 代理方式 | 实现原理 | 适用场景 | 核心类 |
|---|---|---|---|
| JDK 动态代理 | 基于接口,运行时通过Proxy.newProxyInstance()生成代理类(继承Proxy,实现目标接口) |
目标类实现了接口 | java.lang.reflect.Proxy、InvocationHandler |
| CGLIB 动态代理 | 基于继承,运行时通过 ASM 字节码框架生成目标类的子类作为代理类 | 目标类无接口 | org.springframework.cglib.proxy.Enhancer、MethodInterceptor |
二、动态代理的「动态」本质
「动态」体现在运行时动态生成代理类,而非编译期手动编写:
- 编译期:仅定义横切逻辑(如
InvocationHandler/MethodInterceptor),无需为每个目标类写代理类; - 运行时:
- JDK 代理:根据目标接口和
InvocationHandler,动态生成字节码并加载为代理类; - CGLIB 代理:动态生成目标类的子类,重写目标方法并植入横切逻辑;
- JDK 代理:根据目标接口和
- 核心优势:无论多少个目标对象,只需一套横切逻辑,即可动态生成代理,无需重复编码。
三、为 100 个对象动态代理的实战方案
核心思路:统一规则 + 批量创建 + 容器管理,以 Spring 环境为例:
方案 1:Spring AOP 原生批量代理(推荐)
利用 Spring AOP 的「切点表达式」匹配 100 个对象的类 / 方法,自动生成代理:
java
// 1. 定义切面类(横切逻辑)
@Aspect
@Component
public class CommonAspect {
// 切点:匹配com.example.service包下所有类的所有方法(覆盖100个对象)
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// 前置通知(横切逻辑)
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) {
System.out.println("日志:执行方法" + joinPoint.getSignature().getName());
}
}
// 2. 配置开启AOP(SpringBoot自动开启,XML需<aop:aspectj-autoproxy/>)
- 原理:Spring 容器初始化时,扫描匹配
@Pointcut的 Bean,自动为其生成动态代理,无需手动创建代理对象; - 优势:零手动代理代码,Spring 容器统一管理 100 个代理对象。
方案 2:手动批量创建代理(非 Spring 环境)
java
public class ProxyFactory {
// 横切逻辑(统一的InvocationHandler)
private static final InvocationHandler COMMON_HANDLER = (proxy, method, args) -> {
// 前置逻辑
System.out.println("日志:执行方法" + method.getName());
// 执行目标方法
Object result = method.invoke(target, args);
// 后置逻辑
System.out.println("日志:方法执行完成");
return result;
};
// 批量创建JDK代理
public static Map<String, Object> createProxyBatch(List<Object> targetList) {
Map<String, Object> proxyMap = new HashMap<>();
for (Object target : targetList) {
// 动态生成代理对象
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
COMMON_HANDLER
);
proxyMap.put(target.getClass().getName(), proxy);
}
return proxyMap; // 返回100个代理对象
}
}
关键补充
- 性能优化:CGLIB 默认缓存生成的代理类,JDK 代理也可通过缓存避免重复生成;
- 混合场景:100 个对象中既有有接口类也有无接口类,Spring AOP 会自动选择 JDK/CGLIB 代理。
12. 是否用过 maven install、maven test;git(make install 是安装本地 jar 包)
深度解析
核心考点
考察 Maven 和 Git 的核心命令使用经验,以及「本地 jar 包安装」的实操,是后端开发必备的工程化技能。
核心拆解
- Maven 命令:区分
test(测试)、install(编译 + 测试 + 打包 + 安装到本地仓库)的用途; - 本地 jar 包安装:
mvn install:install-file是核心,解决第三方 jar 包无法从中央仓库下载的问题; - Git 核心:日常开发的提交、分支、合并、拉取等操作,体现工程化协作能力。
实战易错点
- 混淆
mvn install和mvn deploy:install安装到本地仓库,deploy部署到远程仓库; - 本地 jar 包安装时参数错误:groupId/artifactId/version 需与项目依赖一致,否则无法引用。
标准答案
一、Maven 核心命令使用经验
1. mvn test
- 用途:执行项目中的单元测试(JUnit/TestNG),编译测试代码并运行测试用例;
- 实战场景:开发完成后,先执行
mvn test验证代码逻辑,避免提交有测试失败的代码; - 关键补充:可通过
-Dtest=TestClassName#methodName指定单个测试类 / 方法,提升效率。
2. mvn install
- 用途:执行「编译→测试→打包→安装到本地 Maven 仓库」全流程;
- 核心价值:
- 本地仓库生成 jar/war 包,供本地其他项目依赖;
- 打包后的文件存于
target目录,安装后的文件存于~/.m2/repository;
- 与
mvn package的区别:package仅打包到target,不安装到本地仓库。
3. 本地 jar 包安装(解决第三方 jar 无法下载)
当需要引用非中央仓库的 jar 包(如自研组件、定制化 jar),使用mvn install:install-file:
bash
# 核心命令
mvn install:install-file \
-Dfile=xxx.jar \ # 本地jar包路径
-DgroupId=com.example \ # 依赖的groupId
-DartifactId=xxx \ # 依赖的artifactId
-Dversion=1.0.0 \ # 版本号
-Dpackaging=jar # 打包类型
- 安装后,项目中可直接通过
<dependency>引用该 jar 包。
二、Git 使用经验(补充)
日常开发核心操作:
- 代码拉取:
git clone/git pull; - 分支管理:
git checkout -b feature/xxx(创建特性分支)、git merge(合并分支); - 代码提交:
git add .→git commit -m "xxx"→git push; - 版本回退:
git reset --hard 提交ID(本地回退)、git revert(远程回退,保留提交记录); - 冲突解决:
git merge后手动解决冲突,再git add→git commit。
关键补充
make install是 Linux 下的编译安装命令,与 Maven 无关,需注意区分;- Maven 常用参数:
-DskipTests(跳过测试)、-U(强制更新快照依赖)。
13. Tomcat 的各种配置,如何配置 docBase
深度解析
核心考点
考察 Tomcat 核心配置(server.xml/web.xml/context.xml)、docBase的作用及配置方式,是后端运维的基础题。
核心拆解
- Tomcat 配置分层:全局配置(server.xml)→ 应用配置(context.xml)→ 项目配置(web.xml);
docBase的核心作用:指定 Web 应用的「实际文件路径」,可脱离 Tomcat 默认目录部署项目;- 配置易错点:
docBase路径书写错误、权限不足导致项目无法访问。
实战意义
docBase是 Tomcat 灵活部署的核心,常用于「项目文件与 Tomcat 分离部署」「多域名映射不同项目」等场景。
标准答案
一、Tomcat 核心配置文件
| 配置文件 | 作用 | 核心配置项 |
|---|---|---|
| conf/server.xml | Tomcat 全局配置(端口、连接器、引擎、主机) | <Connector>(端口、协议)、<Engine>(默认主机)、<Host>(域名、应用部署) |
| conf/web.xml | 全局 Web 应用配置(MIME 类型、默认 servlet、错误页面) | <mime-mapping>、<servlet>、<error-page> |
| conf/context.xml | 全局应用上下文配置(数据源、资源链接) | <Resource>(数据库连接池)、<ResourceLink> |
| webapps / 项目 / WEB-INF/web.xml | 项目专属配置(servlet、filter、listener) | 项目级 servlet、filter 映射 |
二、docBase 的配置(核心)
1. docBase 的作用
指定 Web 应用的「实际物理文件路径」,Tomcat 通过docBase找到项目的静态资源、WEB-INF 等目录,支持绝对路径和相对路径。
2. 配置方式(按优先级排序)
方式 1:server.xml 中<Host>下配置(全局生效)
适用于多应用部署、域名映射,修改conf/server.xml的<Host>节点:
XML
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<!-- 配置docBase:映射域名到指定目录 -->
<Context path="/myapp" <!-- 访问路径:http://localhost:8080/myapp -->
docBase="/opt/myapp" <!-- 项目实际路径(绝对路径) -->
reloadable="true" <!-- 热部署:修改项目文件自动重启 -->
debug="0"
privileged="true"/>
</Host>
path:访问路径(为空则是根应用,http://localhost:8080/);docBase:项目实际路径(绝对路径推荐,避免相对路径问题);reloadable="true":开发环境开启,生产环境关闭(影响性能)。
方式 2:context.xml 配置(全局 / 项目级)
- 全局:修改
conf/context.xml,所有应用生效; - 项目级:在项目
META-INF/context.xml中配置:
XML
<Context docBase="/opt/myapp" reloadable="false">
<!-- 其他配置(如数据源) -->
</Context>
方式 3:单独创建 XML 文件(推荐,不修改 server.xml)
在conf/Catalina/localhost下创建myapp.xml(文件名 = 访问路径):
XML
<Context docBase="/opt/myapp" reloadable="false"/>
- 访问路径:http://localhost:8080/myapp;
- 优势:无需重启 Tomcat,新增 / 修改 XML 文件即可生效,避免修改 server.xml 导致全局风险。
三、关键注意事项
docBase路径权限:Tomcat 进程需有该目录的读权限,否则报 404/500;- 相对路径:
docBase="../myapp"表示 Tomcat 根目录的上级目录下的 myapp; - 生产环境:
reloadable="false"(关闭热部署),autoDeploy="false"(关闭自动部署),提升性能。
14. Spring 的 Bean 配置的几种方式
深度解析
核心考点
考察 Spring Bean 的配置演进(XML→注解→JavaConfig)、不同方式的适用场景,是 Spring 核心基础题。
核心拆解
- 配置方式演进:XML(早期)→ 注解(简化)→ JavaConfig(纯代码,类型安全);
- 适用场景:XML 适用于多环境配置分离,注解适用于快速开发,JavaConfig 适用于复杂配置(如条件化 Bean);
- 实战易错点:注解扫描范围过大导致 Bean 重复创建,JavaConfig 与 XML 混合配置时的优先级问题。
标准答案
Spring Bean 的配置方式主要有 4 种,按主流程度排序:
方式 1:注解配置(最主流,SpringBoot 默认)
通过注解标记类为 Bean,Spring 自动扫描并创建实例。
核心注解
@Component:通用 Bean 注解(标注在类上);@Controller/@Service/@Repository:@Component的衍生注解,分别用于控制层 / 服务层 / 持久层;@Autowired:依赖注入(按类型),配合@Qualifier按名称注入;@Configuration:标记配置类(替代 XML)。
配置示例
java
// 1. 标记Bean
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
}
// 2. 开启注解扫描(SpringBoot自动开启,XML需<context:component-scan base-package="com.example"/>)
@SpringBootApplication // 包含@ComponentScan,扫描当前包及子包
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
方式 2:JavaConfig 配置(纯代码,类型安全)
通过@Configuration+@Bean注解,用代码替代 XML 配置,适用于复杂 Bean(如数据源、线程池)。
java
// 配置类
@Configuration
public class BeanConfig {
// 手动创建Bean,方法名=Bean名称
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setUrl("jdbc:mysql://localhost:3306/test");
ds.setUsername("root");
return ds;
}
// 依赖其他Bean(直接调用方法)
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
方式 3:XML 配置(传统方式,适用于老项目)
在applicationContext.xml中配置 Bean,适用于多环境配置分离。
XML
<!-- 1. 配置普通Bean -->
<bean id="userService" class="com.example.service.UserService">
<!-- 依赖注入 -->
<property name="userMapper" ref="userMapper"/>
</bean>
<bean id="userMapper" class="com.example.mapper.UserMapper"/>
<!-- 2. 配置复杂Bean(数据源) -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
</bean>
方式 4:混合配置(XML + 注解 + JavaConfig)
适用于老项目改造,可通过@ImportResource引入 XML,@Import引入 JavaConfig:
java
@Configuration
@ImportResource("classpath:applicationContext.xml") // 引入XML配置
@Import(BeanConfig.class) // 引入其他JavaConfig
public class MixConfig {
// 自定义Bean
}
关键补充
- Bean 的作用域:默认
singleton(单例),可通过@Scope("prototype")/<bean scope="prototype">改为原型; - 懒加载:
@Lazy/<bean lazy-init="true">,Bean 在首次使用时创建。
15. web.xml 的配置
深度解析
核心考点
考察 web.xml 的核心配置项(servlet、filter、listener、错误页面)、配置顺序及作用,是 JavaWeb 基础题。
核心拆解
- web.xml 是 Web 应用的部署描述文件,定义了应用的核心组件和行为;
- 配置顺序:
context-param→listener→filter→servlet(SpringMVC 的DispatcherServlet是核心); - 实战意义:即使 SpringBoot 简化了配置,理解 web.xml 仍能排查老项目的部署问题。
标准答案
一、web.xml 的核心作用
web.xml(部署描述符)是 JavaEE 规范的核心配置文件,位于WEB-INF目录下,定义 Web 应用的:
- 全局上下文参数;
- 监听器(Listener);
- 过滤器(Filter);
- Servlet(核心处理组件);
- 错误页面、MIME 类型、会话超时等。
二、核心配置项及示例
XML
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
version="4.0">
<!-- 1. 全局上下文参数(Spring配置文件路径) -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- 2. 监听器(Spring容器初始化) -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 3. 过滤器(编码过滤、权限过滤) -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<!-- 过滤器映射 -->
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 所有请求都经过该过滤器 -->
</filter-mapping>
<!-- 4. Servlet(SpringMVC核心DispatcherServlet) -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup> <!-- 启动时加载,优先级1(数字越小优先级越高) -->
</servlet>
<!-- Servlet映射 -->
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern> <!-- 所有请求交给DispatcherServlet处理 -->
</servlet-mapping>
<!-- 5. 会话超时(单位:分钟) -->
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<!-- 6. 错误页面配置 -->
<error-page>
<error-code>404</error-code>
<location>/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/500.html</location>
</error-page>
</web-app>
三、关键配置规则
- 配置顺序:
context-param→listener→filter→servlet(违反顺序可能导致组件初始化失败); load-on-startup:Servlet 启动优先级,数字越小越先加载,DispatcherServlet通常设为 1;- SpringBoot 中:web.xml 被
@SpringBootApplication+ 自动配置替代,但可通过@ServletComponentScan扫描 Servlet/Filter/Listener。
16. Spring 的监听器
深度解析
核心考点
考察 Spring 监听器的核心原理(观察者模式)、自定义监听器的实现,以及 Spring 内置监听器的用途,是 Spring 事件驱动的基础题。
核心拆解
- 监听器本质:观察者模式,实现「事件发布 - 监听」的解耦;
- Spring 事件体系:
ApplicationEvent(事件)→ApplicationListener(监听器)→ApplicationEventPublisher(发布者); - 实战场景:系统启动完成后初始化数据、配置变更通知、业务事件监听(如订单创建后发送短信)。
标准答案
一、Spring 监听器的核心原理
Spring 监听器基于「观察者模式」实现,核心组件:
| 组件 | 作用 | 示例 |
|---|---|---|
| ApplicationEvent | 事件基类,所有自定义事件需继承它 | ContextRefreshedEvent(容器刷新完成)、ContextClosedEvent(容器关闭) |
| ApplicationListener | 监听器接口,实现onApplicationEvent()方法处理事件 |
自定义监听器需实现该接口或用@EventListener注解 |
| ApplicationEventPublisher | 事件发布者,通过publishEvent()发布事件 |
Spring 容器(ApplicationContext)实现该接口 |
二、Spring 内置监听器(常用)
| 内置事件 | 触发时机 | 用途 |
|---|---|---|
| ContextRefreshedEvent | Spring 容器刷新完成(所有 Bean 初始化完毕) | 系统启动后初始化数据、加载缓存 |
| ContextStartedEvent | 容器启动(调用start()) |
重启组件(如线程池) |
| ContextClosedEvent | 容器关闭(调用close()) |
释放资源(如关闭连接池、线程池) |
| RequestHandledEvent | Web 请求处理完成(SpringMVC) | 记录请求日志、统计接口耗时 |
三、自定义监听器(两种方式)
方式 1:实现 ApplicationListener 接口
java
// 1. 自定义事件
public class OrderCreateEvent extends ApplicationEvent {
private Long orderId;
public OrderCreateEvent(Object source, Long orderId) {
super(source);
this.orderId = orderId;
}
// getter/setter
}
// 2. 自定义监听器
@Component // 交给Spring管理
public class OrderCreateListener implements ApplicationListener<OrderCreateEvent> {
@Override
public void onApplicationEvent(OrderCreateEvent event) {
// 处理事件:如发送短信、更新库存
System.out.println("订单创建成功,订单ID:" + event.getOrderId());
}
}
// 3. 发布事件
@Service
public class OrderService {
@Autowired
private ApplicationContext context; // 事件发布者
public void createOrder(Long orderId) {
// 业务逻辑:创建订单
// 发布事件
context.publishEvent(new OrderCreateEvent(this, orderId));
}
}
方式 2:@EventListener 注解(推荐,简化代码)
无需实现接口,直接在方法上标注注解:
java
@Component
public class OrderListener {
// 监听OrderCreateEvent事件
@EventListener
public void handleOrderCreate(OrderCreateEvent event) {
System.out.println("注解式监听:订单ID=" + event.getOrderId());
}
// 条件监听:仅处理订单ID>100的事件
@EventListener(condition = "#event.orderId > 100")
public void handleLargeOrder(OrderCreateEvent event) {
System.out.println("大额订单预警:" + event.getOrderId());
}
}
关键补充
- 异步监听:在
@EventListener上添加@Async,实现异步处理事件(需开启@EnableAsync); - 事件传播:Spring 事件默认同步执行,异步监听需注意异常处理(避免事件处理失败影响主流程)。
17. Zookeeper 的实现机制,有缓存,如何存储注册服务的
深度解析
核心考点
考察 ZooKeeper 的核心原理(数据模型、一致性、Watcher)、服务注册的存储方式,以及缓存机制,是分布式架构的高频题。
核心拆解
- ZooKeeper 本质:分布式协调服务,基于「树形节点(ZNode)」存储数据,保证强一致性;
- 服务注册存储:将服务信息存储在临时节点 / 持久节点,配合 Watcher 实现服务发现;
- 缓存机制:客户端本地缓存 ZNode 数据,减少网络请求,通过 Watcher 更新缓存。
实战易错点
- 混淆临时节点和持久节点:服务注册常用临时节点(服务宕机自动删除);
- 忽略 Watcher 的一次性:Watcher 触发后需重新注册,否则无法接收后续变更。
标准答案
一、ZooKeeper 核心实现机制
1. 数据模型
ZooKeeper 采用「树形文件系统」结构,每个节点称为 ZNode,核心特性:
- 节点类型:
- 持久节点(PERSISTENT):创建后永久存在,除非手动删除;
- 临时节点(EPHEMERAL):与客户端会话绑定,会话失效自动删除(服务注册核心);
- 顺序节点(SEQUENTIAL):节点名自动加序号(如
/service/order0000000001)。
- 数据特性:每个 ZNode 存储少量数据(≤1MB),适合存储配置、服务地址等元数据。
2. 一致性机制
基于 ZAB 协议(ZooKeeper Atomic Broadcast),保证分布式一致性:
- 角色:Leader(主节点,处理写请求)、Follower(从节点,处理读请求,同步 Leader 数据)、Observer(观察者,仅同步数据,不参与投票);
- 写请求:所有写请求转发给 Leader,Leader 广播提案,半数以上 Follower 确认后提交;
- 读请求:直接由 Follower/Observer 处理,保证最终一致性(读可能获取旧数据)。
3. Watcher 机制(事件监听)
- 客户端注册 Watcher 到 ZNode,节点变更时 ZooKeeper 推送事件给客户端;
- 特性:一次性(触发后需重新注册)、异步通知、轻量级。
4. 客户端缓存
- ZooKeeper 客户端会缓存已访问的 ZNode 数据,减少网络请求;
- 缓存更新:通过 Watcher 机制,节点变更时客户端接收事件,更新本地缓存;
- 优势:提升读性能,降低服务端压力。
二、ZooKeeper 存储注册服务的流程(服务注册与发现)
以 Dubbo 为例,核心流程:
1. 服务提供者(注册)
服务启动
创建临时节点:/dubbo/com.example.UserService/providers/ip:port
存储服务元数据(协议、端口、权重)
维持会话,节点随会话存活
bash
graph TD
A[服务启动] --> B[创建临时节点:/dubbo/com.example.UserService/providers/ip:port]
B --> C[存储服务元数据(协议、端口、权重)]
C --> D[维持会话,节点随会话存活]
服务启动
创建临时节点:/dubbo/com.example.UserService/providers/ip:port
存储服务元数据(协议、端口、权重)
维持会话,节点随会话存活
- 节点路径示例:
/dubbo/com.example.UserService/providers/192.168.1.10:20880; - 临时节点:服务宕机→会话失效→节点自动删除,实现服务下线自动感知。
2. 服务消费者(发现)
监听/dubbo/com.example.UserService/providers节点
获取所有子节点(服务地址),缓存到本地
节点变更(新增/删除)→Watcher触发→更新本地缓存
从缓存中选择服务地址调用
graph TD
A[启动] --> B[监听/dubbo/com.example.UserService/providers节点]
B --> C[获取所有子节点(服务地址),缓存到本地]
C --> D[节点变更(新增/删除)→Watcher触发→更新本地缓存]
D --> E[从缓存中选择服务地址调用]
启动
监听/dubbo/com.example.UserService/providers节点
获取所有子节点(服务地址),缓存到本地
节点变更(新增/删除)→Watcher触发→更新本地缓存
从缓存中选择服务地址调用
3. 核心配置示例(伪代码)
java
// 服务注册
public void registerService(String serviceName, String address) {
String path = "/service/" + serviceName + "/" + address;
// 创建临时节点,存储服务元数据
zkClient.createEphemeral(path, "protocol=dubbo&port=20880");
}
// 服务发现
public List<String> discoverService(String serviceName) {
String path = "/service/" + serviceName;
// 注册Watcher,监听子节点变更
zkClient.registerWatcher(path, (event) -> {
// 节点变更,更新本地缓存
updateServiceCache(serviceName);
});
// 从缓存获取服务地址
return serviceCache.get(serviceName);
}
关键补充
- 服务注册优化:使用顺序临时节点避免节点名冲突;
- 缓存失效:客户端可定时刷新缓存,或通过 Watcher 保证实时性;
- 高可用:ZooKeeper 集群(奇数个节点,推荐 3/5 个),避免单点故障。
18. IO 会阻塞吗?readLine 是不是阻塞的
深度解析
核心考点
考察 Java IO 的阻塞特性、readLine()的阻塞机制,以及阻塞 IO 与非阻塞 IO 的区别,是 IO 编程的基础题。
核心拆解
- IO 分类:阻塞 IO(BIO)、非阻塞 IO(NIO)、异步 IO(AIO);
readLine()的阻塞本质:无数据时等待,直到读到换行符 / 流结束 / 异常;- 实战意义:阻塞 IO 易导致线程池耗尽,需结合线程池或 NIO 优化。
标准答案
一、IO 是否会阻塞?
大部分传统 IO(BIO)是阻塞的,NIO/AIO 可实现非阻塞:
| IO 类型 | 阻塞特性 | 核心类 | 适用场景 |
|---|---|---|---|
| 阻塞 IO(BIO) | 读写操作无数据时,线程阻塞等待 | InputStream/OutputStream、Socket/ServerSocket |
简单场景、低并发 |
| 非阻塞 IO(NIO) | 读写操作无数据时,立即返回,不阻塞 | Selector/Channel/Buffer |
高并发、网络编程(如 Netty) |
| 异步 IO(AIO) | 读写操作异步执行,完成后回调 | AsynchronousSocketChannel |
大文件读写、高延迟场景 |
二、readLine () 是否阻塞?
BufferedReader.readLine() 是阻塞方法,核心特性:
- 阻塞触发条件:
- 流中无数据时,线程阻塞,直到有数据可读;
- 读到换行符(
\n/\r\n)或流结束(EOF)时,返回读取的字符串; - 流关闭 / 异常时,返回
null。
- 示例验证:
java
public class ReadLineTest {
public static void main(String[] args) throws IOException {
// 读取控制台输入(无输入时,readLine()阻塞)
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = br.readLine(); // 此处阻塞,直到输入回车
System.out.println("输入内容:" + line);
}
}
- 非阻塞改造:需结合 NIO 的
Selector和SocketChannel,避免readLine()的阻塞:
java
// NIO非阻塞读取
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false); // 设置为非阻塞
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer); // 无数据时返回0,不阻塞
关键补充
- 阻塞 IO 的问题:高并发下线程数过多,导致线程上下文切换频繁,性能下降;
- 解决方案:使用 NIO(如 Netty)、线程池(控制线程数)、设置超时时间(如
Socket.setSoTimeout())。
19. 用过 spring 的线程池还是 java 的线程池?
深度解析
核心考点
考察 Java 原生线程池与 Spring 线程池的区别、使用场景,以及实战中的选择思路,是并发编程的高频题。
核心拆解
- Java 原生线程池:
ThreadPoolExecutor(核心),灵活但需手动配置; - Spring 线程池:
ThreadPoolTaskExecutor(封装原生线程池),集成 Spring 生态(如注解@Async); - 选择原则:简单场景用 Spring 线程池(注解简化),复杂场景用原生线程池(精细控制)。
标准答案
一、Java 原生线程池(基础)
1. 核心类
ThreadPoolExecutor:线程池核心实现,构造参数控制核心线程数、最大线程数、空闲时间等;- 工具类:
Executors(创建常用线程池,如newFixedThreadPool/newCachedThreadPool)。
2. 实战示例
java
// 手动创建线程池(推荐,避免Executors的默认参数陷阱)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, // 空闲线程存活时间(秒)
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 提交任务
executor.submit(() -> {
// 业务逻辑
System.out.println("原生线程池执行任务");
});
// 关闭线程池(优雅关闭)
executor.shutdown();
二、Spring 线程池(封装与简化)
1. 核心类
ThreadPoolTaskExecutor:Spring 对ThreadPoolExecutor的封装,支持 XML / 注解配置;@Async:注解标记方法异步执行,自动提交到 Spring 线程池。
2. 实战示例
步骤 1:配置线程池
java
@Configuration
@EnableAsync // 开启异步注解
public class ThreadPoolConfig {
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(100); // 任务队列容量
executor.setKeepAliveSeconds(60); // 空闲线程存活时间
executor.setThreadNamePrefix("spring-async-"); // 线程名前缀
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
executor.initialize(); // 初始化
return executor;
}
}
步骤 2:使用 @Async 注解
java
@Service
public class AsyncService {
// 指定线程池名称
@Async("taskExecutor")
public void asyncTask() {
System.out.println("Spring线程池执行任务:" + Thread.currentThread().getName());
}
}
三、使用经验与选择
- 优先用 Spring 线程池的场景 :
- Spring 项目(如 SpringBoot);
- 简单异步任务(如短信发送、日志记录);
- 需快速开发,减少手动配置。
- 优先用 Java 原生线程池的场景 :
- 非 Spring 项目;
- 复杂并发场景(如自定义任务队列、拒绝策略);
- 需精细控制线程池生命周期(如动态调整核心线程数)。
- 核心注意事项 :
- 避免使用
Executors创建线程池(默认队列无界,易 OOM); - 线程池必须手动关闭(Spring 容器关闭时自动关闭
ThreadPoolTaskExecutor); - 拒绝策略需根据业务选择(如
CallerRunsPolicy适合核心业务,AbortPolicy适合非核心业务)。
- 避免使用
20. 字符串的格式化方法
深度解析
核心考点
考察 Java 字符串格式化的常用方法(String.format()、System.out.printf()、MessageFormat),是基础语法题,需体现实战选择思路。
核心拆解
- 基础格式化:
String.format()(最常用),支持占位符(% s/% d/% f); - 高级格式化:
MessageFormat(支持重复占位符、本地化); - 实战场景:日志输出、动态拼接字符串、国际化文案。
标准答案
Java 字符串格式化主要有 3 种方法,按常用程度排序:
方法 1:String.format ()(最主流)
基于 C 语言printf风格,支持各种类型的占位符,返回格式化后的字符串。
核心占位符
| 占位符 | 类型 | 示例 |
|---|---|---|
| %s | 字符串 | String.format("姓名:%s", "张三") → 姓名:张三 |
| %d | 整数 | String.format("年龄:%d", 20) → 年龄:20 |
| %f | 浮点数 | String.format("金额:%.2f", 99.9) → 金额:99.90 |
| %t | 日期时间 | String.format("时间:%tF", new Date()) → 时间:2026-03-11 |
| %n | 换行符 | 跨平台换行(替代\n) |
示例
java
// 基础格式化
String info = String.format("用户:%s,年龄:%d,余额:%.2f", "李四", 25, 1000.5);
System.out.println(info); // 输出:用户:李四,年龄:25,余额:1000.50
// 数字格式化(补零、对齐)
String num = String.format("编号:%06d", 123); // 补零到6位
System.out.println(num); // 输出:编号:000123
方法 2:System.out.printf ()(控制台输出专用)
与String.format()语法一致,直接输出到控制台,无需手动打印。
java
System.out.printf("订单ID:%s,金额:%.2f%n", "ORDER123", 599.0);
// 输出:订单ID:ORDER123,金额:599.00
方法 3:MessageFormat.format ()(高级格式化)
支持重复占位符、本地化、数字 / 日期格式化,适合复杂场景。
java
// 重复占位符
String msg = MessageFormat.format("你好{0},你已下单{1}件商品,总价{2,number,###,###.00}元",
"王五", 3, 1299.9);
System.out.println(msg); // 输出:你好王五,你已下单3件商品,总价1,299.90元
// 本地化日期
MessageFormat mf = new MessageFormat("日期:{0,date,yyyy年MM月dd日}");
String date = mf.format(new Object[]{new Date()});
System.out.println(date); // 输出:日期:2026年03月11日
关键补充
- 性能:
String.format()底层用Formatter,频繁格式化建议用StringBuilder拼接; - 占位符顺序:
String.format()的占位符按顺序匹配,MessageFormat支持按索引重复使用; - 空值处理:
%s可格式化 null 为 "null",需手动处理空值避免脏数据。
总结(核心要点回顾)
- AOP 与动态代理:底层是 JDK/CGLIB 动态代理,批量代理靠统一切点 / 工厂模式,Spring AOP 自动扫描匹配 Bean;
- Maven/Git :
mvn install安装到本地仓库,mvn test执行单元测试,本地 jar 包用install:install-file安装; - Tomcat 配置 :
docBase指定项目实际路径,推荐在conf/Catalina/localhost创建 XML 配置; - Spring Bean 配置:注解(主流)、JavaConfig(类型安全)、XML(老项目),可混合使用;
- ZooKeeper 服务注册:存储在临时节点,配合 Watcher 实现服务发现,客户端缓存减少网络请求;
- IO 阻塞 :BIO 阻塞,NIO 非阻塞,
readLine()是阻塞方法; - 线程池 :Spring
ThreadPoolTaskExecutor(注解简化),JavaThreadPoolExecutor(精细控制); - 字符串格式化 :
String.format()最常用,MessageFormat适合复杂场景