Spring编程常见错误

94 阅读8分钟

Spring Core

隐式扫描不到Bean的定义

对于 Spring Boot 而言,关键点在于 Application.java 中使用了 SpringBootApplication 注解。而这个注解继承了另外一些注解,具体定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
      @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//省略非关键代码
}

当 Spring Boot 启动时,ComponentScan 的启用意味着会去扫描出所有定义的 Bean,扫描位置由ComponentScan中的basePackages决定,当 basePackages 为空时,扫描的包会是 declaringClass 所在的包,即使用了SpringBootApplication注解所在的类的包。也可以使用ComponentScans显示声明需要扫描的包。

public @interface ComponentScan {

/**
 * Base packages to scan for annotated components.
 * <p>{@link #value} is an alias for (and mutually exclusive with) this
 * attribute.
 * <p>Use {@link #basePackageClasses} for a type-safe alternative to
 * String-based package names.
 */
@AliasFor("value")
String[] basePackages() default {};
//省略其他非关键代码
}

原型Bean被固定

当一个单例的 Bean,使用 autowired 注解标记其属性时,你一定要注意这个属性值会被固定下来。即使这个属性的Bean使用了@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)。即可通过反射设置给对应的 field,这个 field 的执行只发生了一次,所以后续就固定起来了,它并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYPE 而改变。可以通过applicationContext.getBean(ServiceImpl.class)解决。

过多赠与,无所适从

image.png

  • 当一个 Bean 被构建时,核心包括两个基本步骤: 执行 AbstractAutowireCapableBeanFactory#createBeanInstance 方法:通过构造器反射构造出这个 Bean,在此案例中相当于构建出 StudentController 的实例;
  • 执行 AbstractAutowireCapableBeanFactory#populate 方法:填充(即设置)这个 Bean,在本案例中,相当于设置 StudentController 实例中被 @Autowired 标记的 dataService 属性成员。

这个问题在于我们依赖的Bean有多个实现类,填充这个被依赖的Bean时有多个候选,所以无法选择。解决办法是可以通过将属性名和 Bean 名字精确匹配,这样就可以让注入选择不犯难。

通过bean 名字引用时忽略大小写

可以通过@Qualifier(beanName)注解,显示注入某个Bean,如果Bean没有显示指定名称,需要注意名称首字母大小写问题。这是没有显示指定名称生成beanName的核心代码,如果一个类名是以两个大写字母开头的,则首字母不变,其它情况下默认首字母变成小写。

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);
}

也可以显示指定beanName:

@Repository("CassandraDataService")
public class CassandraDataService implements DataService {
  //省略实现
}

引用内部类的Bean遗忘类名

假如定义了如下的内部类bean:

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

引用时需要通过如下方式:

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

@Value没有注入预期值

一般都会因为 @Value 常用于 String 类型的装配而误以为 @Value 不能用于非内置对象的装配,实际上这是一个常见的误区,不仅仅可以支持注入字符串。我们使用 @Value 更多是用来装配 String,而且它支持多种强大的装配方式,典型的方式参考下面的示例:

@Value("我是字符串")
private String text; 

//注入系统参数、环境变量或者配置文件中的值
@Value("${ip}")
private String ip

//注入其他Bean属性,其中student为bean的ID,name为其属性
@Value("#{student.name}")
private String name;

但是需要注意标记了@Value注解的属性,注入在解析 @Value传入的 字符串时,其实是有顺序的(查找的源是存在 CopyOnWriteArrayList 中,在启动时就被有序固定下来),一个一个“源”执行查找,在其中一个源找到后,就可以直接返回了。来源包括系统参数、环境变量或者配置文件中的值,所以我们注入时需要避免命名冲突。

注入集合

如下代码,需要注入students时,可以通过直接装配和收集方式注入。

@RestController
@Slf4j
public class StudentController {

    private List<Student> students;

    public StudentController(List<Student> students){
        this.students = students;
    }

    @RequestMapping(path = "students", method = RequestMethod.GET)
    public String listStudents(){
       return students.toString();
    };

}

收集方式:

@Bean
public Student student1(){ return createStudent(1, "xie");}

@Bean
public Student student2(){ return createStudent(2, "fang");}

直接装配

@Bean
public List<Student> students(){
    Student student3 = createStudent(3, "liu");
    Student student4 = createStudent(4, "fu");
    return Arrays.asList(student3, student4);
} 

但是不能两种方式混用,不能一部分元素通过第一种,一部分元素通过第二种来同时注入。

构造器内抛空指针异常

以下代码会在执行构造器方法时抛出空指针异常:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService {
  @Autowired
  private LightService lightService;
  public LightMgrService() {
    lightService.check();
  }
}

原因在于Spring 初始化单例类的一般过程,基本都是 getBean()->doGetBean()->getSingleton(),如果发现 Bean 不存在,则调用 createBean()->doCreateBean() 进行实例化,而用来实例化 Bean 的 createBeanInstance 方法通过依次进行实例化 Bean,注入 Bean 依赖,以及初始化 Bean (例如执行 @PostConstruct 标记的方法 ),,现在我们知道了问题的根源,就是在于使用 @Autowired 直接标记在成员属性上而引发的装配行为是发生在构造器执行之后的。

纠正方法: 第一种是隐式调用,当使用下面的代码时,构造器参数 LightService 会被自动注入 LightService 的 Bean,从而在构造器执行时,不会出现空指针。可以说,使用构造器参数来隐式注入是一种 Spring 最佳实践。

@Component
public class LightMgrService {

    private LightService lightService;

    public LightMgrService(LightService lightService) {
        this.lightService = lightService;
        lightService.check();
    }
}

第二种,使用@PostConstruct注解,这个注解标记的方法执行在注入Bean依赖之后:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService {
  @Autowired
  private LightService lightService;
  
  @PostConstruct
  public void init() {
       lightService.check();
  }
}

第三种,实现InitializingBean接口,重写afterPropertiesSet方法来实现相关逻辑,因为这个方法是在初始化时才执行,此时Bean已经注入。

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class LightMgrService implements InitializingBean {
    @Autowired
    private LightService lightService;
  
    @Override
    public void afterPropertiesSet() throws Exception {
        lightService.check();
    }
}

Spring Web

当 @PathVariable 遇到 /

如下代码:

@RestController
@Slf4j
public class HelloWorldController {
    @RequestMapping(path = "/hi1/{name}", method = RequestMethod.GET)
    public String hello1(@PathVariable("name") String name){
        return name;
        
    };  
}

在name中不要包含特殊字符 ‘/’ ,例如 /hi1/xiao/ming, 其中在将Path解析为Controller中的执行方法,进而寻找执行方法时/hi1/xiao/min匹配"/hi1/{name}"不会成功。

错误使用 @RequestParam、@PathVarible 等注解

在使用这两个注解时,要明确的指定参数。比如下面的代码,在本地测试时能够通过,但是线上运行就会报错。

@RequestMapping(path = "/hi1", method = RequestMethod.GET)
public String hi1(@RequestParam("name") String name){
    return name;
};

@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestParam String name){
    return name;
};

原因是在在本地测试时,IDEA通常会开启下面两个参数:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
   <configuration>
        <debug>true</debug>
        <parameters>true</parameters>
    </configuration>
</plugin>

这两个参数控制了一些 debug 信息是否加进 class 文件中。我们可以开启这两个参数来编译,然后使用下面的命令来查看信息

javap -verbose HelloWorldController.class

image.png

在运行过程中,如果没有指定RequestParam的参数,那MethodParameters中的name为空,不能正常运行。

未考虑参数是否可选

在 使用@RequestParam, 需要注明参数是否必须没有指明默认值时,会查看这个参数是否必须,如果必须但是没有传,则按错误处理。

public @interface RequestParam {  
  
/**  
* Alias for {@link #name}.  
*/  
@AliasFor("name")  
String value() default "";  
  
/**  
* The name of the request parameter to bind to.  
* @since 4.2  
*/  
@AliasFor("value")  
String name() default "";  
  
/**  
* Whether the parameter is required.  
* <p>Defaults to {@code true}, leading to an exception being thrown  
* if the parameter is missing in the request. Switch this to  
* {@code false} if you prefer a {@code null} value if the parameter is  
* not present in the request.  
* <p>Alternatively, provide a {@link #defaultValue}, which implicitly  
* sets this flag to {@code false}.  
*/  
boolean required() default true;  
  
/**  
* The default value to use as a fallback when the request parameter is  
* not provided or has an empty value.  
* <p>Supplying a default value implicitly sets {@link #required} to  
* {@code false}.  
*/  
String defaultValue() default ValueConstants.DEFAULT_NONE;  
  
}
  • 设置 @RequestParam 的默认值
  • 设置 @RequestParam 的 required 值, 非必须为false
  • 标记任何名为 Nullable 且 RetentionPolicy 为 RUNTIME 的注解
  • 参数使用Optional类型

对象参数校验私效

即对于 RequestBody 接受的对象参数而言,要启动 Validation,必须将对象参数标记上 @Validated 或者其他以 @Valid 关键字开头的注解。

import lombok.Data;
import javax.validation.constraints.Size;
@Data
public class Student {
    @Size(max = 10)
    private String name;
    private short age;
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@Validated
public class StudentController {
    @RequestMapping(path = "students", method = RequestMethod.POST)
    public void addStudent(@Validated @RequestBody Student student){
        log.info("add new student: {}", student.toString());
        //省略业务代码
    };
}

嵌套参数校验

例如:

public class Student {
    @Size(max = 10)
    private String name;
    private short age;   
    private Phone phone;
}
@Data
class Phone {
    @Size(max = 10)
    private String number;
}

对于phone.number的校验不会生效,因为在校验student时吗,根据成员字段是否标记了 @Valid 来决定(记录)这个字段以后是否做级联校验,所以如果phone的熟悉需要继续校验,需要phone加上注解@Valid。

嵌套事物回滚

Spring 在处理事务过程中,propagation有个默认的传播属性 REQUIRED,在整个事务的调用链上,任何一个环节抛出的异常都会导致全局回滚。当子事务声明为 Propagation.REQUIRES_NEW 时,会创建新的事务,让这个子事务单独回滚,不会影响到主事务。