1. 概要

進行中のRedditWebアプリのケーススタディを継続して、アプリケーションをよりユーザーフレンドリーで使いやすくすることを目的として、新しいラウンドの改善を行いましょう。

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つの非常に興味深い質問は、投稿が何度も再送信されても、必要な牽引力が得られない場合、最後の試行の後でそれをオンのままにするかどうかです。 さて、すべての興味深い質問と同様に、ここでの答えは「状況によって異なります」です。 通常の投稿の場合は、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 :ユーザーが自分の投稿に到達させたい最小の賛成率–賛成率は、% ofの総投票数が賛成票を獲得する方法を表します[最大=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;
}

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

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. 重要なログをメールで送信

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

まず、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>

次に、SMTPAppenderlogback.xmlに追加します。

<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>example@example.com</to>
        <from>example@example.com</from>
        <username>example@example.com</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. サブレディットをキャッシュする

結局のところ、自動補完サブレディットは高価です。 投稿をスケジュールするときにユーザーがサブレディットの入力を開始するたびに、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データがすべてメモリにロードされたので、 RedditAPIを押すことなくsubredditを検索できます。

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

もちろん、subredditの提案を公開するAPIは同じままです。

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

7. 指標

最後に、いくつかの単純なメトリックをアプリケーションに統合します。 これらの種類のメトリックの構築に関する詳細については、ここで詳細に説明しました

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

このケーススタディは順調に成長しています。 このアプリは、実際にはRedditAPIを使用してOAuthを実行するための簡単なチュートリアルとして開始されました。 現在、Redditのパワーユーザーにとって便利なツールに進化しています。特に、スケジュール設定と再送信のオプションについてです。

最後に、私はそれを使用しているので、Redditへの私自身の提出物は一般的にもっと多くの蒸気を拾っているように見えます、それでそれは常に見るのが良いです。