一、工程创建
使用IDEA创建一个Spring Boot工程spring-traps,选择基本依赖
二、Bean名称的“陷阱”
Spring通过@Component、@Controller、@Service、@Repository注解将类注入到IoC容器中,默认的Spring Bean的名称是类名首字母小写,TeslaService -> teslaService,如果是TESLAService,那么默认的Bean的名称是什么?
创建一个controller包,增加TeslaController,并增加@Controller注解
@Controller public class TeslaController { } 复制代码
新建测试类TeslaControllerTest,测试该类在容器中的名称为teslaController
将TeslaController重命名为TESLAController,再次执行测试,打印出Bean的名称为TESLAController,与原类名相同
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且第一个和第二个字母都是大写,那么返回原类名,否则将首字母变成小写返回。
建议:
- 规范命名规则,第一个和第二个字符不要都大写
- 注解中指定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(); } 复制代码
执行测试方法
获取对象失败
在Service类中TeslaService类上增加@Service注解 执行getServiceBeanBySpring测试方法,可以正常输出TeslaService对象
使用New关键字获取对象
在TeslaControllerTest中新增一个测试方法
@Test public void getServiceBeanByNew(){ TeslaController teslaController = new TeslaController(); teslaController.getServiceBean(); } 复制代码
执行测试,输出TeslaService对象为null
未成功扫描到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(); } 复制代码
执行测试方法
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属性
@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(); } 复制代码
启动主程序类
获取应用上下文成功
自定义监听器获取应用上下文
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()); 复制代码
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注解注释后,直接启动主程序
自定义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()的返回获取到应用上下文"); 复制代码
重新启动主程序
通过实现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()); } } 复制代码
执行测试用例
单例模式下多线程操作统一个Bean,会导致Bean状态不一致的现象
测试是否为单例
@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的劣势:
- 线程不安全