Google Code Prettify

2019年4月5日 星期五

部署 RESTful service 到 docker

我用 spring boot 寫了一個 hello 的 RESTful service,如下,傳入名稱,服務就會回覆 Hello $name。
@RestController
public class HelloController {

@RequestMapping("/hello/{name}")
public String hello(@PathVariable String name) {
return "Hello " + name + " !";
}
}
這個 RESTful service 我打包成 jar 檔,要部署到 docker 上執行。打包好的 jar 檔名為 Hello-1.0.0.jar,在 docker 中要安裝 open JDK 11 (openjdk-11_linux-x64_bin.tar.gz),Dockerfile 是 docker 提供的自動化部署方式,繼續往下看之前,可以先看一下這一篇 -- 使用 Dockerfile 創建 docker 鏡像
  • Dockerfile
FROM centos:latest
MAINTAINER "Steven Shi"<hi.steven@gmail.com>
ADD openjdk-11_linux-x64_bin.tar.gz /usr/local
COPY Hello-1.0.0.jar /home
RUN ln -s /usr/local/jdk-11 /usr/bin/java
ENV JAVA_HOME /usr/local/jdk-11
ENV PATH $JAVA_HOME/bin:$PATH
EXPOSE 9080
CMD java -jar /home/Hello-1.0.0.jar
    • FROM centos:latest: 指定這個 image 的基礎鏡像為 centos 的最新版。
    • ADD openjdk-11_linux-x64_bin.tar.gz /usr/local: 把 jdk 部署到 /usr/local 目錄下,ADD 指令不只會 copy 指定的檔案到指定目錄,還具有解壓縮的功能。
    • COPY Hello-1.0.0.jar /home: 把 RESTful service 的 jar 檔部署到 /home 目錄下。
    • EXPOSE 9080: 這個 container 對外開放出 9080 這個 port。
    • CMD java -jar /home/Hello-1.0.0.jar: 在 container 啟動時,讓 hello 服務啟動。
  • 部署服務到 docker
docker build -t steven/hello .
在上面那三個檔案所在目錄執行上述指令,就會在 docker 產生鏡像,名稱為 steven/hello,注意,最後面有個句點,指出是在目前目錄。
  • 啟動 container
docker run -p 9080:9080 --name hello-container -d steven/hello
    • -p 9080:9080 : container 外要連入 container 的 port 也設為 9080 和 container 開放給外界的 port 相連通 (port 號不需要一樣)。
    • --name : container 啟動後的名稱。
    • -d : 啟動為 daemon 狀態,後面接的是鏡像名稱。
  • 測試
curl localhost:9080/hello/Steven
用 curl 發一個 http request 如上,會傳回 Hello Steven,確認服務部署成功。

2019年1月24日 星期四

Spring Security: AuthenticationManager

前一篇 (Spring Security: 資料庫認證、授權) 說明了怎麼使用資料庫認證,但是,看完可能會有點混淆,似乎 Spring Security 的認證、授權,就一定要使用到 WebSecurityConfigurerAdapter,Spring Security 一定要在 Web 中使用? 當然不是! 這裡以官方文件 Spring Security Reference 中的範例來說明。
  • AuthenticationExample
public class AuthenticationExample {

  private static AuthenticationManager am = new SampleAuthenticationManager();

  public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while (true) {
      System.out.println("Please enter your username:");
      String name = in.readLine();
      System.out.println("Please enter your password:");
      String password = in.readLine();
      try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
      } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
      }
    }
  
    System.out.println("Successfully authenticated. Security context contains: " +
                        SecurityContextHolder.getContext().getAuthentication());
  }
}
  • SampleAuthenticationManager
public class SampleAuthenticationManager implements AuthenticationManager {
  static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

  static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
  }

  public Authentication authenticate(Authentication auth)
      throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
      return new UsernamePasswordAuthenticationToken(
                 auth.getName(), auth.getCredentials(), AUTHORITIES
             );
    }

    throw new BadCredentialsException("Bad Credentials");
  }
}
  • 執行結果
Please enter your username:
bob
Please enter your password:
password
Authentication failed: Bad Credentials
Please enter your username:
bob
Please enter your password:
bob
Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: bob; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
程式簡單的要使用者從命令列輸入帳號、密碼,然後檢查帳密是否合法,這裡把帳密當成同一字串,所以第二次輸入 bob / bob 時,認證才成功,現在來說明一下程式。 SampleAuthenticationManager 這個類別實作 AuthenticationManager 介面,這個介面很簡單,只有一個 method - authenticate(Authentication authentication),只需要傳入一個實作 Authentication 介面的類別物件就行,範例傳入的是 UsernamePasswordAuthenticationToken 類別的物件。 UsernamePasswordAuthenticationToken 這個類別非常簡單的,它實作 Authentication,用來放使用者的帳號、密碼 (如果必要還可以放角色,範例放入了預設的角色 USER。),然後在 authenticate method 裡,就可以看到,我們取出帳號、密碼來比對,相同就回傳 token,失敗拋出 exception。 回到主程式,可以看到取得 authenticate 的回傳值後,範例程式將它放入 SecurityContextHolder 的 context 的 authentication 裡,為什麼?
Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(result);
這是 spring security 的要求,在認證成功後,要將使用者的 token 放入 SecurityContext 裡,未來 spring security 需要使用者的權限時,會自行取用。

2019年1月10日 星期四

Spring Batch: multiple datasource

在 spring boot 環境中,使用 spring batch,如果需要連線兩個不同資料庫,要怎麼做呢?
  • 在 application.properties 定義多個 dataSource
batch.datasource.driver-class-name=oracle.jdbc.OracleDriver
batch.datasource.url=jdbc:oracle:thin:@192.168.50.12:1521:testDB1
batch.datasource.username=dbuser1
batch.datasource.password=p@ssword

db1.datasource.driver-class-name=oracle.jdbc.OracleDriver
db1.datasource.url=jdbc:oracle:thin:@192.168.50.12:1521:testDB1
db1.datasource.username=dbuser1
db1.datasource.password=p@ssword

db2.datasource.driver-class-name=oracle.jdbc.OracleDriver
db2.datasource.url=jdbc:oracle:thin:@192.168.51.168:1521:testDB2
db2.datasource.username=dbuser2
db2.datasource.password=p@ssword
如上,db1、db2 分別連到不同的兩個資料庫,但是,除此之外,還要設定一個給 spring batch 使用,所以有 batch (第一個) 的 dataSource 設定,雖然 spring batch 與 db1 使用同一個資料庫,還是要另外設定 dataSource,否則在執行到 spring batch 的 tasklet 時,如果裡面有存取到 db1、db2 的資料庫時,會抓不到 transaction。
  • 設定多個 dataSource
@Configuration
public class MultipleDataSourceConfig {
    @Autowired
    private Environment env;
 
    @Bean(name = "batchDatasource")
    @ConfigurationProperties(prefix = "batch.datasource")
    public DataSource batchDataSource() {
        return DataSourceBuilder.create()
           .driverClassName(env.getProperty("batch.datasource.driver-class-name"))
           .url(env.getProperty("batch.datasource.url"))
           .username(env.getProperty("batch.datasource.username"))
           .password(env.getProperty("batch.datasource.password"))
           .build();
    }

    @Bean(name = "db1Datasource")
    @Primary
    @ConfigurationProperties(prefix = "db1.datasource")
    public DataSource db1DataSource() {
        return DataSourceBuilder.create()
           .driverClassName(env.getProperty("db1.datasource.driver-class-name"))
           .url(env.getProperty("db1.datasource.url"))
           .username(env.getProperty("db1.datasource.username"))
           .password(env.getProperty("db1.datasource.password"))
           .build();
    }
 
    @Bean(name = "db2Datasource")
    @ConfigurationProperties(prefix = "db2.datasource")
    public DataSource db2DataSource() {
        return DataSourceBuilder.create()
           .driverClassName(env.getProperty("db2.datasource.driver-class-name"))
           .url(env.getProperty("db2.datasource.url"))
           .username(env.getProperty("db2.datasource.username"))
           .password(env.getProperty("db2.datasource.password"))
           .build();
    }
}
  • 為每個 dataSource 設定相關的 transactionManager
    • BatchDbConfig.java
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
   entityManagerFactoryRef = "batchEntityManagerFactory",
   basePackages = { "idv.steven.batch.dao" },
   transactionManagerRef = "batchTransactionManager"
)
public class BatchDbConfig {
 @Autowired
 @Qualifier("batchDatasource")
 private DataSource datasource;
 
    private JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();

        adapter.setDatabase(Database.ORACLE);
        adapter.setShowSql(true);
        adapter.setGenerateDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.Oracle12cDialect");

        return adapter;
    }
    
    /**
     * 載入 Entity
     * @return
     */
    @Bean
    public LocalContainerEntityManagerFactoryBean batchEntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(datasource);
        emf.setPackagesToScan(new String[] { "idv.steven.batch.entity" });
        emf.setJpaVendorAdapter(this.jpaVendorAdapter());
        emf.setSharedCacheMode(SharedCacheMode.NONE);

        return emf;
    }
    
    @Bean(name = "batchTransactionManager")
    public PlatformTransactionManager batchTransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(this.batchEntityManagerFactory().getObject());
        
        return tm;
    }
}
    • DB1DbConfig.java
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
   entityManagerFactoryRef = "db1EntityManagerFactory",
   basePackages = { "idv.steven.database.db1.dao" },
   transactionManagerRef = "db1TransactionManager"
)
public class DB1DbConfig {
 @Autowired
 @Qualifier("db1Datasource")
 private DataSource datasource;
 
    private JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();

        adapter.setDatabase(Database.ORACLE);
        adapter.setShowSql(true);
        adapter.setGenerateDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.Oracle12cDialect");

        return adapter;
    }
    
    /**
     * 載入 Entity
     * @return
     */
    @Primary
    @Bean
    public LocalContainerEntityManagerFactoryBean db1EntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(datasource);
        emf.setPackagesToScan(new String[] { "idv.steven.database.db1.entity" });
        emf.setJpaVendorAdapter(this.jpaVendorAdapter());
        emf.setSharedCacheMode(SharedCacheMode.NONE);

        return emf;
    }
    
    @Primary
    @Bean(name = "db1TransactionManager")
    public PlatformTransactionManager db1TransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(this.db1EntityManagerFactory().getObject());
        
        return tm;
    }
}
    • DB2DbConfig.java
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
   entityManagerFactoryRef = "db2EntityManagerFactory",
   basePackages = { "idv.steven.database.db2.dao" },
   transactionManagerRef = "db2TransactionManager"
)
public class DB2DbConfig {
 @Autowired
 @Qualifier("db2Datasource")
 private DataSource datasource;
 
    private JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();

        adapter.setDatabase(Database.ORACLE);
        adapter.setShowSql(true);
        adapter.setGenerateDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.Oracle12cDialect");

        return adapter;
    }
    
    /**
     * 載入 Entity
     * @return
     */
    @Bean
    public LocalContainerEntityManagerFactoryBean db2EntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(datasource);
        emf.setPackagesToScan(new String[] { "idv.steven.database.db2.entity" });
        emf.setJpaVendorAdapter(this.jpaVendorAdapter());
        emf.setSharedCacheMode(SharedCacheMode.NONE);

        return emf;
    }
    
    @Bean(name = "db2TransactionManager")
    public PlatformTransactionManager db2TransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(this.db2EntityManagerFactory().getObject());
        
        return tm;
    }
}
三個 transaction manager 需有一個設定為 primary,萬一有任何的 dao 沒有指明用那一個,系統才知道預設是那個。
  • 指定 spring batch 使用那一個 dataSource
@Configuration
@EnableBatchProcessing
public class BatchConfig {
    @Bean
    BatchConfigurer configurer(@Qualifier("batchDatasource") DataSource dataSource){
  return new DefaultBatchConfigurer(dataSource);
    }
}

在有 @EnableBatchProcessing 的類別裡,建立一個 BatchConfigurer 的 bean,指定 spring batch 要使用的 dataSource。