зеркало из
https://github.com/iharh/notes.git
synced 2025-10-31 21:56:08 +02:00
m
Этот коммит содержится в:
родитель
71f35a11de
Коммит
9b7ad5ddc6
@ -1,3 +1,6 @@
|
|||||||
|
https://github.com/IsmaelMartinez/teams-for-linux
|
||||||
|
https://flathub.org/apps/com.github.IsmaelMartinez.teams_for_linux
|
||||||
|
|
||||||
https://techcommunity.microsoft.com/t5/microsoft-teams-blog/microsoft-teams-progressive-web-app-now-available-on-linux/ba-p/3669846
|
https://techcommunity.microsoft.com/t5/microsoft-teams-blog/microsoft-teams-progressive-web-app-now-available-on-linux/ba-p/3669846
|
||||||
|
|
||||||
chrome://apps
|
chrome://apps
|
||||||
|
|||||||
@ -4,6 +4,8 @@ https://docs.axoniq.io/reference-guide/axon-framework/sagas/implementation
|
|||||||
https://github.com/eventuate-tram/eventuate-tram-sagas
|
https://github.com/eventuate-tram/eventuate-tram-sagas
|
||||||
|
|
||||||
2023
|
2023
|
||||||
|
https://habr.com/ru/articles/744460/
|
||||||
|
axon
|
||||||
https://www.baeldung.com/cs/saga-pattern-microservices
|
https://www.baeldung.com/cs/saga-pattern-microservices
|
||||||
https://www.vinsguru.com/choreography-saga-pattern-with-spring-boot/
|
https://www.vinsguru.com/choreography-saga-pattern-with-spring-boot/
|
||||||
https://www.vinsguru.com/orchestration-saga-pattern-with-spring-boot/
|
https://www.vinsguru.com/orchestration-saga-pattern-with-spring-boot/
|
||||||
|
|||||||
380
pl/java/libfws/spring/boot/mvc/docs/courses/linkedin.txt
Обычный файл
380
pl/java/libfws/spring/boot/mvc/docs/courses/linkedin.txt
Обычный файл
@ -0,0 +1,380 @@
|
|||||||
|
2019
|
||||||
|
https://www.linkedin.com/learning/spring-spring-mvc-2
|
||||||
|
Incoming HTTP Request is Processed by
|
||||||
|
DispatcherServlet->HandlerMapping->Controller (Front Controller DP)
|
||||||
|
->Controller->...processing...->back-to-DispatcherServlet
|
||||||
|
->ViewResolver
|
||||||
|
|
||||||
|
Instead of String method at Controller we can use
|
||||||
|
ModelAndView
|
||||||
|
.setViewName
|
||||||
|
.setStatus
|
||||||
|
.addObject(name, value)
|
||||||
|
SearchController {
|
||||||
|
@GetMapping("/search")
|
||||||
|
public String search(@RequestParam("search") String search, Model model) {
|
||||||
|
...
|
||||||
|
List<Product> products = searchRepository.searchByName(search);
|
||||||
|
model.addAttribute("products", products);
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RegistrationController {
|
||||||
|
@PostMapping("/registeruser")
|
||||||
|
public String registerUser(@ModelAttribute("newuser") User user) { // <form method="post" modelAttribute="newuser" ...>
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HomeController {
|
||||||
|
...
|
||||||
|
@ModelAttribute("newuser")
|
||||||
|
public User getDefaultUser() { return new User(); }
|
||||||
|
@ModelAttribute("genderItems")
|
||||||
|
public List<String> getGenderItems() { return List.of("Male", "Female", "Other"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
javax.validation -> hibernate validator
|
||||||
|
annotate with @Valid our beans at controller and add BindingResult param right after the annotated one
|
||||||
|
and add <form:errors path="someAttr".../> to our form elements
|
||||||
|
in order to localize error messages, add resources/ValidationMessages.properties, add msg.key=msg.value
|
||||||
|
and use ... message="{msg.key}"
|
||||||
|
|
||||||
|
binders extend ModelAttribute-s
|
||||||
|
User {
|
||||||
|
|
||||||
|
private Date dateOfBirth; // <form:input path="dateOfBirth" type="date" .../>
|
||||||
|
}
|
||||||
|
RegistrationController {
|
||||||
|
@InitBinder
|
||||||
|
public void initBinder(WebDataBinder binder) {
|
||||||
|
binder.registerCustomEditor(Date.class, "dateOfBirth", new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
converters extend binders concept, transform request data to desired types
|
||||||
|
built in converters present, but we can provide custom ones
|
||||||
|
|
||||||
|
public enum Gender { MALE, FEMALE, OTHER }
|
||||||
|
User {
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private Gender gender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class StringToEnumConverter implements .core.convert.Converter<String, Gender> {
|
||||||
|
@Override
|
||||||
|
public Gender convert(String s) {
|
||||||
|
... regurn Gender.GGG ...;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationConfig {
|
||||||
|
...
|
||||||
|
@Override
|
||||||
|
protected void addFormatters(FormatterRegistry registry) {
|
||||||
|
registry.addConverter(new StringToEnumConverter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HandlerMapping - maps request to handler
|
||||||
|
HandlerAdapter - invokes the handler
|
||||||
|
ViewResolver - helps resolve views
|
||||||
|
LocaleResolver - helps with l10n and i18n support
|
||||||
|
ThemeResolver
|
||||||
|
HandlerExceptionResolver - helps with exception handling
|
||||||
|
ExceptionHandlerExceptionResolver - define ex-handling methods in controllers (annotated by @ExceptionHandler)
|
||||||
|
SimpleMappingExceptionResolver - maps each ex-n class with an error page
|
||||||
|
DefaultHandlerExceptionResolver - default, maps exceptions to error codes
|
||||||
|
ResponseStatusExceptionResolver - resolves custom exceptions using status code defined in @ResponseStatus (for ex-ns)
|
||||||
|
to disable standard error resolver, we need to add property
|
||||||
|
spring.autoconfigure.exclude=ord.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
|
||||||
|
|
||||||
|
public class Login {
|
||||||
|
private String login;
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
|
HomeController {
|
||||||
|
... @ModelAttribute("login") public Login getDefaultLogin() { return new Login(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApplicationException extends RuntimeException { ... }
|
||||||
|
|
||||||
|
LoginController {
|
||||||
|
@PostMapping("/login")
|
||||||
|
public String login(@ModelAttribute("login") Login login) {
|
||||||
|
User user = userRepository.searchByName(login.getUsername());
|
||||||
|
if (user == null) {
|
||||||
|
throw new ApplicationException("User not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ExceptionHandler(ApplicationException.class)
|
||||||
|
public String handleException(ApplicationException ex, WebRequest request) {
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for global handler
|
||||||
|
@ControllerAdvice
|
||||||
|
public class ApplicationExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(ApplicationException.class)
|
||||||
|
public String handleException(ApplicationException ex, WebRequest request) {
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
...
|
||||||
|
@ControllerAdvice
|
||||||
|
public class DefaultModelAttributeController {
|
||||||
|
@ModelAttribute("newuser")
|
||||||
|
public User getDefaultUser() { return new User(); }
|
||||||
|
@ModelAttribute("genderItems")
|
||||||
|
public List<String> getGenderItems() { return List.of("Male", "Female", "Other"); }
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableAsync
|
||||||
|
https://stackoverflow.com/questions/36768234/spring-enableasync-breaks-bean-initialization-order
|
||||||
|
https://www.baeldung.com/spring-async
|
||||||
|
...ApplicationAsync {
|
||||||
|
@Override
|
||||||
|
protected void configureAsyncSupport(AsyncSupportConfigurer configurer)
|
||||||
|
configurer.setTaskExecutor(mvcTaskExecutor());
|
||||||
|
configurer.setDefaultTimeout(5000);
|
||||||
|
}
|
||||||
|
@Bean public AsyncTaskExecutor mvcTaskExecutor() { ThreadPoolTaskExecutor... }
|
||||||
|
}
|
||||||
|
... SearchController {
|
||||||
|
public Callable<String> search(..., HttpServletRequest request) {
|
||||||
|
log.info(request.isAsyncSupported(), Thread.currentThread().getName());
|
||||||
|
return () -> {
|
||||||
|
log.info(Thread.currentThread().getName());
|
||||||
|
all the business logic
|
||||||
|
}
|
||||||
|
|
||||||
|
we can also use DeferredResult
|
||||||
|
@Autowired
|
||||||
|
private AsyncTaskExecutor executor;
|
||||||
|
|
||||||
|
|
||||||
|
public DeferredResult<String> search(..., HttpServletRequest request) {
|
||||||
|
DeferredResult<String> defferedResult = new DeferredResult<>();
|
||||||
|
...
|
||||||
|
executor.execute(() -> {
|
||||||
|
...
|
||||||
|
defferedResult.setResult(search);
|
||||||
|
});
|
||||||
|
return defferedResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
View
|
||||||
|
ViewResolver - resp by resolving views by names
|
||||||
|
InternalResourceViewResolver
|
||||||
|
ResourceBundleViewResolver
|
||||||
|
XmlViewResolver
|
||||||
|
VelocityViewResolver, FreeMakerViewResolver, ...
|
||||||
|
ChainedViewResolver
|
||||||
|
|
||||||
|
DispatcherServlet
|
||||||
|
HandlerMapping, HandlerAdapter (calls interceptors)
|
||||||
|
Pre-process Interceptors 1..n
|
||||||
|
and then (iff all are OK) - calls a Controller
|
||||||
|
after all business-logic -
|
||||||
|
Post-process
|
||||||
|
|
||||||
|
Impl-s of HandlerInterceptorAdapter base class
|
||||||
|
preHandle
|
||||||
|
postHandle
|
||||||
|
afterCompletion - after response is completed to the client
|
||||||
|
ThemeChangeInterceptor
|
||||||
|
LocaleChangeInterceptor
|
||||||
|
|
||||||
|
public class LoggingInterceptor extends HandlerInterceptorAdapter {
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
at ApplicationConfig {
|
||||||
|
@Override
|
||||||
|
protected void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
... @SessionAttributes, @SessionAttribute
|
||||||
|
@RequestAttribute
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@SessionAttributes("login") // @ModelAttribute should exist
|
||||||
|
public class LoginController {
|
||||||
|
|
||||||
|
// already exposed by ControllerAdvice
|
||||||
|
// @ModelAttribute("login") public Login getDefaultLogin() { return new Login(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class UserProfileController {
|
||||||
|
@PostMapping("/userprofile")
|
||||||
|
public String getUserProfile(@SessionAttribute("login") Login login, Model model) {
|
||||||
|
// and similar for @RequestAttribute
|
||||||
|
model.setAttribute("username"); // or use ${sessionScope.username} at jsp
|
||||||
|
return "profile"; // use login in jsp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginController {
|
||||||
|
...
|
||||||
|
@PostMapping("/login")
|
||||||
|
{
|
||||||
|
session.setMaxInactiveTimeout(...)
|
||||||
|
...
|
||||||
|
... return "forward:/userprofile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class LogoutController {
|
||||||
|
@GetMapping("/logout")
|
||||||
|
public String logout(HttpSession session) {
|
||||||
|
session.invalidate();
|
||||||
|
return "login";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class RedirectionController {
|
||||||
|
@GetMapping("/redirectToLinkedIn")
|
||||||
|
public String redirectToLinkedIn() {
|
||||||
|
return "redirect:http://www.linkedin.com"; // absolute URL is needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeResolver facilitates theme resolution
|
||||||
|
ThemeSource and default impl - ResourceBundleThemeSource
|
||||||
|
CookieThemeResolver
|
||||||
|
SessionThemeResolver
|
||||||
|
FixedThemeResolver
|
||||||
|
|
||||||
|
resources/client-theme1.properties
|
||||||
|
styleSheet=/css/style1.css
|
||||||
|
resources/client-theme2.properties
|
||||||
|
styleSheet=/css/style2.css
|
||||||
|
|
||||||
|
... ApplicationConfig {
|
||||||
|
@Bean
|
||||||
|
public ThemeResolver themeResolver() {
|
||||||
|
CookieThemeResolver cookieThemeResolver = new CookieThemeResolver();
|
||||||
|
cookieThemeResolver.setCookieName("theme");
|
||||||
|
cookieThemeResolver.setDefaultThemeName("client-theme1");
|
||||||
|
return cookieThemeResolver;
|
||||||
|
}
|
||||||
|
... // also it's important to add an interceptor
|
||||||
|
@Override
|
||||||
|
protected void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/*")
|
||||||
|
registry.addInterceptor(new ThemeChangeInterceptor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// next thing - define at our jsp
|
||||||
|
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
|
||||||
|
...
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
|
||||||
|
...
|
||||||
|
</head>
|
||||||
|
// we can check with ...?theme=client-theme2... ... resolver will store this in cookie
|
||||||
|
|
||||||
|
l10n - is to use props for msgs
|
||||||
|
i10n - is more than l10n, including locale treatment
|
||||||
|
|
||||||
|
LocaleResolver
|
||||||
|
AcceptHeaderLocaleResolver - default one
|
||||||
|
CookieLocaleResolver
|
||||||
|
SessionLocaleResolver
|
||||||
|
|
||||||
|
resources/messages.properties
|
||||||
|
label.home=home
|
||||||
|
label.search=search
|
||||||
|
label.login=login
|
||||||
|
label.linkedin=linkedin
|
||||||
|
resources/messages_sv.properties // make sure to have UTF-8
|
||||||
|
label.home=hem
|
||||||
|
label.search=sok
|
||||||
|
label.login=logga in
|
||||||
|
label.linkedin=...
|
||||||
|
|
||||||
|
// at jsp - use <spring:message code="label.home"/>
|
||||||
|
// default resolver takes locale from client settings of browser
|
||||||
|
|
||||||
|
... ApplicationConfig {
|
||||||
|
@Bean
|
||||||
|
public LocaleResolver localeResolver() {
|
||||||
|
CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
|
||||||
|
cookieLocaleResolver.setCookieName("locale");
|
||||||
|
cookieLocaleResolver.setDefaultLocale(Locale.US);
|
||||||
|
return cookieLocaleResolver;
|
||||||
|
}
|
||||||
|
... // also it's important to add an interceptor
|
||||||
|
@Override
|
||||||
|
protected void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/*")
|
||||||
|
registry.addInterceptor(new ThemeChangeInterceptor());
|
||||||
|
registry.addInterceptor(new LocaleChangeInterceptor()); // can customize with setParamName if need non-default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
REST: spring uses HttpMessageConvertors for JSON (with Jackson), no view name needed
|
||||||
|
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class ProductsRestController {
|
||||||
|
...
|
||||||
|
@GetMapping("/rest/products")
|
||||||
|
@ResponseBody
|
||||||
|
public List<Product> getProducts() {
|
||||||
|
return productRepository.findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@RestController // implies @ResponseBody for every method
|
||||||
|
public class ProductsRestController {
|
||||||
|
...
|
||||||
|
@GetMapping("/rest/products")
|
||||||
|
public ResponseEntity<List<Product>> getProductsByParam(@RequestParam("name") String name) {
|
||||||
|
List<Product> products = productRepository.searchByName(name)
|
||||||
|
return new ResponseEntity<>(products, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
@GetMapping("/rest/products/{id}")
|
||||||
|
public ResponseEntity<List<Product>> getProductsByPathVar(@PathVariable("name") String name) {
|
||||||
|
List<Product> products = productRepository.searchByName(name)
|
||||||
|
return new ResponseEntity<>(products, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoginFailureException extends Exception {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class LoginRestController {
|
||||||
|
...
|
||||||
|
|
||||||
|
@PostMapping("/rest/loginuser")
|
||||||
|
public ResponseEntity<?> loginUser(@RequestBody Login login) {
|
||||||
|
User user = userRepository.searchByName(login.getUsername());
|
||||||
|
if (user == null) {
|
||||||
|
return new ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
||||||
|
}
|
||||||
|
if (user.getUsername().equals(login.getUsername)) && ... pwd) {
|
||||||
|
return new ResponseEntity<>("Welcome " + user.getUsername(), HttpStatus.OK);
|
||||||
|
} else {
|
||||||
|
throw new LoginFailureException("username or password is incorrect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(LoginFailureException.class)
|
||||||
|
public ResponseEntity<?> handle Login(LoginFailureException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,3 +6,53 @@ Udemy - Learn integration testing with Spring Boot
|
|||||||
Advanced Spring - Effective Integration Testing with Spring Boot
|
Advanced Spring - Effective Integration Testing with Spring Boot
|
||||||
magnet:?xt=urn:btih:afd24823c218c0cc876504cb86723cfc44b9ab29
|
magnet:?xt=urn:btih:afd24823c218c0cc876504cb86723cfc44b9ab29
|
||||||
! 201.5m, non-complete
|
! 201.5m, non-complete
|
||||||
|
|
||||||
|
https://www.linkedin.com/learning/spring-boot-test-driven-development
|
||||||
|
AopTestUtils
|
||||||
|
ReflectionTestUtils
|
||||||
|
MvcTestUtils, Mvc test client, MockMvc
|
||||||
|
|
||||||
|
Unit Test
|
||||||
|
@ExtendsWith(MockitoExtension.class)
|
||||||
|
Integration
|
||||||
|
@SpringBootTest(classes=MyConfig.class)
|
||||||
|
@AutoConfigureTestDatabase(replace=Replace.ANY)
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@Autowired
|
||||||
|
MockMvc mockMvc;
|
||||||
|
someTestMethod() {
|
||||||
|
mockMvc.perform(get("/someEndpoint"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().string(containsString("some-uuid")))
|
||||||
|
}
|
||||||
|
|
||||||
|
https://www.linkedin.com/learning/advanced-spring-effective-integration-testing-with-spring-boot
|
||||||
|
@SpringBootTest
|
||||||
|
@DataJpaTest (without web layer), @DataJdbcTest, ... @DataRedisTest
|
||||||
|
@WebMvcTest (loads web-layer only to the context without persistence layer), @RestClientTest, @WebServiceClientTest, @WebFluxTest
|
||||||
|
@MockBean ? for UT
|
||||||
|
BDDMockito lib
|
||||||
|
instead of when(methodCall).then(doSomething)
|
||||||
|
given(methodCall).will(doSomething)
|
||||||
|
|
||||||
|
TestEntityManager mgr
|
||||||
|
mgr.persistFlushFind(obj) -> to bypass hbm L1 cache
|
||||||
|
|
||||||
|
@ExtendsWith(MockitoExtension.class)
|
||||||
|
@InjectMock - uses constructor or setter injection
|
||||||
|
@Mock - replaces whole class entirely
|
||||||
|
@Spy - takes existing object and replaces only some methods
|
||||||
|
@MockBean and @SpyBean
|
||||||
|
|
||||||
|
@ExceptionHandler+@ResponseStatus()
|
||||||
|
or @ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
public void someMethod(SomeEx-n)
|
||||||
|
mockito given().willThrow(...)
|
||||||
|
|
||||||
|
@RestClientTest (MockRestServiceServer, only for RestTemplate)
|
||||||
|
WireMock is better
|
||||||
|
|
||||||
|
SpringCloudContract (generate tests by contract def-n),
|
||||||
|
StubRunner fetches generated jar and spins stub-server at config-d port
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureStubRunner(ids="group:artifact:+:8080", stubsMode=StubRunnerProperties.StubsMode.LOCAL)
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
https://github.com/tkaczmarzyk/specification-arg-resolver
|
||||||
|
https://github.com/tkaczmarzyk/specification-arg-resolver/tree/master/src/main/java/net/kaczmarzyk/spring/data/jpa/web/annotation
|
||||||
|
https://github.com/tkaczmarzyk/specification-arg-resolver/tree/master/src/main/java/net/kaczmarzyk/spring/data/jpa/domain
|
||||||
|
|
||||||
|
2017
|
||||||
|
https://blog.tratif.com/2017/11/23/effective-restful-search-api-in-spring/
|
||||||
Загрузка…
x
Ссылка в новой задаче
Block a user