Spring Boot 2 Webアプリがあり、サイトの訪問者をCookieで識別し、ページビューの統計を収集する必要があります。したがって、すべてのWebリクエストをインターセプトする必要があります。私が書かなければならないコードは地獄を呼ぶよりも複雑です(Springリアクターが解決するはずだった問題そのもの)。
これがコードです:
package mypack.conf;
import Java.time.LocalDateTime;
import Java.util.ArrayList;
import Java.util.List;
import Java.util.Map;
import Java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
import org.springframework.http.HttpCookie;
import org.springframework.http.ResponseCookie;
import org.springframework.web.reactive.config.ResourceHandlerRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import mypack.dao.PageViewRepository;
import mypack.dao.UserRepository;
import mypack.domain.PageView;
import mypack.domain.User;
import mypack.security.JwtProvider;
import reactor.core.publisher.Mono;
@Configuration
@ComponentScan(basePackages = "mypack")
@EnableReactiveMongoRepositories(basePackages = "mypack")
public class WebConfig implements WebFluxConfigurer {
@Autowired
@Lazy
private UserRepository userRepository;
@Autowired
@Lazy
private PageViewRepository pageViewRepository;
@Autowired
@Lazy
JwtProvider jwtProvider;
@Bean
public WebFilter sampleWebFilter() {
return new WebFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String uri = exchange.getRequest().getURI().toString();
String path = exchange.getRequest().getPath().pathWithinApplication().value();
HttpCookie cookie = null;
String token = "";
Map<String, List<HttpCookie>> cookies = exchange.getRequest().getCookies();
try {
if((exchange.getRequest().getCookies().containsKey("_token") )
&& (exchange.getRequest().getCookies().getFirst("_token"))!=null ) {
cookie = exchange.getRequest().getCookies().getFirst("_token");
token = cookie.getValue();
return userRepository.findByToken(token).map(user -> {
exchange.getAttributes().put("_token", user.getToken());
PageView pg = PageView.builder().createdDate(LocalDateTime.now()).URL(uri).build();
pageViewRepository.save(pg).subscribe(pg1 -> {user.getPageviews().add(pg1); });
userRepository.save(user).subscribe();
return user;
})
.flatMap(user-> chain.filter(exchange)); // ultimately this step executes regardless user exist or not
// handle case when brand new user first time visits website
} else {
token = jwtProvider.genToken("guest", UUID.randomUUID().toString());
User user = User.builder().createdDate(LocalDateTime.now()).token(token).emailId("guest").build();
userRepository.save(user).subscribe();
exchange.getResponse().getCookies().remove("_token");
ResponseCookie rcookie = ResponseCookie.from("_token", token).httpOnly(true).build();
exchange.getResponse().addCookie(rcookie);
exchange.getAttributes().put("_token", token);
}
} catch (Exception e) {
e.printStackTrace();
}
return chain.filter(exchange);
} // end of Mono<Void> filter method
}; // end of New WebFilter (anonymous class)
}
}
その他の関連クラス:
@Repository
public interface PageViewRepository extends ReactiveMongoRepository<PageView, String>{
Mono<PageView> findById(String id);
}
@Repository
public interface UserRepository extends ReactiveMongoRepository<User, String>{
Mono<User> findByToken(String token);
}
@Data
@AllArgsConstructor
@Builder
@NoArgsConstructor
public class User {
@Id
private String id;
private String token;
@Default
private LocalDateTime createdDate = LocalDateTime.now();
@DBRef
private List<PageView> pageviews;
}
Data
@Document
@Builder
public class PageView {
@Id
private String id;
private String URL;
@Default
private LocalDateTime createdDate = LocalDateTime.now();
}
Gradleファイルの関連部分:
buildscript {
ext {
springBootVersion = '2.0.1.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.springframework.boot:spring-boot-starter-webflux')
compile('org.springframework.security:spring-security-oauth2-client')
compile('org.springframework.security.oauth:spring-security-oauth2:2.3.4.RELEASE')
runtime('org.springframework.boot:spring-boot-devtools')
compileOnly('org.projectlombok:lombok')
compile "org.springframework.security:spring-security-jwt:1.0.9.RELEASE"
compile "io.jsonwebtoken:jjwt:0.9.0"
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('io.projectreactor:reactor-test')
compile('com.fasterxml.jackson.core:jackson-databind')
}
問題は次の行にあります。
PageView pg = PageView.builder()。createdDate(LocalDateTime.now())。URL(uri).build(); pageViewRepository.save(pg).subscribe(pg1-> {user.getPageviews()。add(pg1);});
これにより、ブラウザがハングします(応答を待ち続けます)。
基本的に私が欲しいのはこれです:ブロックもブラウザーをハングさせるので、webfilterコードでは機能しないblock()を使用してはいけません。ページビューをmongo dbに保存します。保存後、ページビューには有効なmongodb IDが含まれます。これは、ユーザーエンティティのページビューリストに参照として保存する必要があります。したがって、それがdbに保存された後にのみ、次のステップはユーザーのページビューリストの更新です。次のステップは、ユーザーを更新する可能性があり、ユーザーの保存も必要になる可能性がある下流のコントローラーメソッドに影響を与えずに、更新されたユーザーを保存することです。これはすべて、指定されたWebFilterコンテキストで機能するはずです。
この問題を解決するには?
提供されるソリューションでは、コントローラーアクションに渡す前に、ユーザーがwebfilterに保存されていることを確認する必要があります。このアクションの一部では、クエリ文字列パラメーターとは異なる値でユーザーも保存されます。
私があなたを正しく理解している場合、フィルター(および要求自体)がブロックされないように、データベースで長い操作を非同期に実行する必要がありますか?
この場合、私に役立つ次の解決策をお勧めします。
_@Bean
public WebFilter filter() {
return (exchange, chain) -> {
ServerHttpRequest req = exchange.getRequest();
String uri = req.getURI().toString();
log.info("[i] Got request: {}", uri);
var headers = req.getHeaders();
List<String> tokenList = headers.get("token");
if (tokenList != null && tokenList.get(0) != null) {
String token = tokenList.get(0);
log.info("[i] Find a user by token {}", token);
return userRepo.findByToken(token)
.map(user -> process(exchange, uri, token, user))
.then(chain.filter(exchange));
} else {
String token = UUID.randomUUID().toString();
log.info("[i] Create a new user with token {}", token);
return userRepo.save(new User(token))
.map(user -> process(exchange, uri, token, user))
.then(chain.filter(exchange));
}
};
}
_
ここで、ロジックを少し変更し、トークンの値を(Cookieではなく)適切なヘッダーから取得して、実装を簡素化します。
したがって、トークンが存在する場合は、そのユーザーを見つけようとします。トークンが存在しない場合は、新しいユーザーを作成します。ユーザーが見つかったか、正常に作成された場合は、process
メソッドが呼び出されています。その後、結果に関係なく、chain.filter(exchange)
を返します。
メソッドprocess
は、トークン値をリクエストの適切な属性に配置し、updateUserStat
のメソッドuserService
を非同期的に呼び出します。
_private User process(ServerWebExchange exchange, String uri, String token, User user) {
exchange.getAttributes().put("_token", token);
userService.updateUserStat(uri, user); // async call
return user;
}
_
ユーザーサービス:
_@Slf4j
@Service
public class UserService {
private final UserRepo userRepo;
private final PageViewRepo pageViewRepo;
public UserService(UserRepo userRepo, PageViewRepo pageViewRepo) {
this.userRepo = userRepo;
this.pageViewRepo = pageViewRepo;
}
@SneakyThrows
@Async
public void updateUserStat(String uri, User user) {
log.info("[i] Start updating...");
Thread.sleep(1000);
pageViewRepo.save(new PageView(uri))
.flatMap(user::addPageView)
.blockOptional()
.ifPresent(u -> userRepo.save(u).block());
log.info("[i] User updated.");
}
}
_
このメソッドの継続時間に関係なく、リクエストが遅延なく機能することを確認するために、テストのために少し遅延を追加しました。
トークンでユーザーが見つかった場合:
_2019-01-06 18:25:15.442 INFO 4992 --- [ctor-http-nio-3] : [i] Got request: http://localhost:8080/users?test=1000
2019-01-06 18:25:15.443 INFO 4992 --- [ctor-http-nio-3] : [i] Find a user by token 84b0f7ec-670c-4c04-8a7c-b692752d7cfa
2019-01-06 18:25:15.444 DEBUG 4992 --- [ctor-http-nio-3] : Created query Query: { "token" : "84b0f7ec-670c-4c04-8a7c-b692752d7cfa" }, Fields: { }, Sort: { }
2019-01-06 18:25:15.445 DEBUG 4992 --- [ctor-http-nio-3] : find using query: { "token" : "84b0f7ec-670c-4c04-8a7c-b692752d7cfa" } fields: Document{{}} for class: class User in collection: user
2019-01-06 18:25:15.457 INFO 4992 --- [ntLoopGroup-2-2] : [i] Get all users...
2019-01-06 18:25:15.457 INFO 4992 --- [ task-3] : [i] Start updating...
2019-01-06 18:25:15.458 DEBUG 4992 --- [ntLoopGroup-2-2] : find using query: { } fields: Document{{}} for class: class User in collection: user
2019-01-06 18:25:16.459 DEBUG 4992 --- [ task-3] : Inserting Document containing fields: [URL, createdDate, _class] in collection: pageView
2019-01-06 18:25:16.476 DEBUG 4992 --- [ task-3] : Saving Document containing fields: [_id, token, pageViews, _class]
2019-01-06 18:25:16.479 INFO 4992 --- [ task-3] : [i] User updated.
_
ここで、ユーザーの更新が独立した_task-3
_スレッドで実行され、ユーザーが「get all users」リクエストの結果をすでに持っていることがわかります。
トークンが存在せず、ユーザーが作成された場合:
_2019-01-06 18:33:54.764 INFO 4992 --- [ctor-http-nio-3] : [i] Got request: http://localhost:8080/users?test=763
2019-01-06 18:33:54.764 INFO 4992 --- [ctor-http-nio-3] : [i] Create a new user with token d9bd40ea-b869-49c2-940e-83f1bf79e922
2019-01-06 18:33:54.765 DEBUG 4992 --- [ctor-http-nio-3] : Inserting Document containing fields: [token, _class] in collection: user
2019-01-06 18:33:54.776 INFO 4992 --- [ntLoopGroup-2-2] : [i] Get all users...
2019-01-06 18:33:54.777 INFO 4992 --- [ task-4] : [i] Start updating...
2019-01-06 18:33:54.777 DEBUG 4992 --- [ntLoopGroup-2-2] : find using query: { } fields: Document{{}} for class: class User in collection: user
2019-01-06 18:33:55.778 DEBUG 4992 --- [ task-4] : Inserting Document containing fields: [URL, createdDate, _class] in collection: pageView
2019-01-06 18:33:55.792 DEBUG 4992 --- [ task-4] : Saving Document containing fields: [_id, token, pageViews, _class]
2019-01-06 18:33:55.795 INFO 4992 --- [ task-4] : [i] User updated.
_
トークンはあるがユーザーが見つからない場合:
_2019-01-06 18:35:40.970 INFO 4992 --- [ctor-http-nio-3] : [i] Got request: http://localhost:8080/users?test=150
2019-01-06 18:35:40.970 INFO 4992 --- [ctor-http-nio-3] : [i] Find a user by token 184b0f7ec-670c-4c04-8a7c-b692752d7cfa
2019-01-06 18:35:40.972 DEBUG 4992 --- [ctor-http-nio-3] : Created query Query: { "token" : "184b0f7ec-670c-4c04-8a7c-b692752d7cfa" }, Fields: { }, Sort: { }
2019-01-06 18:35:40.972 DEBUG 4992 --- [ctor-http-nio-3] : find using query: { "token" : "184b0f7ec-670c-4c04-8a7c-b692752d7cfa" } fields: Document{{}} for class: class User in collection: user
2019-01-06 18:35:40.977 INFO 4992 --- [ntLoopGroup-2-2] : [i] Get all users...
2019-01-06 18:35:40.978 DEBUG 4992 --- [ntLoopGroup-2-2] : find using query: { } fields: Document{{}} for class: class User in collection: user
_
私のデモプロジェクト: sb-reactive-filter-demo
コントローラーにリクエストを渡す前に、ページビューを作成し、Webフィルターでユーザーを非ブロック的に更新する別のバリアント:
@Bean
public WebFilter filter() {
return (exchange, chain) -> {
ServerHttpRequest req = exchange.getRequest();
String uri = req.getURI().toString();
log.info("[i] Web Filter: received the request: {}", uri);
var headers = req.getHeaders();
List<String> tokenList = headers.get("token");
if (tokenList != null && tokenList.get(0) != null) {
String token = tokenList.get(0);
Mono<User> foundUser = userRepo
.findByToken(token)
.doOnNext(user -> log.info("[i] Web Filter: {} has been found", user));
return updateUserStat(foundUser, exchange, chain, uri);
} else {
String token = UUID.randomUUID().toString();
Mono<User> createdUser = userRepo
.save(new User(token))
.doOnNext(user -> log.info("[i] Web Filter: a new {} has been created", user));
return updateUserStat(createdUser, exchange, chain, uri);
}
};
}
private Mono<Void> updateUserStat(Mono<User> userMono, ServerWebExchange exchange, WebFilterChain chain, String uri) {
return userMono
.doOnNext(user -> exchange.getAttributes().put("_token", user.getToken()))
.doOnNext(u -> {
String token = exchange.getAttribute("_token");
log.info("[i] Web Filter: token attribute has been set to '{}'", token);
})
.flatMap(user -> pageViewRepo.save(new PageView(uri)).flatMap(user::addPageView).flatMap(userRepo::save))
.doOnNext(user -> {
int numberOfPages = 0;
List<PageView> pageViews = user.getPageViews();
if (pageViews != null) {
numberOfPages = pageViews.size();
}
log.info("[i] Web Filter: {} has been updated. Number of pages: {}", user, numberOfPages);
})
.then(chain.filter(exchange));
}
このコードは、次の結果を生成します。
1)トークンが存在しない:新しいユーザーの作成、ページビューの作成、新しいユーザーの更新、リクエストをコントローラーに渡す
2019-01-20 14:39:10.033 [ctor-http-nio-3] : [i] Web Filter: received the request: http://localhost:8080/users?test=784
2019-01-20 14:39:10.110 [ctor-http-nio-3] : Inserting Document containing fields: [token, _class] in collection: user
2019-01-20 14:39:10.206 [ntLoopGroup-2-2] : [i] Web Filter: a new User(id=5c446bee24c86426ac6c0ae5, token=fba944cd-decb-4923-9757-724da5a60061) has been created
2019-01-20 14:39:10.212 [ntLoopGroup-2-2] : [i] Web Filter: token attribute has been set to 'fba944cd-decb-4923-9757-724da5a60061'
2019-01-20 14:39:11.227 [ parallel-1] : Inserting Document containing fields: [URL, createdDate, _class] in collection: pageView
2019-01-20 14:39:11.242 [ntLoopGroup-2-2] : Saving Document containing fields: [_id, token, pageViews, _class]
2019-01-20 14:39:11.256 [ntLoopGroup-2-2] : [i] Web Filter: User(id=5c446bee24c86426ac6c0ae5, token=fba944cd-decb-4923-9757-724da5a60061) has been updated. Number of pages: 1
2019-01-20 14:39:11.289 [ntLoopGroup-2-2] : [i] Controller: handling 'get all users' request. Token attribute is 'fba944cd-decb-4923-9757-724da5a60061'
2019-01-20 14:39:11.369 [ntLoopGroup-2-2] : find using query: { } fields: Document{{}} for class: class io.github.cepr0.demo.User in collection: user
2)トークンが存在する:既存のユーザーを見つけ、ページビューを作成し、ユーザーを更新し、リクエストをコントローラーに渡します
2019-01-20 14:51:21.983 [ctor-http-nio-3] : [i] Web Filter: received the request: http://localhost:8080/users?test=538
2019-01-20 14:51:22.074 [ctor-http-nio-3] : Created query Query: { "token" : "b613b810-cc36-4961-ad2e-db44f52cd2dd" }, Fields: { }, Sort: { }
2019-01-20 14:51:22.092 [ctor-http-nio-3] : find using query: { "token" : "b613b810-cc36-4961-ad2e-db44f52cd2dd" } fields: Document{{}} for class: class User in collection: user
2019-01-20 14:51:22.102 [ntLoopGroup-2-2] : [i] Web Filter: User(id=5c434c2eb338ac3530cbd56d, token=b613b810-cc36-4961-ad2e-db44f52cd2dd) has been found
2019-01-20 14:51:22.102 [ntLoopGroup-2-2] : [i] Web Filter: token attribute has been set to 'b613b810-cc36-4961-ad2e-db44f52cd2dd'
2019-01-20 14:51:23.103 [ parallel-2] : Inserting Document containing fields: [URL, createdDate, _class] in collection: pageView
2019-01-20 14:51:23.115 [ntLoopGroup-2-2] : Saving Document containing fields: [_id, token, pageViews, _class]
2019-01-20 14:51:23.117 [ntLoopGroup-2-2] : [i] Web Filter: User(id=5c434c2eb338ac3530cbd56d, token=b613b810-cc36-4961-ad2e-db44f52cd2dd) has been updated. Number of pages: 13
2019-01-20 14:51:23.118 [ntLoopGroup-2-2] : [i] Controller: handling 'get all users' request. Token attribute is 'b613b810-cc36-4961-ad2e-db44f52cd2dd'
2019-01-20 14:51:23.119 [ntLoopGroup-2-2] : find using query: { } fields: Document{{}} for class: class User in collection: user
3)トークンは存在するがユーザーが見つからない:コントローラーにリクエストを渡す
2019-01-20 14:52:41.842 [ctor-http-nio-3] : [i] Web Filter: received the request: http://localhost:8080/users?test=513
2019-01-20 14:52:41.844 [ctor-http-nio-3] : Created query Query: { "token" : "-b613b810-cc36-4961-ad2e-db44f52cd2dd" }, Fields: { }, Sort: { }
2019-01-20 14:52:41.845 [ctor-http-nio-3] : find using query: { "token" : "-b613b810-cc36-4961-ad2e-db44f52cd2dd" } fields: Document{{}} for class: class User in collection: user
2019-01-20 14:52:41.850 [ntLoopGroup-2-2] : [i] Controller: handling 'get all users' request. Token attribute is 'null'
2019-01-20 14:52:41.850 [ntLoopGroup-2-2] : find using query: { } fields: Document{{}} for class: class User in collection: user
デモ: sb-reactive-filter-demo(branch:update-user-in-web-filter)
ページビューの統計情報を収集するには、戦略を変更し、代わりにアクチュエータとマイクロメーターを使用することをお勧めします。
metrics
)を公開します/actuator/metrics
に移動し、サーバーHTTPリクエストのメトリックを選択します( リファレンスドキュメント を参照)。マイクロメーターはより多くの方法を提供し、メトリックを正しく取得するのに役立ちます。たとえば、時間を測定するときにGCの一時停止を考慮したり、ヒストグラム/パーセンタイルなどを提供したりします。