Spring知识点(2)

 

一、有哪些依赖注入方式?

  • 构造方法注入:通过调用类的构造方法,推荐用于强依赖(没有依赖对象就没法工作)。这也是官方推荐的注入方式。好处:依赖不可变(final修饰)、更安全。
@Component
public class UserService {
    private final UserDao userDao;

    // Spring 会自动注入 UserDao
    @Autowired
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}
  • Setter注入:适合可选依赖(有无都能运行)
@Component
public class UserService {
    private UserDao userDao;

    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}
  • 字段注入:直接在字段上加@Autowired。(简单,但不利于单元测试mock,因为会依赖Spring框架)

二、Spring有哪些自动装配的方式

1、什么是自动装配

  装配:把bean之间的依赖关系配置清楚

  自动装配:让Spring容器自己根据规则把依赖对象诸如进去,而不是开发者手动写。这样可以减少配置,提升开发效率。

2、Spring提供了哪几种自动装配类型

  • no(默认值):不自动装配,必须显式依赖(XML里面写<property>)
  • byName:根据属性名找到和Bean的id一样的Bean注入
  • byType:根据属性的类型找到容器里唯一匹配的Bean注入
  • constructor:根据构造方法参数的类型,去容器里找匹配的 Bean 注入。

【注意】

在 Spring Boot 和注解驱动里,主要用:

  • @Autowired(默认 byType,可结合 @Qualifier 指定名字)

  • @Resource(JDK自带,默认 byName,找不到再 byType)

  • @Inject(JSR-330 标准注解,行为类似 @Autowired)

// 接口
public interface UserDao {
    void save();
}

// 第一个实现
@Component("mysqlUserDao")
public class UserDaoMysql implements UserDao {
    @Override
    public void save() {
        System.out.println("保存到 MySQL 数据库");
    }
}

// 第二个实现
@Component("oracleUserDao")
public class UserDaoOracle implements UserDao {
    @Override
    public void save() {
        System.out.println("保存到 Oracle 数据库");
    }
}

 

三、Spring中Bean的作用域有哪些

  • singleton(默认):在一个Spring 容器(ApplicationContext)中,只创建一个 Bean 实例。每次 getBean() 拿到的都是同一个对象。
  • prototype:每次调用 getBean(),都会新建一个对象。
  • request(仅 Web 应用中有效):每个 HTTP 请求都会创建一个新的 Bean。Bean 生命周期和一次请求绑定,请求结束后 Bean 被销毁。
  • session(仅 Web 应用中有效):每个 HTTP 会话(Session)只对应一个 Bean。同一个 Session 里的请求共享 Bean,不同 Session 用不同 Bean。
@Component
@Scope("prototype")   // 或 "request" / "session"
public class UserService {}
  • Session = 会话

  • 在 Web 里,Session 表示 用户从打开浏览器访问网站,到关闭浏览器/超时退出的这一段交互过程

四、Spring中的单例Bean会存在线程安全问题吗

  Spring的单例Bean不是天然线程安全的。是否有问题,取决于Bean是否有状态。

  • 无状态Bean(只做方法调用,不保存共享数据):线程安全
  • 有状态Bean(持有成员变量,并且会被多个线程同时读写):线程不安全

  解决办法:

  • 改为多例(prototype):每次请求都新建一个实例-->不共享-->没有线程安全问题。但这样会失去单例的优势,容器管理和性能都会受影响。
  • 避免在Bean中设计为无状态,方法里只用局部变量
  • 使用ThreadLocal 保存状态(推荐 ✅):给每个线程准备一份独立副本,避免了线程之间的数据覆盖。
@Service
public class UserContextService {
    private ThreadLocal<String> currentUser = new ThreadLocal<>();

    public void setUser(String user) {
        currentUser.set(user);  // 每个线程有自己独立的副本
    }

    public String getUser() {
        return currentUser.get();  // 取的就是当前线程的值
    }

    public void clear() {
        currentUser.remove();  // 防止内存泄漏(重要!)
    }
}

 

五、说说循环依赖

1、什么是循环依赖

  两个或多个Bean互相依赖,形成“死循环”,只在单例下会出现。如果是prototype的话会无限套娃。

  那Spring能解决哪些情况?

  • 两边都是构造器注入(不支持):无法提前暴露“半成品”对象,会直接报错。
  • 两边都是setter/字段注入(支持):Spring创建A会调用构造方法得到一个空对象,把A的工厂放到三级缓存。给A注入依赖时发现需要B,就去创建B。-->创建B的时候发现需要A,就去一级、二级缓存找,找不到就调用三级缓存的ObjectFactory得到一个“早期A”,把这个早期A放进二级缓存,然后注入到B。B创建完后,返回给A。完成A的初始化。这时候A、B都是成品Bean,放进一级缓存。
  • 一边构造器、一边setter注入->看情况(A构造器、Bsetter):若Spring先创建A,构造器立刻需要B,但B还没创建-->报错❌。若Spring先创建B,B可以先用setter注入半成品A-->✅ 能成功。

六、Spring怎么解决循环依赖

1️⃣ 一级缓存:singletonObjects

  • 存放 完全创建好的单例 Bean(成品)。

  • 以后再来 getBean(),直接从这里拿。

2️⃣ 二级缓存:earlySingletonObjects

  • 存放 提前曝光的单例 Bean(早期引用)

  • 这里的 Bean 已经实例化,但可能还没注入属性、没初始化。

  • 如果别的 Bean 需要,可以先用它占个坑,后面再补全。

3️⃣ 三级缓存:singletonFactories

  • 存放 ObjectFactory(对象工厂),可以生成早期 Bean。

  • 为什么要三级?因为有些 Bean 可能需要 AOP 代理,必须等真正用到的时候才生成代理对象 → 所以放工厂。

👉 三级缓存的目的:保证 Bean 能被提前曝光,同时还能支持 代理增强(AOP)

 

七、为什么要三级缓存?二级不行吗

  A是一个Bean,它需要被代理(比如加了@Transactional),如果只用二级缓存,把原始的A暴露出去,B会拿到“未代理的A”。后面再生成代理对象的时候,B里注入的还是旧的原始对象-->功能失效。

  三级缓存的作用:三级缓存存放的是 ObjectFactory,不是 Bean 本身。当别的 Bean 需要 A 时,可以通过这个工厂去获取“真正的早期对象”:

  • 如果 A 不需要代理,就返回原始对象;

  • 如果 A 需要代理,就在这里生成代理对象返回。

  所以,二级缓存只能解决循环依赖,但无法保证AOP代理生效。

八、@Autowired的实现原理

@Service
public class UserService {
    @Autowired
    private UserDao userDao;
}
  • Spring实例化UserService,AutowiredAnnotationBeanPostProcessor扫描到userDao字段上有@Autowired,容器里找userDao类型的Bean,反射调用:
field.setAccessible(true);
field.set(userService, userDaoImpl);

 


AOP

九、说说什么是AOP

  面向切面编程,简单说就是把一些业务逻辑中的相同的代码抽取到一个独立的模块中,让业务逻辑更加清爽。AOP可以将遍布应用各处的功能分离出来形成可重用的组件。

  AOP的核心就是动态代理,如果实现了接口就用JDK动态代理,否则用CGLIB动态代理。

  核心概念:

  • 切面:切点+通知的组合
  • 切点:要拦截的方法
  • 通知:增强的逻辑
    • 前置通知Before
    • 后置通知After
    • 环绕通知Around
    • 异常通知:AfterThrowing
    • 最终通知AfterReturning
//1.业务类(目标对象)
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void addUser() {
        System.out.println("执行 UserService.addUser()");
    }
}

//2.切面类(ASpect+advice)
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspect {

    // 切点:匹配 UserService 的 addUser 方法
    @Pointcut("execution(* com.example.demo.UserService.addUser(..))")
    public void addUserPointcut() {}

    // 前置通知
    @Before("addUserPointcut()")
    public void beforeAddUser() {
        System.out.println("Before: 准备添加用户");
    }

    // 后置通知
    @After("addUserPointcut()")
    public void afterAddUser() {
        System.out.println("After: 添加用户完成");
    }
}

//3.启动类(开启AOP)
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy // 开启 AOP 支持
public class DemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(DemoApplication.class, args);

        UserService userService = context.getBean(UserService.class);
        userService.addUser();
    }
}

//结果
Before: 准备添加用户
执行 UserService.addUser()
After: 添加用户完成

 

posted @ 2025-09-27 11:10  筱倩  阅读(66)  评论(0)    收藏  举报