你有没有掉进去过这些Spring的“陷阱“(上)

简介: 你有没有掉进去过这些Spring的“陷阱“(上)

一、工程创建

使用IDEA创建一个Spring Boot工程spring-traps,选择基本依赖

f76fa2f4f8c54daa94729445b4b58fb4_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

二、Bean名称的“陷阱”

  Spring通过@Component、@Controller、@Service、@Repository注解将类注入到IoC容器中,默认的Spring Bean的名称是类名首字母小写,TeslaService -> teslaService,如果是TESLAService,那么默认的Bean的名称是什么?

创建一个controller包,增加TeslaController,并增加@Controller注解

@Controller
public class TeslaController {
}
复制代码

新建测试类TeslaControllerTest,测试该类在容器中的名称为teslaController

999783b90e404d9a961d669cab9443a2_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

将TeslaController重命名为TESLAController,再次执行测试,打印出Bean的名称为TESLAController,与原类名相同

c77728fe347b4198a94e34bb8807ad10_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

Bean的名称生成的方法generateBeanName是在BeanNameGenerator 接口中定义的,AnnotationBeanNameGenerator类实现了BeanNameGenerator接口并实现了 generateBeanNamef方法,而该方法默认调用的是buildDefaultBeanName,buildDefaultBeanName方法代码如下:

protected String buildDefaultBeanName(BeanDefinition definition) {
   String beanClassName = definition.getBeanClassName();
   Assert.state(beanClassName != null, "No bean class name set");
   String shortClassName = ClassUtils.getShortName(beanClassName);
   return Introspector.decapitalize(shortClassName);
}
复制代码

该方法又调用了decapitalize,源码如下:

public static String decapitalize(String name) {
    if (name == null || name.length() == 0) {
        return name;
    }
    // 如果长度大于1,且第一个和第二个字符都是大写
    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                    Character.isUpperCase(name.charAt(0))){
        // 直接返回名称            
        return name;
    }
    // 将name转化为字符数组
    char chars[] = name.toCharArray();
    // 将第一个字符变成小写
    chars[0] = Character.toLowerCase(chars[0]);
    // 返回字符串
    return new String(chars);
}
复制代码

decapitalize方法中会将获取的类名进行判断,如果长度大于1且第一个和第二个字母都是大写,那么返回原类名,否则将首字母变成小写返回。

建议:

  1. 规范命名规则,第一个和第二个字符不要都大写
  2. 注解中指定Bean的名称

三、@Autowire的“陷阱”

有时在Controller类中@Autowire注入Service中的类,测试时会出现Service类异常的问题,这大概有以下几种情况

没有把Service类注册到Spring容器中新增一个service包,增加TeslaService

public class TeslaService {
}
复制代码

在TestController类中使增加TeslaService属性,@Controller注解注释

@Controller
public class TeslaController {
    @Autowired
    private TeslaService teslaService;
    public void getServiceBean(){
        System.out.println(teslaService);
    }
}
复制代码

在TeslaControllerTest类中增加测试方法

@Test
public void getServiceBeanBySpring(){
    context = new AnnotationConfigApplicationContext("com.citi");
    TeslaController teslaController = context.getBean(TeslaController.class);
    teslaController.getServiceBean();
}
复制代码

执行测试方法

eab823fbdb1c4a0b9700f0643e465b5a_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

获取对象失败

在Service类中TeslaService类上增加@Service注解 执行getServiceBeanBySpring测试方法,可以正常输出TeslaService对象

94bd90ee471e40699b47ef1c0632409e_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

使用New关键字获取对象

在TeslaControllerTest中新增一个测试方法

@Test
public void getServiceBeanByNew(){
    TeslaController teslaController = new TeslaController();
    teslaController.getServiceBean();
}
复制代码

执行测试,输出TeslaService对象为null

f11ab106d0ec4646b40108f02feaa0b5_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

未成功扫描到Service类在com package下新增一个新的package并命名为outer,新增一个OuterService属性及 getOutServiceBean方法

@Service
public class OuterService {
}
复制代码

在TeslaController中增加属性OuterService及获取OuterService Bean的方法

@Controller
public class TeslaController {
    @Autowired
    private TeslaService teslaService;
    @Autowired
    private OuterService outerService;
    public void getServiceBean(){
        System.out.println(teslaService);
    }
    public void getOutServiceBean(){
       System.out.println(outerService);
    }
}
复制代码

TeslaControllerTest测试类中新增测试方法getOutServiceBean

@Test
public void getOutServiceBeanBySpring(){
    context = new AnnotationConfigApplicationContext("com");
    TeslaController teslaController = context.getBean(TeslaController.class);
    teslaController.getOutServiceBean();
}
复制代码

执行测试方法

41b370a30d384b198b1dc7b64f6d42f5_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

Spring Boot默认扫描主程序类所在的包,也可以使用注解@ComponentScan,自定义扫描的包路径。 在SpringTrapsApplication类上增加注解 @ComponentScan(basePackages = "com")

再次执行测试

cf4a09db981849668d4d88247df008c2_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

@ComponentScan注解属性

  • 默认value属性,也就是basePackages
  • includeFilters,包括指定的packages
  • excludeFilters,排除指定的packages

四、获取应用上下文的“陷阱”

Spring 容器的核心是负责管理对象,管理整个Bean的生命周期,从创建->装配->销毁。而应用上下文是Spring容器的一种实现,也可以用于管理Bean

  • BeanFactory,这是最简答的容器接口,拥有基本的DI功能
  • ApplicationContext,可以解析配置文件,配置管理Bean

新增一个包context,新增一个类ApplicationContextStore用来保存Spring 应用下上文(Application Context),包含了ApplicationContext属性

@Slf4j
public class ApplicationContextStore {
    private static ApplicationContext applicationContext;
    // getter方法
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
    // setter方法
    public static void setApplicationContext(ApplicationContext applicationContext) {
        log.info("Set ApplicationContext");
        ApplicationContextStore.applicationContext = applicationContext;
    }
}
复制代码

自定义初始化类获取应用上下文

新增一个自定义的应用上下文初始化类CustAPIntitializer实现ApplicationContextInitializer

@Slf4j
public class CustAPIntitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        // 设置应用上下文
        ApplicationContextStore.setApplicationContext(applicationContext);
        // 获取应用上下文
        ApplicationContext context = ApplicationContextStore.getApplicationContext();
        log.info("通过" + this.getClass().getSimpleName() + "完成保存ApplicationContext应用上下文");
    }
}
复制代码

在主启动类中注册自定义的CustAPInitializer,修改main方法

public static void main(String[] args) {
    // SpringApplication.run(SpringTrapsApplication.class, args);
    SpringApplication application = new SpringApplication(SpringTrapsApplication.class);
    // 注册自定义的Intitializer
    application.addInitializers(new CustAPIntitializer());
    application.run();
}
复制代码

启动主程序类

2ed8d4ddcd934df283024466004d56dc_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

获取应用上下文成功

自定义监听器获取应用上下文

ApplicationListener是Spring事件通知机制,该机制是基于观察者模式的典型应用

观察者模式是多个观察者对主题对象进行监听,一旦主题对象发生变化会自动通知观察者,Spring中的观察者就是ApplicationListener,可以通过观察者获取事件

增加listener包,新增CustAPListener,泛型填写的就是想要获取的事件ApplicationContextEvent,通过事件可以获取到ApplicationContext

@Slf4j
@Component
public class CustAPListener implements ApplicationListener<ApplicationContextEvent> {
    @Override
    public void onApplicationEvent(ApplicationContextEvent event) {
        ApplicationContextStore.setApplicationContext(event.getApplicationContext());
        ApplicationContext context = ApplicationContextStore.getApplicationContext();
        // 初始化完成
        log.info("通过" + this.getClass().getSimpleName() + "完成保存ApplicationContext应用上下文");
    }
}
复制代码

将Spring Boot启动类中的注册CustAPInitializer代码注释,重新启动主程序

// 注释该段代码
// application.addInitializers(new CustAPIntitializer());
复制代码

1bea29a4bf2f44428f6ff85b612218d1_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

Spring Boot启动程序的返回获取应用上下文

直接修改主程序的main方法,定义变量接收SpringApplication.run的返回

public static void main(String[] args) {
    // SpringApplication.run(SpringTrapsApplication.class, args);
    SpringApplication application = new SpringApplication(SpringTrapsApplication.class);
    // 注册自定义的Intitializer
    // application.addInitializers(new CustAPIntitializer());
    ApplicationContext context = application.run();
    ApplicationContextStore.setApplicationContext(context);
    System.out.println("通过SpringApplication.run()的返回获取到应用上下文");
}
复制代码

将CustAPListener类上的@Component注解注释后,直接启动主程序

3033c0f06c434f01b5908f2ee539e042_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

自定义Aware类获取应用上下文

ApplicationContextAware:Spring的Aware接口,即获取Spring容器的接口

新建一个aware包,新增一个CustAPAware

@Slf4j
@Component
public class CustAPAware implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextStore.setApplicationContext(applicationContext);
        log.info("通过" + this.getClass().getSimpleName() + "完成保存ApplicationContext应用上下文");
    }
}
复制代码

将主程序中这两段代码注释掉

// ApplicationContextStore.setApplicationContext(context);
// System.out.println("通过SpringApplication.run()的返回获取到应用上下文");
复制代码

重新启动主程序

1abca286f6184c1888187b0dcb82fa47_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

通过实现ApplicationContextAware接口封装一个应用上下文的工具类ApplicationContextUtil

新增一个utils包,增加工具类ApplicationContextUtil,封装ApplicationContext接口中getBean的三种方式。

@Slf4j
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (ApplicationContextUtil.applicationContext == null){
            ApplicationContextUtil.applicationContext = applicationContext;
        }
    }
    public static  ApplicationContext getApplicationContext(){
        return ApplicationContextUtil.applicationContext;
    }
    // 封装getBean方法
    public static Object getBeanByName(String name){
        log.info("通过Bean Name获取Bean的方法被调用");
        return getApplicationContext().getBean(name);
    }
    public static <T> T getBeanByClass(Class<T> tClass){
        log.info("通过Bean Type获取Bean的方法被调用");
        return getApplicationContext().getBean(tClass);
    }
    public static <T> T getBeanByNameAndClass(String name, Class<T> tClass){
        log.info("通过Bean Name和Bean Type获取Bean的方法被调用");
        return getApplicationContext().getBean(name,tClass);
    }
}
复制代码

五、多实例的Spring Bean中的“陷阱”

默认生成的Bean时单例的,所有线程共享的。根据Bean中是否定义了一些变量存储全局信息可以将Bean划分为有状态的Bean和无状态的Bean。

验证默认情况下生成的Bean是单例的

新建一个scope包,新增加一个类

@Slf4j
@Component
public class Porsche {
    // 定义一个全局变量
    private List<String> porscheNames = null;
    @PostConstruct
    public void init(){
        log.info(this.getClass().getSimpleName() + "开始初始化");
        this.porscheNames = new ArrayList<>(100);
    }
    public void add(String name){
        this.porscheNames.add(name);
    }
    public int getSize(){
        return this.porscheNames.size();
    }
    public List<String> getPorscheNames(){
        return this.porscheNames;
    }
}
复制代码

新增测试类PorscheTest,对Porsche类进行测试

public class PorscheTest extends SpringTrapsApplicationTests {
    @Test
    public void testBeanStatus(){
        Porsche porsche1 = ApplicationContextUtil.getBeanByClass(Porsche.class);
        Porsche porsche2 = ApplicationContextUtil.getBeanByClass(Porsche.class);
        porsche1.add("Taycan");
        porsche1.add("Macan");
        List<String> porscheNames1 = porsche1.getPorscheNames();
        System.out.println(porscheNames1);
        porsche2.add("Porsche 911");
        System.out.println(porscheNames1.toString());
    }
}
复制代码

执行测试用例

3d248153eb2043e98c20781bc06ca6df_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

单例模式下多线程操作统一个Bean,会导致Bean状态不一致的现象

测试是否为单例

@Test
public void testSingleton(){
    Porsche porsche1 = ApplicationContextUtil.getBeanByClass(Porsche.class);
    Porsche porsche2 = ApplicationContextUtil.getBeanByClass(Porsche.class);
    System.out.println("是否为单例:" + (porsche1 == porsche2));
}
复制代码

执行测试

4ca3f79bbe59463795a760a04e239d2f_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

获取的两个Bean相等,是同一个Bean,是单例的

多实例模式(原型模式prototype)

Porsche类上增加@Scope注解,设置为多实例模式@Scope("prototype") 再次执行测试类PorscheTest中的两个测试方法

d3d6534519fa40b1a0e2a25eac29c04c_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.png

此时已变成多例模式,对其中一个Bean的操作不会影响另外一个的状态,从容器中获取的两个Bean并不相同。

单例Bean的优势:

  • 减少新生成实例的消耗,减少了创建也就减少了垃圾回收,节省内存空间,并且可以快速的获取到Bean 单例Bean的劣势:
  • 线程不安全


相关文章
|
Java Maven Spring
你有没有掉进去过这些Spring Boot中的“陷阱“(上)(二)
你有没有掉进去过这些Spring Boot中的“陷阱“(上)
你有没有掉进去过这些Spring Boot中的“陷阱“(上)(二)
|
前端开发 Java 数据库连接
你有没有掉进去过这些Spring Boot中的“陷阱“(上)(一)
你有没有掉进去过这些Spring Boot中的“陷阱“(上)
你有没有掉进去过这些Spring Boot中的“陷阱“(上)(一)
|
前端开发 安全 Java
你有没有掉进去过这些Spring MVC中的“陷阱“(下)
你有没有掉进去过这些Spring MVC中的“陷阱“(下)
你有没有掉进去过这些Spring MVC中的“陷阱“(下)
|
JSON 前端开发 Java
你有没有掉进去过这些Spring MVC中的“陷阱“(上)
你有没有掉进去过这些Spring MVC中的“陷阱“(上)
你有没有掉进去过这些Spring MVC中的“陷阱“(上)
|
XML 存储 缓存
你有没有掉进去过这些 Spring 的“陷阱“(下)
你有没有掉进去过这些 Spring 的“陷阱“(下)
你有没有掉进去过这些 Spring 的“陷阱“(下)
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
166 2
|
3月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
13天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
26 2
 SpringBoot入门(7)- 配置热部署devtools工具
|
9天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
21 2
下一篇
无影云桌面