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