SpringBoot IOC与AOP(一)

news2024/11/18 19:44:21

IOC AOP
一、 分层解耦
内聚: 软件中各个功能模块内部的功能联系
耦合: 衡量软件中各个层/模块之间的依赖、关联的程度
软件设计原则:高内聚、低耦合
​ 控制反转:Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想成为控制反转

​ 依赖注入:Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称为依赖注入。

​ Bean对象: IOC容器中创建、管理的对象,称为bean
1.1 IOC - 控制反转 详细
把某个对象交给IOC容器管理,需要添加如下注解之一:
在这里插入图片描述
声名bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认是类名首字母小写

使用以上四个注解都可以生命bean,但是在Springboot集成web开发中,声名控制器bean只能用@Controller

bean的四大注解想要生效,需要被组件扫描注解@ComponentScan扫描

@ComponentScan注解虽然没有显示配置,但是实际上已经包含在了启动类生命注解@SpringBootApplication中,默认扫描的范围是启动类所在包及其子包

如下包名是从“java”包后开始的,但是下面这种不推荐,我们希望的是按照Spring的规范,将包设置在启动类所在包及其子包

@ComponentScan({"dao","com.zhangjingqi"})

1.2 DI - 依赖注入 详解
@Autowired 注解,默认是按照类型进行的,如果存在多个相同的bean,会报错。

EmpServiceA 实现 EmpService类,EmpServiceB 实现 EmpService类,我们在某个地方注入EmpService对象时便会出现注入错误。

解决方案

@Primary 设置bean的优先级
​ 如果我们想要哪个bean填入容器,可以在类名之上添加@Primary

@Qualifier 指定bean的名字

   @Qualifier("empServiceA")
   @Autowired
   private EmpService empService;

@Resource 按照名称注入

@Autowired 注解默认按照类型注入,@Resource默认按照类名进行注入

   @Resource(name = "empServiceB")
   private EmpService empService;

二、AOP
2.1 了解
Spring的第二大核心,第一大核心是IOC

AOP:面向切面编程、面向方面编程,其实就是面向特定方法编程

实现:

**动态代理是面向切面编程最主流的实现。**而SpringAOP是Spring框架的高级技术,目的是在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程

为什么要面向方法编程?

​ 场景:案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时,找到耗时较长的业务进行优化

​ 按照之前的方式,就是在方法开始前和开时候分别获取一个时间,两个时间相减就是执行耗时,但是这种方式是非常繁琐的
————————————————
版权声明:本文为CSDN博主「我爱布朗熊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_51351637/article/details/130779252二、AOP

2.1 了解
Spring的第二大核心,第一大核心是IOC

AOP:面向切面编程、面向方面编程,其实就是面向特定方法编程

实现:

**动态代理是面向切面编程最主流的实现。**而SpringAOP是Spring框架的高级技术,目的是在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程

为什么要面向方法编程?

​ 场景:案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时,找到耗时较长的业务进行优化

​ 按照之前的方式,就是在方法开始前和开时候分别获取一个时间,两个时间相减就是执行耗时,但是这种方式是非常繁琐的
在这里插入图片描述
如果我们基于AOP,面向方法编程,我们可以做到在不改动原始方法的基础上,来针对原始的方法进行编程,可以是对原始方法功能的增强,也可以改变原始方法的功能

比如我们现在要统计方法的耗时,我们只需要定义一个模板方法,将公共的代码定义在模板方法中

原始业务方法在这里指的是需要统计执行耗时的业务方法。而这样面向一个或者多个方法进行编程,就称为面向切面编程

比如我们调用list()方法,此时并不会直接执行原始的list方法,而是自动的去执行模板方法。

模板中所定义的代码逻辑其实是创建出来的代理对象方法中的逻辑
在这里插入图片描述
2.2 快速入门 - AOP 开发步骤
需求:统计各个业务层方法执行耗时

2.2.1 Maven依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2.2 代码实现
针对于特定方法根据业务需要进行编程

@Slf4j
@Component //交给容器IOC进行管理
@Aspect //加上这个注解表示不是一个普通的类,而是一个AOP类,在此类中定义模板方法
public class TimeAspect {

//  参数是一个表达式,表示针对哪些特定方法进行编程
//  com.zhangjingqi.service 包名
//  第一个*代表任意返回值 第二个*代表类名或者接口名  第三个*代表方法名
    @Around("execution(* com.zhangjingqi.service.*.*(..))") //切入点表达式
    public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long begin = System.currentTimeMillis();

//      result 原始方法执行返回值
        Object result = proceedingJoinPoint.proceed();//调用原始方式运行

        long end = System.currentTimeMillis();

//      proceedingJoinPoint.getSignature() 获取方法的签名,我们就知道是哪个方法了
//      如: List com.zhangjingqi.service.impl.DeptServiceImpl.list()执行耗时:239ms
        log.info(proceedingJoinPoint.getSignature() + "执行耗时:{}ms", end - begin);

//      原始方法的返回值我们需要返回回去
        return result;
    }
}

2.2.3 AOP 应用场景及优势
应用场景

记录操作日志
权限控制
事务管理
在这里插入图片描述
优势

代码无侵入
减少重复代码
提高开发效率
维护方便
2.3 核心概念
2.3.1 连接点 - JoinPoint
连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)

通知:Advice,指那些重读的逻辑,也就是共性功能(最终体现为一个方法)

切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用(就是实际被AOP控制的方法)

我们通常会使用下面的切入点表达式来描述切入点

@Around("execution(* com.zhangjingqi.service.*.*(..))") 

切面:Aspect,描述通知与切入点的对应关系(通知+切入点),被@Aspect注解修饰的类我们一般称为切面类

目标对象:Target,通知所应用的对象。

在这里插入图片描述
2.3.2 AOP执行流程
通知如何与目标对象结合在一起对目标对象中的方法进行功能增强的?

​ ①SpringAOP是基于动态代理技术来实现的。程序运行的时候会自动的基于动态代理技术为目标对象生成一个对应的代理对象。

​ ② 在代理对象中就会对目标对象中的原始方法进行功能的增强。

​ 如何来增强的?增强的逻辑是什么样子的?

​ 其实就是我们的通知

​ ③最终在Spring容器中注入的是代理对象,调用的方法也是代理对象中的对应方法

在这里插入图片描述
2.4 通知
2.4.1 通知类型
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行,出现异常后后置代码不会执行。(因为原始方法出现异常了)
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
@After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing:异常后通知,此注解标注的通知方法在发生异常后执行

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class MyAspect1 {
    //前置通知
    @Before("execution(* com.zhangjingqi.service.*.*(..))")
    public void before(JoinPoint joinPoint) {
        log.info("before ...");
    }

    //环绕通知
    @Around("execution(* com.zhangjingqi.service.*.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint)
            throws Throwable {
        log.info("around before ...");
        //调用目标对象的原始方法执行
        Object result = proceedingJoinPoint.proceed();
        //原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
        log.info("around after ...");
        return result;
    }

    //后置通知
    @After("execution(* com.zhangjingqi.service.*.*(..))")
    public void after(JoinPoint joinPoint) {
        log.info("after ...");
    }

    //返回后通知(程序在正常执行的情况下,会执行的后置通知)
    @AfterReturning("execution(* com.zhangjingqi.service.*.*(..))")
    public void afterReturning(JoinPoint joinPoint) {
        log.info("afterReturning ...");
    }

    //异常通知(程序在出现异常的情况下,执行的后置通知)
    @AfterThrowing("execution(* com.zhangjingqi.service.*.*(..))")
    public void afterThrowing(JoinPoint joinPoint) {
        log.info("afterThrowing ...");
    }
}

注意事项

@Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来执行原始方法,其他通知不需要考虑原始方法的执行

@Around环绕通知的方法的返回值,必须指定为Object,来接收原始方法的返回值

​ 如果不return,在调用这个方法的地方时拿不到返回值的

对切入点表达式进行抽取

//   生命切入点表达式的注解,切点
    @Pointcut("execution(* com.zhangjingqi.service.*.*(..))")
    private void pt(){

    }

    //前置通知
    @Before("pt()")
    public void before(JoinPoint joinPoint) {
        log.info("before ...");
    }

其他类中也可以进行抽取,只需要定位到切入点表达式的位置即可。

@Slf4j
@Component
@Aspect
public class MyAspect2 {
//引用MyAspect1切面类中的切入点表达式
@Before("com.zhangjingqi.aspect.MyAspect1.pt()")
public void before(){
log.info("MyAspect2 -> before ...");
 }
}

2.4.2 通知顺序
​ 当有多个切面的切入点都匹配到了目标方法,目标方法运行,多个通知方法都会被执行。

​ 下面研究多个切面类的通知顺序。同个切面类的通知顺序不再研究

不同切面类中,默认按照切面类的类名字母排序
​ 目标方法前的通知方法:字母排名靠前的先执行

​ 目标方法后的通知方法:字母排名靠前的后执行

使用@Order(数字)加在切面类上来控制顺序
2.5 切入点表达式
切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中哪些方法需要加入通知
常见形式
​ execution(…):根据方法的签名来匹配

​ @annotation(…):根据注解匹配

2.5.1 execution
​ 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配

​ 下面来描述的时候,可以基于接口。也可以基于实现类

execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)

​ 其中?表示可省略的部分

访问修饰符:可省略,比如public、protected
包名.类名:可省略,但是不建议
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
2.5.1.1 execution通配符
*:单个独立的任意符号,可以匹配任意返回值、包名、类名、方法名、方法参数等信息来匹配

​ 此案例表示返回值人任意,二级包任意,类或接口任意,方法参数任意但是有且只有一个

execution(* com.*.service.*.update(*)

匹配类名以Service结尾,方法以delete开头的方法

execution(void
com.itheima.service.impl.*Service.delete*(java.lang.Integer)
)

…:多个连续的任意符号,可以通配任意层级的包,或者任意类型、任意个数的参数

​ 层级包任意,方法的参数任意

execution(* com.zhangjingqi..DeptService.*(..)

返回值任意,方法名任意,方法参数任意

execution(* *(..)

2.5.1.2 execution表达式案例

@Pointcut("execution(* com.zhangjingqi.service.*.*(..))")

省略异常

execution(public void
com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)
)

省略方法访问修饰符

参数是全类名

execution(void
com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)
)

使用"…"省略包名

execution(public void  com..DeptServiceImpl.delete(java.lang.Integer))

省略包名类名
​ 指定方法名。

​ 不建议将包名和方法名省略。一旦省略,将表达式的范围扩大,一是影响匹配的效率,而是可能匹配到其他不需要的方法

execution(public void  delete(java.lang.Integer))

匹配所有的方法
​ 此时表示匹配DeptServiceImpl类中的所有方法

execution(public void  com..DeptServiceImpl.*(java.lang.Integer))

使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式

execution(* com.zhangjingqi.service.DeptService.list(..)) ||
execution(* com.zhangjingqi.service.DeptService.delete(..))

2.5.1.3 切入点表达式建议
所有业务方法名在命名时尽量规范,方便切入点快速匹配。
​ 如查询方法find开头,更新类方法update开头

描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性

在满足业务需要的前提下,尽量缩小切入点的匹配范围

​ 包名匹配进行不使用“…”,使用“*”匹配单个包

2.5.2 @annotation
用于匹配标识有特定注解的方法

@Before("@annotation(com.zhangjingqi.anno.MyLog)")

简化下列表达式

execution(* com.zhangjingqi.service.DeptService.list(..)) ||
execution(* com.zhangjingqi.service.DeptService.delete(..))

实现步骤

编写自定义注解

在业务类要做为连接点的方法上添加自定义注解

创建自定义注解类

@Retention(RetentionPolicy.RUNTIME)//描述注解什么时候生效的:运行时有效
@Target(ElementType.METHOD)//当前注解可以作用在哪些地方
public @interface MyLog {
}

添加自定义注解

@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() { ... }

@Override
@MyLog //自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) { ... }

切面类

@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Before("@annotation(com.zhangjingqi.anno.MyLog)")
   public void before(){
   log.info("MyAspect6 -> before ...");

//后置通知
@After("@annotation(com.zhangjingqi.anno.MyLog)")
   public void after(){
   log.info("MyAspect6 -> after ...");
 }
}

2.5.3 切入点表达式总结
execution切入点表达式
​ 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式

​ 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过

​ execution切入点表达式描述比较繁琐

annotation 切入点表达式
​ 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但

​ 是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

2.6 连接点
​ 被AOP控制的方法,目标对象中所有的方法都可以被AOP控制,在Spring AOP中又特制方法的执行
在这里插入图片描述
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法类名、方法参数等
​ 对于@Around通知,获取连接点信息只能用ProceedingJoinPoint
在这里插入图片描述
​ 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint父类型

在这里插入图片描述
对于@Around通知,为什么获取连接点信息只能用ProceedingJoinPoint?

在Spring AOP中,@Around通知是最为强大和灵活的通知类型,它可以决定是否执行连接点,以及如何处理连接点返回的结果。因此,@Around通知需要通过ProceedingJoinPoint参数来获取连接点信息。

​ ProceedingJoinPoint是JoinPoint的子类,同时也是JoinPoint的扩展版本。JoinPoint表示连接点,也就是被Advice修饰的方法。而ProceedingJoinPoint除了表示连接点外,还具有一个proceed()方法,该方法是执行目标方法的关键。在@Before和@After通知中,JoinPoint足以满足需要,因为它们只需要获取连接点信息即可,不需要执行目标方法。但在@Around中,除了获取连接点信息,还需要控制目标方法的执行,因此需要用到ProceedingJoinPoint。

​ 在@Around通知中,可以通过ProceedingJoinPoint的proceed()方法,手动控制目标方法的执行。例如,可以在proceed()方法前后进行一些预处理或后处理。同时,ProceedingJoinPoint还提供了一些其他的工具方法,例如getArgs()获取目标方法参数,getSignature()获取目标方法签名等,这些方法在编写@Around通知时也非常有用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/799747.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java 悲观锁 乐观锁

锁可以从不同的角都分类。其中乐观锁和悲观锁是一种分类方式 一、悲观锁、乐观锁定义 悲观锁就是我们常说到的锁。对于悲观锁来说&#xff0c;他总是认为每次访问共享资源时会发生冲突&#xff0c;所以必须每次数据操作加上锁&#xff0c;以保证临界区的程序同一时间只能有一个…

文件上传漏洞 -- uploadlabs为例

文件上传漏洞原理 一些web应用程序中允许上传图片、视频、头像和许多其他类型的文件到服务器中。 文件上传漏洞就是利用服务端代码对文件上传路径变量过滤不严格将可执行的文件上传到一个到服务器中 &#xff0c;再通过URL去访问以执行恶意代码。 非法用户可以利用上传的恶意脚…

如何使用 Flatpak 在 Linux 上安装 ONLYOFFICE 桌面编辑器?

Flatpak 是一款与 Linux 发行版无关的软件实用工具&#xff0c;可用于在 Linux 上构建和分发桌面端应用。其可帮助您安装第三方 Linux 应用程序&#xff0c;无需安装库或处理依赖。 ONLYOFFICE 桌面版是什么 ONLYOFFICE 编辑器桌面版是一款全面的办公工具&#xff0c;提供了文…

常用抓包命令

tcpdump的命令参数介绍 tcpdump选项可划分为四大类型&#xff1a; 1.控制抓包行为 2.控制信息如何显示 3.控制显示什么数据 4.过滤命令 一个电脑是可以有多个网卡的&#xff01; 易错&#xff1a;ping命令式指定网口要-I ,-i表示ping的时间间隔、tcpdump指定网口-i 。 nsloo…

优惠券秒杀(二)

库存超卖问题分析 库存超卖问题其本质就是多个线程操作共享数据产生的线程安全问题&#xff0c;即当一个线程在执行操作共享数据的多条代码的过程中&#xff0c;其他线程也参与了进来&#xff0c;导致了线程安全问题的产生。例如&#xff1a;线程1发送请求&#xff0c;查询库存…

openGauss学习笔记-22 openGauss 简单数据管理-HAVING子句

文章目录 openGauss学习笔记-22 openGauss 简单数据管理-HAVING子句22.1 语法格式22.2 参数说明22.3 示例 openGauss学习笔记-22 openGauss 简单数据管理-HAVING子句 HAVING子句可以让我们筛选分组后的各组数据。 WHERE子句在所选列上设置条件&#xff0c;而HAVING子句则在由…

Facebook Shop商店如何开通?6个步骤

Facebook作为全球领先的社交平台&#xff0c;一直以来是跨境玩家的必争之地。据统计&#xff0c;目前它活跃用户27亿人/月&#xff0c;访问量21亿/天。近年来社媒电商红利当头&#xff0c;而Meta 于2020年5月推出的Facebook Shop也一直备受关注 。这也是用户的在facebook上网购…

108、RocketMQ的底层实现原理(不需要长篇大论)

RocketMQ的底层实现原理 RocketMQ由NameServer集群、Producer集群、Consumer集群、Broker集群组成&#xff0c;消息生产和消费的大致原理如下: Broker在启动的时候向所有的NameServer注册&#xff0c;并保持长连接&#xff0c;每30s发送一次心跳Producer在发送消息的时候从Na…

Tomcat的基本使用,如何用Maven创建Web项目、开发完成部署的Web项目

Tomcat 一、Tomcat简介二、Tomcat基本使用三、Maven创建Web项目3.1 Web项目结构3.2开发完成部署的Web项目3.3创建Maven Web项目3.3.1方式一3.3.2方式二&#xff08;个人推荐&#xff09; 总结 一、Tomcat简介 Web服务器&#xff1a; Web服务器是一个应用程序&#xff08;软件&…

RNN架构解析——GRU模型

目录 GRU模型实现优点和缺点 GRU模型 实现 优点和缺点

day46-SSM

0目录 SSM 1.SSM框架集成 1.1 创建数据库、表、工程&#xff0c;引入依赖 1.2 配置web.xml&#xff08;前端控制器和字符过滤器&#xff09; 1.3 配置applicationContext.xml 1.4 实现增删改查功能 可以用Model对象替代HttpServletRequest 详情页面&#xff1a;Ma…

超宽带人员定位系统源码 智慧工厂人员定位系统源码

超宽带人员定位系统源码 智慧工厂人员定位系统源码 随着工业信息化技术的发展&#xff0c;大型制造企业对人员、车辆、物资的管理要求越来越细致&#xff0c;企业希望更科学的调度每一个生产元素&#xff0c;从而突破管理瓶颈&#xff0c;进一步提高生产效率及企业安全管理和服…

[计算机入门] 操作项目

2.9 操作项目 2.9.1 新建项目 方法一&#xff1a; 切换到主页选项卡&#xff0c;点击新建项目&#xff0c;在弹出的项目中&#xff0c;点击要新建文件类型。如果是要新建文件夹&#xff0c;只需要点击当前选项卡新建组中的新建文件夹即可。 方法二&#xff1a; 在当前文件夹…

[OnWork.Tools]系列 02-安装

下载地址 百度网盘 历史版本连接各种版本都有,请下载版本号最高的版本 链接&#xff1a;https://pan.baidu.com/s/1aOT0oUhiRO_L8sBCGomXdQ?pwdn159提取码&#xff1a;n159 个人链接 http://on8.top:5000/share.cgi?ssiddb2012fa6b224cd1b7f87ff5f5214910 软件安装 双…

华为刷题:HJ3明明随机数

import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main {public static void main(String[] args) {Scanner scan new Scanner(System.in);int N scan.nextInt();int[] arr new int[N];for (int i 0; i < N; i) {int n sca…

哈希表的简单模拟实现

文章目录 底层结构哈希冲突闭散列定义哈希节点定义哈希表**哈希表什么情况下进行扩容&#xff1f;如何扩容&#xff1f;**Insert()函数Find()函数二次探测HashFunc()仿函数Erase()函数全部的代码 开散列定义哈希节点定义哈希表Insert()函数Find()函数Erase()函数总代码 初识哈希…

vue基础-axios封装/同步请求

&#x1f4d6; 本章介绍 Vue 项目中如何使用 Axios 封装 http 请求&#xff0c;请求/响应拦截器部分写的比较简单&#xff0c;后续项目中可以补充。 &#x1f4a6; 1、/src/utils/目录下建立一个htttp.js 导入axios设置axios请求参数创建axios实例请求拦截器响应拦截器封装ge…

文件上传

js绕过 打开网页尝试上传一句话木马&#xff0c;发现只能上传图片文件 审计源代码&#xff0c;发现使用一个checkfile函数js对文件类型进行了屏蔽 于是我们修改网页代码&#xff0c;去除返回值的检查函数 checkFile() 上传成功&#xff0c;使用蚁剑连接 连接成功 .htaccess绕…

F5 LTM 知识点和实验 3-负载均衡中的负载算法

第三章&#xff1a;负载均衡中的负载算法 负载算法分为静态的和动态的。静态的连接分布模式是预先设置的&#xff0c;流量处理中是不会变化的&#xff0c;动态的连接分布模式也是预先设置的&#xff0c;但是连接分布会根据某些因素的改变而调整。 轮询&#xff08;round robi…

基于python和pygame实现的植物大战僵尸

游戏的实现流程和思路&#xff1a; 游戏资源准备&#xff1a; 加载所有游戏中需要用到的图像资源&#xff0c;如植物、僵尸、子弹、背景等&#xff0c;并将它们保存在GFX字典中。 游戏状态管理&#xff1a; 定义了一个抽象基类State&#xff0c;表示游戏中的不同状态&#xff0…