First Round of Improvements to the Reddit Application – Reddit应用程序的第一轮改进

最后修改: 2015年 5月 13日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The Reddit web application Case Study is moving along nicely – and the small web application is shaping up and slowly becoming usable.

Reddit网络应用案例研究进展顺利–小型网络应用正在成型并慢慢变得可用。

In this installment, we’re going to be making small improvements to the existing functionality – some externally facing, some not – and generally making the app better.

在这一期中,我们将对现有的功能进行小的改进–有些是面向外部的,有些不是–总的来说是使应用程序更好

2. Setup Checks

2.设置检查

Let’s start with some simple – but useful – checks that need to run when the application is bootstrapped:

让我们从一些简单–但有用–的检查开始,这些检查需要在应用程序启动时运行。

@Autowired
private UserRepository repo;

@PostConstruct
public void startupCheck() {
    if (StringUtils.isBlank(accessTokenUri) || 
      StringUtils.isBlank(userAuthorizationUri) || 
      StringUtils.isBlank(clientID) || StringUtils.isBlank(clientSecret)) {
        throw new RuntimeException("Incomplete reddit properties");
    }
    repo.findAll();
}

Note how we’re using the @PostConstruct annotation here to hook into the lifecycle of the application, after the dependency injection process is over.

请注意,我们在这里使用@PostConstruct注解来钩住应用程序的生命周期,在依赖注入过程结束后。

The simple goals are:

简单的目标是。

  • check if we have all the properties we need to access the Reddit API
  • check that the persistence layer is working (by issuing a simple findAll call)

If we fail – we do so early.

如果我们失败了–我们很早就失败了。

3. The “Too Many Requests” Reddit Problem

3. “请求太多 “的Reddit问题

The Reddit API is aggressive in rate limiting requests that aren’t sending a unique “User-Agent“.

Reddit API对没有发送唯一的”User-Agent“的请求进行了积极的速率限制。

So – we need to add in this unique User-Agent header to our redditRestTemplate – using a custom Interceptor:

所以–我们需要在我们的redditRestTemplate中加入这个独特的User-Agent头–使用一个自定义的Interceptor

3.1. Create Custom Interceptor

3.1.创建自定义拦截器

Here is our custom interceptor – UserAgentInterceptor:

这里是我们的自定义拦截器–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. Configure redditRestTemplate

3.2.配置redditRestTemplate

We of course need to set this interceptor up with the redditRestTemplate we’re using:

我们当然需要用我们正在使用的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. Configure H2 Database for Testing

4.为测试配置H2数据库

Next – let’s go ahead and set up an in-memory DB – H2 – for testing. We need to add this dependency to our pom.xml:

接下来–让我们继续前进,建立一个内存数据库–H2–用于测试。我们需要在我们的pom.xml中添加这个依赖关系。

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

And define a persistence-test.properties:

并定义一个 persistence-test.properties

## DataSource Configuration ###
jdbc.driverClassName=org.h2.Driver
jdbc.url=jdbc:h2:mem:oauth_reddit;DB_CLOSE_DELAY=-1
jdbc.user=sa
jdbc.pass=
## Hibernate Configuration ##
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=update

5. Switch to Thymeleaf

5.改用百里香叶

JSP is out and Thymeleaf is in.

JSP出局,Thymeleaf入局。

5.1. Modify pom.xml

5.1.修改pom.xml

First, we need to add these dependencies to our 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. Create ThymeleafConfig

5.2.创建ThymeleafConfig

Next – a simple 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;
    }
}

And add it to our ServletInitializer:

并将其添加到我们的ServletInitializer

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

5.3. Modify home.html

5.3.修改home.html

And a quick modification of the homepage:

并对主页进行了快速修改。

<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. Logout

6.注销

Now – let’s do some improvements that are actually visible to the end user of the application. We’ll start with logout.

现在–让我们做一些对应用程序的终端用户来说实际可见的改进。我们将从注销开始。

We’re adding a simple logout option into the application by modifying our security config:

我们通过修改我们的安全配置,在应用程序中添加一个简单的注销选项。

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

7. Subreddit Autocomplete

7.Subreddit Autocomplete

Next – let’s implement a simple autocomplete functionality for the filling it the subreddit; writing it manually is not a good way to go, since there’s a fair chance to get it wrong.

下一步–让我们实现一个简单的自动完成功能,用于填写subreddit;手动编写不是一个好办法,因为有相当大的机会会出错。

Let’s start with the client side:

让我们从客户端开始。

<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>

Simple enough. Now, the server side:

够简单了。现在,服务器方面。

@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. Check If Link Is Already on Reddit

8.检查链接是否已经在Reddit上

Next – let’s see how to check if a link is already submitted before to Reddit.

接下来,让我们看看如何检查一个链接是否已经提交给Reddit。

Here is our submissionForm.html:

这里是我们的submissionForm.html

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

<a href="#" onclick="checkIfAlreadySubmitted()">Check if already submitted</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("Not submitted before");
            }else{
                $("#checkResult").show().html(
               'Already submitted <b><a target="_blank" href="http://www.reddit.com'
               +result[0].data.permalink+'">here</a></b>');
            }
        });
    }
    else{
        $("#checkResult").show().html("Too short url and/or subreddit");
    }
}           
</script>

And here is our controller method:

而这里是我们的控制器方法。

@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

9.部署到Heroku

Finally – we’re going to set up deployment to Heroku – and use their free tier to power the sample app.

最后–我们将设置部署到Heroku–并使用他们的免费层来驱动示例应用程序。

9.1. Modify pom.xml

9.1.修改pom.xml

First, we will need to add this Web Runner plugin to the 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>

Note – we will use Web Runner to launch our app on Heroku.

注意 – 我们将使用Web Runner在Heroku上启动我们的应用程序。

We’re going to be using Postgresql on Heroku – so we’ll need to have a dependency to the driver:

我们将在Heroku上使用Postgresql – 所以我们需要对驱动程序有一个依赖性。

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

9.2. The Procfile

9.2.Procfile

We need to define the process that will run on the server in a Procfile – as follows:

我们需要在Procfile中定义将在服务器上运行的进程 – 如下所示。

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

9.3. Create Heroku App

9.3.创建Heroku应用程序

To create a Heroku app from your project, we’ll simply:

为了从你的项目中创建一个Heroku应用程序,我们将简单地。

cd path_to_your_project
heroku login
heroku create

9.4. Database Configuration

9.4.数据库配置

Next – we need to configure our database using our app’s Postgres database properties.

接下来–我们需要使用我们应用程序的Postgres数据库属性来配置我们的数据库。

For example, here is persistence-prod.properties:

例如,这里是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

Note that we need to get the database details [host name, database name, user and password] form the Heroku dashborad.

注意,我们需要从Heroku dashborad中获得数据库的详细信息[主机名、数据库名、用户和密码]。

Also – like in most cases, the keyword “user” is a reserved word in Postgres, so we need to change our “User” entity table name:

另外–和大多数情况一样,关键词 “user “在Postgres中是一个保留词,所以我们需要改变我们的”User“实体表名称。

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

9.5. Push Code to Heoku

9.5 推送代码到黑库

Now – let’s push code to Heroku:

现在–让我们把代码推送到Heroku。

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

10. Conclusion

10.结论

In this forth part of our Case Study, the focus were small but important improvements. If you’ve been following along, you can see how this is shaping up to be an interesting and useful little app.

在我们案例研究的第四部分中,重点是小而重要的改进。如果你一直在关注,你可以看到这如何形成一个有趣和有用的小应用程序。