程序员面试经典问题解答:java篇-2

11. AOP 的底层实现,动态代理是如何动态,假如有 100 个对象,如何动态的为这 100 个对象代理

深度解析

核心考点

考察 AOP 的核心实现原理(动态代理)、「动态」的本质,以及批量代理的实战思路,是 Spring 核心原理的高频题。

核心拆解
  • AOP 的本质:横切逻辑与业务逻辑解耦,底层依赖动态代理实现;
  • 动态代理的「动态」:运行时生成代理类,而非编译期硬编码,无需为每个目标类编写代理类;
  • 批量代理的关键:统一的代理规则(如基于接口 / 类的匹配规则)+ 工厂模式 / 容器管理。
实战易错点
  • 混淆 JDK 动态代理和 CGLIB 的适用场景:JDK 代理要求目标类实现接口,CGLIB 可代理无接口类;
  • 批量代理时忽略性能:100 个对象代理需控制代理创建频率,避免重复生成代理类。

标准答案

一、AOP 的底层实现(动态代理)

AOP(面向切面编程)的核心是「在不修改业务代码的前提下,为方法增加横切逻辑(如日志、事务、权限)」,底层依赖两种动态代理方式:

表格

代理方式 实现原理 适用场景 核心类
JDK 动态代理 基于接口,运行时通过Proxy.newProxyInstance()生成代理类(继承Proxy,实现目标接口) 目标类实现了接口 java.lang.reflect.ProxyInvocationHandler
CGLIB 动态代理 基于继承,运行时通过 ASM 字节码框架生成目标类的子类作为代理类 目标类无接口 org.springframework.cglib.proxy.EnhancerMethodInterceptor
二、动态代理的「动态」本质

「动态」体现在运行时动态生成代理类,而非编译期手动编写:

  1. 编译期:仅定义横切逻辑(如InvocationHandler/MethodInterceptor),无需为每个目标类写代理类;
  2. 运行时:
    • JDK 代理:根据目标接口和InvocationHandler,动态生成字节码并加载为代理类;
    • CGLIB 代理:动态生成目标类的子类,重写目标方法并植入横切逻辑;
  3. 核心优势:无论多少个目标对象,只需一套横切逻辑,即可动态生成代理,无需重复编码。
三、为 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 installmvn deployinstall安装到本地仓库,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 使用经验(补充)

日常开发核心操作:

  1. 代码拉取:git clone/git pull
  2. 分支管理:git checkout -b feature/xxx(创建特性分支)、git merge(合并分支);
  3. 代码提交:git add .git commit -m "xxx"git push
  4. 版本回退:git reset --hard 提交ID(本地回退)、git revert(远程回退,保留提交记录);
  5. 冲突解决:git merge后手动解决冲突,再git addgit 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 导致全局风险。
三、关键注意事项
  1. docBase路径权限:Tomcat 进程需有该目录的读权限,否则报 404/500;
  2. 相对路径:docBase="../myapp" 表示 Tomcat 根目录的上级目录下的 myapp;
  3. 生产环境: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-paramlistenerfilterservlet(SpringMVC 的DispatcherServlet是核心);
  • 实战意义:即使 SpringBoot 简化了配置,理解 web.xml 仍能排查老项目的部署问题。

标准答案

一、web.xml 的核心作用

web.xml(部署描述符)是 JavaEE 规范的核心配置文件,位于WEB-INF目录下,定义 Web 应用的:

  1. 全局上下文参数;
  2. 监听器(Listener);
  3. 过滤器(Filter);
  4. Servlet(核心处理组件);
  5. 错误页面、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>
三、关键配置规则
  1. 配置顺序:context-paramlistenerfilterservlet(违反顺序可能导致组件初始化失败);
  2. load-on-startup:Servlet 启动优先级,数字越小越先加载,DispatcherServlet通常设为 1;
  3. 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/OutputStreamSocket/ServerSocket 简单场景、低并发
非阻塞 IO(NIO) 读写操作无数据时,立即返回,不阻塞 Selector/Channel/Buffer 高并发、网络编程(如 Netty)
异步 IO(AIO) 读写操作异步执行,完成后回调 AsynchronousSocketChannel 大文件读写、高延迟场景
二、readLine () 是否阻塞?

BufferedReader.readLine() 是阻塞方法,核心特性:

  1. 阻塞触发条件:
    • 流中无数据时,线程阻塞,直到有数据可读;
    • 读到换行符(\n/\r\n)或流结束(EOF)时,返回读取的字符串;
    • 流关闭 / 异常时,返回null
  2. 示例验证:
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);
    }
}
  1. 非阻塞改造:需结合 NIO 的SelectorSocketChannel,避免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());
    }
}
三、使用经验与选择
  1. 优先用 Spring 线程池的场景
    • Spring 项目(如 SpringBoot);
    • 简单异步任务(如短信发送、日志记录);
    • 需快速开发,减少手动配置。
  2. 优先用 Java 原生线程池的场景
    • 非 Spring 项目;
    • 复杂并发场景(如自定义任务队列、拒绝策略);
    • 需精细控制线程池生命周期(如动态调整核心线程数)。
  3. 核心注意事项
    • 避免使用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",需手动处理空值避免脏数据。

总结(核心要点回顾)

  1. AOP 与动态代理:底层是 JDK/CGLIB 动态代理,批量代理靠统一切点 / 工厂模式,Spring AOP 自动扫描匹配 Bean;
  2. Maven/Gitmvn install安装到本地仓库,mvn test执行单元测试,本地 jar 包用install:install-file安装;
  3. Tomcat 配置docBase指定项目实际路径,推荐在conf/Catalina/localhost创建 XML 配置;
  4. Spring Bean 配置:注解(主流)、JavaConfig(类型安全)、XML(老项目),可混合使用;
  5. ZooKeeper 服务注册:存储在临时节点,配合 Watcher 实现服务发现,客户端缓存减少网络请求;
  6. IO 阻塞 :BIO 阻塞,NIO 非阻塞,readLine()是阻塞方法;
  7. 线程池 :SpringThreadPoolTaskExecutor(注解简化),JavaThreadPoolExecutor(精细控制);
  8. 字符串格式化String.format()最常用,MessageFormat适合复杂场景
相关推荐
senijusene1 小时前
TCP并发服务器:poll和epoll的多路复用
开发语言·php
浅碎时光8071 小时前
Qt (按钮/显示/输入/容器类控件 布局管理器)
开发语言·qt
bubiyoushang8882 小时前
OFDM系统信道估计MATLAB实现(LS、MMSE、DCT、LRMMSE方法)
开发语言·网络·matlab
Felven2 小时前
C. Dora and Search
c语言·开发语言
John Song4 小时前
Python创建虚拟环境的方式对比与区别?
开发语言·python
geovindu4 小时前
python: Bridge Pattern
python·设计模式·桥接模式
搞程序的心海4 小时前
Python面试题(一):5个最常见的Python基础问题
开发语言·python
宝贝儿好7 小时前
【强化学习实战】第十一章:Gymnasium库的介绍和使用(1)、出租车游戏代码详解(Sarsa & Q learning)
人工智能·python·深度学习·算法·游戏·机器学习
程序媛一枚~10 小时前
✨✨✨使用Python,OpenCV及图片拼接生成❤️LOVE❤️字样图,每张小图加随机颜色边框,大图加随机大小随机颜色边框
图像处理·python·opencv·numpy·图像拼接