«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("Incomplete reddit properties");
    }
    repo.findAll();
}

Обратите внимание, как мы используем аннотацию @PostConstruct. здесь, чтобы подключиться к жизненному циклу приложения после завершения процесса внедрения зависимостей.

Простые цели:

    проверить, есть ли у нас все свойства, необходимые для доступа к API Reddit; проверить, работает ли уровень персистентности (выполнив простой вызов findAll)

Если мы потерпим неудачу, мы сделать это рано.

3. Проблема Reddit «Слишком много запросов»

Reddit API агрессивно ограничивает скорость запросов, которые не отправляют уникальный «User-Agent».

Итак, нам нужно добавить этот уникальный заголовок User-Agent в наш шаблон redditRestTemplate, используя собственный Interceptor:

3.1. Создать собственный перехватчик

Вот наш собственный перехватчик — 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 для тестирования

Далее — давайте продолжим и настроим in -БД памяти – H2 – для тестирования. Нам нужно добавить эту зависимость в наш pom.xml:

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

И определить файл 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. Переключиться на Thymeleaf

JSP отсутствует, а Thymeleaf включен. ~ ~~ 5.1. Изменить pom.xml

Во-первых, нам нужно добавить следующие зависимости в наш pom.xml:

5.2. Создайте ThymeleafConfig

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

Далее — простой ThymeleafConfig:

И добавьте его в наш ServletInitializer:

@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;
    }
}

5.3. Modify home.html

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

И быстрое изменение домашней страницы:

6. Выход

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

Теперь давайте сделаем некоторые улучшения, которые на самом деле видны конечному пользователю приложения. Мы начнем с выхода из системы.

Мы добавляем в приложение простую опцию выхода из системы, изменив нашу конфигурацию безопасности:

7. Автозаполнение сабреддита

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

Далее — давайте реализуем простую функцию автозаполнения для заполнения сабреддита; писать это вручную — не лучший способ, так как есть шанс ошибиться.

Начнем с клиентской части:

Достаточно просто. Теперь серверная сторона:

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

8. Проверить, есть ли ссылка уже на Reddit

@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();
}

Далее — давайте посмотрим, как проверить, была ли ссылка уже отправлена ​​ранее на Reddit.

Вот наш submitForm.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>

9. Развертывание на Heroku

@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();
}

Наконец, мы настроим развертывание на Heroku — и используйте их бесплатный уровень для запуска примера приложения.

9.1. Измените pom.xml

Во-первых, нам нужно добавить этот плагин Web Runner в pom.xml:

Примечание. Мы будем использовать Web Runner для запуска нашего приложения на Heroku.

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

Мы собираемся использовать Postgresql на Heroku, поэтому нам понадобится зависимость от драйвера:

9.2. Procfile

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

Нам нужно определить процесс, который будет выполняться на сервере в Procfile – следующим образом:

9.3. Создание приложения Heroku

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

Чтобы создать приложение Heroku из вашего проекта, мы просто:

9.4. Конфигурация базы данных

cd path_to_your_project
heroku login
heroku create

Далее — нам нужно настроить нашу базу данных, используя свойства базы данных Postgres нашего приложения.

Например, вот файл persistence-prod.properties:

Обратите внимание, что нам нужно получить сведения о базе данных [имя хоста, имя базы данных, пользователь и пароль] из информационной панели Heroku.

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

Кроме того, как и в большинстве случаев, ключевое слово «user» является зарезервированным словом в Postgres, поэтому нам нужно изменить имя таблицы сущностей «User»:

9.5. Отправка кода в Heoku

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

Теперь — давайте отправим код в Heroku:

10. Заключение

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

«В этой четвертой части нашего тематического исследования основное внимание уделялось небольшим, но важным улучшениям. Если вы следили за новостями, вы можете увидеть, как это превращается в интересное и полезное маленькое приложение.

«