Статьи

Как быстро начать работу с Spring 4.0 для создания простого REST-подобного API (пошаговое руководство)

Как быстро начать работу с Spring 4.0 для создания простого REST-подобного API (пошаговое руководство)

Еще одно руководство по созданию Web API с помощью Spring MVC. Не очень сложный. Просто прохождение. Полученное приложение будет обслуживать простой API, будет использовать Mongo в качестве постоянного хранилища и будет защищено Spring Security.

Начало работы — POM

Конечно, я все еще большой поклонник Maven, поэтому проект основан на Maven. Поскольку имеется Spring 4.0 RC2, я решил использовать его новое управление зависимостями, которое приводит к следующему pom.xml: Это довольно просто, так как это относится к приложению Spring MVC. Новым является элемент dependencyManagement . Дополнительную информацию об этом можно найти здесь: http://spring.io/blog/2013/12/03/spring-framework-4-0-rc2-available

конфигурация

Приложение настраивается с использованием JavaConfig. Я разделил это на несколько частей:

ServicesConfig

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Configuration
public class ServicesConfig {
 
    @Autowired
    private AccountRepository accountRepository;
 
    @Bean
    public UserService userService() {
        return new UserService(accountRepository);
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

Нет компонентного сканирования. Действительно просто.

PersistenceConfig

Конфигурация MongoDB со всеми доступными репозиториями. В этом простом приложении у нас есть только один репозиторий, поэтому конфигурация действительно проста.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Configuration
class PersistenceConfig {
 
    @Bean
    public AccountRepository accountRepository() throws UnknownHostException {
        return new MongoAccountRepository(mongoTemplate());
    }
 
    @Bean
    public MongoDbFactory mongoDbFactory() throws UnknownHostException {
        return new SimpleMongoDbFactory(new Mongo(), "r");
    }
 
    @Bean
    public MongoTemplate mongoTemplate() throws UnknownHostException {
        MongoTemplate template = new MongoTemplate(mongoDbFactory(), mongoConverter());
        return template;
    }
 
    @Bean
    public MongoTypeMapper mongoTypeMapper() {
        return new DefaultMongoTypeMapper(null);
    }
 
    @Bean
    public MongoMappingContext mongoMappingContext() {
        return new MongoMappingContext();
    }
 
    @Bean
    public MappingMongoConverter mongoConverter() throws UnknownHostException {
        MappingMongoConverter converter = new MappingMongoConverter(mongoDbFactory(), mongoMappingContext());
        converter.setTypeMapper(mongoTypeMapper());
        return converter;
    }
}

SecurityConfig

Теоретически Spring Security 3.2 может быть полностью настроен с помощью JavaConfig. Для меня это все еще теория, поэтому я использую XML здесь:

1
2
3
@Configuration
@ImportResource("classpath:spring-security-context.xml")
public class SecurityConfig {}

И XML: Как видите, для API будет использоваться базовая аутентификация.

WebAppInitializer

Нам не нужен файл web.xml, поэтому мы используем следующий код для настройки веб-приложения:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Order(2)
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
 
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
 
    @Override
    protected Class[] getRootConfigClasses() {
        return new Class[] {ServicesConfig.class, PersistenceConfig.class, SecurityConfig.class};
    }
 
    @Override
    protected Class[] getServletConfigClasses() {
        return new Class[] {WebMvcConfig.class};
    }
 
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);
        return new Filter[] {characterEncodingFilter};
    }
 
    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {       
        registration.setInitParameter("spring.profiles.active", "default");
    }
}

WebAppSecurityInitializer

Новое в Spring Security 3.

1
2
3
4
@Order(1)
public class WebAppSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
 
}

WebMvcConfig

Конфигурация диспетчерского сервлета. Действительно простой. Только важнейшие компоненты для создания простого API.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
@ComponentScan(basePackages = { "pl.codeleak.r" }, includeFilters = {@Filter(value = Controller.class)})
public class WebMvcConfig extends WebMvcConfigurationSupport {
 
    private static final String MESSAGE_SOURCE = "/WEB-INF/i18n/messages";
 
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        RequestMappingHandlerMapping requestMappingHandlerMapping = super.requestMappingHandlerMapping();
        requestMappingHandlerMapping.setUseSuffixPatternMatch(false);
        requestMappingHandlerMapping.setUseTrailingSlashMatch(false);
        return requestMappingHandlerMapping;
    }
 
    @Bean(name = "messageSource")
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename(MESSAGE_SOURCE);
        messageSource.setCacheSeconds(5);
        return messageSource;
    }
 
    @Override
    public Validator getValidator() {
        LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
        validator.setValidationMessageSource(messageSource());
        return validator;
    }
 
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

И это конфиг. Просто.

IndexController

Чтобы убедиться, что конфигурация в порядке, я создал IndexController, который обслуживает простой текст «Hello, World», например:

01
02
03
04
05
06
07
08
09
10
@Controller
@RequestMapping("/")
public class IndexController {
 
    @RequestMapping
    @ResponseBody
    public String index() {
        return "This is an API endpoint.";
    }
}

Когда вы запустите приложение, вы должны увидеть этот текст в браузере.

Создание API

UserService

Чтобы завершить настройку Spring Security, на самом деле все еще нужна одна часть: UserService, экземпляр которого был создан ранее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class UserService implements UserDetailsService {
 
    private AccountRepository accountRepository;
 
    public UserService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }
 
 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  Account account = accountRepository.findByEmail(username);
  if(account == null) {
   throw new UsernameNotFoundException("user not found");
  }
  return createUser(account);
 }
 
 public void signin(Account account) {
  SecurityContextHolder.getContext().setAuthentication(authenticate(account));
 }
 
 private Authentication authenticate(Account account) {
  return new UsernamePasswordAuthenticationToken(createUser(account), null, Collections.singleton(createAuthority(account))); 
 }
 
 private User createUser(Account account) {
  return new User(account.getEmail(), account.getPassword(), Collections.singleton(createAuthority(account)));
 }
 
 private GrantedAuthority createAuthority(Account account) {
  return new SimpleGrantedAuthority(account.getRole());
 }
 
}

Требовалось создать конечную точку API, которая обрабатывает 3 метода: получает вход в систему пользователя, получает всех пользователей (не совсем безопасно), создает новую учетную запись. Итак, давайте сделаем это.

Счет

Аккаунт будет нашим первым монго документом. Это действительно легко:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@SuppressWarnings("serial")
@Document
public class Account implements java.io.Serializable {
 
    @Id
    private String objectId;
 
    @Email
    @Indexed(unique = true)
    private String email;
 
    @JsonIgnore
    @NotBlank
    private String password;
 
    private String role = "ROLE_USER";
 
    private Account() {
 
    }
 
    public Account(String email, String password, String role) {
        this.email = email;
        this.password = password;
        this.role = role;
    }
 
   // getters and setters
}

вместилище

Я начал с интерфейса:

1
2
3
4
5
6
7
8
public interface AccountRepository {
 
    Account save(Account account);
 
    List findAll();
 
    Account findByEmail(String email);
}

И позже с его реализацией Mongo:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MongoAccountRepository implements AccountRepository {
 
    private MongoTemplate mongoTemplate;
 
    public MongoAccountRepository(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }
 
    @Override
    public Account save(Account account) {
        mongoTemplate.save(account);
        return account;
    }
 
    @Override
    public List findAll() {
        return mongoTemplate.findAll(Account.class);
    }
 
    @Override
    public Account findByEmail(String email) {
        return mongoTemplate.findOne(Query.query(Criteria.where("email").is(email)), Account.class);
    }
}

Контроллер API

Итак, мы почти у цели. Нам нужно предоставить контент пользователю. Итак, давайте создадим нашу конечную точку:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Controller
@RequestMapping("api/account")
class AccountController {
 
    private AccountRepository accountRepository;
 
    @Autowired
    public AccountController(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }
 
    @RequestMapping(value = "current", method = RequestMethod.GET)
    @ResponseStatus(value = HttpStatus.OK)
    @ResponseBody
    @PreAuthorize(value = "isAuthenticated()")
    public Account current(Principal principal) {
        Assert.notNull(principal);
        return accountRepository.findByEmail(principal.getName());
    }
 
    @RequestMapping(method = RequestMethod.GET)
    @ResponseStatus(value = HttpStatus.OK)
    @ResponseBody
    @PreAuthorize(value = "isAuthenticated()")
    public Accounts list() {
        List accounts = accountRepository.findAll();
        return new Accounts(accounts);
    }
 
    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(value = HttpStatus.CREATED)
    @ResponseBody
    @PreAuthorize(value = "permitAll()")
    public Account create(@Valid Account account) {
        accountRepository.save(account);
        return account;
    }
 
    private class Accounts extends ArrayList {
        public Accounts(List accounts) {
            super(accounts);
        }
    }
}

Надеюсь, вы заметили, что мы общаемся напрямую с хранилищем, поэтому пароли не будут закодированы. Небольшая деталь будет исправлена ​​позже, если хотите. Пока все в порядке.

Заканчивать

Последнее, что мне нужно, это какой-то обработчик ошибок, чтобы потребитель мог видеть сообщения об ошибках в JSON вместо HTML. Это просто с Spring MVC и советом @Controller.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@ControllerAdvice
public class ErrorHandler {
 
    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ErrorResponse errorResponse(Exception exception) {
        return new ErrorResponse(exception.getMessage());
    }
 
}
 
public class ErrorResponse {
    private String message;
    public ErrorResponse(String message) {
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

Если вы хотите увидеть более продвинутое использование @ControllerAdvice в Spring 4, прочитайте
этот пост .

Тестирование приложения

Как фанат юнит-тестов, я должен был сначала создать юнит-тесты. Но… я просто хотел протестировать новый инструмент: Почтальон (расширение Chrome). Так я и сделал:

Получить аккаунты (не авторизованные):

gna1

Почтовый аккаунт (не требует аутентификации:

gna2

Получить аккаунты (авторизованные):

gna3

Получить текущий аккаунт (авторизованный):

gna4

Мы сделали

Это все на данный момент. Надеюсь, вам понравилось, как я наслаждался созданием проекта. Проект и этот пост заняли у меня около 3 часов. Большую часть времени я потратил на выяснение конфигурации безопасности (я хотел, чтобы она была полностью на Java) и написание этого пошагового руководства.