我是靠谱客的博主 坚定彩虹,最近开发中收集的这篇文章主要介绍Spring第四讲:使用Spring过程中遇到的问题,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

本文是Spring第四讲:在使用Spring过程中遇到的问题review

文章目录

      • 问题的来源
      • 1、坑点1 Spring Bean的定义常见错误
        • 案例1:隐式扫描不到Bean的定义
        • 案例2:定义的Bean缺少隐式依赖
        • 案例3:原型Bean被固定
      • 2、坑点2 Spring Bean依赖注入常见错误
        • 案例1:required a single bean, but 2 were found
        • 案例2:显式引用Bean时首字母忽略大小写
        • 案例3:引用内部类的Bean遗忘类名
        • 案例4:@Value没有注入预期的值
        • 案例5:@Value没有注入预期的值
      • 3、坑点3 Spring Bean依赖注入常见错误
      • 2 使用Spring AOP实现日志系统 20210322

问题的来源

1、我和同事在生产环境中经常遇到问题;
2、Stack Overflow 网站上的一些高频问题;
3、常用搜索引擎检索到的一些高频问题。

Spring可能产生的问题如图所示:
在这里插入图片描述
Spring Core:Spring Core 包括 Bean 定义、注入、AOP 等核心功能,可以说它们是Spring 的基石。不管是做 Spring Web 开发,还是使用 Spring Cloud 技术栈,都绕不开这些功能。重点介绍在这些功能使用上的常见问题。
Spring Web 篇:大多项目使用 Spring 还是为了进行 Web 开发,所以我也梳理了从请求URL 解析、Header 解析、Body 转化到授权等 Web 开发中绕不开的问题。不难发现,它们正好涵盖了从一个请求到来,到响应回去这一完整流程。
Spring 补充篇:这部分会重点介绍 Spring 测试、Spring 事务、SpringData 相关问题。最后,系统总结下 Spring 使用中发生问题的根本原因。

1、坑点1 Spring Bean的定义常见错误

使用@Bean注解的向容器中注册组件,我们团队的瓦力同学就踩过这个坑

  • 我们在使用注解方式向Spring的IOC容器中注入JavaBean时,如果没有在@Bean注解中明确指定bean的名称,就使用当前方法的名称(首字母小写)来作为bean的名称;如果在@Bean注解中明确指定了bean的名称,则使用@Bean注解中指定的名称来作为bean的名称。

案例1:隐式扫描不到Bean的定义

背景:使用 Spring Boot 来快速构建Web应用,包结构如下:
在这里插入图片描述
然后发现,我们找不到HelloWorldController 这个 Bean 了。

原因: SpringBootApplication注解 —》ComponentScan注解(当 Spring Boot 启动时,ComponentScan的启用意味着会去扫描出所有定义的 Bean)

  • 扫描的位置由basePackages属性指定,默认为启动类所在的包

解决方案:显式配置 @ComponentScan。

@SpringBootApplication
// 支持多个包的扫描范围指定 
@ComponentScans(value = { @ComponentScan(value ="com.spring.puzzle.class1.example1.controller") })
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

案例2:定义的Bean缺少隐式依赖

背景:把一个类定义成bean

@Service
public class ServiceImpl {
	private String serviceName;
	public ServiceImpl(String serviceName){
		this.serviceName = serviceName;
	}
}

报下面这种错误:

Parameter 0 of constructor in com.spring.puzzle. class1.example2. ServiceImpl required a bean of type ‘java.lang.String’ that could not be found.

原因:创建bean是通过寻找构造器和通过反射调用构造器创建实例。
在使用 Spring时,我们不能直接显式使用 new 关键字来创建实例。Spring只能是去寻找依赖来作为构造器调用参数。
我们可以调用 createArgumentArray 方法来构建调用构造器的参数数组,而这个方法的最终实现是从 BeanFactory 中获取 Bean,
在这里插入图片描述
根据参数来寻找对应的 Bean,在本案例中,如果找不到对应的Bean 就会抛出异常,提示装配失败。

解决方案:我们定义一个类为 Bean,如果再显式定义了构造器,那么这个 Bean 在构建时,会自动根据构造器参数定义寻找对应的 Bean,然后反射创建出这个 Bean。

@Bean
public String serviceName(){
	return "MyServiceName";
}

补充:倘若serviceName的类型变为List,即如下代码所示,还会报错吗?

@Service
public class ServiceImpl {
	private List<String> serviceNames;
	public ServiceImpl(List<String> serviceNames){
		this.serviceNames = serviceNames;
		System.out.println(this.serviceNames);
	}
}

运行程序会发现这并不会报错,而是输出 []。
原因:我们可以直接定位构建构造器调用参数的代码所在地(即ConstructorResolver #resolveAutowiredArgument)

案例3:原型Bean被固定

背景:在定义 Bean 时,有时候我们会使用原型 Bean(即每次请求生成新的对象),

// bean的定义
@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}

// bean的使用
@RestController
public classHelloWorldController{
	@Autowired
	private ServiceImpl serviceImpl;
	@RequestMapping(path = "hi", method = RequestMethod.GET)
	public String hi () {
		return "helloworld, service is:" + serviceImpl;
	}
}

结果,我们会发现,不管我们访问多少次 http://localhost:8080/hi,访问的结果都是不变的。

原因:当一个属性成员 serviceImpl 声明为 @Autowired 后,那么在创建HelloWorldController 这个 Bean 时,会先使用构造器反射出实例,然后来装配各个标记为 @Autowired 的属性成员。

关键执行代码

protected void inject(Object bean, @Nullable String beanName, @Nullable Proper ) {
	Field field = (Field) this.member;
	Object value;
	//寻找“bean”
	if(this.cached) {
		value = resolvedCachedArgument(beanName, this.cachedFieldValue);
	} else {
		// 省略其他非关键代码
		value = beanFactory.resolveDependency(desc,beanName,autowiredBeanNames,	);		
	}
	if(value!=null){
		//将bean设置给成员字段
		ReflectionUtils.makeAccessible(field);
		field.set(bean, value);
	}
}

从代码可知:待我们寻找到要自动注入的 Bean 后,即可通过反射设置给对应的 field。这个 field 的执行只发生了一次,所以后续就固定起来了,它并不会因为 ServiceImpl 标记了SCOPE_PROTOTYPE 而改变

解决方案
有两种,第一种是自动注入 ApplicationContext

@RestController
public class HelloWorldController {
	@Autowired
	private ApplicationContext applicationContext;
	@RequestMapping(path = "hi", method = RequestMethod.GET)
	public String hi(){
		return "helloworld,service is:" + getServiceImpl();
	}
	public ServiceImpl getServiceImpl(){
		return applicationContext.getBean(ServiceImpl.class);
	}
}

第二种方法是使用 Lookup 注解
添加一个 getServiceImpl 方法,不过这个方法是被 Lookup 标记的。代码如下:

@RestController
public class HelloWorldController { 
	@RequestMapping(path = "hi", method = RequestMethod.GET)
	public String hi() {
		return "helloworld, service is: " + getServiceImpl();
	}
	@Lookup
	public ServiceImpl getServiceImpl(){
		return null;
	}
}

Lookup 是如何生效的?
在这里插入图片描述因为标记了 Lookup 而走入了CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor,这个方法的关键实现参考 LookupOverrideMethodInterceptor#intercept:

private final BeanFactoryowner;
public Object intercept(Object obj, Method method, Object[] args, MethodProxy) {
	LookupOverride lo =(LookupOverride)getBeanDefinition().
getMethodOverrides();
	Assert.state(lo!= null, "LookupOverride not found");
	Object[] argsToUse = (args.length > 0 ? args:null);
	//if no-arg,don't
	if(StringUtils.hasText(lo.getBeanName())) {
		return(argsToUse!=null this.owner.getBean(lo.getBeanName(),argsToU)
		this.owner.getBean(lo.getBeanName()));
	} else {
		return(argsToUse!=null?this.owner.getBean(method.getReturnType(),a)
			this.owner.getBean(method.getReturnType()));
	}
}

方法调用最终并没有走入案例代码实现的 return null 语句,而是通过BeanFactory来获取 Bean。所以从这点也可以看出,其实在我们的 getServiceImpl 方法实现中,随便怎么写都行,这不太重要。


2、坑点2 Spring Bean依赖注入常见错误

案例1:required a single bean, but 2 were found

背景:我们在开发一个学籍管理系统案例,需要提供一个 API 根据学生的学号(ID)来移除学生,学生的信息维护肯定需要一个数据库来支撑,所以大体上可以实现如下:

@RestController
@Slf4j
@Validated
public class StudentController {
	@Autowired
	DataService dataService;
	@RequestMapping(path = "students/{id}", method = RequestMethod.DELETE)
	public void deleteStudent(@PathVariable("id") @Range(min = 1,max= 100) id) {
		dataService.deleteStudent(id);
	}
}

//DataService 是一个接口,其实现依托于 Oracle
public interface DataService {
	void deleteStudent(int id);
}
@Repository
@Slf4j
public class OracleDataService implements DataService{
	@Override
	public void deleteStudent(int id) {
		log.info("delete student info maintained by oracle");
	}
}

@Repository
@Slf4j
public class CassandraDataService implements DataService{
	@Override
	public void deleteStudent(int id) {
		log.info("delete student info maintained by cassandra");
	}
}

程序启动时,报错信息如下:
在这里插入图片描述
原因:需要对@Autowired 实现的依赖注入的原理有一定的了解。
当一个 Bean 被构建时,核心包括两个基本步骤:
1、执行 AbstractAutowireCapableBeanFactory#createBeanInstance 方法:通过构造器
反射构造出这个 Bean,在此案例中相当于构建出 StudentController 的实例;
2、执行 AbstractAutowireCapableBeanFactory#populate 方法:填充(即设置)这个Bean,在本案例中,相当于设置 StudentController 实例中被 @Autowired 标记的dataService属性成员。

  • “填充”过程的关键就是执行各种 BeanPostProcessor 处理器(找出合适的 DataService 的 bean 并设置给StudentController#dataService)
    • 1、寻找出所有需要依赖注入的字段和方法,
    • 2、根据依赖信息寻找出依赖并完成注入,
@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable Property){
	Field field =(Field)this.member;
	Object value;
	//省略非关键代码
		try {
			DependencyDescriptor desc = new DependencyDescriptor(field,this.req);
			//寻找“依赖”,desc为"dataService"的DependencyDescriptor
			value = beanFactory.resolveDependency(desc, beanName,autowiredBeanNa); //重点
		}
	}
	//省略非关键代码
	if(value!=null){
		ReflectionUtils.makeAccessible(field);
		//装配“依赖”
		field.set(bean,value);
	}
}

调试信息如下所示:
在这里插入图片描述
我们根据 DataService 这个类型来找出依赖时,我们会找出 2 个依赖,分别为 CassandraDataService 和 OracleDataService。在这样的情况下,如果同时满足以下两个条件则会抛出本案例的错误:
1、调用 determineAutowireCandidate 方法来选出优先级最高的依赖,但是发现并没有优先级可依据。

  • 优先级的决策是先根据 @Primary 来决策,其次是 @Priority 决策,最后是根据 Bean 名字的严格匹配来决策

2、@Autowired 要求是必须注入的(即 required 保持默认值为 true),或者注解的属性类型并不是可以接受多个 Bean 的类型,例如数组、Map、集合。

解决方案打破上述两个条件中的任何一个即可,即让候选项具有优先级或压根可以不去选择
要同时支持多种 DataService,且能在不同业务情景下精确匹配到要选择到的 DataService,我们可以使用下面的方式去修改:

// 根据 Bean 名字的严格匹配来决策
@Autowired
DataService oracleDataService;
  • 修改方式的精髓在于将属性名和 Bean 名字精确匹配,这样就可以让注入选择不犯难:需要 Oracle 时指定属性名为 oracleDataService,需要 Cassandra 时则指定属性名为 cassandraDataService。

解决方案2:即采用 @Qualifier 来显式指定引用的是那种服务,例如采用下面的方式:

@Autowired
@Qualifier("cassandraDataService")
DataService dataService;

Bean 只有一个(即精确匹配),所以压根不会出现后面的决策过程。

案例2:显式引用Bean时首字母忽略大小写

背景:使用 @Qualifier 时,我们有时候会犯另一个经典的小错误,就是我们可能会忽略Bean 的名称首字母大小写。

@Autowired
@Qualifier("CassandraDataService")
DataService dataService;

运行程序,会报错如下:

Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘studentController’: Unsatisfied dependency expressed through field ‘dataService’; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No
qualifying bean of type ‘com.spring.puzzle.class2.example2.DataService’ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true), @org.springframework.beans.factory.annotation.Qualifier(value=CassandraDataService)}

原因:对于 Bean 的名字,如果没有显式指明,就应该是类名,不过首字母应该小写。
@Qualifier注解解析的大体流程只有两步:看 Bean 有没有显式指明名称,如果有则用显式名称,如果没有则产生一个默认名称。很明显,在我们的案例中,是没有给 Bean 指定名字的,所以产生的Bean 的名称就是生成的默认名称,查看默认名的产生方buildDefaultBeanName。

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

public static String decapitalize(String name) {
	if( name==null||name.length()==0){
		return name;
	}
	if(name.length()>1 && Character.isUpperCase(name.charAt(1))&& Character.isUpperCase(name.charAt(0))){
		return name;
	}
	char chars[] = name.toCharArray();
	chars[0] = Character.toLowerCase(chars[0]);
	return new String(chars);
}

结论:如果一个类名是以两个大写字母开头的,则首字母不变,其它情况下默认首字母变成小写。
SQLiteDataService 的 Bean,其名称应该就是类名本身,而 CassandraDataService 的Bean 名称则变成了首字母小写(cassandraDataService)

解决方案
方案1:引用处纠正首字母大小写问题:

  • 倘若熟悉源码,使用这种方式
@Autowired
@Qualifier("cassandraDataService")
DataService dataService;

方案2:定义处显式指定 Bean 名字,我们可以保持引用代码不变,而通过显式指明CassandraDataService 的 Bean 名称为 CassandraDataService 来纠正这个问题。

  • 倘若不想纠结于首字母到底是大写还是小写,使用这种方式避免困扰
@Repository("CassandraDataService")
@Slf4j
public class CassandraDataService implements DataService {
	//省略实现
}

案例3:引用内部类的Bean遗忘类名

背景:需要定义一个内部类来实现一种新的 DataService

public class StudentController {
	@Repository
	public static class InnerClassDataService implements DataService{
		@Override
		public void deleteStudent(int id) {
			//空实现
		}
	}
	//省略其他非关键代码
}


// 显示引用该bean
@Autowired
@Qualifier("innerClassDataService")
DataService innerClassDataService;

报错“找不到 Bean”。

原因
Spring对 class 名字的处理,

public static String getShortName(String className) {
	Assert.hasLength(className,"Class name must not be empty");
	int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
	int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR);
	if(nameEndIndex==-1){
		nameEndIndex=className.length();
	}
	String shortName = className.substring(lastDotIndex + 1, nameEndIndex);
	shortName = shortName.replace(INNER_CLASS_SEPARATOR,PACKAGE_SEPARATOR);
	return shortName;
}

在经过这个方法的处理后,我们得到的其实是下面这个名称:StudentController.InnerClassDataService
最后经过 Introspector.decapitalize 的首字母变换,最终获取的 Bean 名称如下:studentController.InnerClassDataService

解决方案

@Autowired
@Qualifier("studentController.InnerClassDataService")
DataService innerClassDataService;

案例4:@Value没有注入预期的值

背景
原因

解决方案

案例5:@Value没有注入预期的值

背景
原因

解决方案


3、坑点3 Spring Bean依赖注入常见错误

背景

原因

解决方案

2 使用Spring AOP实现日志系统 20210322

该日志系统使用的技术有
1、Spring AOP 用于在指定的方法执行前后做一些额外的逻辑处理
2、RocketMQ 用来解耦日志的生产和消费逻辑,防止由于引入日志而打挂系统
3、Apollo 用作配置中心,起是否记录日志的开关作用
遇到的问题
1、在切入点的后置处理逻辑中,应当使用@AfterReturning注解
@AfterReturning与@After的区别

  • @AfterReturning 返回通知方法 连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行
  • @After 后置通知方法 后置方法在连接点方法完成之后执行,无论连接点方法执行成功还是出现异常,都将执行后置方法

而且在使用@AfterReturning时,不要使用try catch捕获异常,不然,返回通知方法还是会执行。
关于AspectJ切面注解中的五种通知注解

  • @Before 前置通知,在方法执行之前执行
  • @After 后置通知,在方法执行之后执行
  • @AfterReturning 返回通知,在方法有返回结果后执行
    • returning 能够将目标方法的返回值传到切面增强方法里
@AfterReturning(pointcut = "testAfterReturing()",returning = "rvt")
public void logTestAfterReturing4(String rvt){
    System.out.println("测试AfterReturning---returning:"+str);
}
  • @AfterThrowing 异常通知,在方法抛出异常后执行
  • @Around 环绕通知,围绕着方法执行
    具体可以看这篇文章:https://blog.csdn.net/u010502101/article/details/78823056

最后

以上就是坚定彩虹为你收集整理的Spring第四讲:使用Spring过程中遇到的问题的全部内容,希望文章能够帮你解决Spring第四讲:使用Spring过程中遇到的问题所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(48)

评论列表共有 0 条评论

立即
投稿
返回
顶部