【Spring Boot實戰】Spring高級話題

Spring其他功能

  • Spring Aware
  • 多執行緒
  • Schedule
  • Conditional
  • 組合註解、元註解
  • 開啟支持
  • 測試

【Spring Aware】

  • 需要讓Bean意識到Spring容器的存在,需要使用Aware
  • 使用後程式會和Spring框架耦合
  • Aware interface
    BeanNameAware 獲得容器中Bean的名稱
    BeanFactoryAware 獲得當前bean factory
    ApplicationContextAware 獲得當前application context
    MessageSourceAware 獲得message source,獲得文本信息
    ApplicationEventPublisherAware 應用事件發布器,可以發布事件
    ResourceLoaderAware 獲得資源加載器,可以獲得外部資源文件
    ApplicationContextAware集成了其他interface,因此implement ApplicationContextAware,就可以獲得所有服務

【範例】

  • Service
@Service
public class AwareService implements BeanNameAware,ResourceLoaderAware{ // 實作BeanNameAware, ResourceLoaderAware
	
	private String beanName;
	private ResourceLoader loader;
	
	@Override // 實作ResourceLoaderAware要覆寫此方法
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.loader = resourceLoader;
	}

	@Override // 實作BeanNameAware要覆寫此方法
	public void setBeanName(String name) {
		this.beanName = name;
	}
	
	public void outputResult(){
		System.out.println("Bean: " + beanName); // Bean: awareService
		
		Resource resource = 
				loader.getResource("classpath:com/myPackage/test.txt");
		try{
			
			System.out.println("ResourceLoader: " + IOUtils.toString(resource.getInputStream())); // ResourceLoader: [test.txt內容]
                         
		   }catch(IOException e){
			e.printStackTrace();
		   }
	}
}
  • Configuration
@Configuration
@ComponentScan("com.myPackage.aware")
public class AwareConfig {

}
  • Context
public class Main {
	public static void main(String[] args) {
		
		AnnotationConfigApplicationContext context =
                       new AnnotationConfigApplicationContext(AwareConfig.class);
		
		AwareService awareService = context.getBean(AwareService.class);
		awareService.outputResult();
		
		context.close();
	}
}

【多執行緒】

  • Spring透過TaskExecutor實現多執行緒
  • ThreadPoolTaskExecutor為implement TaskExecutor介面的class,且基於多執行緒池
  • Configuration中使用@EnableAsync,開啟異步任務的支持
  • Bean的方法中使用@Async,聲明為異步任務

【範例】

  • Configuration
@Configuration
@ComponentScan("com.wisely.highlight_spring4.ch3.taskexecutor")
@EnableAsync // 開啟對異步任務的支持 
public class TaskExecutorConfig implements AsyncConfigurer{// 實現AsyncConfigurer

	@Override
	public Executor getAsyncExecutor() {// 覆寫getAsyncExecutor方法,返回TaskExecutor
		 ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
	        taskExecutor.setCorePoolSize(5);
	        taskExecutor.setMaxPoolSize(10);
	        taskExecutor.setQueueCapacity(25);
	        taskExecutor.initialize();
	        return taskExecutor;
	}

	@Override
	public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
		return null;
	}

}
  • Service
@Service
public class AsyncTaskService {
	
	@Async // 聲明此方法為異步方法,如註解在class,則表示整個class的方法都是異步方法
    public void executeAsyncTask(Integer i){
        System.out.println("執行任務A: "+i);
    }

    @Async
    public void executeAsyncTaskPlus(Integer i){
        System.out.println("執行任務B: "+(i+1));
    }

}
  • Context
public class Main {
	
	public static void main(String[] args) {
		 AnnotationConfigApplicationContext context =
	                new AnnotationConfigApplicationContext(TaskExecutorConfig.class);
		 
		 AsyncTaskService asyncTaskService = context.getBean(AsyncTaskService.class);
		 
		 for(int i =0 ;i<10;i++){
			 asyncTaskService.executeAsyncTask(i);
			 asyncTaskService.executeAsyncTaskPlus(i);
	        }
	        context.close();
	}
}

【Schedule】

  • Configuration中使用@EnableScheduling,開啟對Schedule的支持
  • 執行計畫的方法上,使用@Scheduled
  • Schedule有三種類型:cron、fixedRate、fixedDelay
fixedRate:前次任務「開始執行時」,計算間隔時間,並執行下一次任務
fixedDelay:前次任務「執行完畢時」,計算間隔時間,並執行下一次任務

【範例】

  • Service
@Service
public class ScheduledTaskService {
	
	  private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

	  @Scheduled(fixedRate = 5000) // 使用fixedRate,設定每五秒執行一次
	  public void reportCurrentTime() {
	       System.out.println("每五秒鐘執行 " + dateFormat.format(new Date()));
	   }

	  @Scheduled(cron = "0 28 11 ? * *"  ) // 使用cron,指定每天11:28執行
	  public void fixTimeExecution(){
	      System.out.println("指定時間執行" + dateFormat.format(new Date())+"Ö´ÐÐ");
	  }

}
  • Configuration
@Configuration
@ComponentScan("com.myPackage.taskscheduler")
@EnableScheduling // 開啟Spring對Schedule的支持
public class TaskSchedulerConfig {

}
  • Context
public class Main {
	public static void main(String[] args) {
		 AnnotationConfigApplicationContext context =
	                new AnnotationConfigApplicationContext(TaskSchedulerConfig.class);
		 
	}

}

【Conditional】

  • 根據滿足某個特定條件,創建特定的Bean

【範例】

  • Condition1
public class WindowsCondition implements Condition { // 實作Condition介面

	// 實作matches方法
    public boolean matches(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("os.name").contains("Windows");
    }

}
  • Condition2
public class LinuxCondition implements Condition {

    public boolean matches(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("os.name").contains("Linux");
    }

}
  • Service interface
public interface ListService {
	public String showListCmd();
}
  • Service1
public class WindowsListService implements ListService {

	@Override
	public String showListCmd() {
		return "dir";
	}

}
  • Service2
public class LinuxListService implements ListService{

	@Override
	public String showListCmd() {
		return "ls";
	}

}
  • Configuration
@Configuration
public class ConditionConifg {
    @Bean
    @Conditional(WindowsCondition.class) // 符合此Condition條件,會產生這個Bean
    public ListService windowsListService() {
        return new WindowsListService();
    }

    @Bean
    @Conditional(LinuxCondition.class) 
    public ListService linuxListService() {
        return new LinuxListService();
    }
}
  • Context
public class Main {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext context =
			new AnnotationConfigApplicationContext(ConditionConifg.class);
		
		ListService listService = context.getBean(ListService.class);
		
		
		System.out.println(context.getEnvironment().getProperty("os.name") 
				+ "系統下的列表命令為: " 
				+ listService.showListCmd());
		
		context.close();
	}
}

【組合註解、元註解】

  • 組合註解:包含其他註解的功能(被元註解寄生的註解),因此也擁有所包含元註解的功能,如:@Configuration
  • 元註解:可以註解其他註解的註解(註解的最小單位?),如:@Component
@Configuration包含了@Component的功能
組合註解與元註解為相對關係,組合註解在其他場合,也可以成為元註解

【範例】將@Configuration、@ComponentScan結合

  • 自訂組合註解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration // 組合@Configuration元註解
@ComponentScan // 組合@ComponentScan元註解
public @interface WiselyConfiguration {
	
	String[] value() default {}; // 覆蓋value參數

}
  • Service
@Service
public class DemoService {
	
	public void outputResult(){
		System.out.println("透過自訂組合註解,所得到的Bean");
	}

}
  • Configuration
@WiselyConfiguration("com.myPackage.annotation") // 使用自訂組合註解
public class DemoConfig {

}
  • Context
public class Main {
	
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context =
			new AnnotationConfigApplicationContext(DemoConfig.class);
		
		DemoService demoService =  context.getBean(DemoService.class);
		
		demoService.outputResult();
		
		context.close();
	}

}

【開啟支持】

  • 透過@Enable*,可開啟Spring對各種服務的支持
    @EnableAspectJAutoProxy AspectJ自動代理
    @EnableAsync 異步方法
    @EnableScheduling 計畫任務
    @EnableWebNvc Web MVC
    @EnableConfigurationProperties @ConfigurationProperties註解配置Bean
    @EnableJpaRepositories Spring Data JPA Repository
    @EnableTransactionManagement 註解式事務
    @EnableCaching 註解式緩存
  • @Enable中,都有@Import註解,導入Configuration,代表這些註解其實是導入了一些自動配置的Bean
  • 配置方式主要分成三種類型
    • 直接導入配置:固定註冊一個Bean
    • 依據條件選擇配置:根據條件,註冊不同的Bean
    • 動態註冊:透過AnnotationMetadata,動態註冊Bean

【直接導入配置】

  • 註解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class) // 直接導入SchedulingConfiguration
@Documented
public @interface EnableScheduling {

}
  • Configuration
@Configuration // 註冊ScheduledAnnotationBeanPostProcessor的Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

	@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
		return new ScheduledAnnotationBeanPostProcessor();
	}

}

【依據條件選擇配置】

  • 註解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
	// 條件都有預設值
	Class<? extends Annotation> annotation() default Annotation.class;
	boolean proxyTargetClass() default false;
	AdviceMode mode() default AdviceMode.PROXY;
	int order() default Ordered.LOWEST_PRECEDENCE;

}
  • AsyncConfigurationSelector
// 最原始的interface為ImportSelector,需覆寫selectImports方法
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {

	private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME =
			"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";

	@Override
	public String[] selectImports(AdviceMode adviceMode) { // 根據adviceMode的不同,回傳不同的配置
		switch (adviceMode) {
			case PROXY:
				return new String[] { ProxyAsyncConfiguration.class.getName() };
			case ASPECTJ:
				return new String[] { ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME };
			default:
				return null;
		}
	}

}

【動態註冊】

  • 註解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

	boolean proxyTargetClass() default false;
	boolean exposeProxy() default false;

}
  • AspectJAutoProxyRegistrar
// 實現ImportBeanDefinitionRegistrar,並覆寫registerBeanDefinitions方法
// ImportBeanDefinitionRegistrar作用為在運行時,自動添加Bean到已有的配置中(不是選擇配置,而是選擇Bean)
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	// AnnotationMetadata:獲得當前配置上的註解
	// BeanDefinitionRegistry:註冊Bean
	public void registerBeanDefinitions(
			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

		AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

		AnnotationAttributes enableAspectJAutoProxy =
				AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
		if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
			AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
		}
		if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
			AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
		}
	}

}

【測試】

  • Spring透過Spring TestContex Framework對集成測試提供支持,不依賴於特定的測試框架
  • SpringJUnit4ClassRunner:此class提供Spring TestContex Framework的功能
    • @ContextConfiguration:配置Application Context
    • @ActiveProfiles:確定活動的profile

【範例】

  • Bean
public class TestBean {
	private String content;

	public TestBean(String content) {
		super();
		this.content = content;
	}

	public String getContent() {
		return content;
	}

	public void setContent(String content) {
		this.content = content;
	}
	
}
  • Configuration
@Configuration
public class TestConfig {
	@Bean
	@Profile("dev")
	public TestBean devTestBean() {
		return new TestBean("from development profile");
	}

	@Bean
	@Profile("prod")
	public TestBean prodTestBean() {
		return new TestBean("from production profile");
	}

}
  • Test
// 此檔案路徑於src/test/java
@RunWith(SpringJUnit4ClassRunner.class) // 使用SpringJUnit4ClassRunner執行
@ContextConfiguration(classes = {TestConfig.class}) // 選擇要執行的Configuration
@ActiveProfiles("prod") // 選擇要使用的profile
public class DemoBeanIntegrationTests {
	@Autowired
	private TestBean testBean;

	@Test // 聲明為測試代碼
	public void prodBeanShouldInject(){
		String expected = "from production profile";
		String actual = testBean.getContent();
		Assert.assertEquals(expected, actual); // 比對測試結果
	}

	
}