老涂的咁仔店

記載生活點滴、敗家記事、程式撰寫...etc

Guava Cache 使用說明

2018-08-29

Tags: 程式語言 , java , guava

基本用法

請參考下列文章

CacheBuilder class 裡的 expireAfterWrite, expireAfterAccess, refreshAfterWrite method 區別

  • expireAfterAccess

  • expireAfterWrite

    • 一定時間內沒有創建或覆寫時,會移除該 cache key,下次取的時候從 raw data (e.g. database) 裡取
    • 在更新 cache key 對應內容時,會 block 所有存取該 cache key 的所有 thread
    • expireAfterWrite 是時間到了就 expire。expireAfterAccess 則是以 上次取存的時間點 為基準來記算何時會 expire,也就是說只要一直被存取就不會 expire
  • refreshAfterWrite

    • 指定時間內沒有被創建或覆寫,在指定時間過後再次訪問時,會去更新 cache key 對應之值,在新值沒有到來之前,始終返回舊值。注意!是再次訪問,也就是未訪問前可能都不會更新。
    • 在更新 cache key 對應內容時,不會 block 所有存取該 cache key 的 thread
    • CacheBuilder 要產生 LoadingCache 時才可以叫用這個 method,不然會產生 refreshAfterWrite requires a LoadingCache exception
    • 跟 expireAfterAccess(或 expireAfterWrite) 區別
      • 指定時間過後,expireAfterAccess(或 expireAfterWrite) 是 remove cache key,下次訪問是 同步(sync) 去獲取返回新值。新值未取回前,所有要取新值 的 thread 都會被 block。
      • refresh 是指定時間後,不會 remove cache key,直到下次訪問 才以 非同步(async) 方式 refresh cache。在 refresh cache 動作完成前,大多數針對該 cache key 的請求 thread 會立刻返回舊值,不會被 block
    • refreshAfterWrite 行為分析請參考下列文章

CacheBuilder class 裡的 expireAfterWrite, refreshAfterWrite method 可以一起使用

二者一起使用時的行為請參考下列文章

  • 深入Guava Cache的refresh和expire刷新機制

    重點是這一段

    可以看出refreshAfterWrite和expireAfterWrite兩種方式各有優缺點,各有使用場景。那麼能否在refreshAfterWrite和expireAfterWrite找到一個折中?比如說控制緩存每1s進行refresh,如果超過2s沒有訪問,那麼則讓緩存失效,下次訪問時不會得到舊值,而是必須得待新值加載。由於guava官方文檔沒有給出一個詳細的解釋,查閱一些網上資料也沒有得到答案,因此只能對源碼進行分析,尋找答案。經過分析,當同時使用兩者的時候,可以達到預想的效果,這真是一個好消息吶!

  • 官方文件說明

    重點是這一段,不過我沒有很懂它表達的意思

    In contrast to expireAfterWrite, refreshAfterWrite will make a key eligible for refresh after the specified duration, but a refresh will only be actually initiated when the entry is queried. (If CacheLoader.reload is implemented to be asynchronous, then the query will not be slowed down by the refresh.) So, for example, you can specify both refreshAfterWrite and expireAfterWrite on the same cache, so that the expiration timer on an entry isn't blindly reset whenever an entry becomes eligible for a refresh, so if an entry isn't queried after it comes eligible for refreshing, it is allowed to expire.

reference document


如何在 spring 實作的 restful api 裡使用 validation annotation

2017-12-29

Tags: 程式語言 , java , spring

如何實作

spring 實作的 restful api,要針對輸入參數進行內容格式驗証,可分成下列儿類

針對 @RequestBody 進行 validation

實作程式碼重點如下

@RestController
@RequestMapping("/validateAnnotationDemo")
public class ValidateAnnotationDemoController {
    @RequestMapping(value = "/validateRequestBody", produces = MediaType.APPLICATION_JSON_VALUE)
    public User validateRequestBody(@Valid @RequestBody User user){
        ...etc;
    }

    private static class User{
        @NotBlank
        private String name;
        ...etc;
    }
}
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleMethodArgumentNotValidException(HttpServletRequest req, MethodArgumentNotValidException e) {
        ...etc;
    }
}
  1. @RequestBody 前面要加上 @Valid
  2. pojo (在此例指 User class) 要加上 validation annotation (e.g. @NotBlank, @NotNull ...etc)
  3. validate fail 會丟出 MethodArgumentNotValidException,要自己寫 handler 決定哪些異常明細回傳給前端

針對 @RequestParam 進行 validation

實作程式碼重點如下

@Configuration
@ComponentScan
public class WebConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}
@Validated
@RestController
@RequestMapping("/validateAnnotationDemo")
public class ValidateAnnotationDemoController {
    @RequestMapping(value = "/validateRequestParam", produces = MediaType.APPLICATION_JSON_VALUE)
    public Map<String,Object> validateRequestParam(@NotBlank(message = "message must not be blank") @RequestParam String message){
        ...etc;
    }
}
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity handleConstraintViolationException(HttpServletRequest req, ConstraintViolationException e){
        ...etc;
    }
}
  1. 要注冊 MethodValidationPostProcessor bean,不然無法對 @RequestParam 進行 validation
  2. 在 @RestController 前面加上 @Validated
  3. 在 @RequestParam 前面加上 validation annotation (e.g. @NotBlank, @NotNull ...etc)
  4. validate fail 會丟出 ConstraintViolationException,要自己寫 handler 決定哪些異常明細回傳給前端

完整範例

WebConfig.java

@Configuration
@ComponentScan
public class WebConfig {

    /**
     * 注冊 MethodValidationPostProcessor bean 之後,Controller class 開頭加的 @Validated 與 method 裡針
     * 對 @RequestParam annotation 加的 validate annotation (e.g. @NotBlank, @NotNull ...etc)才會生效,
     * 沒注冊時就算程式碼裡有加這些 annotation 還是不會有作用
     *
     * @return
     */
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

ValidateAnnotationDemoController.java

@Validated
@RestController
@RequestMapping("/validateAnnotationDemo")
public class ValidateAnnotationDemoController {

    //在 class 開頭加了 @Validated 之後,針對 @RequestParam 加的 validation annotation (e.g. @NotBlank, @NotNull...etc) 才會生效
    @RequestMapping(value = "/validateRequestParam", produces = MediaType.APPLICATION_JSON_VALUE)
    public Map<String,Object> validateRequestParam(
            @NotBlank(message = "message must not be blank") @RequestParam String message){
        Map<String,Object> result = new HashMap<>();
        result.put("message", message);
        return result;
    }

    // 加對 @Valid 之後,會驗証 pojo (此例中指 User instance) 內容格式是否正確
    @RequestMapping(value = "/validateRequestBody", produces = MediaType.APPLICATION_JSON_VALUE)
    public User validateRequestBody(@Valid @RequestBody User user){
        return user;
    }

    private static class User{
        @NotBlank
        private String name;

        // 針對 list of pojo 的資料格式進行驗証,要加上 @NotEmpty, @Valid 二個 annotation
        @NotEmpty
        @Valid
        private List<ContactInfo> contactInfoList;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public List<ContactInfo> getContactInfoList() {
            return contactInfoList;
        }

        public void setContactInfoList(List<ContactInfo> contactInfoList) {
            this.contactInfoList = contactInfoList;
        }
    }

    private static class ContactInfo{
        @NotBlank
        private String address;

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }
    }
}

GlobalExceptionHandler.java

/**
 * Controller 發生 uncatch exception 情況時,會統一在這個 class 被處理。
 */
@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public ResponseEntity handleException(HttpServletRequest req, Exception e){
        logger.error(e.getMessage(), e);
        String errorMsg = (e.getMessage() == null) ? e.getClass().getSimpleName() : e.getMessage();
        Map<String,Object> error = Collections.singletonMap("error", errorMsg);
        return ResponseEntity.status(500).body(error);
    }

    /**
     * Controller 裡標注 @RequestParam 的變數在 validate fail 時會丟出 ConstraintViolationException。這個 method
     * 專門處理此類 exception
     *
     * @param req
     * @param e
     *
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity handleConstraintViolationException(HttpServletRequest req, ConstraintViolationException e){
        logger.error(e.getMessage(), e);
        // "@NotBlank @RequestParam String myArg" 這樣的 validate 寫法在 validate fail 時無法得知 "哪個輸入參數名稱" 驗証失敗,這是 java reflection 本身的限制。
        // 用這類語法時要改寫成 "@NotBlank(myArg must not be blank) @RequestParam String myArg",程式裡的 validate annotation 要寫出 "完整出錯明細",
        // 不然在處理 ConstraintViolationException 時只會知道驗証失敗的原因,卻不知道是哪個輸入參數名稱驗証失敗。
        List<String> errorMessages = e.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.toList());
        Map<String,Object> error = Collections.singletonMap("error", errorMessages);
        return ResponseEntity.status(400).body(error);
    }

    /**
     * Controller 裡標注 @RequestBody 的變數在 validate fail 時會丟出 MethodArgumentNotValidException。這個 method
     * 專門處理此類 exception
     *
     * @param req
     * @param e
     *
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity handleMethodArgumentNotValidException(HttpServletRequest req, MethodArgumentNotValidException e) {
        logger.error(e.getMessage(), e);
        List<String> errorMessages = e.getBindingResult().getFieldErrors()
                .stream()
                .map(fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage()) // 記錄 "fieldName + validateFailMessage"
                .collect(Collectors.toList());
        Map<String,Object> error = Collections.singletonMap("error", errorMessages);
        return ResponseEntity.status(400).body(error);
    }
}

reference document


MAC 新手入門筆記

2016-03-04

Tags: mac

儿個月前因為工作需要摸了一陣子 MAC 筆電,有一些簡單心得,所以做個筆記以備未來不時之需 :)

MAC OS 入門影片

  1. 英文
  2. 中文

MAC OS 入門文件

SSH 相關 console command

  1. 產生 SSH 公私鑰

    ssh-keygen -t rsa
    
  2. Storing Passphrases in the Keychain, 免除每次 ssh 連線都要指定 private key 所在位置

    ssh-add -K /path/to/private/key/file
    
  3. 相關參考文件

利用 homebrew 指令安裝軟體

homebrew 可以幫你用 console command 的方式安裝與管理各種軟體,可以讓你在這方面省掉很多時間,強烈推薦要用這個好物。下面列的是我常會利用 homebrew 安裝的軟體。

# install xcode command tool
xcode-select --install
# check xcode command tool
xcode-select -p

# install homebrew
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
# check homebrew install success or not
brew doctor
brew update

# install homebrew-cask
brew tap caskroom/cask
brew install brew-cask

# i install these soft below by brew & brew-cask

brew install wget
brew install curl
brew install git
brew install htop-osx

# install java7
brew cask install caskroom/versions/java7
# install java8
brew cask install java

brew cask install intellij-idea-ce
brew cask install eclipse-jee

brew install maven
brew install gradle

brew cask install sourcetree
brew cask install cyberduck
brew cask install dbeaver-enterprise

brew cask install google-chrome
brew cask install firefox
brew cask install flash

brew cask install bettertouchtool

其它實用的教學文件


spring boot 官方文件摘要

2015-09-09

Tags: 程式語言 , java , gradle , spring

這篇內容寫的是 spring boot 1.2.5 官方文件 重點摘要,不做 spring boot 基礎教學。要看懂這些內容有下列前題假設

  1. 會用 java 寫 spring boot 程式
  2. 會用 gradle 寫 build script

如果你完全不懂 spring boot,建議看一下 Bootify your spring application (投影片)(錄影),看完後會對 spring boot 有基本認知,有助於日後快速上手。

spring-boot-teach 教學程式解說

我針對官方文件寫了個 spring-boot-teach 教學程式 放在 github 上,請把它抓回來看看程式怎麼寫、怎麼運作。此外,這節出現的範列在內容上有前後關係,所以這節內容不要跳著看,不然會發生看完後卻看不懂在講什麼的鳥事

程式目錄結構

教學程式目錄結構

application-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="hello" class="cloudtu.bean.Hello"/>
</beans>

這個 spring xml 設定檔會在 java config 時被匯入

application.properties

server.port=8080
# enable Tomcat's HTTP response compression
server.tomcat.compression=on

# set actuator HTTP port  
management.port=8081
# 只許可經由內網網址連入 actuator,可避免監控資料外流
management.address=127.0.0.1

welcome.first-message=java run anywhere
welcome.secondMessage=spring boot is easy to use

appVersion=1.0.0

db.prd.name=prd123
db.dev.name=dev456
# 設定要啟用哪些 profile
spring.profiles.active=dev

application.properties 是 spring boot 預設的外部設定檔。因為我針對 actuator 功能設定了 management.port=8081management.address=127.0.0.1,所以只能用 http://127.0.0.1:8081/${actuator_endpoint} 這類網址才可以取得 actuator 提供的資訊

author.properties

author.name=cloudtu
author.department=RD team

除了 application.properties 之外自訂的設定檔,之後會提供給 author bean 使用

Application.java

package cloudtu;

...etc
import cloudtu.bean.Author;
import cloudtu.bean.DbName;

// @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
@SpringBootApplication
// 匯入 XML 格式的 spring 設定檔
@ImportResource("classpath:application-context.xml")
// 設定 property file 設定檔的來源位置
@PropertySource("classpath:author.properties")
public class Application {
    ...etc

    // author.properties 裡的 author.* 設定會自動綁定到 authorName, authorDept 變數
    @Value("${author.name}") private String authorName;
    @Value("${author.department}") private String authorDept;

    @Bean
    public Author author(){
        Author author = new Author();
        // 手工指定 author bean 裡的各項 setter 設定
        author.setName(authorName);
        author.setDepartment(authorDept);
        return author;
    }

    @Bean
    // application.properties 裡的 db.prd.* 設定會自動綁定到 prdDbName bean
    @ConfigurationProperties(prefix="db.prd")
    @Profile("prd")
    public DbName prdDbName(){
        return new DbName();
    }

    @Bean
    // application.properties 裡的 db.dev.* 設定會自動綁定到 devDbName bean
    @ConfigurationProperties(prefix="db.dev")
    @Profile("dev")
    public DbName devDbName(){
        return new DbName();
    }

    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);
        ...etc
    }
}

程式碼關鍵處都加了註解,所以看完後你應該知道裡面各個 annotation 用法,不過有儿個地方再補充說明一下。
1. @ConfigurationProperties 可以把 *.property 設定檔的設定值經由各個 bean 的 setter 進行自動綁定動作
2. 在 application.properties 裡設定了 spring.profiles.active=dev,所以程式裡有 @Profile("dev") 宣告的 devDbName bean 在執行時會產生,但是有 @Profile("prd") 宣告的 prdDbName bean 在執行時不會產生

Welcome.java

package cloudtu.bean;

...etc
@Component
// application.properties 裡的 welcome.* 設定會自動綁定到 welcome bean
@ConfigurationProperties(prefix="welcome")
public class Welcome {
    private String firstMessage;
    private String secondMessage;

    // application.properties 裡的 welcome.first-message 設定綁定到 firstMessage instance variable
    public void setFirstMessage(String firstMessage) {
        this.firstMessage = firstMessage;
    }
    // application.properties 裡的 welcome.secondMessage 設定綁定到 secondMessage instance variable
    public void setSecondMessage(String secondMessage) {
        this.secondMessage = secondMessage;
    }
    ...etc
}

這個範例也是在介紹 @ConfigurationProperties 用法,不過你會發現設定檔裡的名稱格式不一致, welcome.first-message- 當變數名稱裡的分隔符號,另一個 welcome.secondMessage 則是用 lower camel case 命名方式。這兩種設定方式 spirng boot 都可以接受

AppVersion.java

package cloudtu.bean;

...etc
@Component
public class AppVersion {
    // application.properties 裡的 appVersion 設定會自動綁定到 appVersion 變數
    @Value("${appVersion}") String appVersion;
    ...etc
}

application.properties 裡的設定可以用 @Value("${appVersion}") 這類的宣告方式直接綁定到 bean 裡面

Filters.java

package cloudtu.web;

...etc
@Configuration
public class Filters{
    ...etc

    // spring boot 預設的 url mapping 是  "/*",所以任何 request 都會經過 firstFilter bean
    @Component
    private static class FirstFilter implements Filter{
        ...etc
    }

    private static class SecondFilter implements Filter{
        ...etc
    }

    @Bean
    public FilterRegistrationBean secondFilter(){
        FilterRegistrationBean registration = new FilterRegistrationBean(new SecondFilter());
        // 改寫掉預設的 url mapping,只有 "/hello" 的 request 會經過 secondFilter bean
        registration.addUrlPatterns("/hello");
        return registration;
    }
}
  1. Filter 可以直接加 @Component 讓它變成 spring 裡的 bean,firstFilter bean 就是這樣宣告。但是這樣宣告 filter 會讓它的 url mapping 固定都是 /*
  2. 如果想自定 url mapping,要用 FilterRegistrationBeanFilter 給包起來,secondFilter bean 就是在講自訂 url mapping 的寫法

官方文件裡與教學程式相關的章節

官方範列程式

官方在 github 有放 spring boot 官方範列程式,搞不懂某些功能怎麼用時可以去那邊找找有沒有解答

Testing

官方文件裡較重要相關章節條列如下

監控管理

官方文件裡較重要相關章節條列如下

Build by gradle

spring-boot-teach 教學程式裡的 build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE")
    }
}

// 引用 spring boot plugin
apply plugin: "spring-boot"
...etc

configurations {
    providedRuntime

    // spring boot 預設使用 logback 但是我要用 log4j,所以必需把 spring-boot-starter-logging 排除掉
    compile.exclude module: "spring-boot-starter-logging"
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    // spring-boot-starter-actuator 提供 "監控 app 運作狀況" 功能
    compile("org.springframework.boot:spring-boot-starter-actuator")
    // 改用 log4j 不用預設的 logback
    compile("org.springframework.boot:spring-boot-starter-log4j")
    ...etc
}

範例解說如下
1. 引用了 spring boot plugin 之後就可以利用它提供的各項專屬 task (e.g. bootRun, bootRepackage) 進行開發
2. spring boot 預設的 log 是 logback,我把它換成了 log4j。因為所有 spring-boot-starter-* 的 pom 都相依於 spring-boot-starter-logging,spring-boot-starter-logging 又相依於 logback,所以要加上 compile.exclude module: "spring-boot-starter-logging" 把所有 pom 裡的 spring-boot-starter-logging 相依都排除掉,不然程式裡的 log 會出現部份相依 logback 部份相依 log4j 的問題

常用的 gradle 指令

  • gradle bootRun
    • Run the project with support for auto-detecting main class and reloading static resources
    • 直接執行 compile 好的 class,不會打包成 war 或 jar
  • gradle bootRepackage
    • Repackage existing JAR and WAR archives so that they can be executed from the command line using 'java -jar'
    • 先把程式打包成 executable jar (or war),然後再執行 jar (or war)

官方文件裡較重要相關章節

‘How-to’ guides

官方文件有列出常見問題與解答,下列儿項是我目前比較會遇到的項目


gradle learning path

2015-09-03

Tags: 程式語言 , java , groovy , gradle

我用 gradle 當 build tool 已經好一陣子,通常照著網路上的 gradle 範例去修改就可以讓它滿足我的需求,真是超方便。可是對於 gradle script 為啥長這個樣子? 它的運作原理為何? 我一直搞不懂所以然。這次花了些時間終於了解它的基礎原理到底是怎麼回事。下述是我學習 gradle 基礎原理的 learning path,請照條列順序依序閱讀,看完後就可以對 gradle 知其然又知其所以然!

  1. 想要 gradle 快速上手? 看 popcorn 的 "Gradle起步走(投影片影片)" 就對啦
  2. 對 gradle 有點 fu 之後就可以把 grdle in action 這本書拿來看了。整本書分成三個 part,只要看 part1, part2 就夠了,part3 是比較進階的應用,未來有需要的話再來看
  3. grdle in action 看完後可能會覺的對 gradle script 的 groovy 語法理解太少,這時必需看一些 groovy 語法說明,所以下面這些資料都看一看吧。
  4. 前面的都看完後就可以開始看下列講 gradle 運作原理的文章,看完後就會知道 gradle script 語法其實是 groovy closure 與 delegate pattern 的組合
  5. 前述資料都看完後對於 gradle 官網的 user guide DSL Referencejavadoc 應該就都看的懂,而不會在查閱時有摸不著頭緒的感覺了


較舊的文章在 archive 專區.