Redditアプリケーションの2回目の改善
1. 概要
アプリケーションをよりユーザーフレンドリーで使いやすくすることを目的として、ongoing Reddit web app case studyを新しいラウンドの改善で続けましょう。
2. スケジュールされた投稿のページネーション
まず、スケジュールされた投稿with paginationをリストして、全体を見て理解しやすくしましょう。
2.1. ページ付けされた操作
Spring Dataを使用して必要な操作を生成し、Pageableインターフェースを利用してユーザーのスケジュールされた投稿を取得します。
public interface PostRepository extends JpaRepository {
Page findByUser(User user, Pageable pageable);
}
そして、これがコントローラーメソッドgetScheduledPosts()です。
private static final int PAGE_SIZE = 10;
@RequestMapping("/scheduledPosts")
@ResponseBody
public List getScheduledPosts(
@RequestParam(value = "page", required = false) int page) {
User user = getCurrentUser();
Page posts =
postReopsitory.findByUser(user, new PageRequest(page, PAGE_SIZE));
return posts.getContent();
}
2.2. ページ付けされた投稿を表示する
それでは、フロントエンドに簡単なページ付け制御を実装しましょう。
Post title
そして、プレーン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(''+post.title+' ');
});
});
}
今後、この手動テーブルはより成熟したテーブルプラグインにすぐに置き換えられますが、今のところ、これは問題なく機能します。
3. ログインしていないユーザーにログインページを表示する
ユーザーがルートにアクセスすると、they should get different pages if they’re logged in or notになります。
ユーザーがログインしている場合、ホームページ/ダッシュボードが表示されます。 ログインしていない場合は、ログインページが表示されます。
@RequestMapping("/")
public String homePage() {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
return "home";
}
return "index";
}
4. 再送信後の詳細オプション
Redditで投稿を削除して再送信することは、便利で非常に効果的な機能です。 ただし、これに注意し、have full controlを実行する必要がある場合と実行しない場合に注意する必要があります。
たとえば、すでにコメントが含まれている投稿を削除したくない場合があります。 結局のところ、コメントはエンゲージメントであり、プラットフォームと投稿にコメントする人々を尊重したいと思います。
So – that’s the first small yet highly useful feature we’ll add –コメントがない場合にのみ投稿を削除できるようにする新しいオプション。
答えるもう1つの非常に興味深い質問は、投稿が何度も再送信されても、必要な牽引力が得られない場合、最後の試行の後もそのままにしておくかどうかです。 さて、すべての興味深い質問と同様に、ここでの答えは「依存します」です。 通常の投稿の場合は、1日と呼んでそのままにしておくことがあります。 ただし、それが非常に重要な投稿であり、それが何らかの牽引力を確実に得られるようにしたい場合は、最後に削除する可能性があります。
これは、ここで構築する2番目の小さいが非常に便利な機能です。
最後に、論争の的となっている投稿はどうですか? 投稿には2票のredditがあります。これは、賛成票が必要なため、または100票と98票の反対票があるためです。 最初のオプションは、牽引力が得られないことを意味し、2番目のオプションは、多くの牽引力が得られ、投票が分割されることを意味します。
So – this is the third small feature we’re going to add –投稿を削除する必要があるかどうかを判断するときに、この賛成票と反対票の比率を考慮に入れる新しいオプション。
4.1. Postエンティティ
まず、Postエンティティを変更する必要があります。
@Entity
public class Post {
...
private int minUpvoteRatio;
private boolean keepIfHasComments;
private boolean deleteAfterLastAttempt;
}
3つのフィールドは次のとおりです。
-
minUpvoteRatio:ユーザーが自分の投稿に到達させたい最小の賛成率–賛成率は、総投票数の%が賛成票を獲得する方法を表します[最大= 100、最小= 0]
-
keepIfHasComments:必要なスコアに達していないにもかかわらずコメントがある場合、ユーザーが投稿を保持するかどうかを決定します。
-
deleteAfterLastAttempt:必要なスコアに到達せずに最後の試行が終了した後、ユーザーが投稿を削除するかどうかを決定します。
4.2. スケジューラー
次に、これらの興味深い新しいオプションをスケジューラーに統合しましょう。
@Scheduled(fixedRate = 3 * 60 * 1000)
public void checkAndDeleteAll() {
List 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()の実装–checking if the post failed to reach the predefined goal/scoreです。
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()を変更して、正常に再送信された投稿のredditIDをnullに設定する必要があります。
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に新しい変更を追加する必要があります。
5. 重要なログをメールで送信
次に、ログバック構成にすばやく便利な設定(emailing of important logs (ERROR level))を実装します。 これはもちろん、アプリケーションのライフサイクルの早い段階でエラーを簡単に追跡するのに非常に便利です。
まず、pom.xmlにいくつかの必要な依存関係を追加します。
javax.activation
activation
1.1.1
javax.mail
mail
1.4.1
次に、logback.xmlにSMTPAppenderを追加します。
ERROR
ACCEPT
DENY
smtp.example.com
[email protected]
[email protected]
[email protected]
password
%logger{20} - %m
これで終わりです。これで、デプロイされたアプリケーションは、問題が発生したときにメールで送信します。
6. サブレディットをキャッシュする
結局、auto-completing subreddits expensive。 ユーザーが投稿をスケジュールするときにsubredditの入力を開始するたびに、これらのsubredditを取得してユーザーにいくつかの提案を表示するには、Reddit APIにアクセスする必要があります。 理想的ではありません。
Reddit APIを呼び出す代わりに、人気のあるsubredditをキャッシュし、それらを使用してオートコンプリートします。
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);
}
}
これは成熟した実装ですか? No. さらに何か必要ですか? いいえ、私たちはしません。 先に進む必要があります。
6.2. サブブレディットオートコンプリート
次に、サービスにInitializingBeanを実装させることにより、the subreddits are loaded into memory on application startupを確認しましょう。
public void afterPropertiesSet() {
loadSubreddits();
}
private void loadSubreddits() {
subreddits = new ArrayList();
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データがすべてメモリにロードされたので、we can search over the subreddits without hitting the Reddit API:
public List searchSubreddit(String query) {
return subreddits.stream().
filter(sr -> sr.startsWith(query)).
limit(9).
collect(Collectors.toList());
}
もちろん、subredditの提案を公開するAPIは同じままです。
@RequestMapping(value = "/subredditAutoComplete")
@ResponseBody
public List subredditAutoComplete(@RequestParam("term") String term) {
return service.searchSubreddit(term);
}
7. 測定基準
最後に、いくつかの簡単な指標をアプリケーションに統合します。 これらの種類のメトリックの構築に関する詳細については、I wrote about them in some detail hereを参照してください。
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への私自身の提出物は一般的にもっと多くの勢いを増しているように見えます。