Reddit應用首輪改進

REST,Security,Spring
Remote
1
06:44 PM · Dec 01 ,2025

1. 概述

Reddit 網頁應用程序案例研究進展順利——小型應用程序正在成型並逐漸變得可用。

在本篇中,我們將對現有功能進行一些小改進——有些是面向外部的,有些不是——並且總體上使應用程序變得更好

2. 設置檢查

讓我們從一些簡單但有用的檢查開始,這些檢查需要在應用程序啓動時運行:

@Autowired
private UserRepository repo;

@PostConstruct
public void startupCheck() {
    if (StringUtils.isBlank(accessTokenUri) || 
      StringUtils.isBlank(userAuthorizationUri) || 
      StringUtils.isBlank(clientID) || StringUtils.isBlank(clientSecret)) {
        throw new RuntimeException("缺少 Reddit 屬性");
    }
    repo.findAll();
}

請注意,我們在這裏使用 @PostConstruct 標註來鈎入應用程序的生命週期,在依賴注入過程結束後。

簡單的目標是:

  • 檢查我們是否擁有訪問 Reddit API 所需的所有屬性
  • 檢查持久層是否正常工作(通過發出簡單的 findAll 調用)

如果失敗,我們會盡早失敗。

3. Reddit API 請求限制問題

Reddit API 對未發送包含唯一 "User-Agent" 的請求非常嚴格地限制請求速率。

因此,我們需要在我們的 redditRestTemplate 中添加此唯一的 User-Agent 標頭,使用自定義 Interceptor

3.1. 創建自定義 Interceptor

這是我們的自定義攔截器 – UserAgentInterceptor

public class UserAgentInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(
      HttpRequest request, byte[] body, 
      ClientHttpRequestExecution execution) throws IOException {

        HttpHeaders headers = request.getHeaders();
        headers.add("User-Agent", "Schedule with Reddit");
        return execution.execute(request, body);
    }
}

3.2. 配置 redditRestTemplate

當然,我們需要將此攔截器配置到我們使用的 redditRestTemplate 中:

@Bean
public OAuth2RestTemplate redditRestTemplate(OAuth2ClientContext clientContext) {
    OAuth2RestTemplate template = new OAuth2RestTemplate(reddit(), clientContext);
    List<ClientHttpRequestInterceptor> list = new ArrayList<ClientHttpRequestInterceptor>();
    list.add(new UserAgentInterceptor());
    template.setInterceptors(list);
    return template;
}

4. 配置 H2 數據庫用於測試

接下來,讓我們設置一個內存數據庫——H2——用於測試。我們需要將此依賴項添加到我們的 pom.xml 中:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.187</version>
</dependency>

並定義一個 persistence-test.properties:

## 數據源配置 ###
jdbc.driverClassName=org.h2.Driver
jdbc.url=jdbc:h2:mem:oauth_reddit;DB_CLOSE_DELAY=-1
jdbc.user=sa
jdbc.pass=
## Hibernate 配置 ##
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=update

5. 切換到 Thymeleaf

JSP 已被淘汰,Thymeleaf 已經進入。

5.1. 修改 pom.xml

首先,我們需要將這些依賴項添加到我們的 pom.xml 中:


<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>2.1.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring4</artifactId>
    <version>2.1.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity3</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

5.2. 創建 ThymeleafConfig

接下來 – 一個簡單的 ThymeleafConfig:


@Configuration
public class ThymeleafConfig {
    @Bean
    public TemplateResolver templateResolver() {
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
        templateResolver.setPrefix("/WEB-INF/jsp/");
        templateResolver.setSuffix(".jsp");
        return templateResolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver());
        templateEngine.addDialect(new SpringSecurityDialect());
        return templateEngine;
    }

    @Bean
    public ViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine());
        viewResolver.setOrder(1);
        return viewResolver;
    }
}

並將它添加到我們的 ServletInitializer:


@Override
protected WebApplicationContext createServletApplicationContext() {
    AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
    context.register(PersistenceJPAConfig.class, WebConfig.class, 
      SecurityConfig.class, ThymeleafConfig.class);
    return context;
}

5.3. 修改 home.html

以及主頁的快速修改:


<html>
<head>
<title>Schedule to Reddit</title>
</head>
<body>
<div class="container">
        <h1>Welcome, <small><span sec:authentication="principal.username">Bob</span></small></h1>
        <br/>
        <a href="posts" >My Scheduled Posts</a>
        <a href="post" >Post to Reddit</a>
        <a href="postSchedule" >Schedule Post to Reddit</a>
</div>
</body>
</html>

6. 登出

現在,讓我們進行一些對最終用户可見的改進。我們先從登出功能開始。

通過修改我們的安全配置,我們在應用程序中添加了一個簡單的登出選項:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .....
        .and()
        .logout()
        .deleteCookies("JSESSIONID")
        .logoutUrl("/logout")
        .logoutSuccessUrl("/");
}

7. Subreddit Autocomplete

接下來,讓我們實現一個簡單的自動完成功能,用於填寫subreddit;手動輸入並非最佳選擇,因為存在出錯的風險。

我們先從客户端開始:

<input id="sr" name="sr"/>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js"></script>
<script>
$(function() {
    $( "#sr" ).autocomplete({
        source: "/subredditAutoComplete"
    });
});
</script>

簡單易行。現在,我們進入服務器端:

@RequestMapping(value = "/subredditAutoComplete")
@ResponseBody
public String subredditAutoComplete(@RequestParam("term") String term) {
    MultiValueMap<String, String> param = new LinkedMultiValueMap<String, String>();
    param.add("query", term);
    JsonNode node = redditRestTemplate.postForObject(
      "https://oauth.reddit.com//api/search_reddit_names", param, JsonNode.class);
    return node.get("names").toString();
}

8. 檢查鏈接是否已提交到 Reddit

接下來,讓我們看看如何檢查鏈接是否已提交到 Reddit。

這是我們的 submissionForm.html

<input name="url" />
<input name="sr">

<a href="#" onclick="checkIfAlreadySubmitted()">檢查是否已提交</a>
<span id="checkResult" style="display:none"></span>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script>
$(function() {
    $("input[name='url'],input[name='sr']").focus(function (){
        $("#checkResult").hide();
    });
});
function checkIfAlreadySubmitted(){
    var url = $("input[name='url']").val();
    var sr = $("input[name='sr']").val();
    if(url.length >3 && sr.length > 3){
        $.post("checkIfAlreadySubmitted",{url: url, sr: sr}, function(data){
            var result = JSON.parse(data);
            if(result.length == 0){
                $("#checkResult").show().html("未提交之前");
            }else{
                $("#checkResult").show().html(
               '已提交 <b><a target="_blank" href="http://www.reddit.com'
               +result[0].data.permalink+'">此處</a></b>');
            }
        });
    }
    else{
        $("#checkResult").show().html("URL 和/或子版塊太短");
    }
}           
</script>

這是我們的控制器方法:

@RequestMapping(value = "/checkIfAlreadySubmitted", method = RequestMethod.POST)
@ResponseBody
public String checkIfAlreadySubmitted(
  @RequestParam("url") String url, @RequestParam("sr") String sr) {
    JsonNode node = redditRestTemplate.getForObject(
      "https://oauth.reddit.com/r/" + sr + "/search?q=url:" + url + "&restrict_sr=on", JsonNode.class);
    return node.get("data").get("children").toString();
}

9. Deployment to Heroku

終於——我們將設置部署到Heroku——並使用他們的免費層來為示例應用程序供電。

9.1. Modify pom.xml

首先,我們需要將這個 Web Runner 插件添加到 pom.xml:


    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-dependency-plugin</artifactId>
        <version>2.3</version>
        <executions>
            <execution>
                <phase>package</phase>
                <goals><goal>copy</goal></goals>
                <configuration>
                    <artifactItems>
                        <artifactItem>
                            <groupId>com.github.jsimone</groupId>
                            <artifactId>webapp-runner</artifactId>
                            <version>7.0.57.2</version>
                            <destFileName>webapp-runner.jar</destFileName>
                        </artifactItem>
                    </artifactItems>
                </configuration>
            </execution>
        </executions>
    </plugin>

注意——我們將使用Web Runner來在Heroku上啓動我們的應用程序。

我們將使用Postgresql在Heroku上——因此我們需要對驅動程序具有依賴關係:


    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>9.4-1201-jdbc41</version>
    </dependency>

9.2. The Procfile

我們需要定義將在服務器上運行的過程,在 Procfile 中,如下所示:


    web:    java $JAVA_OPTS -jar target/dependency/webapp-runner.jar --port $PORT target/*.war

9.3. Create Heroku App

要從您的項目創建Heroku應用程序,我們只需:


    cd path_to_your_project
    heroku login
    heroku create

9.4. Database Configuration

接下來——我們需要使用應用程序的 Postgres 數據庫屬性配置數據庫。

例如,以下是 persistence-prod.properties:


    ## DataSource Configuration ##
    jdbc.driverClassName=org.postgresql.Driver
    jdbc.url=jdbc:postgresql://hostname:5432/databasename
    jdbc.user=xxxxxxxxxxxxxx
    jdbc.pass=xxxxxxxxxxxxxxxxx

    ## Hibernate Configuration ##
    hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
    hibernate.hbm2ddl.auto=update

請注意,我們必須從Heroku儀錶板獲取數據庫詳細信息[主機名、數據庫名、用户名和密碼]。

此外——就像在大多數情況下一樣,"user" 是 Postgres 中的一個 保留字,因此我們需要更改我們的 " User " 實體表名:


    @Entity
    @Table(name = "APP_USER")
    public class User { .... }

9.5. Push Code to Heoku

現在——讓我們將代碼推送到Heroku:


    git add .
    git commit -m "init"
    git push heroku master

10. 結論

在本案例研究的第四部分中,重點在於一些小但重要的改進。如果您一直跟隨,您可以看到它正在成型為一個有趣且有用的小應用程序。

發佈 評論

Some HTML is okay.