Redditアプリケーションの2回目の改善

1概要

私たちのリンクを続けましょう:/case-study-red-app-with-spring[進行中のReddit Webアプリケーションのケーススタディ]では、アプリケーションをよりユーザーフレンドリーで使いやすくすることを目的とした新しい改善が行われています。

2スケジュールされた投稿のページ付け

まず、全体を見やすく、理解しやすくするために、スケジュールされた投稿をページ付けして** 表示しましょう。

2.1. ページ付けされた操作

Spring Dataを使用して必要な操作を生成し、 Pageable インターフェイスを使用してユーザーの予定投稿を取得します。

public interface PostRepository extends JpaRepository<Post, Long> {
    Page<Post> findByUser(User user, Pageable pageable);
}

そしてこれが私たちのコントローラメソッド getScheduledPosts() です:

private static final int PAGE__SIZE = 10;

@RequestMapping("/scheduledPosts")
@ResponseBody
public List<Post> getScheduledPosts(
  @RequestParam(value = "page", required = false) int page) {
    User user = getCurrentUser();
    Page<Post> posts =
      postReopsitory.findByUser(user, new PageRequest(page, PAGE__SIZE));

    return posts.getContent();
}

2.2. ページ区切り付きの投稿を表示

それでは - フロントエンドに簡単なページネーションコントロールを実装しましょう:

<table>
<thead><tr><th>Post title</th></thead>
</table>
<br/>
<button id="prev" onclick="loadPrev()">Previous</button>
<button id="next" onclick="loadNext()">Next</button>

そして、ここにプレーンなjQueryを使ってページを読み込む方法があります。

$(function(){
    loadPage(0);
});

var currentPage = 0;
function loadNext(){
    loadPage(currentPage+1);
}

function loadPrev(){
    loadPage(currentPage-1);
}

function loadPage(page){
    currentPage = page;
    $('table').children().not(':first').remove();
    $.get("api/scheduledPosts?page="+page, function(data){
        $.each(data, function( index, post ) {
            $('.table').append('<tr><td>'+post.title+'</td><td></tr>');
        });
    });
}

先に進むと、この手動のテーブルはより成熟したテーブルプラグインに素早く置き換えられるでしょうが、今のところ、これはうまく機能します。

** 3ログインページをログインしていないユーザーに表示する

**

ユーザーがルートにアクセスすると、 ログインしているかどうかにかかわらず 別のページが表示されます。

ユーザーがログインしている場合は、自分のホームページ/ダッシュボードが表示されます。ログインしていない場合は、ログインページが表示されます。

@RequestMapping("/")
public String homePage() {
    if (SecurityContextHolder.getContext().getAuthentication() != null) {
        return "home";
    }
    return "index";
}

** 4再送信後の詳細オプション

**

Redditで投稿を削除して再送信することは便利で非常に効果的な機能です。しかし、私たちはそれに注意して、いつ、そしていつやるべきでないかを** 完全に制御する必要があります。

例えば ​​- すでにコメントがある場合、投稿を削除したくないかもしれません。一日の終わりには、コメントはエンゲージメントであり、我々はプラットフォームとポストにコメントしている人々を尊重したいと思います。

  • だから - これが私たちが追加する最初の小さいながらも非常に便利な機能です** - コメントがない場合にのみ投稿を削除できる新しいオプション

答えるべきもう一つの非常に興味深い質問は - ポストが何度も何度も再提出されるけれどもそれでもそれが必要とする牽引力を得ないならば - 我々は最後の試みの後それを残すかどうか?まあ、他の面白い質問と同じように、ここでの答えは「それは違います」です。通常の投稿の場合は、1日に電話をかけてお任せください。しかし、それが非常に重要な投稿であり、それが何らかの牽引力を得ることを本当に確かめたいのであれば、最後にそれを削除するかもしれません。

  • これが、ここで作成する2つ目の小さいながらも非常に便利な機能です。

最後に - 物議を醸す記事はどうですか?そこに肯定的な投票があるため、またはそれが100の正と98の負の投票を持っているので投稿はredditに2票を持つことができます。最初の選択肢はそれが牽引力を得ていないことを意味し、2番目の選択肢はそれが多くの牽引力を得ていることと投票が分割されていることを意味します。

  • だから - これは私たちが追加しようとしている3番目の小さな機能です。** - 投稿を削除する必要があるかどうかを判断するときに、この上下の比率を考慮する新しいオプション

4.1. Post エンティティ

まず、 Post エンティティを修正する必要があります。

@Entity
public class Post {
    ...
    private int minUpvoteRatio;
    private boolean keepIfHasComments;
    private boolean deleteAfterLastAttempt;
}

これが3つのフィールドです。

  • minUpvoteRatio :ユーザーが自分の投稿を希望する最小の投票率

reach - 支持率は、総投票数のうち何%が支持を得ているかを表す[最大= 100、最小= 0]

  • keepIfHasComments :ユーザーが自分の投稿を残したいかどうかを決定します

必要なスコアに達していないにもかかわらずコメントがある場合

  • deleteAfterLastAttempt :ユーザーが削除するかどうかを決定します

最後の試みが終わった後のポストは、必要な得点に達することなく終わります。

4.2. スケジューラ

では、これらの興味深い新しいオプションをスケジューラに統合しましょう。

@Scheduled(fixedRate = 3 **  60 **  1000)
public void checkAndDeleteAll() {
    List<Post> submitted =
      postReopsitory.findByRedditIDNotNullAndNoOfAttemptsAndDeleteAfterLastAttemptTrue(0);

    for (Post post : submitted) {
        checkAndDelete(post);
    }
}

もっとおもしろいところは、 checkAndDelete() の実際のロジックです。

private void checkAndDelete(Post post) {
    if (didIntervalPass(post.getSubmissionDate(), post.getTimeInterval())) {
        if (didPostGoalFail(post)) {
            deletePost(post.getRedditID());
            post.setSubmissionResponse("Consumed Attempts without reaching score");
            post.setRedditID(null);
            postReopsitory.save(post);
        } else {
            post.setNoOfAttempts(0);
            post.setRedditID(null);
            postReopsitory.save(post);
        }
    }
}

これが didPostGoalFail() の実装です。

private boolean didPostGoalFail(Post post) {
    PostScores postScores = getPostScores(post);
    int score = postScores.getScore();
    int upvoteRatio = postScores.getUpvoteRatio();
    int noOfComments = postScores.getNoOfComments();
    return (((score < post.getMinScoreRequired()) ||
             (upvoteRatio < post.getMinUpvoteRatio())) &&
           !((noOfComments > 0) && post.isKeepIfHasComments()));
}

また、Redditから Post 情報を取得するロジックを変更する必要があります。これにより、より多くのデータを確実に収集できます。

public PostScores getPostScores(Post post) {
    JsonNode node = restTemplate.getForObject(
      "http://www.reddit.com/r/" + post.getSubreddit() +
      "/comments/" + post.getRedditID() + ".json", JsonNode.class);
    PostScores postScores = new PostScores();

    node = node.get(0).get("data").get("children").get(0).get("data");
    postScores.setScore(node.get("score").asInt());

    double ratio = node.get("upvote__ratio").asDouble();
    postScores.setUpvoteRatio((int) (ratio **  100));

    postScores.setNoOfComments(node.get("num__comments").asInt());

    return postScores;
}

Reddit APIからスコアを抽出するために、スコアを表すために単純な値オブジェクトを使用しています。

public class PostScores {
    private int score;
    private int upvoteRatio;
    private int noOfComments;
}

最後に、正常に再送信された投稿の redditID null に設定するために checkAndReSubmit() を変更する必要があります。

private void checkAndReSubmit(Post post) {
    if (didIntervalPass(post.getSubmissionDate(), post.getTimeInterval())) {
        if (didPostGoalFail(post)) {
            deletePost(post.getRedditID());
            resetPost(post);
        } else {
            post.setNoOfAttempts(0);
            post.setRedditID(null);
            postReopsitory.save(post);
        }
    }
}

ご了承ください:

  • checkAndDeleteAll() :3分ごとに実行され、存在するかどうかを確認します。

投稿が試行されたため削除できます ** getPostScores() :投稿の\ {スコア、投票率、投票数

コメント}

4.3. スケジュールページを変更する

schedulePostForm.html に新しい変更を追加する必要があります。

<input type="number" name="minUpvoteRatio"/>
<input type="checkbox" name="keepIfHasComments" value="true"/>
<input type="checkbox" name="deleteAfterLastAttempt" value="true"/>

5重要なログをメールで送信

次に、私たちのログバック設定にすばやく便利な設定を実装します - 重要なログの電子メール送信( ERROR レベル)** 。これは、アプリケーションのライフサイクルの早い段階でエラーを簡単に追跡するのに非常に便利です。

まず、 pom.xml に必要な依存関係をいくつか追加します。

<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<dependency>
    <groupId>javax.mail</groupId>
    <artifactId>mail</artifactId>
    <version>1.4.1</version>
</dependency>

次に、 logback.xmlに SMTPAppender__を追加します。

<configuration>

    <appender name="STDOUT" ...

    <appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>

        <smtpHost>smtp.example.com</smtpHost>
        <to>[email protected]</to>
        <from>[email protected]</from>
        <username>[email protected]</username>
        <password>password</password>
        <subject>%logger{20} - %m</subject>
        <layout class="ch.qos.logback.classic.html.HTMLLayout"/>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="EMAIL"/>
    </root>

</configuration>

そしてそれはそれについてです - 今、デプロイされたアプリケーションはそれが起こると同時にどんな問題にでも電子メールを送るでしょう。

6. サブクレジットのキャッシュ

結局のところ、 自動補完subredditsは高価です 。投稿をスケジュールするときにユーザーがサブクレジットの入力を開始するたびに、これらのサブクレジットを取得してユーザーにいくつかの提案を表示するには、Reddit APIにアクセスする必要があります。

理想的ではありません。

Reddit APIを呼び出すのではなく、人気のあるサブクレジットをキャッシュしてオートコンプリートに使用します。

6.1. サブクレジットを取得する

まず、最も人気のあるサブクレジットを取得し、それらを普通のファイルに保存しましょう。

public void getAllSubreddits() {
    JsonNode node;
    String srAfter = "";
    FileWriter writer = null;
    try {
        writer = new FileWriter("src/main/resources/subreddits.csv");
        for (int i = 0; i < 20; i++) {
            node = restTemplate.getForObject(
              "http://www.reddit.com/" + "subreddits/popular.json?limit=100&after=" + srAfter,
              JsonNode.class);
            srAfter = node.get("data").get("after").asText();
            node = node.get("data").get("children");
            for (JsonNode child : node) {
                writer.append(child.get("data").get("display__name").asText() + ",");
            }
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                logger.error("Error while getting subreddits", e);
            }
        }
        writer.close();
    } catch (Exception e) {
        logger.error("Error while getting subreddits", e);
    }
}

これは成熟した実装ですか?いいえ、それ以上必要ですか。いいえ、違います。先へ進む必要があります。

6.2. サブブレッドオートコンプリート

次に、サービスに InitializingBean を実装させることで、 サブクレジットがアプリケーションの起動時に メモリに読み込まれるようにします。

public void afterPropertiesSet() {
    loadSubreddits();
}
private void loadSubreddits() {
    subreddits = new ArrayList<String>();
    try {
        Resource resource = new ClassPathResource("subreddits.csv");
        Scanner scanner = new Scanner(resource.getFile());
        scanner.useDelimiter(",");
        while (scanner.hasNext()) {
            subreddits.add(scanner.next());
        }
        scanner.close();
    } catch (IOException e) {
        logger.error("error while loading subreddits", e);
    }
}

subredditデータがすべてメモリにロードされたので、 Reddit APIを押すことなくsubredditsを検索できます

public List<String> searchSubreddit(String query) {
    return subreddits.stream().
      filter(sr -> sr.startsWith(query)).
      limit(9).
      collect(Collectors.toList());
}

サブクレジットの提案を公開するAPIはもちろん同じです。

@RequestMapping(value = "/subredditAutoComplete")
@ResponseBody
public List<String> subredditAutoComplete(@RequestParam("term") String term) {
    return service.searchSubreddit(term);
}

7. メトリックス

最後に、いくつかの簡単な指標をアプリケーションに統合します。これらの種類の測定基準を構築することについてもっと多くのために、リンクしなさい:/spring-rest-api-metrics[ここでそれらについていくらか詳細に書いた]

7.1. サーブレットフィルタ

これは単純な MetricFilter です。

@Component
public class MetricFilter implements Filter {

    @Autowired
    private IMetricService metricService;

    @Override
    public void doFilter(
      ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
        HttpServletRequest httpRequest = ((HttpServletRequest) request);
        String req = httpRequest.getMethod() + " " + httpRequest.getRequestURI();

        chain.doFilter(request, response);

        int status = ((HttpServletResponse) response).getStatus();
        metricService.increaseCount(req, status);
    }
}

また、 ServletInitializer に追加する必要があります。

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    super.onStartup(servletContext);
    servletContext.addListener(new SessionListener());
    registerProxyFilter(servletContext, "oauth2ClientContextFilter");
    registerProxyFilter(servletContext, "springSecurityFilterChain");
    registerProxyFilter(servletContext, "metricFilter");
}

7.2. メトリックサービス

そして、これが私たちの MetricService です:

public interface IMetricService {
    void increaseCount(String request, int status);

    Map getFullMetric();
    Map getStatusMetric();

    Object[][]getGraphData();
}

7.3. メトリックコントローラ

そして彼女は、HTTP経由でこれらのメトリクスを公開することを担当する基本的なコントローラです。

@Controller
public class MetricController {

    @Autowired
    private IMetricService metricService;

   //

    @RequestMapping(value = "/metric", method = RequestMethod.GET)
    @ResponseBody
    public Map getMetric() {
        return metricService.getFullMetric();
    }

    @RequestMapping(value = "/status-metric", method = RequestMethod.GET)
    @ResponseBody
    public Map getStatusMetric() {
        return metricService.getStatusMetric();
    }

    @RequestMapping(value = "/metric-graph-data", method = RequestMethod.GET)
    @ResponseBody
    public Object[][]getMetricGraphData() {
        Object[][]result = metricService.getGraphData();
        for (int i = 1; i < result[0].length; i++) {
            result[0][i]= result[0][i].toString();
        }
        return result;
    }
}

8結論

このケーススタディは順調に成長しています。このアプリは実際にはReddit APIを使ってOAuthを実行するための簡単なチュートリアルとして始まりました。今、それはRedditパワーユーザーのための便利なツールに発展しています - 特にスケジュールと再提出オプションに関して。

最後に、私はこれを使っているので、Redditへの私の自身の応募は一般により多くの勢いを増しているように見えます。