一、工程创建
使用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且第一个和第二个字母都是大写,那么返回原类名,否则将首字母变成小写返回。
建议:
- 规范命名规则,第一个和第二个字符不要都大写
- 注解中指定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的劣势:
- 线程不安全