news 2026/10/6 4:09:22

一文搞懂AOP:切面、通知与动态代理原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一文搞懂AOP:切面、通知与动态代理原理

最近好几个准备跳槽的同行跑来问我同一个问题:到底什么是AOP?有些人已经背过了“面向切面编程”这个定义,但真把一段业务代码放到他面前,让他说清楚切面应该切哪里、底层又是怎么把通知织进去的,就含糊了。AOP(Aspect Oriented Programming,面向切面编程)解决的核心问题,就是把日志、权限校验、事务管理、性能统计这类散落在各个业务方法里的公共逻辑,从业务代码中剥离出来,统一放到“切面”里处理。这篇文章我不打算给你堆术语,而是直接从一段让人头疼的重复代码讲起,把AOP的整套思路、底层代理机制、使用场景和常见面试题一次说清楚,适合刚开始学Spring的初学者,也适合要系统梳理AOP原理准备面试的同学。

1. 从复制粘贴的日志代码,到横切关注点

1.1 基础设施代码正在淹没核心业务

很多同学第一次感受到AOP的价值,都源于一个非常朴素却极其真实的场景——重复代码太多了。想象你正在维护一个订单服务,里面至少十几个公开方法。每个方法进来第一件事打日志,中间可能要做权限判断,最后还得包一层事务。如果不借助任何框架能力,你写出来的代码大概是这个样子:

public Order createOrder(OrderDTO dto) { // 日志 System.out.println("[createOrder] start, params: " + dto); // 权限校验 if (!SecurityUtil.hasPermission("ORDER_CREATE")) { throw new ForbiddenException("无权限"); } long start = System.currentTimeMillis(); try { // 真正的业务逻辑 Order order = doCreate(dto); // 耗时统计 System.out.println("[createOrder] cost: " + (System.currentTimeMillis() - start) + "ms"); return order; } catch (Exception e) { // 异常日志 System.out.println("[createOrder] error: " + e.getMessage()); throw e; } }

这样的代码写一个还好,写十个呢?更麻烦的是,后面如果你想调整日志格式,或者把权限校验的逻辑换一套,就得打开十几个文件逐个改动。要是哪天某个方法漏贴了日志代码,排查线上问题的时候就只能干瞪眼。我见过一个老项目,一个Controller类里所有方法前面的日志打印几乎都是复制粘贴出来的,加一个入参,就要全局搜索替换接口签名,改完之后被漏掉的那几个方法,就成了下次事故的导火索。

1.2 横切关注点:OOP的纵向模块化解不了横向问题

要理解AOP,得先分清一个概念:横切关注点(Cross-cutting Concerns)。对象导向编程(OOP)的模块化是纵向的,它按照业务领域给系统切块,订单是一块,用户是一块,商品是一块。日志、权限、事务这些逻辑呢?它们不属于任何一个单一领域,而是横向贯穿了订单、用户、商品所有模块。你没办法把它完整地塞进订单模块里,因为用户模块也要打印日志,商品模块也要做权限校验。

这种逻辑在架构里有个专门的名字,叫横切关注点。它们有个共同特点:如果不做处理,要么把所有业务代码都“污染”一遍,要么就得靠人肉纪律来保证每个方法都手动调用公共方法。OOP处理不了这个问题的根因,是它的继承和组合机制只会把公共代码下沉到父类或工具类,但“在哪一个方法上调用”这件事,还是得开发者自己控制。

1.3 提取公共方法的局限在哪

有同学说,我把日志封装成LogUtil.info(),然后在每个方法里调一行,不也解决了吗?确实,抽公共方法能解决实现方式重复的问题,但解决不了调用位置重复的问题。你仍然要在每个方法里显式写一行调用代码。这时候AOP换了个思路:既然“在哪些方法上做日志”这件事是可以描述的,那我能不能把这些描述告诉框架,让框架在运行的时候自动往目标方法上加逻辑,业务代码彻底不用关心?

AOP的回答是“可以”。它把“在哪些方法上增强”定义成切点,把“增强什么逻辑”定义成通知,把“切点和通知的结合体”定义成切面,再由框架在合适的时机完成织入。这一整套机制下来,业务方法的核心代码干干净净,公共逻辑统一治理。理解了这个初衷,后续的所有概念都只是在不同层面回答同一个问题:怎么把横切逻辑和业务逻辑安全地分离。

2. 五个核心术语,用一个登机流程说清楚

2.1 连接点、切点、通知、切面、织入

AOP有五个核心术语,初学者很容易搞混。先记住一句话:切点决定切哪里,通知决定干什么,切面是把二者组装起来的成品,织入是组装的时机。

我把这五个术语用机场安检来打比方。你从值机到登机,中间经过安检口,安检员会检查你的证件和随身行李。在这个场景里:

  • 连接点(Join Point):所有可能被检查的位置,比如值机、托运、安检、登机口。在AOP里,连接点就是程序执行过程中的某个点,Spring AOP中通常指方法调用。
  • 切点(Pointcut):实际上要拦截的位置,比如“只有国际航班的安检口才检查签证”。切点是一个表达式,用来筛选连接点。
  • 通知(Advice):安检员的具体检查动作,比如核对护照、扫描行李。对应到代码里,就是你要在目标方法前后执行的逻辑。
  • 切面(Aspect):把“国际航班安检口”(切点)和“核对护照”(通知)组合起来的完整模块。
  • 织入(Weaving):在什么时候把安检员安排到安检口上岗。对应AOP就是在运行期把通知逻辑应用到目标方法的过程。

概念关系可以用一个通俗的公式表达:切面等于切点加通知。当你说自己写了一个切面时,你其实同时指定了两样东西——在哪些方法上生效(切点),以及生效时执行什么逻辑(通知)。

2.2 一个下单方法的完整对照

还是用订单系统举例。假设我要统计所有下单方法的耗时,最简单的方式是定义一个切面,切点指向OrderService类里的所有public方法,通知使用环绕通知,在目标方法前后各取一次时间:

@Aspect @Component public class OrderCostAspect { @Around("execution(* com.example.service.OrderService.*(..))") public Object recordCost(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); try { // 调用真正的目标方法 Object result = pjp.proceed(); return result; } finally { long cost = System.currentTimeMillis() - start; System.out.println("方法 " + pjp.getSignature().getName() + " 耗时 " + cost + "ms"); } } }

这个类就是切面。@Around注解里的字符串就是切点表达式,execution(* com.example.service.OrderService.*(..))的意思很直白:匹配com.example.service包下OrderService这个类的任意方法,对参数不做限制。被@Around修饰的recordCost方法就是通知,它会在匹配到的目标方法执行前后发挥作用。pjp.proceed()是接住目标方法的执行流程,你现在在它前边写的代码会在目标方法之前运行,后边的代码在目标方法返回或抛出异常之后运行。

连接点在这里指的是OrderService里的所有方法,因为按照切点表达式,它们都是可能被拦截的方法;切点筛出的实际拦截范围,就是这个类里的全部方法。当Spring容器启动完毕、OrderService被调用时,框架完成织入,把recordCost这个通知逻辑和createOrder等目标方法绑定到一起,耗时统计就开始生效了。整个过程,你在OrderService里看不到任何一行统计逻辑的代码,这正是AOP想要的效果。

2.3 通知类型的细节差异

通知有五种类型,写切面时最常用的是@Around,但其他几种也有各自的适用场景。简单区分:

  • @Before:目标方法执行前执行。适合做权限校验、参数校验,但如果这里抛出异常会影响目标方法的执行。
  • @AfterReturning:目标方法正常返回后执行,可以在注解里通过returning指定返回值参数。适合记录成功日志或对返回结果做加工。
  • @AfterThrowing:目标方法抛出异常后执行,适合记异常日志,注意它不一定能捕获所有异常,因为有些框架会先把异常处理掉。
  • @After:目标方法结束后执行,类似finally块,无论正常返回还是抛异常都会执行。
  • @Around:可以完全控制目标方法的调用时机,前后逻辑都能写。理论上其他四种都能用它实现,但语义上不如专用注解清晰。

一段真实的经验是:能用@AfterReturning或@AfterThrowing表达的场景,尽量不要全都用@Around,否则整个切面都是ProceedingJoinPoint.proceed()的包装,读起来很累。比如异步任务执行完要更新状态这种逻辑,用@AfterReturning一眼就能看出意图。

3. 底层原理:JDK动态代理和CGLIB,为什么必须在运行期织入

3.1 没有动态代理,普通类无法被运行时增强

AOP的概念本身并不依赖Java,但Spring AOP能在运行期给普通Bean增添行为,靠的是Java的动态代理能力。一个最常见的误解是:AOP就是反射、注解加上拦截器。这个说法不够准确,反射和注解只是描述元数据的手段,真正让“调用目标方法前先执行一段通知”这件事成立的,是容器里那些Bean根本不是原始对象,而是代理对象。

当Spring创建被切面匹配到的Bean时,它检测到该Bean需要被增强,于是不直接返回原始对象,而是根据目标类的情况生成一个代理对象,放入容器。外部调用方从容器里拿到的、注入到其他Bean里的,都是这个代理对象。代理对象内部先执行通知逻辑,再通过invoke或invokeSuper把实际调用转发给原始对象。因为绝大多数Spring应用都通过容器管理Bean,调用方不直接new对象,所以这种运行时织入策略可以无侵入地生效。

3.2 JDK动态代理:只认接口

JDK动态代理是Java标准库自带的代理方式,核心是java.lang.reflect.Proxy和InvocationHandler。它有个硬性限制:只能为接口生成代理。如果你的目标类实现了接口,比如OrderService实现了OrderServiceInterface,那么JDK动态代理会创建一个新的类,也实现这个接口,内部持有一个InvocationHandler。外部通过接口方法调用时,所有方法都会被转发给handler的invoke方法。

public class JdkProxyDemo { public static void main(String[] args) { OrderServiceInterface target = new OrderServiceImpl(); OrderServiceInterface proxy = (OrderServiceInterface) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxyObj, method, methodArgs) -> { System.out.println("前置通知"); Object result = method.invoke(target, methodArgs); System.out.println("后置通知"); return result; } ); proxy.createOrder(); } }

这种实现方案在Spring早年被广泛使用,因为它不依赖第三方库,生成的代理类结构也清晰。但问题同样明显:如果目标类压根没实现接口,就没办法用JDK动态代理了。早年很多同事写Service层习惯先写接口再写实现,其中一个原因正是为了满足JDK动态代理的需要。

3.3 CGLIB:用继承生成子类代理

CGLIB(Code Generation Library)走的是另一条路:它通过动态生成目标类的子类来实现代理。代理类是目标类的子类,在代理类中重写父类能被重写的方法,在重写的方法里插入通知逻辑,再调用super的原始方法。

这种方式不要求目标类实现接口,适用范围更广。但代价也很明确:

  • 目标类不能是final,因为final类无法被继承。
  • 目标方法不能是final,因为final方法无法在子类中重写。
  • 对私有方法不适用,因为子类也看不到父类的私有方法。

这里有一个早期Spring版本踩过的坑:CGLIB代理对目标类的构造方法调用策略有过多次变更,Spring 4.0之后才开始使用objenesis绕过构造方法来创建代理对象,如果没有正确配置,某些带复杂构造器参数的类在生成代理时容易报错。好在新版本框架基本处理好了这些问题,但对原理的理解仍然能帮你在排查诡异问题时快速定位方向。

3.4 Spring到底怎么选择代理方式的

Spring AOP的代理选择逻辑经历过明显变化。早期的规则是:目标类实现了接口,就用JDK动态代理;没有实现接口,就直接用CGLIB。后来Spring Boot时代全面默认使用CGLIB作为代理实现,除非你在配置里显式指定proxyTargetClass=false并要求目标类有接口。为什么Spring Boot更偏爱CGLIB?最现实的原因是:很多类确实没实现接口,比如写Controller时,大部分人只写类不写接口;而且同一个接口如果有多个实现,JDK动态代理就只能按接口代理,Spring仍然需要额外判断哪个实现是被增强的目标。

一个经常被忽视的坑是:如果你依赖了aop相关依赖,但目标类没有接口且类或方法带有final修饰,在旧版本Spring中切面日志不会生效但也不会报错,看起来像是“切面压根没跑”。遇到这种情况,第一反应应该就是去查目标类的final修饰符。

Spring AOP和AspectJ的关系也值得在这里说清楚。Spring AOP只支持方法级别的拦截,而且只能在运行时通过代理实现切面逻辑;AspectJ则是一个更完整的AOP框架,可以修改字节码,在编译期、类加载期完成织入,支持字段访问、构造函数调用等更细粒度的连接点。Spring官方只是借用了AspectJ的注解语法(比如@Aspect、@Around这些注解定义在aspectjweaver里),实际运行时仍用自己的代理机制,这种模式通常被称为“Spring AOP with AspectJ annotations”。这两个概念面试不要混为一家。

4. 实际项目中用得最多的三种自定义切面

4.1 统一日志与耗时统计

最常见的场景还是日志。写一个贴合团队需求的日志切面,需要完整输出调用链路:类名、方法名、入参、返回值、耗时。在实现时有一个经验值得分享:入参不要直接打印整个对象数组,因为某些对象重写了toString且包含敏感信息,要么统一序列化并脱敏,要么只选择安全的基础类型字段。

一个相对完善的写法大致如下:

@Aspect @Component public class AccessLogAspect { private static final Logger log = LoggerFactory.getLogger(AccessLogAspect.class); @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)") public Object logController(ProceedingJoinPoint pjp) throws Throwable { // 获取方法签名 MethodSignature signature = (MethodSignature) pjp.getSignature(); long start = System.currentTimeMillis(); try { Object result = pjp.proceed(); log.info("[访问日志] 请求: {}.{} 耗时: {}ms 返回: {}", signature.getDeclaringType().getSimpleName(), signature.getName(), System.currentTimeMillis() - start, JSON.toJSONString(result)); return result; } catch (Throwable e) { log.error("[访问日志] 请求: {}.{} 异常: {}", signature.getDeclaringType().getSimpleName(), signature.getName(), e.getMessage()); throw e; } } }

这里切点表达式用到了@annotation(...),凡是标注@RequestMapping的方法都会被拦下来,相比execution匹配所有Controller方法,它更灵活,也天然对新加的Controller生效。注意catch到异常后一定要重新抛出,否则业务上的异常会被切面吞掉,调用方得到的就永远是成功的响应了。

4.2 基于自定义注解的权限校验

权限校验几乎是AOP除日志之外最经典的落地场景。思路是先自定义一个注解,比如@RequirePermission("order:create"),然后在需要控制的接口方法上标注它,切面负责在目标方法执行前检查当前登录用户的权限集合:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequirePermission { String value(); }
@Aspect @Component public class PermissionAspect { @Before("@annotation(requirePermission)") public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) { // 从当前登录上下文获取用户 User currentUser = LoginContext.getCurrentUser(); if (currentUser == null || !currentUser.getPermissions().contains(requirePermission.value())) { throw new ForbiddenException("无权限访问: " + requirePermission.value()); } } }

这里注意@annotation(requirePermission)这个写法的细节:它表示把方法上的注解对象作为同名参数传入通知方法,如果你写成@annotation(com.example.annotation.RequirePermission),通知方法里就接收不到这个实例了。权限校验放在@Before里很合理,因为一旦校验失败就不应该执行核心业务逻辑。真正生产环境还会配合Spring Security或Shiro,但自定义注解加切面的思路,在黑名单管理、操作审计场景仍然很好用,尤其是需要兼容老系统的权限改造时。

4.3 分布式锁与接口幂等

事务、缓存、异步这些能力在Spring里很多本身就是靠AOP实现的。比如@Transactional会注册一个事务相关的切面,@Cacheable注册了缓存切面。你自己动手写一个分布式锁切面,会发现在扣库存、积分赠送这些操作上特别有价值:通过注解指定锁的key,切面在方法执行前加锁,执行后释放锁,业务方法内部不需要关心加锁失败怎么处理。

@Around("@annotation(distributedLock)") public Object handleLock(ProceedingJoinPoint pjp, DistributedLock distributedLock) throws Throwable { String lockKey = SpELParser.parse(distributedLock.key(), pjp); boolean locked = lockClient.tryLock(lockKey, distributedLock.waitTime(), distributedLock.leaseTime()); if (!locked) { throw new BizException("系统繁忙,请稍后再试"); } try { return pjp.proceed(); } finally { lockClient.unlock(lockKey); } }

这个切面的核心价值不只是“加锁释放锁”,而是把加锁逻辑和业务逻辑解耦了。新人来写业务时,只要在方法上加一个注解、配好key的参数表达式,就能获得统一的分布式锁能力,不用再关心锁客户端API怎么调、异常后锁有没有释放。这也印证了AOP最核心的价值主张:把横切关注点沉淀成团队能力,而不是每次都在业务里重复低水平实现。

5. 切面不生效?这五个坑我踩过

5.1 同类内部方法调用导致切面失效

这是AOP新手遇到最多的诡异问题。同一个类里方法A加上了切面,方法A内部又调用方法B,B也加了切面,结果B的切面没执行。原因是Spring AOP的代理机制:容器里注入的是代理对象,但方法A是在目标对象内部执行的,它内部的this指向的仍然是原始对象,调用this.b()时根本没有经过代理对象,通知自然无法接入。

解决方式有三种。第一种是把B单独拆到另一个Bean里去,通过注入那个Bean来调用,这是最推荐的做法,也符合“不同横切关注点拆开”的设计原则;第二种是@Transactional这类注解开启exposeProxy=true后使用AopContext.currentProxy()拿到当前代理对象再调用;第三种是放弃同类调用,在切面代码里通过pjp.getTarget()反射调用同样会再次触发拦截,很多场景下会造成死循环,这个方案更适用于高级扩展而非常规业务。

5.2 切点表达式写宽或写窄

execution表达式的语法看起来简单,写起来容易出错。几个关键节点:execution(* com.example..*Service.*(..))和execution(* com.example.*Service.*(..))的区别,不只是从单个包变成子包匹配的问题。com.example..表示com.example及其所有子孙包,com.example.只匹配com.example这个包下直接声明的类。如果你希望管理模块下所有Service接口的实现类方法,通常要写成execution(* com.example.manager..*Service.*(..))这类的形式。

还有一个细节是返回类型匹配。*代表任意返回类型,但如果你写成void,就只能匹配返回值是void的方法,void在切点里也是合法的返回类型描述符。我在实际项目里犯过的错是:想匹配User getXxx()这样的方法但切点写成了get*,然后发现连getAllUsers()这种不带参数的方法也没被拦截,后来才意识到切点匹配的是完整的方法名通配,而不是方法名开头。排查这类问题,优先使用Spring的切点匹配日志,或者临时在通知方法里打印joinPoint.getSignature()看实际匹配到的方法列表。

5.3 final修饰与代理模式冲突

接上之前讲过的底层机制,如果你明确配置了CGLIB代理,但目标类是final或者目标方法是final,代理生成阶段就会出问题。Spring Boot 2.x以后默认强制CGLIB,所以一个被标记为final的Service类,在启动时可能直接抛出"Final method cannot be mocked"或代理创建失败这类错误。有些情况下错误不明显,框架会退化为普通Bean,切面静默失效,这时候排查方向就会跑到切点表达式上去。建议项目里明确一个约束:被AOP管理的类,不添加final修饰,不在切点匹配的方法上加final。

5.4 多切面的执行顺序

当一个方法同时被多个切面命中,比如日志切面、权限切面、分布式锁切面,它们的执行顺序如果没有约定,就会出现“权限没校验就先打印了日志”“锁还没释放就发出了埋点事件”这类奇特的现象。Spring用@Order注解或实现Ordered接口控制顺序,数值越小越先执行。一个实践中合理的顺序是:事务和分布式锁这类资源型切面排最外层,权限校验其次,参数校验再次,日志统计最内层贴近业务。这个顺序的含义是,先拿锁、再校验权限、处理业务、最后记日志,避免在无权限访问时白白加锁或产生误导性的成功日志。

5.5 Spring AOP只能拦截容器管理的Bean

自己用new关键字创建的对象,肯定不会被Spring AOP管理。Spring AOP的动态代理是在Bean创建和初始化阶段完成的,只有从容器里拿Bean才能拿到代理对象。如果你在工具类里直接new OrderServiceImpl()然后调用带切面的方法,切面一定不会生效。这一点很基础,但真出问题时容易被忽略,尤其是配合一些老的静态工厂写法。

6. 面试高频:IoC和AOP的原理怎么答才能加分

6.1 面试官到底在问什么

热搜里“ioc和aop的原理面试”指的就是Spring两大核心特性IoC(控制反转)和AOP。面试官问这个问题的目的通常有三个层次:第一,看你能不能从定义层面说清楚它们分别解决什么问题;第二,看你有没有真正理解Spring容器如何把Bean创建好、如何把代理对象注入进来;第三,看你在项目里有没有实际用过AOP,以及能不能说出动态代理的底层细节。很多人背了大量概念仍然拿不到好评价,问题就出在第三个层次。

一个合格的回答应该包含完整链条:IoC让开发人员从自己new对象、自己管理依赖,变成把控制权交给容器,由容器负责创建和注入依赖;AOP则是在这个基础上为容器创建的Bean增加运行时增强能力,核心实现是动态代理。打个比方,IoC回答的是“对象从哪里来”的问题,AOP回答的是“对象的行为能不能在不动代码的情况下被扩展”的问题。

6.2 实战向答题框架参考

第一层,先用一句话给定义:面向切面编程,核心是把日志、事务、权限等横切关注点从业务逻辑中抽离出来,通过切面和通知对方法进行统一增强。

第二层,讲清楚为什么需要它:OOP解决纵向模块化问题,但无法解决日志这类横向贯穿多个模块的重复逻辑,AOP通过横切解决这个问题。

第三层,说底层原理:Spring AOP在运行时通过JDK动态代理或者CGLIB生成代理对象,把通知织入到目标类的方法执行前后。JDK动态代理要求目标类实现接口,CGLIB通过生成子类来代理,Spring Boot默认使用CGLIB。如果目标类或方法是final,CGLIB会失败。

第四层,说明Spring AOP与AspectJ的关系:自己基于动态代理实现方法级别拦截,只是复用了AspectJ的注解语法,完整的AspectJ还能在编译期修改字节码,支持更细粒度的连接点。

第五层,结合自己项目给例子:可以说项目里使用自定义注解加切面实现了接口幂等性判断,或者用切面统一记录Controller访问日志、统计耗时。说具体一点,比如切点怎么写的、通知选的哪一种、遇到过什么坑,这比背出所有概念更能让面试官确认你是真的用过。围绕“aop使用场景”这个热搜点,重点讲一两个最能体现切面价值的独立场景,没必要把日志、权限、事务、缓存全背一遍。

6.3 “aop面向界面编程”这个说法是哪来的

对了,热搜里还有一个“aop面向界面编程”的说法,可能是输入法问题或者早期翻译版本造成的误解。AOP里的A是Aspect,不是Interface。Aspect在这里是切面,不是界面或接口。一定要记住正确说法是“面向切面编程”,面试时嘴瓢说出“面向切片”“面向切块”都还可以接受,说“面向界面”会让面试官怀疑你概念来源有问题。

7. 我的一点实操总结

AOP这个概念其实不难,难的是把“动态代理”和“织入时机”这种抽象的运行机制和实际开发结合起来。我自己的实践经验是:刚开始用AOP时会克制一点,只做日志和权限校验两个最直观的场景,等理解了代理失效的边界、切点表达式的匹配规则之后,再逐步把分布式锁、幂等控制、审计记录这些更复杂的能力用注解加切面的方式沉淀下去。用AOP的正确姿势永远是“切面替业务扛住横切逻辑”,而不是“把一切逻辑都往切面里塞”。项目里出现过把非常厚重的业务计算往通知里写,导致排查问题时切面代码成了第二业务代码的教训,这是最需要避免的。如果你正在学习Spring或者准备面试,按照这篇文章的顺序把问题想清楚:先想它解决什么问题,再看它怎么实现,最后找一个自己的项目场景亲手写一个切面试试,这套知识就真正内化了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 4:08:54

C语言数制转换实战:栈与除基取余的完整实现

简介:针对初学数据结构的C语言学习者,这份资源以顺序栈为核心,演示如何将十进制数转换为八进制等任意进制,正好补足严蔚敏教材中伪代码不易直接运行的痛点,给出可直接调试的完整实例。压缩包内仅1个PDF文件&#xff0c…

作者头像 李华
网站建设 2026/10/6 4:08:12

OpenShell完全指南:从安装配置到故障排查,找回Windows经典开始菜单

Windows 11 发布后,我身边几乎每周都有人抱怨那个新版开始菜单:磁贴没了、分组逻辑变了、搜索结果混着网页推荐,想快速打开一个控制面板得先想一下图标长什么样。我给人重装系统时最常做的事,就是在装完驱动之后顺手装一个叫 Open…

作者头像 李华
网站建设 2026/10/6 4:08:03

上下文模式(Context-Mode)设计:让大模型在复杂场景下稳定输出

做LLM应用开发,时间久了你会撞上一个特别拧巴的规律:同一个模型,换个场景、换个会话长度,回答质量就像过山车。很多时候问题并不在模型本身,而在context-mode——上下文模式。这个词是我在做客服机器人重构时自己冒出来…

作者头像 李华
网站建设 2026/10/6 4:06:57

GTK入门指南:从控件树到信号回调的图形界面开发实战

1. 先聊清楚:GTK是什么,值不值得学1.1 GTK在Linux生态里的位置最早接触GTK,还是因为想给一个简单的文本处理工具配上图形界面。那会儿我对Linux GUI开发的理解还停留在"Qt很重、用不起,Tkinter又太丑"的阶段。后来做调研…

作者头像 李华
网站建设 2026/10/6 4:05:46

Cursor Mac深度配置指南:解决权限、中文、Git与LSP四大痛点

简介:本资源是一份面向Mac平台Java开发者的Cursor编辑器安装与深度配置指南,解决中文用户在macOS环境下快速落地AI编程工具的核心痛点。压缩包共5个文件(6KB),包含README说明文档(.md)、项目配置…

作者头像 李华
网站建设 2026/10/6 4:05:46

MySQL复习分水岭:DQL查询与表约束核心要点梳理

复习这两章之前,我一直有一种错觉:MySQL的增删改查已经会写了,第三第四章随便看看就行。真正开始二刷黑马程序员这套课才反应过来,第三章和第四章是整个MySQL基础的分水岭——前面你是在“能跑”,这两章才决定你能不能…

作者头像 李华