Конфигурация - oyboy/Jora GitHub Wiki

Файл с настройками проекта

Настройки проекта во время разработки и деплоя несколько отличаются. Например, в первом случае требуются подробные логи, а в другом иные настройки подключения к другим сервисам. Поэтому для удобство созданы два профиля: dev и prod. Для активации профиля dev во время запуска проекта объявляется переменная среды --spring.profiles.active=dev, а во время сборки для продакшена - SPRING_PROFILES_ACTIVE: prod.

application-prod.yml

spring:
  application:
    name: Jora
  freemarker:
    expose-request-attributes: true
  mvc:
    static-path-pattern: /static/**
  datasource:
    generate-unique-name: false
    url: jdbc:mysql://jora-mysql-db:3306/joradb
    username: ${SPRING_DATASOURCE_USERNAME}
    password: ${SPRING_DATASOURCE_PASSWORD}
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: false
    properties:
      hibernate:
        cache:
          use_second_level_cache: false
          use_query_cache: false
  data:
    mongodb:
      host: jora-mongo-db
      database: joradb
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB
  cache:
    redis:
      time-to-live: 900000 #ttl 15 min

server:
  address: 0.0.0.0
  port: 8081

logging:
  level:
    root: INFO
    org.mongodb.driver: WARN
    com.mongodb: WARN
    #org.springframework.messaging: DEBUG
    #org.springframework.web: DEBUG

Как можно заметить, некоторые переменные должны быть сокрыты от всегообщего обозрения, потому создан скрытый файл .env:

DATASOURCE_USERNAME=your_username
DATASOURCE_PASSWORD=your_password
MYSQL_ROOT_PASSWORD=your_root_password

Файл для тестов

В resources создан новый файл application-test.properties. Единственная разница с предыдущим - обращение к другой базе: spring.datasource.url=jdbc:mysql://localhost:3306/joratestdb

MvcConfig

Файл для перенаправления get-запроса с корневой страницы на страницу "/home" (подразумевается, что последняя является главной).

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/home");
    }
}

Перехват исключений

Ситуация: пользователь оказался на удалённой странице и у него вылетела ошибка 500. Я решил обработать эту ситуацию, чтобы его автоматически перенаправляло на страницу, которая гарантированно не может быть удалена. С точки зрения разработчика удалённой страницей может оказаться запись в таблице Project.

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(SQLException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleSQLException(SQLSyntaxErrorException ex) {
        log.info("SQL Syntax Error: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
        return "redirect:/home";
    }
}

Отправка файлов большого размера весьма распространённая практика. При этом желательно, чтобы в логах такая ситуация была изложена весьма кратко. Для этого создан второй перехватчик исключения.

@ExceptionHandler(MaxUploadSizeExceededException.class)
    @ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
    public ResponseEntity<String> handleMaxSizeException(MaxUploadSizeExceededException exc) {
        log.error("Upload size Error: " + exc.getMessage(), HttpStatus.PAYLOAD_TOO_LARGE);
        return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
                .body("Файл слишком большого размера! Максимальный размер: " + exc.getMaxUploadSize());
    }

Может возникнуть ситуация, когда пользователь попадает к ресурсу, к которому он не имеет доступа. Такую ситуацию можно обработать, отправив пользователя на специальную страницу, в которой чётко будет написано, что он не имеет соответсвующих прав доступа.

@ExceptionHandler(AccessDeniedException.class)
    public ModelAndView handleAccessDeniedException(AccessDeniedException ex, Model model) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("redirect:/error/access-denied-error");
        return modelAndView;
    }

SecurityConfig

@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
        http
                .authorizeHttpRequests((requests) -> requests
                        .requestMatchers("/home", "/registration", "/static/css/**").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin((form) -> form
                        .loginPage("/login")
                        .permitAll()
                        .defaultSuccessUrl("/home")
                )
                .logout((logout) -> logout.permitAll())
                .exceptionHandling((exception) -> exception
                        .authenticationEntryPoint((request, response, authException) -> {
                            response.sendRedirect("/login");
                        })
                );
        return http.build();
    }
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

Собственные исключения

public class CustomException{
    //Ошибка присоединение к проекту (пользователь уже там есть)
    public static class UserAlreadyJoinedException extends Exception{
        public UserAlreadyJoinedException(String message) {
            super(message);
        }
    }
    //Ошибка присоединение к проекту (пользователь забанен)
    public static class UserBannedException extends Exception{
        public UserBannedException(String message){
            super(message);
        }
    }
    //Выбрасывается, когда размер создаваемого объекта превышает допустимое значение
    //Выбрасывается функцией createTag в groupService
    public static class LargeSizeException extends Exception{
        public LargeSizeException(String message) {super(message);}
    }
    //Проверка на наличие уже созданного объекта. Например, может быть вызван также методом createTag или
    //методом sendNotificationTo в notificationService, если такой запрос уже существует
    public static class ObjectExistsException extends Exception{
        public ObjectExistsException(String message){super(message);}
    }
    //Очевидно, если пользователь для какой-то задачи не был найден. Выбрасывается в методе sendHelp в taskService
    public static class UserNotFoundException extends Exception{
        public UserNotFoundException(String message){super(message);}
    }
}

Сокеты

Для работы с сокетами нужно в Spring создать конфигурационный файл, в котором будут указаны енд-поинты и включены брокеры сообщений.

package com.main.Jora.notifications;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }
}

RedisConfig

Логика подключения в найстроках приложения к базе Redis является устаревшей, потому требуется создать отдельный класс с необходимыи bean-компонентами.

@Configuration
@RequiredArgsConstructor
@EnableCaching
public class RedisConfig {
    //В зависимости от выбранного профиля (dev или prod) будет использоваться ссылка на нужный хост
    @Value("${API_REDIS_HOST:localhost}")
    private String redisHost;

    @Bean
    public RedisTemplate<Long, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Long, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
    //Настройки подключения
    @Bean
    JedisConnectionFactory jedisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHost, 6379);
        return new JedisConnectionFactory(redisStandaloneConfiguration);
    }
}
⚠️ **GitHub.com Fallback** ⚠️