Как быстро начать работу с 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
|
@Configurationpublic 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
|
@Configurationclass 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")@Documentpublic 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
|
@ControllerAdvicepublic 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). Так я и сделал:
Получить аккаунты (не авторизованные):
Почтовый аккаунт (не требует аутентификации:
Получить аккаунты (авторизованные):
Получить текущий аккаунт (авторизованный):
Мы сделали
Это все на данный момент. Надеюсь, вам понравилось, как я наслаждался созданием проекта. Проект и этот пост заняли у меня около 3 часов. Большую часть времени я потратил на выяснение конфигурации безопасности (я хотел, чтобы она была полностью на Java) и написание этого пошагового руководства.



