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

2022-08-19 16:36:02 浏览数 (1)

一、工程创建

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

二、Bean名称的“陷阱”

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

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

代码语言:javascript复制
@Controller
public class TeslaController {
}

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

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

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

代码语言:javascript复制
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,源码如下:

代码语言:javascript复制
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

代码语言:javascript复制
public class TeslaService {

}

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

代码语言:javascript复制
@Controller
public class TeslaController {

    @Autowired
    private TeslaService teslaService;

    public void getServiceBean(){
        System.out.println(teslaService);
    }
}

在TeslaControllerTest类中增加测试方法

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

执行测试方法

获取对象失败

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

使用New关键字获取对象

在TeslaControllerTest中新增一个测试方法

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

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

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

代码语言:javascript复制
@Service
public class OuterService {

}

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

代码语言:javascript复制
@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

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

执行测试方法

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

再次执行测试

@ComponentScan注解属性

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

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

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

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

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

代码语言:javascript复制
@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

代码语言:javascript复制
@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方法

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

启动主程序类

获取应用上下文成功

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

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

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

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

代码语言:javascript复制
@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代码注释,重新启动主程序

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

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

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

代码语言:javascript复制
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注解注释后,直接启动主程序

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

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

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

代码语言:javascript复制
@Slf4j
@Component
public class CustAPAware implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextStore.setApplicationContext(applicationContext);
        log.info("通过"   this.getClass().getSimpleName()   "完成保存ApplicationContext应用上下文");
    }
}

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

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

重新启动主程序

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

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

代码语言:javascript复制
@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包,新增加一个类

代码语言:javascript复制
@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类进行测试

代码语言:javascript复制
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());
    }
}

执行测试用例

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

测试是否为单例

代码语言:javascript复制
@Test
public void testSingleton(){

    Porsche porsche1 = ApplicationContextUtil.getBeanByClass(Porsche.class);
    Porsche porsche2 = ApplicationContextUtil.getBeanByClass(Porsche.class);
    System.out.println("是否为单例:"   (porsche1 == porsche2));
}

执行测试

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

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

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

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

单例Bean的优势:

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

0 人点赞