推荐基于Lombok的Spring注入方式(基于构造器注入)及快速获取Spring容器中管理的对象

背景

Java的一大特性是封装,我们经常在实际开发中会封装工具类,甚至是慢慢积累自己的通用工具类,然后用于之后的项目开发。

我们在封装工具类的时候,常常会将工具方法设置为static,方便我们使用类名.方法名快速调用。但有些时候,工具方法封装为static会出现问题,最常见的空指针异常(稍后举例)。所以我们不能那样封装。

如果使用普通方法,正常情况下我们想要去调用的话,就要先实例化类的对象,然后使用对象.方法名去调用方法。这样的话会引发一些问题。

  • 每次都要实例化对象,然后调用,在代码层面上不够简洁
  • 如果方法使用的比较频繁,会实例化很多的对象,影响整个系统的效率

解决上述的问题,我们可能会想到单例模式来解决。手写单例模式是比较麻烦的,Spring的IOC容器管理对象,正是用到了单例模式的思想,可以将我们的对象交由Spring来管理。

源码

出自于我和小伙伴们搭建的开源框架:SMPE-ADMIN
参考地址:https://github.com/shiwei-Ren/smpe-admin/blob/main/smpe-common/src/main/java/marchsoft/utils/SpringContextHolder.java

Spring注入方式对比

Spring Bean 的注入方式一般分为三种:

  1. 构造器注入
  2. Setter注入
  3. 基于注解的 @Autowired 自动装配(Field 注入)

Spring官方推荐使用构造器注入。在IDEA中使用@Autowired注入时会进行提示:

Field injection is not recommended.
Inspection info: Spring Team recommends: “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies”

自动装配(Field 注入)

使用方法

该种注入方式是使用开发中最常见的注入方式。

@RestController
@RequestMapping("/api/users")
public class UserController {

	@Autowired
	private IUserService userService;

}

优点

  1. 注入方式简单,只需要使用 @Autowired注解或者 @Resource 注解
  2. 整体代码简洁明了
  3. 新增依赖十分方便,不需要修改原有代码

缺点

  1. 容易出现空指针异常,Field 注入允许构建对象实例的时候依赖的示例对象为空。对于IOC容器以外的环境,除了使用反射来提供它需要的依赖之外,无法复用该实现类。而且将一直是个潜在的隐患,因为你不调用将一直无法发现NullPointException的存在
  2. 对单元测试不友好,如果使用 Field 注入,那么进行单元测试就需要初始化整个Spring 环境,将所有 Bean 实例化
  3. 会出现循环依赖的隐患
public class A {
    @Autowired
    private B b;
}

public class B {
    @Autowired
    private A a;
}
  1. 容易破坏单一职责原则

构造器注入

在Spring4.x版本中推荐的注入方式就是这种

The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state.

简单的翻译一下:这个构造器注入的方式啊,能够保证注入的组件不可变,并且确保需要的依赖不为空。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态

使用方法

@RestController
@RequestMapping("/api/users")
public class UserController {

	private final IUserService userService;
    private final IDeptService deptService;
    private final IRoleService roleService;
    private final UserCacheClean userCacheClean;

	public UserController(IUserService userService, IDeptService deptService,
                          IRoleService roleService, UserCacheClean userCacheClean) {
        this.userService = userService;
        this.deptService = deptService;
        this.roleService = roleService;
        this.userCacheClean = userCacheClean;
    }

}

优点

  1. 依赖不可变 components as immutable objects ,即注入对象为final
  2. 依赖不可为空required dependencies are not null,省去对注入参数的检查。当要实例化FooController的时候,由于只有带参数的构造函数,spring注入时需要传入所需的参数,所以有两种情况:1) 有该类型的参数传入 => ok; 2) 无该类型参数传入,报错
  3. 提升了代码的可复用性:非IOC容器环境可使用new实例化该类的对象。
  4. 避免循环依赖:如果使用构造器注入,在spring项目启动的时候,就会抛出:BeanCurrentlyInCreationException:Requested bean is currently in creation: Is there an unresolvable circular reference?从而提醒你避免循环依赖,如果是field注入的话,启动的时候不会报错,在使用那个bean的时候才会报错。
  5. 保证返回客户端(调用)的代码的时候是完全初始化的状态

缺点

  1. 当注入参数较多时,代码臃肿,降低了可读性。(可参看上文代码,可能开发中需要注入更多的类)

setter注入

在Spring3.x刚推出的时候,推荐使用注入的就是这种:

The Spring team generally advocates setter injection, because large numbers of constructor arguments can get unwieldy, especially when properties are optional. Setter methods also make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is a compelling use case.

Some purists favor constructor-based injection. Supplying all object dependencies means that the object is always returned to client (calling) code in a totally initialized state. The disadvantage is that the object becomes less amenable to reconfiguration and re-injection.

简单翻译:构造器注入参数太多了,显得很笨重,另外setter的方式能用让类在之后重新配置或者重新注入。

使用方法

@RestController
@RequestMapping("/api/users")
public class UserController {

	private IUserService userService;
	
	@Autowired
    public void setUserService(IUserService userService) {
        this.userService = userService;
    }
    
}

优点

  1. 相比构造器注入,当注入参数太多或存在非必须注入的参数时,不会显得太笨重
  2. 允许在类构造完成后重新注入

缺点

  1. 产生了大量的set方法

推荐一种好用的基于构造器注入的Spring注入方式

使用 Lombok 解决构造器注入代码冗余问题

针对构造器注入导致代码过于繁重的问题,我们可以通过 lombok 插件来解决这个问题,使用lombok 的 @RequiredArgsConstructor注解生成一个包含所有常量的构造器

@RequiredArgsConstructor为每个需要特殊处理的字段生成一个带有1个参数的构造函数。所有未初始化的final字段都将获得一个参数,以及任何标记为@NonNull且未在声明位置初始化的字段。对于那些用@NonNull标记的字段,还将生成显式null检查。如果用于标记为@NonNull的字段的任何参数包含null,则构造函数将抛出NullPointerException。参数的顺序与字段在类中出现的顺序相匹配。

使用

参数声明final,快速简洁的注入

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final PasswordEncoder passwordEncoder;
    private final IUserService userService;
    private final IDeptService deptService;
    private final IRoleService roleService;
    private final UserCacheClean userCacheClean;

}

快速获取Spring容器中管理的对象

上述文章说明了该怎么选用Spring的注入方式。但很多情况下,我们也并不想使用使用注入的方式,可能这个对象我也就在本类的一个方法中使用。

不采用注入的方式,如何可以快速地获取容器的对象?

封装方法:

/**
 * 通过SpringContextHolder获取bean对象的工具类
 * <p>
 * 需要将SpringContextHolder注入到bean容器中,所以加@Configuration注解
 *
 */
@Slf4j
@Configuration
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {

    private static ApplicationContext applicationContext = null;

    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) {
        assertContextInjected();
        return (T) applicationContext.getBean(name);
    }

    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    public static <T> T getBean(Class<T> requiredType) {
        assertContextInjected();
        return applicationContext.getBean(requiredType);
    }

    /**
     * 检查ApplicationContext不为空.
     */
    private static void assertContextInjected() {
        if (applicationContext == null) {
            throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" +
                    ".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder.");
        }
    }

    /**
     * 清除SpringContextHolder中的ApplicationContext为Null.
     */
    private static void clearHolder() {
        log.debug("清除SpringContextHolder中的ApplicationContext:"
                + applicationContext);
        applicationContext = null;
    }

    @Override
    public void destroy() {
        SpringContextHolder.clearHolder();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringContextHolder.applicationContext != null) {
            log.warn("SpringContextHolder中的ApplicationContext被覆盖, 原有ApplicationContext为:" + SpringContextHolder.applicationContext);
        }
        SpringContextHolder.applicationContext = applicationContext;
    }
}

使用示例

封装自定义的工具类TicketUtil,使用@Component注入到Spring的Bean容器中。

/**
 * description:Ticket工具类
 *
 * @author RenShiWei
 * Date: 2020/12/4 21:07
 **/
@Component
public class TicketUtil {

    @Value("${ticket.header}")
    private String ticketHead;

    @Value("${ticket.start-with}")
    private String ticketStartWith;

    /**
     * description:从请求中获取ticket
     *
     * @param request /
     * @return ticket
     */
    public String getTicket(HttpServletRequest request) {
        return request.getHeader(ticketHead);
    }

    /**
     * description: 判断ticket是否正确
     *
     * @param request 请求
     * @return token
     */
    public boolean isTicketAllow(HttpServletRequest request) {
        RedisUtils redisUtils = SpringContextHolder.getBean(RedisUtils.class);
        final String requestTicket = getTicket(request);
        if (requestTicket != null && requestTicket.startsWith(ticketStartWith)) {
            return redisUtils.hasKey(requestTicket);
        }
        return false;
    }
    
}

分析

因为需要从请求头中拿出ticket信息,所以方法不能封装成stattic,否则HttpServletRequest为null,不能接收到。

又为了方便使用、管理,就像前文说的将工具类对象交由Spring管理,使用@ComponentTicketUtil注入到Spring容器中。

使用

@ApiOperation(value = "根据ticket获取当前用户信息", notes = "获取ticket \n author:RenShiWei 2020/12/3")
@PostMapping("/user")
public Void getUserByTicket(HttpServletRequest request) {
    TicketUtil ticketUtil = SpringContextHolder.getBean(TicketUtil.class);
    ticketUtil.getTicket(request);
}

注意事项

这样使用违反了设计模式中的迪米特法则(最少知道原则)

详情参考:设计模式(一)——七大原则详解

但我又想了想,好像静态方法也不是直接调用的吗,我们只是从Spring容器中拿对象而已。

IDEA中也非常容易地查看有哪些容器?
在这里插入图片描述


参考文章:
https://www.cnblogs.com/Donnnnnn/p/10405863.html
https://www.jianshu.com/p/4694b1a82546
https://blog.csdn.net/mangosweet/article/details/107097151

好的,下面是一个简单的图书管理系统的Spring Boot实现。 首先,需要创建一个Spring Boot项目,并添加以下依赖: ```xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> ``` 其spring-boot-starter-data-jpa和spring-boot-starter-web是用于JPA和Web开发的依赖,h2是一个嵌入式数据库,lombok是一个Java库,可以减少代码重复和样板代码。 接下来,定义一个Book实体类,表示图书信息: ```java @Entity @Data @NoArgsConstructor @AllArgsConstructor public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private String author; private String publisher; private Date publishDate; private Double price; } ``` 其,@Entity注解表示这是一个JPA实体类,@Data注解是lombok提供的,用于自动生成getter、setter和toString方法,@NoArgsConstructor和@AllArgsConstructor注解是lombok提供的,用于自动生成无参和全参构造方法。 然后,定义一个BookRepository接口,继承自JpaRepository,用于操作Book实体类: ```java public interface BookRepository extends JpaRepository<Book, Long> { } ``` 接下来,定义一个BookController类,用于接收来自客户端的请求,调用BookRepository接口的方法进行数据操作: ```java @RestController @RequestMapping("/books") public class BookController { @Autowired private BookRepository bookRepository; @GetMapping public List<Book> findAll() { return bookRepository.findAll(); } @PostMapping public Book save(@RequestBody Book book) { return bookRepository.save(book); } @GetMapping("/{id}") public Book findById(@PathVariable Long id) { return bookRepository.findById(id).orElse(null); } @PutMapping("/{id}") public Book update(@PathVariable Long id, @RequestBody Book book) { book.setId(id); return bookRepository.save(book); } @DeleteMapping("/{id}") public void deleteById(@PathVariable Long id) { bookRepository.deleteById(id); } } ``` 其,@RestController注解表示这是一个RESTful风格的控制器类,@RequestMapping("/books")注解表示所有请求路径都以/books开头,@Autowired注解表示自动注入BookRepository接口实例。 findAll()方法用于查询所有图书信息,save()方法用于添加新的图书信息,findById()方法用于按照ID查询图书信息,update()方法用于更新图书信息,deleteById()方法用于删除指定ID的图书信息。 最后,运行应用程序,访问http://localhost:8080/books即可打开图书管理系统。 以上是一个简单的Spring Boot图书管理系统的实现,希望能对你有所帮助。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值