仿网站工具,wordpress设置视频图片,品牌设计公司是做什么的,购物平台推荐目录
1. AOP定义#xff1f;
2.常见的AOP使用场景#xff1a;
3.Spring AOP用法
3.1 Spring AOP中的几个核心概念
3.1.1 切面、切点、通知、连接点
3.1.2 切点表达式AspectJ
3.2 使用 Spring AOP 的步骤总结
3.2.1 添加依赖:
3.2.2 定义切面和切点#xff08;切点和…目录
1. AOP定义
2.常见的AOP使用场景
3.Spring AOP用法
3.1 Spring AOP中的几个核心概念
3.1.1 切面、切点、通知、连接点
3.1.2 切点表达式AspectJ
3.2 使用 Spring AOP 的步骤总结
3.2.1 添加依赖:
3.2.2 定义切面和切点切点和通知分开写
3.2.3 定义通知
3.2.4 定义切面切点简化写法
3.2.5 测试
3.2.6 执行结果
4.Spring支持哪些事务事务的实现方式和原理?
4.1编程式事务
4.1.1 定义
4.1.2 优缺点
4.2 声明式事务
4.2.1 定义
4.2.2 优缺点
4.2.3 编程式事务与声明式事务区别
4.2.4 声明式事务失效场景★
1. 异常捕获提前处理
2. 抛出受检异常/检查异常
3.Spring只能代理public方法非public方法导致声明式事务失效没有回滚
4.数据库本身不支持事务
5.多线程调用
6.自己调用自己的内部方法导致类没被spring代理从而失效。
4.3 注解式事务
5.面试题 1. AOP定义
AOP称为面向切面编程用于将那些与业务无关但却对多个对象产生影响的公共行为和逻辑抽取并封装为一个可重用的模块这个模块被命名为“切面”Aspect减少系统中的重复代码降低了模块间的耦合度同时提高了系统的可维护性。
2.常见的AOP使用场景
日志记录Logging在方法调用前后记录日志信息用于跟踪方法执行情况、性能监控或调试。权限检查Security/Authorization在方法执行前验证用户是否有权限执行该操作比如角色检查或资源访问控制。事务管理Transaction Management自动管理数据库事务的开启、提交或回滚保证数据的一致性。异常处理Exception Handling集中处理特定类型的异常比如记录异常信息或执行特定的恢复操作。性能监控Performance Monitoring监控方法执行时间帮助识别和优化性能瓶颈。缓存Caching自动缓存方法的返回结果减少不必要的数据库查询或其他耗时操作。参数校验和转换Parameter Validation and Conversion在方法调用前对参数进行校验或转换确保符合业务逻辑要求。API调用统计API Call Tracking记录API的调用次数、频率等用于分析和优化。SLF4J、Logback、Log4j等日志框架集成通过AOP可以在不修改业务代码的情况下灵活地切换或增强日志框架的功能。自定义注解的处理使用AOP拦截带有特定自定义注解的方法实现特定逻辑如标记某个方法需要审计、限流等
3.Spring AOP用法
3.1 Spring AOP中的几个核心概念 3.1.1 切面、切点、通知、连接点
切面: 切面切点通知
切点切点是用来定义哪些连接点会被切面所拦截的表达式。通过切点可以指定特定的类和方法确保切面只在感兴趣的地方应用。
通知
通知是切面在特定连接点执行的操作。主要有几种类型的通知
前置通知Before: 在目标方法执行之前执行。后置通知After: 在目标方法执行之后执行无论成功与否。返回通知AfterReturning: 在目标方法成功执行后执行。异常通知AfterThrowing: 在目标方法抛出异常时执行。环绕通知Around: 包裹目标方法的调用可以在目标方法执行前后自定义逻辑。
连接点这是一个规定的逻辑概念并没有对应的注解和配置连接点是应用程序执行中的一个点可以是方法调用、对象创建等。Spring AOP 主要关注方法执行的连接点。 3.1.2 切点表达式AspectJ
AspectJ 表达式语法
Pointcut(execution(* com.example.springaop.controller.UserController.*(..)))
AspectJ 语法Spring AOP 切点的匹配语法
切点表达式由切点函数组成其中 execution() 是最常⽤的切点函数⽤来匹配⽅法语法为 execution(修饰符返回类型包.类.⽅法(参数)异常) AspectJ ⽀持三种通配符
* 匹配任意字符只匹配⼀个元素包类或⽅法⽅法参数
… 匹配任意字符可以匹配多个元素 在表示类时必须和 * 联合使⽤。 表示按照类型匹配指定类的所有类必须跟在类名后⾯如 com.cad.Car ,表示继承该类的所有⼦类包括本身 修饰符一般省略
public 公共方法*任意
返回值不能省略
void 返回没有值 String 返回值字符串 * 任意 包通常不省略但可以省略
com.gyf.crm 固定包 com.gyf.crm.*.service crm 包下面子包任意例如com.gyf.crm.staff.service com.gyf.crm… crm 包下面的所有子包含自己 com.gyf.crm.*service… crm 包下面任意子包固定目录 serviceservice 目录任意包 类通常不省略但可以省略
UserServiceImpl 指定类
*Impl 以 Impl 结尾
User* 以 User 开头
* 任意 方法名不能省略
addUser 固定方法
add* 以 add 开头
*DO 以 DO 结尾
* 任意 参数
() 无参
(int) 一个整形
(int,int)两个整型
(…) 参数任意
throws可省略一般不写 表达式示例
execution(* com.cad.demo.User.*(…)) 匹配 User 类⾥的所有⽅法 execution(* com.cad.demo.User.*(…)) 匹配该类的⼦类包括该类的所有⽅法 execution(* com.cad..(…)) 匹配 com.cad 包下的所有类的所有⽅法 execution(* com.cad….(…)) 匹配 com.cad 包下、⼦孙包下所有类的所有⽅法 execution(* addUser(String, int)) 匹配 addUser ⽅法且第⼀个参数类型是 String第⼆个参数类型是 int AOP举例如下
3.2 使用 Spring AOP 的步骤总结
3.2.1 添加依赖:
在 pom.xml 中添加 Spring AOP 的相关依赖。
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId
/dependency3.2.2 定义切面和切点切点和通知分开写 使用 Aspect 注解标记切面类。定义切点使用 Before、After、Around 等注解实现通知。
Aspect // 当前类是一个切面
Component
public class UserAspect {// 定义一个切点设置拦截规则Pointcut(execution(* com.example.springaop.controller.UserController.*(..)))public void pointcut() {}
}3.2.3 定义通知
前置通知 Before通知方法会在目标方法调用之前执行后置通知 After通知方法会在目标方法返回或者抛出异常后调用返回之后通知 AfterReturning通知方法会在目标方法返回后调用抛异常后通知AfterThrowing通知方法会在目标方法爬出异常之后调用环绕通知Around通知包裹了被通知的方法在被通知的方法通知之前和调用之后执行自定义的行为
实现通知方法也就是在什么时机执行什么方法
Aspect // 当前类是一个切面
Component
public class UserAspect {// 定义一个切点设置拦截规则Pointcut(execution(* com.example.springaop.controller.UserController.*(..)))public void pointcut() {}// 定义 pointcut 切点的前置通知Before(pointcut())public void doBefore() {System.out.println(执行前置通知);}// 后置通知After(pointcut())public void doAfter() {System.out.println(执行后置通知);}// 返回之后通知AfterReturning(pointcut())public void doAfterReturning() {System.out.println(执行返回之后通知);}// 抛出异常之后通知AfterThrowing(pointcut())public void doAfterThrowing() {System.out.println(执行抛出异常之后通知);}
}3.2.4 定义切面切点简化写法
简化写法就是切点无参构造方法不写简化后如下
Aspect // 当前类是一个切面
Component
public class UserAspect {// 定义 pointcut设置拦截规则和 切点的前置通知Before(execution(* com.example.springaop.controller.UserController.*(..)))public void doBefore() {System.out.println(执行前置通知);}// 定义 pointcut设置拦截规则和 后置通知After(execution(* com.example.springaop.controller.UserController.*(..)))public void doAfter() {System.out.println(执行后置通知);}//定义 pointcut设置拦截规则和 返回之后通知AfterReturning(execution(* com.example.springaop.controller.UserController.*(..)))public void doAfterReturning() {System.out.println(执行返回之后通知);}// 定义 pointcut设置拦截规则和 抛出异常之后通知AfterThrowing(execution(* com.example.springaop.controller.UserController.*(..)))public void doAfterThrowing() {System.out.println(执行抛出异常之后通知);}
}3.2.5 测试
RestController
RequestMapping(/user)
public class UserController {RequestMapping(/sayhi)public String sayHi() {System.out.println(sayhi 方法被执行);int num 10/0;return 你好java;}RequestMapping(/sayhi2)public String sayHi2() {System.out.println(sayhi2 方法被执行);return 你好java2;}
}3.2.6 执行结果
4.Spring支持哪些事务事务的实现方式和原理?
4.1编程式事务
4.1.1 定义
在代码中显式地手动编写事务管理相关代码如开启事务、提交事务、回滚事务等。较为繁琐不推荐。一般是基于底层的API如TransactionDefinition 和 TransactionTemplate 等核心接口使得开发者完全通过编程的方式来进行事务管理。现在项目比较少使用。\
4.1.2 优缺点
优点提供了更精准的控制相比于声明式事务管理粒度更小。缺点开发者需要在代码中手动实现事务的开启、提交、回滚等操作较为繁琐。
4.2 声明式事务
4.2.1 定义
简答
使用 AOP技术在代码中通过配置进行声明从而实现对事务管理的控制。
详答
声明式事务是指使用注解或 XML 配置的方式来控制事务的提交和回滚。
开发者只需要添加配置即可 具体事务的实现由第三方框架例如Spring AOP实现避免我们直接进行事务操作
使用声明式事务可以将事务的控制和业务逻辑分离开来提高代码的可读性和可维护性。
4.2.2 优缺点
优点 不再需要依赖底层API来硬编码对业务代码没有侵入性。 适用于事务边界清晰、事务属性统一的场合譬如最经典的CRUD业务。
缺点 存在粒度问题。其最小粒度要作用在方法上。 存在一些事务失效的情况。 4.2.3 编程式事务与声明式事务区别
- 编程式事务需要手动编写代码来管理事务 - 而声明式事务可以通过配置文件或注解来控制事务。 4.2.4 声明式事务失效场景★
1. 异常捕获提前处理
解决方案在catch中 throw new RuntimeException(e)抛出异常。 举例说明
在Spring框架中声明式事务管理是通过Spring AOP实现的。这意味着Spring会在方法执行前后添加事务控制的逻辑。通常情况下如果一个方法在事务中执行并且抛出了异常Spring的事务管理器会捕捉到这个异常并进行回滚。但是如果方法内部通过try-catch块捕获了异常并且没有将其抛出那么Spring的事务管理器就无法知道需要回滚事务因为从方法的返回来看一切都是正常的。
Transactional
public void someServiceMethod() {// 业务逻辑代码try {// 一些数据库操作可能会抛出异常if (someCondition) {throw new CustomException(Something went wrong);}} catch (CustomException e) {// 异常被捕获并处理没有重新抛出// 处理异常的逻辑}// 其他业务逻辑代码
}
在这个例子中someServiceMethod方法内部有一个try-catch块它会捕获CustomException异常并处理它没有将异常抛出方法外。由于Spring的事务管理是基于异常传播的如果没有异常被抛出Spring就认为业务执行成功不会触发事务回滚。 解决方案
为了确保事务在异常发生时能够回滚我们需要在catch块中将异常重新抛出这样Spring的事务管理器就能捕获到异常并执行回滚操作。我们可以将异常包装成运行时异常RuntimeException抛出因为运行时异常是unchecked exception不需要声明抛出
Transactional
public void someServiceMethod() {// 业务逻辑代码try {// 一些数据库操作可能会抛出异常if (someCondition) {throw new CustomException(Something went wrong);}} catch (CustomException e) {// 异常被捕获并处理但重新抛出RuntimeExceptionthrow new RuntimeException(Error handling business logic, e);}// 其他业务逻辑代码
}
在这个修改后的例子中即使CustomException在方法内部被捕获我们通过抛出一个新的RuntimeException确保了异常能够传播到Spring的事务管理器从而触发事务回滚。 2. 抛出受检异常/检查异常
解决方案在注解上额外配置rollbackFor属性Transactional(rollbackForException.class) 举例说明
在Spring框架中声明式事务管理是通过Transactional注解来实现的。默认情况下Spring的声明式事务仅在遇到运行时异常RuntimeException或错误Error时才会回滚事务。对于受检异常checked exceptions也就是那些需要在方法签名中用throws关键字声明的异常Spring不会自动回滚事务除非你在Transactional注解中指定。
假设我们有一个服务方法它在事务中执行数据库操作并且可能会抛出一个受检异常
Transactional
public void processData() throws CustomCheckedException {// 业务逻辑代码if (someCondition) {throw new CustomCheckedException(Something went wrong);}// 更多业务逻辑代码
}
在这个例子中CustomCheckedException是一个受检异常如果processData方法内部抛出了这个异常Spring的声明式事务默认不会回滚事务因为CustomCheckedException(Something went wrong)不是非受检异常。
解决方案
为了确保在抛出受检异常时事务能够回滚我们可以在Transactional注解中使用rollbackFor属性来指定哪些异常会导致事务回滚。例如我们可以指定当抛出CustomCheckedException时事务应该回滚
Transactional(rollbackFor CustomCheckedException.class)
public void processData() throws CustomCheckedException {// 业务逻辑代码if (someCondition) {throw new CustomCheckedException(Something went wrong);}// 更多业务逻辑代码
}
在这个修改后的例子中当processData方法内部抛出CustomCheckedException时Spring的声明式事务会回滚事务。 通用解决方案
如果你想要让所有异常都导致事务回滚无论是受检异常还是非受检异常你可以使用rollbackFor属性来指定Exception.class作为参数
Transactional(rollbackFor Exception.class)
public void processData() throws CustomCheckedException {// 业务逻辑代码if (someCondition) {throw new CustomCheckedException(Something went wrong);}// 更多业务逻辑代码
}
使用rollbackFor Exception.class意味着任何类型的Exception包括受检和非受检异常都会导致事务回滚。
但是需要注意
使用rollbackFor Exception.class可能会导致隐藏一些不应该回滚的异常因为有些异常可能是预期内的业务逻辑异常不应该触发事务回滚。 3.Spring只能代理public方法非public方法导致声明式事务失效没有回滚
解决方案改为public
举例说明
Spring使用动态代理来实现AOP功能包括声明式事务管理。对于基于接口的代理Spring使用JDK的Proxy类来创建代理对象这要求目标对象必须实现至少一个接口。对于没有接口的类Spring使用CGLIB库来创建代理对象。无论是JDK代理还是CGLIB代理它们都只能代理public方法因为非public方法在代理对象中无法被正确地拦截和转发。 假设我们有一个类BusinessService其中包含一个非public的方法我们尝试在这个方法上使用Transactional注解
public class BusinessService {private void someMethod() {// 业务逻辑代码}
}
在这个例子中someMethod是一个非public方法即使你在这个方法上添加了Transactional注解Spring也不会为其创建代理因为Spring只能为public方法创建代理。这意味着Transactional注解在这个非public方法上不会生效。
为了确保Transactional注解能够生效你需要将方法改为public
public class BusinessService {Transactionalpublic void someMethod() {// 业务逻辑代码}
}
4.数据库本身不支持事务
解决办法手动编写回滚操作或者迁移到支持事务的数据库中。
在关系型数据库中并非所有的数据库都默认支持事务。例如某些数据库如MySQL的某些存储引擎如MyISAM默认不支持事务而其他存储引擎如InnoDB则支持。 5.多线程调用
解决方案可以尝试以下方法
1、调整事务的隔离级别到更高级别。
2、使用乐观锁/悲观锁
3、使用分布式事务管理器 举例说明
Spring的声明式事务是基于AOP代理的而AOP代理通常只对单线程中的直接方法调用有效。当多个线程调用同一个代理对象的方法时Spring的事务管理可能无法正确识别和管理这些线程中的事务。以下是一些解决方案的详细说明
解决方案1. 调整事务的隔离级别到更高级别 事务隔离级别定义了一个事务可能受其他并发事务影响的程度。不同的数据库和JDBC驱动支持不同的事务隔离级别包括READ_UNCOMMITTED读未提交、READ_COMMITTED读已提交、REPEATABLE_READ可重复读和SERIALIZABLE串行化。提高事务隔离级别可以减少并发事务间的冲突但可能会降低系统的并发性能。
Transactional(isolation Isolation.SERIALIZABLE)
public void transfer(Account from, Account to, BigDecimal amount) {from.setBalance(from.getBalance().subtract(amount));to.setBalance(to.getBalance().add(amount));
}
在这个例子中我们将事务的隔离级别设置为SERIALIZABLE这是最高的隔离级别可以防止脏读、不可重复读和幻读但可能会对性能有较大影响。 解决方案2. 使用乐观锁/悲观锁
乐观锁和悲观锁是两种并发控制机制用于处理并发更新数据时可能出现的问题。
乐观锁假设冲突发生的概率很小只在数据提交更新时检查是否有其他事务修改了数据。通常通过在数据表中添加一个版本号或时间戳字段来实现。
Transactional
public void updateWithOptimisticLocking(Entity entity) {// 获取实体数据Entity found entityManager.find(Entity.class, entity.getId());if (found.getVersion() ! entity.getVersion()) {throw new ConcurrentModificationException(Data has been updated by another transaction);}// 更新实体数据found.setVersion(found.getVersion() 1);
}
悲观锁假设冲突发生的概率很高会在事务开始时锁定数据直到事务结束。这通常通过数据库的锁机制实现如行锁或表锁。
Transactional
public void updateWithPessimisticLocking(Entity entity) {// 使用SELECT FOR UPDATE语句锁定数据Entity found entityManager.createQuery(SELECT e FROM Entity e WHERE e.id :id, Entity.class).setParameter(id, entity.getId()).setLockMode(LockModeType.PESSIMISTIC_WRITE).getSingleResult();// 更新实体数据
}
在这个例子中
Transactional注解标记了这个方法是在一个事务中执行的。entityManager.createQuery创建了一个JPA查询用于从数据库中检索Entity实体。SELECT e FROM Entity e WHERE e.id :id是JPA查询的字符串它指定了从Entity实体类中选择与给定id匹配的实体。.setParameter(id, entity.getId())设置了查询参数id的值。.setLockMode(LockModeType.PESSIMISTIC_WRITE)设置了锁模式为悲观写锁这会触发数据库层面的SELECT FOR UPDATE操作。.getSingleResult()执行查询并返回查询结果。
当这个查询执行时数据库会锁定Entity表中对应id的行直到当前事务结束。这意味着如果另一个事务尝试更新或删除这些被锁定的行它将会被阻塞直到第一个事务提交或回滚释放了锁。 解决方案3. 使用分布式事务管理器
在微服务架构或需要跨多个数据库进行事务管理的场景中可能需要使用分布式事务管理器。Spring提供了对 JTAJava Transaction API的支持可以通过JTA实现分布式事务。
TransactionManagement(TransactionManagementType.JTA)
public class SomeService {Transactionalpublic void performSomeTransaction() {// 执行跨多个数据库的操作}
}
在这个例子中我们使用TransactionManagement注解指定了事务管理类型为JTA这样就可以在多个数据库之间管理事务。 6.自己调用自己的内部方法导致类没被spring代理从而失效。
解决方案可以尝试以下方法1、检查事务传播行为。例如可以使用Propagation.REQUIRED传播行为使得内部方法加入到外部方法的事务中保证事务的一致性。 2、考虑异步调用如果内部方法可以异步执行并且事务一致性的要求不高可以将内部方法改为异步调用让其在独立的线程中执行。通过异步调用可以避免事务嵌套导致的死锁或其他并发问题。 3、使用编程式事务控制。
举例说明
在Spring中声明式事务通常是基于代理的这意味着Spring会为被Transactional注解标记的方法创建一个代理对象并在代理对象的方法调用链中添加事务管理逻辑。如果一个类没有被Spring代理那么Transactional注解将不会生效。这种情况可能发生在类自己调用自己的内部方法时因为这些调用不会经过Spring代理因此事务管理逻辑不会被触发。
例如下面这个自己调用自己的内部方法导致类没被spring代理
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;Service
public class AccountService {private AccountRepository accountRepository;public AccountService(AccountRepository accountRepository) {this.accountRepository accountRepository;}// 外部方法被Spring代理拥有声明式事务管理Transactionalpublic void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {// 获取账户信息Account fromAccount accountRepository.findById(fromAccountId);Account toAccount accountRepository.findById(toAccountId);// 内部方法没有被Spring代理没有声明式事务管理deductAmount(fromAccount, amount);addAmount(toAccount, amount);}// 内部方法非public自己调用自己的方法private void deductAmount(Account account, BigDecimal amount) {account.setBalance(account.getBalance().subtract(amount));accountRepository.save(account); // 这里的保存操作不会在Transactional的保护下执行}private void addAmount(Account account, BigDecimal amount) {account.setBalance(account.getBalance().add(amount));accountRepository.save(account); // 这里的保存操作也不会在Transactional的保护下执行}
}
在这个例子中transferMoney方法被Transactional注解标记意味着Spring会为这个方法创建一个代理并在这个方法执行时管理事务。然而deductAmount和addAmount是私有方法它们被transferMoney内部调用。由于这些私有方法不是通过代理调用的因为它们是同一个类内部的直接调用Spring事务管理器不会为这些私有方法的调用添加事务管理逻辑。
这意味着如果在deductAmount或addAmount中的数据库操作失败事务不会回滚因为这些操作没有被Spring的事务管理器所管理。这就导致了事务失效的问题。 如果我把 deductAmount 和 addAmount方法都由private改为public类型,Spring就可以为它们创建代理了吗
答案不会Spring只能代理 public类型的方法但不是所有的puclic方法都会被代理
因为当一个方法在同一类内部被调用时通常使用 this 关键字尽管在代码中不显式地写出 this默认情况下就是通过当前对象调用的。这种方式会导致方法调用不会被 Spring 的代理机制拦截。 以下是一些解决方案的详细说明
解决方案1检查事务传播行为
Spring支持不同的事务传播行为其中Propagation.REQUIRED是最常用的一种。它表示如果当前存在事务那么就加入该事务如果当前没有事务那么就创建一个新的事务。使用Propagation.REQUIRED可以确保即使内部方法被调用它们也能加入到外部方法的事务中。
public class Service {Transactional(propagation Propagation.REQUIRED)public void outerMethod() {// 外部方法的逻辑innerMethod();}Transactional(propagation Propagation.REQUIRED)public void innerMethod() {// 内部方法的逻辑}
}
在这个例子中即使innerMethod是被outerMethod内部调用由于两者都设置了Propagation.REQUIREDinnerMethod会加入到outerMethod的事务中从而保证了事务的一致性。 解决方案2考虑异步调用
如果内部方法可以异步执行并且对事务一致性的要求不高可以将内部方法改为异步调用。这样内部方法会在独立的线程中执行避免了事务嵌套的问题。
public class Service {Transactionalpublic void outerMethod() {// 外部方法的逻辑innerMethodAsync();}public void innerMethodAsync() {// 内部方法的异步逻辑new Thread(() - {// 执行内部方法的逻辑}).start();}
}
在这个例子中innerMethodAsync在一个新的线程中异步执行因此不会受到外部方法outerMethod的事务影响。 解决方案3使用编程式事务控制
编程式事务控制意味着你将手动管理事务的边界而不是依赖于声明式事务。这可以通过使用PlatformTransactionManager来实现。
public class Service {private PlatformTransactionManager transactionManager;public Service(PlatformTransactionManager transactionManager) {this.transactionManager transactionManager;}public void outerMethod() {TransactionDefinition def new DefaultTransactionDefinition();TransactionStatus status transactionManager.getTransaction(def);try {// 外部方法的逻辑innerMethod();transactionManager.commit(status);} catch (Exception e) {transactionManager.rollback(status);throw e;}}public void innerMethod() {// 内部方法的逻辑}
}
在这个例子中我们使用PlatformTransactionManager手动开始和提交事务。这种方法给了你更多的控制权但同时也增加了代码的复杂性。
注意事项
使用Propagation.REQUIRED可以确保内部方法加入到外部方法的事务中但可能会导致事务嵌套需要谨慎处理。异步调用可以避免事务嵌套问题但可能会使得事务管理更加复杂特别是在需要保证数据一致性的情况下。编程式事务控制提供了最大的灵活性但也需要更多的代码和对事务管理的深入理解。 4.3 注解式事务
基于声明式事务管理使用注解Transactional的方式进行事务的管理。 5.面试题