原创

Spring这些技巧我不会,领导要辞退我

前言

作为一名资深的CRUD Java工程师,我跟Spring打交道的时间不亚于跟对象相处的时间,也为我们搬砖提升了一些效率,可以说是Java程序员的春天。 下面我就结合我对Spring的一些研究以及自己的开发体感,分享一些我们常用的开发姿势。

注入Bean

@Autowired

@Autowired是spring的注解,是spring2.5版本引入的,Autowired只根据type进行注入,不会去匹配name。如果涉及到type无法辨别注入对象时,那需要依赖@Qualifier@Primary注解一起来修饰,如果工程中存在一接口多实现注入Bean时可采用这样的写法。

自动注入的实现依赖Bean后置处理器【AutowiredAnnotationBeanPostProcessor】的拓展接口,关于Spring Bean生命周期中的各种拓展后面会专门分享出来。

@Resource

@Resource是Java自己的注解,@Resource有两个属性是比较重要的,分是name和type;Spring@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。注入的实现依赖Bean后置处理器【CommonAnnotationBeanPostProcessor】的拓展接口,原理类似Autowired

Spring4.x之后不推荐注解这种写法,原因有以下几条

  1. 依赖不可变:加入了final来约束修饰Field,这条是很显然的;
  2. 依赖不可为空:在实例化的时候会检查构造函数参数是否为空,如果为空(未找到改类型的实例对象)则会抛出异常。
  3. 单一职责:当使用构造函数注入时,如果参数过多,你会发现当前类的职责过大,需要进行拆分。
  4. 更利于单元测试:按照其他两种方式注入,当单元测试时需要初始化整个spring的环境,而采用构造方法注入时,只需要初始化需要的类即可,即可以直接实例化需要的类。
  5. 避免IOC容器以外环境调用时潜在的NPE(空指针异常)。
  6. 避免循环依赖。
  7. 保证返回客户端(调用)的代码的时候是完全初始化的状态。

构造函数

官方推荐写法,但是有不少缺点。

  • 代码臃肿
  • 新增Field修改麻烦
  • 当Field多余5个时不符合构造方法的基本规范,显得笨重、臃肿;

如果真的有大量依赖需要注入我们该怎么办呢?

如果有大量依赖需要注入说明该类的职责过于复杂,需要遵从单一性原则进行拆分;

初始化Bean

@PostConstruct

@Component
public class DemoService {
    @PostConstruct
    public void init(){
        System.out.println("初始化ing");
    }
}

实现InitializingBean接口

@Component
public class Test4Service implements InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("属性设置后执行方法");
    }
}

首先实现InitializingBean初始化bean接口,覆盖afterPropertiesSet()方法,然后在该方法完成初始化操作。

XML中配置init-method方法

TestInitMethod

public class TestInitMethod{
    public void testInit(){
        System.out.println("test init-method");        
    }
}

application.xml

<bean id="testInitMethod" class="top.janker.TestInitMethod" init-method="testInit"></bean>

Main主程序

public class Main {
    public static void main(String[] args){
        ClassPathXmlApplicationContext context1 = new ClassPathXmlApplicationContext("spring.xml");
  }
}

这种方法比较复古,注解配置横行的世道,这种使用姿势不是很常用。

从效果上说,以上三种都是实现初始化功能。但是三种并存时执行顺序是怎么样的呢?调用的顺序的关键点在于AbstractAutowireCapableBeanFactory类的initializeBean方法

从代码执行上看会先执行postProcessBeforeInitialization方法,而PostConstruct实现的关键点就在于BeanPostProcessor的一个实现类InitDestroyAnnotationBeanPostProcessor,所以PostConstruct标记的初始化方法就限制性,而图中invokeInitMethods方法中就会调用beanafterPropertiesSet方法,最后调用自定义的init-method方法。

总结,执行顺序:PostConstruct -> initializeBean->init-method

FactoryBean的妙用

看到FactoryBean就想到BeanFactory,傻傻分不清楚,具体有什么区别。

  1. BeanFactory:spring容器的顶层接口,从字面含义就能看出来,就是管理bean的工厂,就是承载员工的厂子。
  2. FactoryBean:工厂bean,跟普通的bean还不太一样,比普通的bean增加一些拓展能力。

在实际应用中有什么妙用吗?我们比较熟悉的mybatis中的SqlSessionFactory对象就是通过SqlSessionFactoryBean类创建的。

开发体验:

定义工厂bean

@Component
public class MyFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        Integer a = 1;
        Integer b = 2;
        return a+b;
    }

    @Override
    public Class<?> getObjectType() {
        return Integer.class;
    }
}

从bean工厂获取实现FactoryBean接口的bean

@Component
public class MyFactoryBeanService implements BeanFactoryAware {
    private BeanFactory beanFactory;
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    public void test(){
        Object myFactoryBean = beanFactory.getBean("myFactoryBean");
        Class<?> aClass = myFactoryBean.getClass();
        System.out.println(myFactoryBean);
        System.out.println(aClass);
        Object bean = beanFactory.getBean("&myFactoryBean");
        Class<?> aClass1 = bean.getClass();
        System.out.println(bean);
        System.out.println(aClass1);
    }
}

测试程序

public class MainConfigTest {
    @Test
    public void testMyFactoryBeanService(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
        MyFactoryBeanService myFactoryBeanService = (MyFactoryBeanService) context.getBean("myFactoryBeanService");
        myFactoryBeanService.test();
    }
}

执行结果

3
class java.lang.Integer
top.janker.MyFactoryBean@55ca8de8
top.janker.MyFactoryBean
  • getBean("myFactoryBean"); 获取的是MyFactoryBeanService类中getObject方法返回的对象,
  • getBean("&myFactoryBean"); 获取的才是MyFactoryBean对象。

类型转换器

spring中目前有三种转换器

  1. Converter<S, T> ,将S类型的对象转换为T类型的对象
  2. ConverterFactory<S, R>,将S类型的对象转换为R类型及其子类的对象
  3. GenericConverter,顾名思义通用的转换器,用于在两个或多个类型之间转换的通用转换器接口。

我们拿Converter举例,在spring中比较典型的就有DateTimeConverters类中的一些转换器,CalendarToLocalDateConverterCalendarToLocalTimeConverterCalendarToLocalDateTimeConverterLocalDateTimeToLocalDateConverter等,各种类型时间对象的转换,我们生产中往往会接口交互存在很多XXEnum与Integer转换问题,每次手动转太费劲了,我们看下怎么通过Converter来解决这个问题。

定义实体

@Data
public class OrderInfo {
    private Long id;
    private String orderNo;
    private StatusEnum status;
}

实现Converter接口

public class StatusConverter implements Converter<Integer, StatusEnum> {

    @Override
    public StatusEnum convert(Integer source) {
        if (source != null) {
            return StatusEnum.toEnum(source);
        }
        return null;
    }
}

将转换器注入spring容器

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StatusConverter());
    }
}

定义接口

@RequestMapping("/Order")
@RestController
public class OrderController {

    @RequestMapping("/query")
    public String query(@RequestBody OrderInfo order) {
        return "ok";
    }
}

外部调用接口自动的就将StatusEnum类型转换为Integer类型。

Spring MVC全局异常处理

RestControllerAdvice注解配合@ExceptionHandler注解,可以捕捉全局的异常,统一返回异常信息即可。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public String handleException(Exception e) {
        if (e instanceof ServiceBizException e) {
            return "服务业务异常";
        }
        if (e instanceof Exception) {
            return "服务器内部异常";
        }
        retur nnull;
    }
}

Spring MVC拦截器、统一认证

spring mvc拦截器根spring拦截器相比,它里面能够获取HttpServletRequestHttpServletResponse 等web对象实例。

spring mvc拦截器顶层接口:HandlerInterceptor ,包含三个接口方法。

  • preHandle 目标方法执行前执行
  • postHandle 目标方法执行后执行
  • afterCompletion 请求完成时执行

为了方便我们一般情况会用HandlerInterceptor接口的实现类HandlerInterceptorAdapter类。

假如有权限认证、日志、统计的场景,可以使用该拦截器。

第一步,继承HandlerInterceptorAdapter类定义拦截器:

public class AuthInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String requestUrl = request.getRequestURI();
        if (checkAuth(requestUrl)) {
            return true;
        }

        return false;
    }

    private boolean checkAuth(String requestUrl) {
        System.out.println("===权限校验===");
        return true;
    }
}

第二步,将该拦截器注册到spring容器:

@Configuration
public class WebAuthConfig extends WebMvcConfigurerAdapter {

    @Bean
    public AuthInterceptor getAuthInterceptor() {
        return new AuthInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor());
    }
}

第三步,在请求接口时spring mvc通过该拦截器,能够自动拦截该接口,并且校验权限。

@EnableXXX

不知道你有没有用过Enable开头的注解,比如:EnableAsyncEnableCachingEnableAspectJAutoProxy等,这类注解就像开关一样,只要在@Configuration定义的配置类上加上这类注解,就能开启相关的功能。

异步执行

new Thread

 new MyThread().start();

ExecutorService

实现Runnable接口

public class Work implements Runnable {

        @Override
        public void run() {
            //do something
        }
}

提交任务

ExecutorService executorService = new ThreadPoolExecutor(1, 4, 50, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
public static void main(String[] args) {
        try {
            executorService.submit(new MyThreadPool.Work());
        } finally {
            executorService.shutdown();
        }

}

@EnableAsync配合@Async

在spring我们可以使用异步开关来搞定异步执行,具体步骤如下:

开启异步执行开关

@EnableAsync
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}

方法上增加@Async注解

@Service
public class TestService {
    @Async
    public String doXXX() {
        //逻辑处理
        //do TODO
        return "ok";
    }
}

然后调用testService.doXXX方法的时候就使用异步执行,不过一般这么使用每次都会创建线程,为了对线程进行调度,我们一般会对spring的线程池进行参数配置,在spring中注入ThreadPoolTaskExecutor即可。

spring异步的核心方法:

AsyncExecutionInterceptor异步执行的拦截器,然后invoke方法中调用doSubmit方法。

doSubmit中实现了异步执行任务

根据返回值不同,处理情况也不太一样,具体分为如下情况:

Spring Cache

spring cache架构图:

它目前支持多种缓存:

我们在这里以caffeine为例,它是spring官方推荐的。

第一步,引入caffeine的相关jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.0</version>
</dependency>

第二步,配置CacheManager,开启EnableCaching

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager(){
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        //Caffeine配置
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                //最后一次写入后经过固定时间过期
                .expireAfterWrite(10, TimeUnit.SECONDS)
                //缓存的最大条数
                .maximumSize(1000);
        cacheManager.setCaffeine(caffeine);
        return cacheManager;
    }
}

第三步,使用Cacheable注解获取数据

@Service
public class OrderService {

   //orderInfo是缓存名称,#orderNo是具体的key,可支持el表达式
   @Cacheable(value = "OrderInfo", key = "#orderNo")
   public OrderInfo getOrderInfo(String orderNo) {
       return orderDao.getOrderInfo(orderNo);
   }
}

这样查询订单信息的时候我们就可以缓存下来,此外对缓存的其他操作还包括:

注解 说明
@EnableCaching 开启缓存。
@Cacheable 可以作用在类和方法上,以键值对的方式缓存类或方法的返回值。
@CachePut 方法被调用,然后结果被缓存。
@CacheEvict 清空缓存。
@Caching 用来组合多个注解标签。

总结

从上述功能功能介绍中我们大致了解一些常用的spring技巧(搬砖功能),有什么需要补充介绍的,可以给我留言,下一期咱们继续。

正文到此结束