티스토리 뷰
1. 우선 빌드 파일에 starter-web 대신 starter-webflux를 지정한다.
1-1 기본적으로 tomcat을 사용할 수 없게 되고, servlet에 관련된 내용은 전부 에러가 발생한다.
1-2 파일 업로드에 사용한 multipart 같은 것도 servlet을 사용하므로 제대로 동작하지 않게 된다.
/*
* This file was generated by the Gradle 'init' task.
*/
plugins {
id 'org.springframework.boot' version '2.3.1.RELEASE'
id "io.freefair.lombok" version "5.1.1"
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
runtimeOnly 'org.springframework.boot:spring-boot-devtools'
implementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.webjars:bootstrap:4.5.0'
implementation 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
group = 'pe.pilseong'
version = '0.0.1-SNAPSHOT'
description = 'demo'
sourceCompatibility = '11'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
2. 실행하게 되면 tomcat 대신 netty를 사용하는 것을 볼 수 있다.
3. MockMvc를 사용한 테스트도 모두 실패하게 된다.
4. Thymeleaf를 사용할 때도 자동으로 template engine이 변경된다.
4-1 아래의 소스는 VSCode에서 ctrl+shift+t 소스검색에서 찾은 ThymeleafWebMvcConfiguration.class 일부이다.
4-2 즉 @ConditionalOnWebApplication(type = Type.REACTIVE) 이 부분이 어떤 라이브러리가 들어간지를 확인한다.
4-3 reactive인 경우는 템플릿엔진을 사용하고 WebMvc의 경우는 ViewResolver를 사용함을 알 수 있다.
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebMvcConfiguration {
@Bean
@ConditionalOnEnabledResourceChain
@ConditionalOnMissingFilterBean(ResourceUrlEncodingFilter.class)
FilterRegistrationBean<ResourceUrlEncodingFilter> resourceUrlEncodingFilter() {
FilterRegistrationBean<ResourceUrlEncodingFilter> registration = new FilterRegistrationBean<>(
new ResourceUrlEncodingFilter());
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return registration;
}
@Configuration(proxyBeanMethods = false)
static class ThymeleafViewResolverConfiguration {
@Bean
@ConditionalOnMissingBean(name = "thymeleafViewResolver")
ThymeleafViewResolver thymeleafViewResolver(ThymeleafProperties properties,
SpringTemplateEngine templateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine);
resolver.setCharacterEncoding(properties.getEncoding().name());
resolver.setContentType(
appendCharset(properties.getServlet().getContentType(), resolver.getCharacterEncoding()));
resolver.setProducePartialOutputWhileProcessing(
properties.getServlet().isProducePartialOutputWhileProcessing());
resolver.setExcludedViewNames(properties.getExcludedViewNames());
resolver.setViewNames(properties.getViewNames());
// This resolver acts as a fallback resolver (e.g. like a
// InternalResourceViewResolver) so it needs to have low precedence
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
resolver.setCache(properties.isCache());
return resolver;
}
private String appendCharset(MimeType type, String charset) {
if (type.getCharset() != null) {
return type.toString();
}
LinkedHashMap<String, String> parameters = new LinkedHashMap<>();
parameters.put("charset", charset);
parameters.putAll(type.getParameters());
return new MimeType(type, parameters).toString();
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafReactiveConfiguration {
@Bean
@ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class)
SpringWebFluxTemplateEngine templateEngine(ThymeleafProperties properties,
ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine();
engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
dialects.orderedStream().forEach(engine::addDialect);
return engine;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebFluxConfiguration {
@Bean
@ConditionalOnMissingBean(name = "thymeleafReactiveViewResolver")
ThymeleafReactiveViewResolver thymeleafViewResolver(ISpringWebFluxTemplateEngine templateEngine,
ThymeleafProperties properties) {
ThymeleafReactiveViewResolver resolver = new ThymeleafReactiveViewResolver();
resolver.setTemplateEngine(templateEngine);
mapProperties(properties, resolver);
mapReactiveProperties(properties.getReactive(), resolver);
// This resolver acts as a fallback resolver (e.g. like a
// InternalResourceViewResolver) so it needs to have low precedence
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
return resolver;
}
private void mapProperties(ThymeleafProperties properties, ThymeleafReactiveViewResolver resolver) {
PropertyMapper map = PropertyMapper.get();
map.from(properties::getEncoding).to(resolver::setDefaultCharset);
resolver.setExcludedViewNames(properties.getExcludedViewNames());
resolver.setViewNames(properties.getViewNames());
}
private void mapReactiveProperties(Reactive properties, ThymeleafReactiveViewResolver resolver) {
PropertyMapper map = PropertyMapper.get();
map.from(properties::getMediaTypes).whenNonNull().to(resolver::setSupportedMediaTypes);
map.from(properties::getMaxChunkSize).asInt(DataSize::toBytes).when((size) -> size > 0)
.to(resolver::setResponseMaxChunkSizeBytes);
map.from(properties::getFullModeViewNames).to(resolver::setFullModeViewNames);
map.from(properties::getChunkedModeViewNames).to(resolver::setChunkedModeViewNames);
}
}
5. Controller에서 block으로 처리한 것들을 풀어주어야 한다.
5-1 reactive(mongo-reactive) 방식을 사용하면서 block을 통해 WebMvc를 사용하는 것이 가능한데,
5-1-1 webflux를 사용하면 바로 아래처럼 에러가 발생한다.
5-2 기존 코드
package pe.pilseong.recipe.controller;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.domain.Recipe;
import pe.pilseong.recipe.service.RecipeService;
@Slf4j
@RequiredArgsConstructor
@Controller
public class IndexController {
private final RecipeService recipeService;
@GetMapping({"", "/", "/index"})
public String getIndexPage(Model model) {
log.debug("getIndexPage in IndexController");
List<Recipe> recipes = recipeService.getRecipes().collectList().block();
model.addAttribute("recipes", recipes);
return "main";
}
}
5-3 아래처럼 더 이상 block 같은 것은 사용할 필요가 없다.
package pe.pilseong.recipe.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.service.RecipeService;
@Slf4j
@RequiredArgsConstructor
@Controller
public class IndexController {
private final RecipeService recipeService;
@GetMapping({"", "/", "/index"})
public String getIndexPage(Model model) {
log.debug("getIndexPage in IndexController");
model.addAttribute("recipes", recipeService.getRecipes().collectList());
return "main";
}
}
5-4 Service 레이어의 구현코드도 변경이 필요하다.
5-4-1 아래 코드의 deleteById는 void 대신 Mono<Void>가 반환되어야 한다. 구독시점을 클라이언트가 필요로 한다.
package pe.pilseong.recipe.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.command.RecipeCommand;
import pe.pilseong.recipe.converter.RecipeCommandToRecipe;
import pe.pilseong.recipe.converter.RecipeToRecipeCommand;
import pe.pilseong.recipe.domain.Recipe;
import pe.pilseong.recipe.exception.NotFoundException;
import pe.pilseong.recipe.repository.reactive.RecipeReactiveRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
@RequiredArgsConstructor
@Slf4j
public class RecipeServiceImpl implements RecipeService {
private final RecipeReactiveRepository recipeReactiveRepository;
private final RecipeCommandToRecipe recipeConverter;
private final RecipeToRecipeCommand commandConverter;
@Override
public Flux<Recipe> getRecipes() {
log.debug("getRecipes in RecipeServiceImpl");
return recipeReactiveRepository.findAll();
}
@Override
public Mono<Recipe> findById(String id) {
return recipeReactiveRepository.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException("recipe not found with id :: " + id)));
}
@Override
public Mono<Recipe> save(RecipeCommand command) {
return recipeReactiveRepository.save(recipeConverter.convert(command));
}
@Override
public Mono<RecipeCommand> findCommandById(String id) {
return recipeReactiveRepository.findById(id)
.map(commandConverter::convert)
.switchIfEmpty(Mono.error(new NotFoundException("recipe not found with id :: " + id)));
}
@Override
public Mono<RecipeCommand> saveRecipeCommand(RecipeCommand command) {
return recipeReactiveRepository.save(recipeConverter.convert(command))
.map(commandConverter::convert);
}
@Override
public void deleteById(String id) {
recipeReactiveRepository.deleteById(id).block();
}
}
5-4-2 아래처럼 변경하였다.
package pe.pilseong.recipe.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.command.RecipeCommand;
import pe.pilseong.recipe.converter.RecipeCommandToRecipe;
import pe.pilseong.recipe.converter.RecipeToRecipeCommand;
import pe.pilseong.recipe.domain.Recipe;
import pe.pilseong.recipe.exception.NotFoundException;
import pe.pilseong.recipe.repository.reactive.RecipeReactiveRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
@RequiredArgsConstructor
@Slf4j
public class RecipeServiceImpl implements RecipeService {
private final RecipeReactiveRepository recipeReactiveRepository;
private final RecipeCommandToRecipe recipeConverter;
private final RecipeToRecipeCommand commandConverter;
@Override
public Flux<Recipe> getRecipes() {
log.debug("getRecipes in RecipeServiceImpl");
return recipeReactiveRepository.findAll();
}
@Override
public Mono<Recipe> findById(String id) {
return recipeReactiveRepository.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException("recipe not found with id :: " + id)));
}
@Override
public Mono<Recipe> save(RecipeCommand command) {
return recipeReactiveRepository.save(recipeConverter.convert(command));
}
@Override
public Mono<RecipeCommand> findCommandById(String id) {
return recipeReactiveRepository.findById(id)
.map(commandConverter::convert)
.switchIfEmpty(Mono.error(new NotFoundException("recipe not found with id :: " + id)));
}
@Override
public Mono<RecipeCommand> saveRecipeCommand(RecipeCommand command) {
return recipeReactiveRepository.save(recipeConverter.convert(command))
.map(commandConverter::convert);
}
@Override
public Mono<Void> deleteById(String id) {
return recipeReactiveRepository.deleteById(id);
}
}
6. validation도 수정해 주어야 한다.
6-1 아래 코드는 WebMvc에서는 잘 돌아가던 코드인데 webflux로는 돌아가지 않는다.
6-1-1 아래의 에러를 보면 BindingResult는 검증 대상인 @ModelAttribute 인자 바로 뒤에 나와야 한다는 말이다.
6-1-2 그런데, 이 코드는 WebMvc에서 문제가 없었고, 보면 알겠지만 에러가 말하는대로 바로 뒤에 지정되어 있다.
@PostMapping("/")
public String saveOrUpdate(@Valid @ModelAttribute("recipe") RecipeCommand command, BindingResult result,
Model model) {
if (result.hasErrors()) {
model.addAttribute("recipe", command);
result.getAllErrors().forEach(error -> {
System.out.println();
log.debug(error.toString());
System.out.println();
});
return VIEW_RECIPE_FORM;
}
RecipeCommand savedCommand = recipeService.saveRecipeCommand(command).block();
return "redirect:/recipe/" + savedCommand.getId() + "/show";
}
6-1-3 아래 코드는 BindingResult가 문제가 있으니 바로 InitBinder에서 받아오도록 수정한 코드이다.
6-1-3-1 하지만 아래처럼 해도 돌아가지 않는다. 문제는 @Valid 이 놈도 정상적으로 실행되지 않는다.
6-1-3-2 에러가 복잡한데 무슨 말인지는 모륵ㅆ다. 0번 인자가 문제가 있다는 것 같긴 하다.
private WebDataBinder webDataBinder;
@InitBinder
public void initBinder(WebDataBinder webDataBinder) {
this.webDataBinder = webDataBinder;
}
@PostMapping("/")
public String saveOrUpdate(@Valid @ModelAttribute("recipe") RecipeCommand command, Model model) {
webDataBinder.validate();
BindingResult result = webDataBinder.getBindingResult();
if (result.hasErrors()) {
model.addAttribute("recipe", command);
result.getAllErrors().forEach(error -> {
System.out.println();
log.debug(error.toString());
System.out.println();
});
return VIEW_RECIPE_FORM;
}
RecipeCommand savedCommand = recipeService.saveRecipeCommand(command).block();
return "redirect:/recipe/" + savedCommand.getId() + "/show";
}
6-1-4 결국 마지막으로 @Valid를 제거하니 잘돌아간다. 코드를 보면 알겠지만 block()이 있어 실패할 때만 돌아간다.
private WebDataBinder webDataBinder;
@InitBinder
public void initBinder(WebDataBinder webDataBinder) {
this.webDataBinder = webDataBinder;
}
@PostMapping("/")
public String saveOrUpdate(@ModelAttribute("recipe") RecipeCommand command, Model model) {
webDataBinder.validate();
BindingResult result = webDataBinder.getBindingResult();
if (result.hasErrors()) {
model.addAttribute("recipe", command);
result.getAllErrors().forEach(error -> {
System.out.println();
log.debug(error.toString());
System.out.println();
});
return VIEW_RECIPE_FORM;
}
RecipeCommand savedCommand = recipeService.saveRecipeCommand(command).block();
return "redirect:/recipe/" + savedCommand.getId() + "/show";
}
7. ErrorHandling 코드도 수정이 필요하다.
7-1 webflux에서는 ModelAndView를 사용할 수 없고 추가적인 에러도 처리해 주어야 한다.
package pe.pilseong.recipe.controller;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.exception.NotFoundException;
@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
@ExceptionHandler(NumberFormatException.class)
public ModelAndView handleNumberFormatException(Exception exception) {
log.error("Handling Number Format Exception");
log.error(exception.getMessage());
ModelAndView mav = new ModelAndView();
mav.setViewName("400error");
mav.addObject("exception", exception);
return mav;
}
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NotFoundException.class)
public ModelAndView handleNotFound(Exception exception) {
log.error("Handling not found Exception");
log.error(exception.getMessage());
ModelAndView mav = new ModelAndView();
mav.setViewName("404error");
mav.addObject("exception", exception);
return mav;
}
}
7-2 ModelAndView 대신 Model을 받아서 처리하면 된다.
7-2-1 null이 반환되는 것을 처리하기 위한 TemplateInputException도 지정해 준다.
package pe.pilseong.recipe.controller;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.thymeleaf.exceptions.TemplateInputException;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.exception.NotFoundException;
@Slf4j
@ControllerAdvice
public class ControllerExceptionHandler {
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
@ExceptionHandler(NumberFormatException.class)
public String handleNumberFormatException(Exception exception, Model model) {
log.error("Handling Number Format Exception");
log.error(exception.getMessage());
model.addAttribute("exception", exception);
return "400error";
}
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler({ NotFoundException.class, TemplateInputException.class })
public String handleNotFound(Exception exception, Model model) {
log.error("Handling not found Exception");
log.error(exception.getMessage());
model.addAttribute("exception", exception);
return "404error";
}
}
8. 저장관련 로직도 수정되어야 한다. block을 사용하지 않고 필요시 subscribe만 처리하여 사용한다.
8-1 아래는 Service 구현 부분이다.
package pe.pilseong.recipe.service;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.command.IngredientCommand;
import pe.pilseong.recipe.converter.IngredientCommandToIngredient;
import pe.pilseong.recipe.converter.IngredientToIngredientCommand;
import pe.pilseong.recipe.domain.Ingredient;
import pe.pilseong.recipe.domain.Recipe;
import pe.pilseong.recipe.repository.reactive.RecipeReactiveRepository;
import pe.pilseong.recipe.repository.reactive.UnitOfMeasureReactiveRepository;
import reactor.core.publisher.Mono;
@Slf4j
@Service
@RequiredArgsConstructor
public class IngredientServiceImpl implements IngredientService {
private final RecipeReactiveRepository recipeReactiveRepository;
private final IngredientToIngredientCommand ingredientCommandConverter;
private final IngredientCommandToIngredient ingredientConverter;
private final UnitOfMeasureReactiveRepository uomReactiveRepository;
@Override
public Mono<IngredientCommand> findByRecipeIdAndIngredientId(final String recipeId, final String ingredientId) {
log.debug("\nfindByRecipeIdAndIngredientId in IngredientServiceImpl");
return recipeReactiveRepository.findById(recipeId).
flatMapIterable(Recipe::getIngredients)
.filter(ingredient ->
ingredient.getId().equalsIgnoreCase(ingredientId)).single().map(ingredient -> {
IngredientCommand command = ingredientCommandConverter.convert(ingredient);
command.setRecipeId(recipeId);
return command;
});
}
@Override
public Mono<IngredientCommand> saveIngredientCommand(IngredientCommand command) {
Objects.requireNonNull(command);
AtomicReference<String> ingredientId = new AtomicReference<>();
AtomicReference<String> recipeId = new AtomicReference<>();
log.debug("saveIngredientCommand in IngredientServiceImpl with command :: " + command.toString());
return recipeReactiveRepository.findById(command.getRecipeId())
.map(recipe -> {
recipeId.set(recipe.getId());
recipe.getIngredients().stream()
.filter(ingredient -> ingredient.getId().equalsIgnoreCase(command.getId()))
.findFirst()
// update ingredient
.map(ingredient -> {
ingredientId.set(command.getId());
ingredient.setDescription(command.getDescription());
ingredient.setAmount(command.getAmount());
return recipe;
})
// add new ingredient
.orElseGet(() -> {
Ingredient newIngredient = ingredientConverter.convert(command);
ingredientId.set(Objects.requireNonNull(newIngredient).getId());
uomReactiveRepository.findById(command.getUom().getId())
.flatMap(unitOfMeasure -> {
newIngredient.setUom(unitOfMeasure);
return Mono.just(unitOfMeasure);
})
.subscribe();
recipe.addIngredient(newIngredient);
return recipe;
});
return recipe;
})
.flatMap(recipe -> recipeReactiveRepository.save(recipe).then(Mono.just(recipe)))
.flatMapIterable(Recipe::getIngredients)
.filter(savedIngredient -> savedIngredient.getId().equalsIgnoreCase(ingredientId.get()))
.flatMap(savedIngredient -> {
IngredientCommand ingredientCommand = ingredientCommandConverter.convert(savedIngredient);
return Mono.just(ingredientCommand);
}).single();
}
@Override
public Mono<Void> deleteByRecipeIdAndIngredientId(final String recipeId, final String ingredientId) {
log.debug("deleteByRecipeIdAnd in IngredientServiceImpl with recipe Id :: " + recipeId + " " + ingredientId);
return recipeReactiveRepository.findById(recipeId)
.map(recipe -> {
log.debug(recipe.getIngredients().toString());
boolean removeIf = recipe.getIngredients().removeIf(ingredient -> ingredient.getId().equals(ingredientId));
if (removeIf) {
log.debug("Some ingredient deleted");
recipeReactiveRepository.save(recipe).subscribe();
}
return Mono.empty();
})
.then();
}
}
8-2 위의 service를 사용하는 controller 이다.
8-2-1 저장과 삭제 부분만 참고하면 된다.
8-2-2 Reactive는 워낙 방법이 많아서 나중에 좀 더 효율적인 코드가 생각나면 적어놓겠다.
8-2-3 webflux에서 thymeleaf를 사용할 때 view name을 바로전에 저장한 객체의 id로 만들어야 하는 경우가 있다.
8-2-3-1 아무리 생각해도 안되어 그냥 포기했다. 나중에 찾으면 붙여 놓을 예정이다.
8-2-3-2 조금 우회하여 저장한 객체를 세부정보를 바로 보여주는 것이 아니라 리스트를 보여주는 것으로 대체했다.
package pe.pilseong.recipe.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import pe.pilseong.recipe.command.IngredientCommand;
import pe.pilseong.recipe.command.UnitOfMeasureCommand;
import pe.pilseong.recipe.service.IngredientService;
import pe.pilseong.recipe.service.RecipeService;
import pe.pilseong.recipe.service.UnitOfMeasureService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Slf4j
@Controller
@RequiredArgsConstructor
public class IngredientController {
/**
*
*/
private static final String RECIPE_INGREDIENT_INGREDIENT_FORM =
"recipe/ingredient/ingredientForm";
private final RecipeService recipeService;
private final IngredientService ingredientService;
private final UnitOfMeasureService uomService;
private WebDataBinder webDataBinder;
@InitBinder("ingredient")
public void initBinder(WebDataBinder webDataBinder) {
this.webDataBinder = webDataBinder;
}
@ModelAttribute("uomList")
public Flux<UnitOfMeasureCommand> populateUoms() {
return uomService.listAllUoms();
}
@GetMapping("/recipe/{id}/ingredients")
public String listIngredients(@PathVariable("id") String id, Model model) {
log.debug("listIngredients in IngredientController");
model.addAttribute("recipe", recipeService.findCommandById(id));
return "recipe/ingredient/list";
}
@GetMapping("/recipe/{recipeId}/ingredient/{ingredientId}/show")
public String showIngredient(
@PathVariable("recipeId") String recipeId,
@PathVariable("ingredientId") String ingredientId, Model model) {
model.addAttribute("ingredient",
ingredientService.findByRecipeIdAndIngredientId(recipeId, ingredientId));
return "recipe/ingredient/show";
}
@GetMapping("/recipe/{recipeId}/ingredient/{ingredientId}/update")
public String showUpdateRecipeIngredient(
@PathVariable("recipeId") String recipeId,
@PathVariable("ingredientId") String ingredientId,
Model model
) {
model.addAttribute("ingredient",
ingredientService.findByRecipeIdAndIngredientId(recipeId, ingredientId));
return RECIPE_INGREDIENT_INGREDIENT_FORM;
}
@PostMapping("/recipe/{recipeId}/ingredient")
public String saveIngredientCommand(
@PathVariable("recipeId") String recipeId,
@ModelAttribute("ingredient") IngredientCommand ingredientCommand,
Model model) {
webDataBinder.validate();
BindingResult results = webDataBinder.getBindingResult();
log.debug("\nsaveIngredientCommand in IngredientController\n");
log.debug("\results in IngredientController\n" + results.toString());
ingredientCommand.setRecipeId(recipeId);
if (results.hasErrors()) {
model.addAttribute("ingredient", ingredientCommand);
log.debug("saveIngredientCommand error occurred in IngredientController\n");
results.getAllErrors().forEach(error-> log.debug(error.toString()));
return RECIPE_INGREDIENT_INGREDIENT_FORM;
}
log.debug("saveIngredientCommand passed in IngredientController\n");
Mono<IngredientCommand> saveIngredientCommand =
ingredientService.saveIngredientCommand(ingredientCommand);
model.addAttribute("ingredient", saveIngredientCommand);
return "redirect:/recipe/{recipeId}/ingredients";
}
@GetMapping("/recipe/{recipeId}/ingredient/new")
public String showNewIngredientForm(@PathVariable("recipeId") String recipeId, Model model) {
IngredientCommand ingredientCommand = new IngredientCommand();
// make them used inside template
ingredientCommand.setRecipeId(recipeId);
ingredientCommand.setUom(new UnitOfMeasureCommand());
model.addAttribute("ingredient", ingredientCommand);
return RECIPE_INGREDIENT_INGREDIENT_FORM;
}
@GetMapping("/recipe/{recipeId}/ingredient/{ingredientId}/delete")
public String deleteIngredient(@PathVariable("recipeId") String recipeId,
@PathVariable("ingredientId") String ingredientId) {
ingredientService.deleteByRecipeIdAndIngredientId(recipeId, ingredientId).subscribe();
return "redirect:/recipe/" + recipeId + "/ingredients";
}
}
'Spring > Spring Advanced' 카테고리의 다른 글
WebFlux : Reactive Stream Specification (0) | 2020.09.07 |
---|---|
WebFlux : Embedded mongo 일반 환경에서 사용하기 (0) | 2020.09.06 |
WebFlux : 데이터를 찾지 못한 경우 Exception 발생하기 (0) | 2020.08.29 |
Spring Advanced : Spring Boot + Security login custom 메소드로 구현하기 (0) | 2020.06.09 |
Spring Advanced : Rest + Security + Thymeleaf 로그인, 회원가입 기능이 포함된 Rest Template CRUD 클라이언트 작성하기 (0) | 2020.05.27 |
- Total
- Today
- Yesterday
- 도커 개발환경 참고
- AWS ARN 구조
- Immuability에 관한 설명
- 자바스크립트 멀티 비동기 함수 호출 참고
- WSDL 참고
- SOAP 컨슈머 참고
- MySql dump 사용법
- AWS Lambda with Addon
- NFC 드라이버 linux 설치
- electron IPC
- mifare classic 강의
- go module 관련 상세한 정보
- C 메모리 찍어보기
- C++ Addon 마이그레이션
- JAX WS Header 관련 stackoverflow
- SOAP Custom Header 설정 참고
- SOAP Custom Header
- SOAP BindingProvider
- dispatcher 사용하여 설정
- vagrant kvm으로 사용하기
- git fork, pull request to the …
- vagrant libvirt bridge network
- python, js의 async, await의 차이
- go JSON struct 생성
- Netflix Kinesis 활용 분석
- docker credential problem
- private subnet에서 outbound IP 확…
- 안드로이드 coroutine
- kotlin with, apply, also 등
- 안드로이드 초기로딩이 안되는 경우
- navigation 데이터 보내기
- 레이스 컨디션 navController
- raylib
- Validation
- RestTemplate
- 설정
- 매핑
- Many-To-Many
- 스프링부트
- Spring Security
- jsp
- Rest
- form
- 로그인
- 스프링
- MYSQL
- 외부파일
- Security
- XML
- one-to-one
- spring boot
- crud
- mapping
- Spring
- WebMvc
- 상속
- hibernate
- 자바
- 설정하기
- 하이버네이트
- login
- one-to-many
- Angular