Перейти к основному содержимому

Первый раунд улучшений приложения Reddit

· 6 мин. чтения

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 для подключения к жизненному циклу приложения после завершения процесса внедрения зависимостей.

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

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

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

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

API Reddit агрессивен в отношении запросов на ограничение скорости, которые не отправляют уникальный « 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 для тестирования

Далее — давайте продолжим и настроим БД в памяти — 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. Переключитесь на тимелеаф

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 :

@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. Автозаполнение субреддита

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

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

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

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

И вот наш метод контроллера:

@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. Развертывание в Heroku

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

9.1. Изменить 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. Прокфайл _ ``

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

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

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

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

cd path_to_your_project
heroku login
heroku create

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

Далее нам нужно настроить нашу базу данных, используя свойства базы данных 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.

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

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

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

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

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

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

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