学习来源:千锋教育和b站免费学习视频

1. javaweb项目开发中的耦合度问题

一般情况下,在Servlet中需要调用Service的方法,则需要在Servlet类中通过new关键字创建Service的实现类对象,例子如下:


// ProductService.java(接口) public interface ProductServive{ public List listProducts(); }
// ProductServiceImpl1.java public class ProductServiveImpl1 implements ProductServive{ public List listProducts(); }
// ProductServiceImpl2.java public class ProductServiveImpl2 implements ProductServive{ public List listProducts(); }

// 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 识别为一个 切面类,它包含横切逻辑(如方法拦截前后操作)。切面可以在目标方法的执行前、执行后,甚至发生异常时,插入特定的逻辑。

切面类主要用途:

  • 日志记录:在方法执行前后记录日志
  • 性能监控:记录方法执行时间
  • 安全检查:在方法调用前检查权限
  • 事务管理:在方法开始前开启事务,结束后提交或回滚事务
  • 异常处理:捕获方法抛出的异常并统一处理

By Liu

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注