学习来源:千锋教育和b站免费学习视频
1. javaweb项目开发中的耦合度问题
一般情况下,在Servlet中需要调用Service的方法,则需要在Servlet类中通过new关键字创建Service的实现类对象,例子如下:
// ProductService.java(接口) public interface ProductServive{ public ListlistProducts(); }
// ProductServiceImpl1.java public class ProductServiveImpl1 implements ProductServive{ public ListlistProducts(); }
// ProductServiceImpl2.java public class ProductServiveImpl2 implements ProductServive{ public ListlistProducts(); }
// ProductListServlet.java public class ProductListServlet extends HttpServlet{ //在Servlet中使用new关键字创建ProductSeriveImpl1对象,增加了Servlet和Service的耦合度 private ProductService productService = new ProductSeriveImpl1(); //do something... }
同理,在Service实现类中需要调用DAO中的方法时,也需要在Service实现类通过new关键字创建DAO实现类对象
所以使用new关键字创建对象有如下缺点:
- 失去了面向接口编程的灵活性
- 代码的侵入性增加(增加了耦合度),降低了代码的灵活性
解决方案:在Servlet中定义Service接口的对象,不使用new关键字创建实现类对象,而是通过反射动态的给Service对象变量赋值
实现方法:使用Spring
2. Spring介绍
spring是一个轻量级的控制反转和面向切面的容器框架,用来解决企业项目开发的复杂度问题——解耦
特点:
- 轻量级:体积小,对代码没有侵入性
- 控制反转:IoC(Inverse of Control),把创建对象的工作交由Spring完成,Spring创建对象时可以完成对象属性赋值(DI 即 Dependency Injection 依赖注入)
- 面向切面:AOP(Aspect Oriented Programming)面向切面编程,可以在不改变原有业务逻辑的情况下实现对业务的增强
- 容器:实例的容器,管理创建的对象
3. 使用IoC
方式:
- 基于XML
- 基于注解
一般使用基于注解的方式,除非一些老项目还在基于XML的方式
基于XML的方式
基于XML:开发者把需要的对象在XML中进行配置,Spring框架读取这个配置文件,根据配置文件的内容来创建对象
a. 首先在创建的maven项目中引入如下依赖:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.23</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> </dependencies>
b. 创建一个DataConfig类作为测试类
@AllArgsConstructor @NoArgsConstructor @Data public class DataConfig { private String url; private String username; private String password; private String driverName; }
c. 创建一个xml配置文件拥有IoC配置,可以随便命名,这里命名为SpringIoC.xml,配置内容如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean class="com.IoC.DataConfig" id="config"> <property name="driverName" value="Driver"> </property> <property name="url" value="localhost:8888"> </property> <property name="username" value="root"> </property> <property name="password" value="root"> </property> </bean> </beans>
在其中我们配置了一个bean对象及其属性值
bean标签的属性:
- class:指定这个 bean 的全限定类名(com.IoC.DataConfig),即 Spring IoC 容器会实例化这个类的对象
- id:指定这个 bean 的唯一标识符,其他地方可以通过这个 ID 来引用该 bean。例如,这里定义的 bean 的 ID 为Config
property标签用来设置 bean 的属性值。它映射到 Java 类中的 setter
方法
property标签的属性:
- name:指类中的属性名
- value:给这个属性赋值
d. 通过ApplicationContext类得到我们要创建的对象
这里直接在main函数中测试
public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("SpringIoC.xml"); System.out.println(context.getBean("config")); } }
输出结果如下:
DataConfig(url=localhost:8888, username=root, password=root, driverName=Driver)
可见成功通过xml文件创建我们的测试类的对象
基于注解的方式
1. 配置类
用一个Java类来替代XML文件,把在XML中的配置的内容放到配置类中
@Configuration public class BeanConfiguration { @Bean public DataConfig dataConfig() { return new DataConfig("localhost:8888", "root", "root", "Driver"); } }
注解解释:
a. @Configuration
功能:
- 这是一个 Spring 框架的注解,表明该类是一个 配置类,类似于传统的 XML 配置文件
- Spring 会在启动时扫描这个类,并将其中定义的@Bean方法返回的对象注册到 Spring 容器中,作为 Bean
作用:
- 替代 XML 配置,用来定义 Spring 应用程序上下文的 Bean
- 使用此注解的类可以包含一个或多个 @Bean注解的方法
示例解释:
如这段代码中,BeanConfiguration类被标注为@Configuration,说明这是一个专门用来配置 Bean 的类。Spring 会识别这个类,并将其加载到应用上下文中
b. @Bean
功能:
- 这是一个方法级别的注解,用于告诉 Spring 这个方法返回的对象是一个 Bean,并将其注册到 Spring 容器中
- Spring 容器会将这个方法的返回值作为一个 受管理的对象,可以在应用中使用依赖注入(Dependency Injection, DI)来访问
特点:
- 方法的名称(比如这里的dataConfig)就是 Bean 的默认名称
- 返回的对象会被 Spring 容器管理,也可以在其他地方注入使用
示例解释:
- 方法 dataConfig被标注为@Bean表示它会返回一个名为dataConfig的 Bean
- Spring 容器会调用这个方法,并将其返回的DataConfig对象注册为 Bean
- 以后其他类中可以通过注入(比如@Autowired)来使用这个DataConfig对象
在配置类中可以指定Bean注解的name和value属性,如:
@Configuration public class BeanConfiguration { @Bean(name = "config") public DataConfig dataConfig() { return new DataConfig("localhost:8888", "root", "root", "Driver"); } }
@Configuration public class BeanConfiguration { @Bean(value = "config") public DataConfig dataConfig() { return new DataConfig("localhost:8888", "root", "root", "Driver"); } }
通过ApplicationContext类得到我们要创建的对象:
public class Main { public static void main(String[] args) { // 方式1: ApplicationContext context = new AnnotationConfigApplicationContext(BeanConfiguration.class); context.getBean(DataConfig.class); //直接通过反射获取DataConfig类的实例 context.getBean("dataConfig"); //当被@Bean注解的方法的名称为dataConfig时 context.getBean("config"); //当指定name或者value属性为config时 } }
public class Main { public static void main(String[] args) { // 方式2: ApplicationContext context = new AnnotationConfigApplicationContext("com.liu.IoC"); context.getBean(DataConfig.class); //直接通过反射获取DataConfig类的实例 context.getBean("dataConfig"); //当被@Bean注解的方法的名称为dataConfig时 context.getBean("config"); //当指定name或者value属性为config时 } }
2. 扫包 + 注解
更简单的方式,不再需要依赖于XML或者配置类,而是直接将bean的创建交给目标类,在目标类添加注解来创建
只需给之前创建配置测试类DataConfig添加几个注解即可代替XML文件或一个Java类来配置目标类的Bean,例如:
@Component @AllArgsConstructor @NoArgsConstructor @Data public class DataConfig { @Value("localhost:8888") private String url; @Value("root") private String username; @Value("root") private String password; @Value("Driver") private String driverName; }
关键注解作用:
a. @Component
作用:
- 将这个类声明为 Spring 的一个组件
- Spring 会自动扫描这个类,并将其实例化为一个 Bean,注册到 Spring 容器中
- 默认情况下,Bean 的名称是类名首字母小写(这里为dataConfig)
b. @Value
作用:
- 用于为类中的字段注入值(例如 URL、用户名、密码等)
- 可以直接写定值(如代码中的
"localhost:8888"
),也可以使用配置文件中的占位符(如${url}
)来动态读取外部配置
如何通过使用配置文件中的占位符(如 ${url}
)来动态读取外部配置,详见知识零食2
通过ApplicationContext类得到我们要创建的对象:
public class Main { public static void main(String[] args) { // 基于注解的方式获取Bean对象——扫包 + 注解 ApplicationContext context = new AnnotationConfigApplicationContext("com.liu.Ioc"); //扫包 System.out.println(context.getBean(DataConfig.class)); } }
补充:如何将一个Bean注入到另一个Bean中
很简单,只需要在给对应类加个@Autowired注解,例如:
@Data @AllArgsConstructor @NoArgsConstructor @Component public class GlobalConfig { @Value("8888") private String port; @Value("/") private String path; @Autowired private DataConfig dataConfig; }
@Autowired注解的详细解释:
@Autowired是 Spring 框架中的一个核心注解,用于实现 依赖注入(Dependency Injection, DI)。它可以让 Spring 自动将某个符合要求的 Bean 注入到使用它的地方。
在这段代码中,@Autowired注解被用于注入DataConfig类的实例到GlobalConfig类中
@Autowired的工作原理
1. 注入方式 @Autowired
的注入是基于类型的(byType)。Spring 会根据注解标注的字段、方法参数或者构造器参数的类型,在 Spring 容器中找到与该类型匹配的 Bean
- 如果找到唯一的 Bean,就会成功注入
- 如果找到多个匹配的 Bean,就需要结合@Qualifier或者@Primary指定具体的 Bean
- 如果没有找到匹配的 Bean,会抛出异常
2. 依赖解析顺序
- 优先匹配类型:首先查看容器中是否有和被注入字段或参数类型一致的 Bean
- 匹配名称(如果有多个):如果有多个类型匹配的 Bean,可以通过字段名称或@Qualifier 注解进一步指定
@Autowired的作用范围
1. 字段注入
@Autowired可以直接注解在类的字段上,Spring 会自动注入该字段所需的 Bean
如上面的示例代码所示
2. 构造器注入
在类的构造器上使用@Autowired,Spring 会自动注入构造器所需的参数
@Component public class GlobalConfig { private final DataConfig dataConfig; @Autowired public GlobalConfig(DataConfig dataConfig) { // 通过构造器注入 this.dataConfig = dataConfig; } }
3. 方法注入
@Autowired也可以用在普通方法上,Spring 会在方法调用时注入所需的参数
@Component public class GlobalConfig { private DataConfig dataConfig; @Autowired public void setDataConfig(DataConfig dataConfig) { // 通过 Setter 方法注入 this.dataConfig = dataConfig; } }
多个候选 Bean 时的冲突解决
如果容器中有多个相同类型的 Bean,Spring 会抛出NoUniqueBeanDefinitionException异常
解决方式:
1. 使用 @Qualifier明确指定 Bean 的名称(tip:@Qualified不能单独使用):
@Component("specificDataConfig") public class DataConfig{ //do something ... }
@Autowired @Qualifier("specificDataConfig") private DataConfig dataConfig;
2. 使用@Primary 设置优先级最高的 Bean: 在需要优先注入的 Bean 上标注@Primary
@Primary @Component public class PrimaryDataConfig extends DataConfig { // 优先被注入 }
依赖必需性控制
默认情况下,@Autowired标注的字段或方法是必须注入的。如果 Spring 容器中找不到对应的 Bean,会抛出异常
解决方式:
1. 设置为非必需: 可以通过 required = false 设置为可选依赖:
@Autowired(required = false) private DataConfig dataConfig;
如果DataConfig类型的 Bean 不存在,dataConfig会被设置为 null
2. 使用@Nullable: 使用@Nullable注解表明该依赖可以为 null
:
@Autowired public void setDataConfig(@Nullable DataConfig dataConfig) { this.dataConfig = dataConfig; }
@Autowired注解的优点
- 简化代码: 自动注入减少了手动创建对象的代码
- 松耦合: 通过依赖注入,让组件彼此之间的依赖更加灵活
- 高效开发: 自动管理 Bean 的创建和依赖关系,无需手动配置
4. AOP
面向切面编程,是一种抽象化的面向对象编程,对面向对象编程的一种补充,底层使用动态代理机制来实现
一般打印日志的代码,业务代码和打印日志耦合
public class CalImpl implements Cal{ @Override public int add(int a, int b) { System.out.println("add方法的参数是{" + a + "," + b + "}"); int result = a + b; System.out.println("add方法的结果是{" + result + "}"); return result; } @Override public int sub(int a, int b) { System.out.println("sub方法的参数是{" + a + "," + b + "}"); int result = a - b; System.out.println("sub方法的结果是{" + result + "}"); return result; } @Override public int mul(int a, int b) { System.out.println("mul方法的参数是{" + a + "," + b + "}"); int result = a * b; System.out.println("mul方法的结果是{" + result + "}"); return result; } @Override public int div(int a, int b) { System.out.println("div方法的参数是{" + a + "," + b + "}"); int result = a / b; System.out.println("div方法的结果是{" + result + "}"); return result; } }
如这段计算器类代码中,日志和业务耦合在一起,AOP要做的就是将日志代码全部抽象出去统一进行处理,计算器方法中只保留核心的业务代码
做到核心业务和非业务代码的解耦合
步骤:
a. pom文件依赖配置
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.23</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.23</version> </dependency> </dependencies>
b. 创建实现类
public interface Cal { public int add(int a, int b); public int sub(int a, int b); public int mul(int a, int b); public int div(int a, int b); }
@Component public class CalImpl implements Cal{ @Override public int add(int a, int b) { int result = a + b; return result; } @Override public int sub(int a, int b) { int result = a - b; return result; } @Override public int mul(int a, int b) { int result = a * b; return result; } @Override public int div(int a, int b) { int result = a / b; return result; } }
c. 创建切面类
@Aspect @Component public class LoggerAspect { @Before("execution(public int com.liu.AOP.CalImpl.*(..))") public void before(JoinPoint joinPoint) { String name = joinPoint.getSignature().getName(); System.out.println(name + "方法的参数是" + Arrays.toString(joinPoint.getArgs())); } @AfterReturning(value = "execution(public int com.liu.AOP.CalImpl.*(..))", returning = "result") public void afterReturning(JoinPoint joinPoint, Object result) { String name = joinPoint.getSignature().getName(); System.out.println(name + "方法的结果是" + result); } }
d. XML文件配置自动扫包 + 开启自动生成代理对象
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd "> <!-- 自动扫包 --> <context:component-scan base-package="com.liu.AOP"> </context:component-scan> <!-- 开启自动生成代理 --> <aop:aspectj-autoproxy> </aop:aspectj-autoproxy> </beans>
e. 测试
public class Main { public static void main(String[] args) { // 基于XML的方式获取Bean对象 ApplicationContext context = new ClassPathXmlApplicationContext("SpringIoC.xml"); Cal cal = context.getBean(Cal.class); System.out.println(cal.add(9, 8)); System.out.println(cal.sub(9, 8)); System.out.println(cal.mul(9, 8)); System.out.println(cal.div(9, 8)); } }
输出:
add方法的参数是[9, 8] add方法的结果是17 17 sub方法的参数是[9, 8] sub方法的结果是1 1 mul方法的参数是[9, 8] mul方法的结果是72 72 div方法的参数是[9, 8] div方法的结果是1 1
@Aspect注解的作用:
@Aspect标注的类会被 Spring AOP 识别为一个 切面类,它包含横切逻辑(如方法拦截前后操作)。切面可以在目标方法的执行前、执行后,甚至发生异常时,插入特定的逻辑。
切面类主要用途:
- 日志记录:在方法执行前后记录日志
- 性能监控:记录方法执行时间
- 安全检查:在方法调用前检查权限
- 事务管理:在方法开始前开启事务,结束后提交或回滚事务
- 异常处理:捕获方法抛出的异常并统一处理