Привет, хабрахабр!
На данный момент тут уже довольно много гайдов по такой связке, но они на мой взгляд во-первых немного устаревшие, во вторых — я считаю что должен быть гайд как сделать что-то осязаемое но простое, чтобы показать что и такое возможно.
Итак, если вам хочется попробовать Spring MVC с сохранением в базе и 0(нулем) файлов xml-конфигураций, прошу под кат!
Конечно хотелось бы сразу запустить приложение, но сначала немного подготовимся.
Вся разработка будет вестись на Intellij IDEA, но не думаю что реализация в другой IDE будет сильно сложнее.
Сначала создадим папку проекта, назовем ее ForHabrahabr
Для нашего проекта в корне нужно создать вот такое дерево папок:
(можно же просто сделать по инструкции для остальных в следующем разделе)
Теперь можно использвать gradlew.bat/gradlew в зависимости от ОС.
В качестве БД выберем MySQL как самую простую для quickstart. Создаем ее на localhost,
в ней создаем базу forhabrahabr, в дальнейшем в ней будем создавать таблички
users
roles
users_roles
posts
likes
Но об этом позже, пока достаточно создать БД.
Итак, для начала откроем наш только что созданный проект в Intellj IDEA, она увидит Gradle и предложит использовать его:
(Welcome to ItelliJ IDEA -> Open -> ForHabrahabr).
В этом окошке просто жмете ок, если его нет(или проблемы с Gradle JVM) — пишите в лс, буду разбираться что не так.
В итоге должен получиться такой проект:
Первым делом создадим пакет для всех классов, назовем его habraspring(обычная папка в src/main/java/), а в нем — первый
класс Application:
Но в таком виде наше приложение еще не запустится, надо показать автоконфигуратору где находится база данных, для этого добавим файл в папку resources/ файл application.properties.
Также надо создать в resources/ папку templates/ для шаблонизатора.
Папка ресурсов будет выглядеть вот так:
Не обращайте внимание на файлы .gitkeep, для работы программы они не нужны, можно их спокойно удалять/не создавать.
Готово, можете впервые запустить ваше приложение без падения.
Для запуска нужно запустить таску bootRun (двойной клик по ней):
Если нет такой панельки, идем в View -> Tool Windows -> Gradle.
В логе приложения будет что-то вроде такого:
Попробуйте теперь зайти по адресу http://localhost:8080, должен работать.
Ну а теперь хотелось бы увидеть немного контента, не так ли?
Для этого нам понадобится создать два класса конфигурации(помните, никаких XML!) во вложенной папке config, а также добавить home.html (аналог index.html) в папку resources.
Файлы конфигурации у нас простейшие, ведь мы их используем для страниц без контроллера:
Ну и простейшая домашняя страничка:
Как должен выглядеть проект на этом этапе можно посмотреть(и скачать) тут:
github.com/MaxPovver/ForHabrahabr/tree/withbasicmvc
*не забудьте в папке проекта написать в консоли git checkout withbasicmvc
Если на данный момент все сделано правильно, по http://localhost:8080 у вас должно выводится
Итак, мы хотим добавить контроллеров, и чтобы доступ к ним выдавался только авторизованным юзерам, но у нас их пока нет.
Для того, чтобы механизм авторизации заработал, нам надо добавить сущность юзера в проект.
Необходимые шаги:
Сначала создадим в бд простейшую табличку users с полями id, username, password.
Теперь создадим подпакет entities для сущностей и создадим в нем класс User:
Никаких hbm.xml не нужно, даже аннотировать поля не нужно(исключение — поле ID, его всегда надо отмечать)
Здесь Spring все делает за нас, достаточно отнаследоваться чтобы он понял что ему генерировать, код же писать не нужно вообще:
Для этого нам надо создать класс реализующий интерфейс UserDetailsService и подлючить его в WebSecurityConfig
Теперь изменим код WebSecurityConfig:
Добавим страничку входа login.html и «секретную» (только для авторизованных) страничку secret.html:
И сделаем новые странички доступными без контроллера, добавив в WebMvcConfig 2 строчки:
Готово! Теперь по адресу http://localhost:8080 у вас должно все выводиться нормально,
а вот по адресу http://localhost:8080/secret Вы пройти не сможете — будет кидать в /login, требуя валидную пару юзер/пароль.
Теперь добавьте в вашу таблицу forhabrahabr.users запись c паролем и логином user, user (или запустите скрипт github.com/MaxPovver/ForHabrahabr/blob/withauth/import_me.sql в вашей дб).
Если вы все сделали правильно, теперь вас должно пускать в /secret.
Итак, мы уже используем полноценное Spring MVC приложение с использованием Spring Security для безопасности и Spring JPA для работы с БД. И никаких XML.
Многое пришлось опустить/не объяснять чтобы не запутать окончательно, но если считаете необходимым добавить что-то уже сейчас — пишите в лс.
Осталось материала еще минимум на одну часть, если, конечно, тема актуальна. (Controllers, EntityToEntity(ManyToOne OneToOne etc), User Roles, Testing etc)
В самой статье пришлось некоторые момоменты пропустить, постараюсь про максимальное их количество написать здесь. Эта часть не нужна для запуска приложения, но может пригодиться в выяснении непонятных моментов.
UPD вторая часть: Spring без XML. Часть 2
На данный момент тут уже довольно много гайдов по такой связке, но они на мой взгляд во-первых немного устаревшие, во вторых — я считаю что должен быть гайд как сделать что-то осязаемое но простое, чтобы показать что и такое возможно.
Итак, если вам хочется попробовать Spring MVC с сохранением в базе и 0(нулем) файлов xml-конфигураций, прошу под кат!
Содержание
1. Подготовка к запуску
1.1 IDE
1.2 Структура папок
1.3 Gradle & Git
1.3.1 Для остальных
1.4 База данных
2. Начинаем кодить
2.1 Создание проекта
2.2 Добавляем первый код
2.3 Контент
3. Добавляем работу с БД
3.1 Сущность «User»
3.2 Репозиторий UsersRepository
3.3 Добавление связи между юзером и Spring Security
4. К чему мы пришли
4.1 Для желающих запустить готовый проект
Конечно хотелось бы сразу запустить приложение, но сначала немного подготовимся.
1. Подготовка к запуску
1.1 IDE
Вся разработка будет вестись на Intellij IDEA, но не думаю что реализация в другой IDE будет сильно сложнее.
1.2 Структура папок
Сначала создадим папку проекта, назовем ее ForHabrahabr
Для нашего проекта в корне нужно создать вот такое дерево папок:
(можно же просто сделать по инструкции для остальных в следующем разделе)
1.3 Gradle & Git
Для самостоятельных
Итак, каркас приложения мы получили.
Теперь добавим в него контроль версий и сборщик.
Для этого в ForHabrahabr добавим .gitignore с вот таким содержанием:
.gradle
.idea
*.iml
build/
Заходим в эту директорию через консоль и пишем
Теперь добавим bulid.gradle со всеми зависимостями которые нам пригодятся в процессе написания приложения.
После чего в консольке в той же директории где build.gradle пишем
Теперь добавим в него контроль версий и сборщик.
Для этого в ForHabrahabr добавим .gitignore с вот таким содержанием:
.gradle
.idea
*.iml
build/
Заходим в эту директорию через консоль и пишем
git init
Теперь добавим bulid.gradle со всеми зависимостями которые нам пригодятся в процессе написания приложения.
build.gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath(«org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE»)
classpath 'mysql:mysql-connector-java:5.1.34'
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
jar {
baseName = 'gs-rest-service'
version = '0.1.0'
}
repositories {
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile(«org.springframework.boot:spring-boot-starter-web»)
compile(«org.springframework.boot:spring-boot-starter-data-jpa»)
compile(«org.springframework.boot:spring-boot-starter-security»)
compile(«org.springframework.boot:spring-boot-starter-thymeleaf»)
compile 'mysql:mysql-connector-java:5.1.31'
compile 'commons-dbcp:commons-dbcp:1.4'
testCompile(«org.springframework:spring-test»)
testCompile(«junit:junit»)
testCompile 'org.springframework.security:spring-security-test:4.0.1.RELEASE'
}
task wrapper(type: Wrapper) {
gradleVersion = '2.3'
}
repositories {
mavenCentral()
}
dependencies {
classpath(«org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE»)
classpath 'mysql:mysql-connector-java:5.1.34'
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
jar {
baseName = 'gs-rest-service'
version = '0.1.0'
}
repositories {
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile(«org.springframework.boot:spring-boot-starter-web»)
compile(«org.springframework.boot:spring-boot-starter-data-jpa»)
compile(«org.springframework.boot:spring-boot-starter-security»)
compile(«org.springframework.boot:spring-boot-starter-thymeleaf»)
compile 'mysql:mysql-connector-java:5.1.31'
compile 'commons-dbcp:commons-dbcp:1.4'
testCompile(«org.springframework:spring-test»)
testCompile(«junit:junit»)
testCompile 'org.springframework.security:spring-security-test:4.0.1.RELEASE'
}
task wrapper(type: Wrapper) {
gradleVersion = '2.3'
}
После чего в консольке в той же директории где build.gradle пишем
gradle wrapper ./gradlew build (или для windows ./gradlew.bat build)
Теперь можно использвать gradlew.bat/gradlew в зависимости от ОС.
1.3.1 Для остальных
- Заходите через консоль в папку где находятся ваши проекты Idea
- git clone github.com/MaxPovver/ForHabrahabr.git
- git cd ForHabrahabr/
- git checkout quikstart
- Все, теперь у вас есть готовая структура проекта.
1.4 База данных
В качестве БД выберем MySQL как самую простую для quickstart. Создаем ее на localhost,
в ней создаем базу forhabrahabr, в дальнейшем в ней будем создавать таблички
users
roles
users_roles
posts
likes
Но об этом позже, пока достаточно создать БД.
2. Начинаем кодить
2.1 Создание проекта
Итак, для начала откроем наш только что созданный проект в Intellj IDEA, она увидит Gradle и предложит использовать его:
(Welcome to ItelliJ IDEA -> Open -> ForHabrahabr).
В этом окошке просто жмете ок, если его нет(или проблемы с Gradle JVM) — пишите в лс, буду разбираться что не так.
В итоге должен получиться такой проект:
2.2 Добавляем первый код
Первым делом создадим пакет для всех классов, назовем его habraspring(обычная папка в src/main/java/), а в нем — первый
класс Application:
Код класса
package habraspring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@ComponentScan
@EnableJpaRepositories(basePackages = {"habraspring"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Но в таком виде наше приложение еще не запустится, надо показать автоконфигуратору где находится база данных, для этого добавим файл в папку resources/ файл application.properties.
С вот таким содержанием
#settings for database spring.datasource.url=jdbc:mysql://localhost/forhabrahabr spring.datasource.username=root spring.datasource.password= spring.datasource.driver-class-name=com.mysql.jdbc.Driver #turned on to enable lazy loading spring.jpa.properties.hibernate.enable_lazy_load_no_trans = true
Также надо создать в resources/ папку templates/ для шаблонизатора.
Папка ресурсов будет выглядеть вот так:
Не обращайте внимание на файлы .gitkeep, для работы программы они не нужны, можно их спокойно удалять/не создавать.
Готово, можете впервые запустить ваше приложение без падения.
Для запуска нужно запустить таску bootRun (двойной клик по ней):
Если нет такой панельки, идем в View -> Tool Windows -> Gradle.
В логе приложения будет что-то вроде такого:
Лог запуска
15:24:47: Executing external task 'bootRun'... :compileJava UP-TO-DATE :processResources :classes :findMainClass :bootRun . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.2.5.RELEASE) 2015-07-11 14:24:49.180 INFO 12590 --- [ main] habraspring.Application : Starting Application on MacBook-Pro-Maksim.local with PID 12590 (/Users/admin/IdeaProjects/ForHabrahabr/build/classes/main started by admin in /Users/admin/IdeaProjects/ForHabrahabr) 2015-07-11 14:24:49.230 INFO 12590 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2eda0940: startup date [Sat Jul 11 14:24:49 MSK 2015]; root of context hierarchy 2015-07-11 14:24:50.029 INFO 12590 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]] 2015-07-11 14:24:50.701 INFO 12590 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$1f1e9ae] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2015-07-11 14:24:50.727 INFO 12590 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2015-07-11 14:24:50.741 INFO 12590 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2015-07-11 14:24:50.746 INFO 12590 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2015-07-11 14:24:51.168 INFO 12590 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http) 2015-07-11 14:24:51.408 INFO 12590 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat 2015-07-11 14:24:51.409 INFO 12590 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.0.23 2015-07-11 14:24:51.601 INFO 12590 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2015-07-11 14:24:51.601 INFO 12590 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2374 ms 2015-07-11 14:24:52.570 INFO 12590 --- [ost-startStop-1] b.a.s.AuthenticationManagerConfiguration : Using default security password: bd1659e1-4c49-43a2-9fd6-2ca7d46e9e23 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/css/**'], [] 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/js/**'], [] 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/images/**'], [] 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/**/favicon.ico'], [] 2015-07-11 14:24:52.614 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: Ant [pattern='/error'], [] 2015-07-11 14:24:52.650 INFO 12590 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/**']]], [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5854c7d0, org.springframework.security.web.context.SecurityContextPersistenceFilter@874f491, org.springframework.security.web.header.HeaderWriterFilter@34c74c36, org.springframework.security.web.authentication.logout.LogoutFilter@609329b3, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@a37632c, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@33a36df4, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@a3153e3, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1b8b1dc9, org.springframework.security.web.session.SessionManagementFilter@5ad0989a, org.springframework.security.web.access.ExceptionTranslationFilter@3e313564, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1fb86c05] 2015-07-11 14:24:52.723 INFO 12590 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*] 2015-07-11 14:24:52.724 INFO 12590 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] 2015-07-11 14:24:52.724 INFO 12590 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*] 2015-07-11 14:24:52.724 INFO 12590 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/] 2015-07-11 14:24:53.410 INFO 12590 --- [ main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default' 2015-07-11 14:24:53.425 INFO 12590 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [ name: default ...] 2015-07-11 14:24:53.500 INFO 12590 --- [ main] org.hibernate.Version : HHH000412: Hibernate Core {4.3.10.Final} 2015-07-11 14:24:53.503 INFO 12590 --- [ main] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found 2015-07-11 14:24:53.505 INFO 12590 --- [ main] org.hibernate.cfg.Environment : HHH000021: Bytecode provider name : javassist 2015-07-11 14:24:53.628 INFO 12590 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {4.0.5.Final} 2015-07-11 14:24:53.711 INFO 12590 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect 2015-07-11 14:24:53.774 INFO 12590 --- [ main] o.h.h.i.ast.ASTQueryTranslatorFactory : HHH000397: Using ASTQueryTranslatorFactory 2015-07-11 14:24:54.244 INFO 12590 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2eda0940: startup date [Sat Jul 11 14:24:49 MSK 2015]; root of context hierarchy 2015-07-11 14:24:54.328 INFO 12590 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest) 2015-07-11 14:24:54.328 INFO 12590 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest) 2015-07-11 14:24:54.356 INFO 12590 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2015-07-11 14:24:54.357 INFO 12590 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2015-07-11 14:24:54.393 INFO 12590 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2015-07-11 14:24:54.723 INFO 12590 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup 2015-07-11 14:24:54.800 INFO 12590 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http) 2015-07-11 14:24:54.803 INFO 12590 --- [ main] habraspring.Application : Started Application in 5.945 seconds (JVM running for 6.529)
Попробуйте теперь зайти по адресу http://localhost:8080, должен работать.
Ну а теперь хотелось бы увидеть немного контента, не так ли?
2.3 Контент
Для этого нам понадобится создать два класса конфигурации(помните, никаких XML!) во вложенной папке config, а также добавить home.html (аналог index.html) в папку resources.
Файлы конфигурации у нас простейшие, ведь мы их используем для страниц без контроллера:
config/MvcConfig.java
package habraspring.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
}
}
config/WebSecurityConfig.java
package habraspring.config;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll();
}
}
Ну и простейшая домашняя страничка:
home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Habrahabr</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Yours home page.</p>
</body>
</html>
Как должен выглядеть проект на этом этапе можно посмотреть(и скачать) тут:
github.com/MaxPovver/ForHabrahabr/tree/withbasicmvc
*не забудьте в папке проекта написать в консоли git checkout withbasicmvc
Если на данный момент все сделано правильно, по http://localhost:8080 у вас должно выводится
Welcome! Yours home page.
3. Добавляем работу с БД
Итак, мы хотим добавить контроллеров, и чтобы доступ к ним выдавался только авторизованным юзерам, но у нас их пока нет.
Для того, чтобы механизм авторизации заработал, нам надо добавить сущность юзера в проект.
Необходимые шаги:
- Добавить класс, описывающий сущность юзера в бд
- Добавить репозиторий новой сущности
- Добавить «связь» между механизмом Spring Security и нашей сущностью
- Все везде зарегистрировать
- «Включить» Spring Security
3.1 Сущность «User»
Сначала создадим в бд простейшую табличку users с полями id, username, password.
Теперь создадим подпакет entities для сущностей и создадим в нем класс User:
entities/User.java
package habraspring.entities;
import javax.persistence.*;
@Entity
@Table(name="users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String username;
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
protected User(){}
public User(String name, String pass) {
username = name;
password = pass;
}
}
Никаких hbm.xml не нужно, даже аннотировать поля не нужно(исключение — поле ID, его всегда надо отмечать)
3.2 Репозиторий UsersRepository
Здесь Spring все делает за нас, достаточно отнаследоваться чтобы он понял что ему генерировать, код же писать не нужно вообще:
UsersRepository.java
package habraspring.repositories;
import habraspring.entities.User;
import org.springframework.data.repository.CrudRepository;
public interface UsersRepository extends CrudRepository<User, Long> {
User findByUsername(String username);
}
3.3 Добавление связи между юзером и Spring Security
Для этого нам надо создать класс реализующий интерфейс UserDetailsService и подлючить его в WebSecurityConfig
utils/MySQLUserDetailsService.java
package habraspring.utils;
import habraspring.entities.User;
import habraspring.repositories.UsersRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Service
public class MySQLUserDetailsService implements UserDetailsService {
@Autowired
UsersRepository users;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails loadedUser;
try {
User client = users.findByUsername(username);
loadedUser = new org.springframework.security.core.userdetails.User(
client.getUsername(), client.getPassword(),
DummyAuthority.getAuth());
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
return loadedUser;
}
static class DummyAuthority implements GrantedAuthority
{
static Collection<GrantedAuthority> getAuth()
{
List<GrantedAuthority> res = new ArrayList<>(1);
res.add(new DummyAuthority());
return res;
}
@Override
public String getAuthority() {
return "USER";
}
}
}
Теперь изменим код WebSecurityConfig:
Заголовок спойлера
package habraspring.config;
import habraspring.utils.MySQLUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
private MySQLUserDetailsService mySQLUserDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(mySQLUserDetailsService);
}
}
Добавим страничку входа login.html и «секретную» (только для авторизованных) страничку secret.html:
Их код
secret.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Secret page</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Login page</title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
И сделаем новые странички доступными без контроллера, добавив в WebMvcConfig 2 строчки:
registry.addViewController("/login").setViewName("login");
registry.addViewController("/secret").setViewName("secret");
Готово! Теперь по адресу http://localhost:8080 у вас должно все выводиться нормально,
а вот по адресу http://localhost:8080/secret Вы пройти не сможете — будет кидать в /login, требуя валидную пару юзер/пароль.
Теперь добавьте в вашу таблицу forhabrahabr.users запись c паролем и логином user, user (или запустите скрипт github.com/MaxPovver/ForHabrahabr/blob/withauth/import_me.sql в вашей дб).
Если вы все сделали правильно, теперь вас должно пускать в /secret.
4. К чему мы пришли
Итак, мы уже используем полноценное Spring MVC приложение с использованием Spring Security для безопасности и Spring JPA для работы с БД. И никаких XML.
4.1 Для желающих запустить готовый проект
- git clone github.com/MaxPovver/ForHabrahabr.git
- cd ForHabrahabr/
- git checkout withauth
- запускаем в своей локальной mysql бд import_me.sql(или создаем руками табличку и данные для нее)
- Открываем через IDEA созданную папку ForHabrahabr
- Нету панельки Gradle? Открываем ее тут
- Запускаем bootRun
- На этом шаге уже должно все работать
Многое пришлось опустить/не объяснять чтобы не запутать окончательно, но если считаете необходимым добавить что-то уже сейчас — пишите в лс.
Осталось материала еще минимум на одну часть, если, конечно, тема актуальна. (Controllers, EntityToEntity(ManyToOne OneToOne etc), User Roles, Testing etc)
Комментарии к первой части
В самой статье пришлось некоторые момоменты пропустить, постараюсь про максимальное их количество написать здесь. Эта часть не нужна для запуска приложения, но может пригодиться в выяснении непонятных моментов.
Читать...
Приведу еще раз его код:
Что делает этот метод? Он привязывает какой-то запрос из адресной строки к какому-то шаблону из папки resources/.
К примеру если у нашего сервера просят показать содержимое "/" или "/home", он вернет home.html.
Аналогично при запросе "/login" вернется login.html.
Рассмотрим данный класс по порядку:
разрешаем отдавать запросы из этого списка любому запросившему:
Все остальное разрешаем открывать только авторизованным пользователям, указываем где находится форма логина, открыв для всех ее и страницу для выхода:
Также мы определяем откуда доставать пользователей нашей системе защиты, для этого используем @Autowired аннотацию, Spring сам подгрузит туда инстанс нужного сервиса:
И передаем его в тот метод, который позволяет определить наш сервис для соединения юзеров Spring Security и юзеров из базы данных.
Рассмотрим имплементацию loadUserByUsername.
Здесь мы снова используем @Autowired чтобы spring подставил в users реализованный интерфейс репозитория юзеров из базы данных и вытаскиваем с его помощью пользователя с заданным никнеймом из базы:
А здесь мы возвращаем «сконвертированного» из сущности базы данных в сущность Spring Security пользователя. Вот только появляется проблема — наш пользователь еще не имеет привязанных ролей(их сделаем позже), так что создадим класс заглушку, выдающий любому существующему юзеру пользовательские права. В случае если юзер не существует — код вылетит раньше с исключением. Дальше Spring сам для этого пользователя проверит соответствие введенного пароля и пароля объекта в базе с таким именем пользователя:
Рассмотрим код построчно:
Добавляя аннотацию Entity мы указываем сканнеру Spring что этот класс нужно привязать к таблице в базе данных.
В аннотации Table мы указываем название таблицы, к которой будем привязывать этот класс (зачастую его тоже можно не указывать, но лучше так не делать, иначе при смене названия таблицы можно словить проблем).
С полями все намного проще — они привязываются автоматически к полям в базе с таким же названием, вообще ничего писать не надо! Только нужно указать какое поле — ID, и как его генерировать для новых сущностей при сохранении в базу.
Дальше идут автоматически сгенерированные геттеры/сеттеры(лучше их делать даже если не нужны, просто на случай если ВНЕЗАПНО захотите туда логики добавить).
Ну а после идут два конструктора — пустой — только для сериализатора, напрямую в программе его использовать нельзя, и user friendly для создание новы юзеров с последующим их сохранением в UsersRepository.
Вот тут перечислены зависимости, которые Gradle автоматически подгрузит если их нет локально:
А тут перечислены зависимости, нужные для проведения интеграционных тестов (о них позже):
MvcConfig
Приведу еще раз его код:
MvcConfig.java
package habraspring.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/login").setViewName("login");
registry.addViewController("/secret").setViewName("secret");
}
}
Что делает этот метод? Он привязывает какой-то запрос из адресной строки к какому-то шаблону из папки resources/.
К примеру если у нашего сервера просят показать содержимое "/" или "/home", он вернет home.html.
Аналогично при запросе "/login" вернется login.html.
WebSecurityConfig
WebSecurityConfig.java
package habraspring.config;
import habraspring.utils.MySQLUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
private MySQLUserDetailsService mySQLUserDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(mySQLUserDetailsService);
}
}
Рассмотрим данный класс по порядку:
разрешаем отдавать запросы из этого списка любому запросившему:
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
Все остальное разрешаем открывать только авторизованным пользователям, указываем где находится форма логина, открыв для всех ее и страницу для выхода:
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
Также мы определяем откуда доставать пользователей нашей системе защиты, для этого используем @Autowired аннотацию, Spring сам подгрузит туда инстанс нужного сервиса:
@Autowired
private MySQLUserDetailsService mySQLUserDetailsService;
И передаем его в тот метод, который позволяет определить наш сервис для соединения юзеров Spring Security и юзеров из базы данных.
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(mySQLUserDetailsService);
}
MySQLUserDetailsService
MySQLUserDetailsService.java
package habraspring.utils;
import habraspring.entities.User;
import habraspring.repositories.UsersRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Service
public class MySQLUserDetailsService implements UserDetailsService {
@Autowired
UsersRepository users;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails loadedUser;
try {
User client = users.findByUsername(username);
loadedUser = new org.springframework.security.core.userdetails.User(
client.getUsername(), client.getPassword(),
DummyAuthority.getAuth());
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
}
return loadedUser;
}
static class DummyAuthority implements GrantedAuthority
{
static Collection<GrantedAuthority> getAuth()
{
List<GrantedAuthority> res = new ArrayList<>(1);
res.add(new DummyAuthority());
return res;
}
@Override
public String getAuthority() {
return "USER";
}
}
}
Рассмотрим имплементацию loadUserByUsername.
Здесь мы снова используем @Autowired чтобы spring подставил в users реализованный интерфейс репозитория юзеров из базы данных и вытаскиваем с его помощью пользователя с заданным никнеймом из базы:
@Autowired
UsersRepository users;
....
User client = users.findByUsername(username);
А здесь мы возвращаем «сконвертированного» из сущности базы данных в сущность Spring Security пользователя. Вот только появляется проблема — наш пользователь еще не имеет привязанных ролей(их сделаем позже), так что создадим класс заглушку, выдающий любому существующему юзеру пользовательские права. В случае если юзер не существует — код вылетит раньше с исключением. Дальше Spring сам для этого пользователя проверит соответствие введенного пароля и пароля объекта в базе с таким именем пользователя:
loadedUser = new org.springframework.security.core.userdetails.User(
client.getUsername(), client.getPassword(),
DummyAuthority.getAuth());
User
User.java
package habraspring.entities;
import javax.persistence.*;
@Entity
@Table(name="users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String username;
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
protected User(){}
public User(String name, String pass) {
username = name;
password = pass;
}
}
Рассмотрим код построчно:
Добавляя аннотацию Entity мы указываем сканнеру Spring что этот класс нужно привязать к таблице в базе данных.
В аннотации Table мы указываем название таблицы, к которой будем привязывать этот класс (зачастую его тоже можно не указывать, но лучше так не делать, иначе при смене названия таблицы можно словить проблем).
@Entity
@Table(name="users")
public class User {
С полями все намного проще — они привязываются автоматически к полям в базе с таким же названием, вообще ничего писать не надо! Только нужно указать какое поле — ID, и как его генерировать для новых сущностей при сохранении в базу.
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String username;
private String password;
Дальше идут автоматически сгенерированные геттеры/сеттеры(лучше их делать даже если не нужны, просто на случай если ВНЕЗАПНО захотите туда логики добавить).
Ну а после идут два конструктора — пустой — только для сериализатора, напрямую в программе его использовать нельзя, и user friendly для создание новы юзеров с последующим их сохранением в UsersRepository.
protected User(){}
public User(String name, String pass) {
username = name;
password = pass;
}
Gradle
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE")
classpath 'mysql:mysql-connector-java:5.1.34'
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'
jar {
baseName = 'gs-rest-service'
version = '0.1.0'
}
repositories {
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-data-jpa")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile 'mysql:mysql-connector-java:5.1.31'
compile 'commons-dbcp:commons-dbcp:1.4'
testCompile("org.springframework:spring-test")
testCompile("junit:junit")
testCompile 'org.springframework.security:spring-security-test:4.0.1.RELEASE'
}
task wrapper(type: Wrapper) {
gradleVersion = '2.3'
}
Вот тут перечислены зависимости, которые Gradle автоматически подгрузит если их нет локально:
dependencies {
compile("org.springframework.boot:spring-boot-starter-web") - для работы Spring MVC
compile("org.springframework.boot:spring-boot-starter-data-jpa") - для работы Spring Jpa(работа с базой данных)
compile("org.springframework.boot:spring-boot-starter-security") - для работы Spring Security
compile("org.springframework.boot:spring-boot-starter-thymeleaf") - для работы шаблонов из resources/tempates
compile 'mysql:mysql-connector-java:5.1.31' - mysql to spring
compile 'commons-dbcp:commons-dbcp:1.4'
А тут перечислены зависимости, нужные для проведения интеграционных тестов (о них позже):
testCompile("org.springframework:spring-test")
testCompile("junit:junit")
testCompile 'org.springframework.security:spring-security-test:4.0.1.RELEASE'
UPD вторая часть: Spring без XML. Часть 2
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
norguhtar
У меня два вопроса.
Зачем gradle? Чтобы еще требовалось ставить groovy для сборки? :)
Второй вопрос касается mvc. Сейчас для добавления новых view и security каждый раз нужно добавлять код и пересобирать. Зачем?
tolkkv
«Чтобы еще требовалось ставить groovy для сборки? :)» в данном случае похоже на «Чтобы еще требовалось ставить maven(заменить на нужное — ant/bazel) для сборки»
groovy не требуется ставить для сборки, требуется запустить лишь ./gradlew build — он сам скачает бинарник gradle (а он в себе уже содержит все необходимое).
При этом скачиваться будет только один раз для всех проектов.
Впрочем, можно поставить и gradle в систему отдельно, но gradlew более гибко. Всегда знаешь, что собираешь нужной версией :)
norguhtar
Maven как правило из коробки есть практически везде. Ну это уже кадлый выбирает то что хочет. А что с mvc и security? Это как бы не очень хорошее решение.
tolkkv
Вопрос времени, может скоро gradle из коробки будет везде :) Хотя конечно вопрос «коробки». apt-get install gradle /apt-get install maven3 — разница не большая) Зато с ./gradlew точно знаешь что не нужно делать даже этого, просто запускай скрипт сборки, все само поставиться в случае отсутствия.
Про второй вопрос: я не автор статьи, но тут у всех подробности разные. Если автору не нужно пересобирать динамическое изменение security, то почему бы и нет? Gradle просто захотелось ответить.
norguhtar
Ага и mvc тоже? На каждый контроллер ходить вписывать. Зачем? Уже давно через через указание сканировать и аннотации controller можно это не вписывать.
MaximChistov Автор
1) Потому что у меня на работе он :) Думаю те, кто задаются таким вопросом, легко переделают Gradle конфиг в maven. Да и ставить ничего не нужно — все уже прилагается к проекту, а недостатющее оно дотянет само.
2) Ну это же не для CMS, изменение security редкое событие, а view обычно идут вместе с контроллером для которого созданы, статичных практически нету :)
norguhtar
а view обычно идут вместе с контроллером для которого созданы, статичных практически нету :)
Тогда зачем их такое количество плодить? Особенно если указать view можно прямо в контроллере, а сам контроллер можно объявить через аннотацию? Вот будет у меня десятка два контроллеров, это уже будет портянка.
MaximChistov Автор
1) посмотрите вторую часть, там вьюха из контроллера отдается :) или такой вариант тоже не устраивает?
2) не будет два десятка)) это же обучающий пример для новичков, а не полноценный проект
norguhtar
Уже давно можно делать return «add». Т.е. просто возвращать строку. Как у вас было принято делать в spring 2.5.
Угу новички как раз и будут делать портянку. «В том учебном проекте было сделано именно так!» В примерах должны быть лучшие практики, а не худшие.
MaximChistov Автор
У меня вообще-то @RestController, а не Controller, в нем у всех методов по умолчанию @ResponseBody :)
Ну у нас разные взгляды на то, что надо давать новичкам. Никто не мешает вам написать статью согласующуюся с вашим взглядом, или написать мне в лс что конкретно стоит поправить в моем.
norguhtar
В add действии то?
MaximChistov Автор
Вернет просто строку add :) А не add.html
norguhtar
Вернет контроллер add.html. Настраивать надо уметь. Я это еще 5 лет назад делал.
habrahabr.ru/post/101546
Для tymleaf:
MaximChistov Автор
В том виде, в котором оно во второй части — не вернет :) А добавлять эти конфигурации в код я не хочу, на мой взгляд нельзя в таких статьях грузить тем, без чего можно обойтись.
Да и потом для контроллеров объявленных с аннотацией Controller оно и без этой кофигурации работает) Как раз стоит лишь вернуть строку. Это в 3 части будет
norguhtar
Для tymleaf насколько помню надо было крутить.
MaximChistov Автор
Добавьте в controllers такой класс:
norguhtar
Вы уж определитесь вернет или нет.
MaximChistov Автор
Попробуйте быть внимательнее. Пятый раз уже одно и то же объясняю))
Если контроллер объявлен как
как, например, Users контроллер, он просто так View возвращать не будет.
Но если он объявлен как
то если вернуть строку он будет возвращать view с таким названием.
norguhtar
Эм. Теперь внимаааательно почитайте что я писал в начале. Я писал про Controller просто. И у вас там было
Причем тут RestController? А Thymeleaf насколько помню в обычном контроллере через возврат строки не подхватывался.
MaximChistov Автор
Потому что это RestContoller:
github.com/MaxPovver/ForHabrahabr/blob/withcontroller/src/main/java/habraspring/controllers/UsersController.java
Собственно только поэтому я и не возвращал там просто «add»
(шестой раз)
В текущей версии — подхватывается.
norguhtar
Тфу ты. Но вообще надо отделять тогда в отдельный контроллер. Опять же во избежание.
Значит меньше писанины.
vayho
Я напишу несколько замечаний/дополнений которые могут быть полезны юзерам:
1. Вместо *.properties можно использовать *.yml — меньше текста, проще читать.
2. Я бы не советовал использовать Thymeleaf, он довольно медленный и у него КРАЙНЕ неудобный синтаксис для шаблонизатора.
3. Приложение можно запустить не только с помощью плагина Gradle но и из класса Application.
4. Gradle для нас оказался сыроват, мы используем менее гибкий но более стабильный Maven.
5. Spring boot умеет обновлять вашу схему в БД с помощью Hibernate. Свойство в application.properties, spring.jpa.hibernate.ddl-auto.
6. Я бы не советовал использовать Spring Data JPA. Ваш код 100% может обойтись без этого модуля, лучше возьмите QueryDSL и напишите всю прослойку Repository самостоятельно. Иначе в определенный момент вы получите кашу из методов-запросов, запросов Query, нативных запросов в SQL или с помощью JPA Criteria API. Если вы все таки решили использовать Spring Data JPA, то не поленитесь посмотреть исходники там есть интересные штуки типо AbstractPersistable.
trix
>Я бы не советовал использовать Thymeleaf, он довольно медленный
довольно медленный — это сколько в попугаях? что именно медленное?
>Иначе в определенный момент вы получите кашу из методов-запросов, запросов Query, нативных запросов в SQL или с помощью JPA Criteria API
собсно, спринг-дата для того и создан, чтобы забыть про criteria api
никаких проблем в смешении методов-запросов и Query не вижу.
нативных запросов вообще должен быть крайний минимум, или зачем там вообще хибер появился? по крайней мере, нативных запросов будет не больше, чем без спринг-даты )
norguhtar
Ниже есть бенчмарк, типа весьма тормоз :)
norguhtar
Предложите другой вариант, который хорошо интегрируется с Spring MVC быстрый и у которого удобный синтаксис. JSTL не предлагать :)
Я посмотрел QueryDSL выглядит не очень. Опять же делать свой репозиторий это возврат к временам до Spring Data. И да с чего у вас каша будет? Вот определен у вас интерфейсы ваших репозиториев, ну размещайте туда все вызовы через аннотации. Ну все же можно. В итоге кода получается меньше и каши меньше.
vayho
> Предложите другой вариант, который хорошо интегрируется с Spring MVC быстрый и у которого удобный синтаксис. JSTL не предлагать :)
Варианты есть: jtwig.org, www.mitchellbosecke.com/pebble/home, github.com/greenlaw110/Rythm, github.com/jreijn/spring-comparing-template-engines.
Мы используем Pebble engine.
> Я посмотрел QueryDSL выглядит не очень. Опять же делать свой репозиторий это возврат к временам до Spring Data. И да с чего у вас каша будет? Вот определен у вас интерфейсы ваших репозиториев, ну размещайте туда все вызовы через аннотации. Ну все же можно. В итоге кода получается меньше и каши меньше.
Это возможно дело вкуса. Я попробую описать причины по которым сейчас я не вижу смысла в Spring Data JPA.
1. Короткие методы — не нужны, да первые ощущения от findAll положительные, но со временем понимаешь что тут у тебя метод findAll а вот тут Query, а там еще Specifications. А нужно чтобы все выглядело одинаково(или хочется).
2. Query не гибкий метод потому что иногда нужны динамические запросы. По итогу используем Criteria API, потому что Specifications нам явно не хватает.
В результате я смотрю на Spring Data JPA и думаю, а зачем мне весь этот сахар если он покрывается стандартным JPA полностью, а плюсов никаких не дает. Только каша из разных способов сделать одно и то же.
С другой стороны JPA мне тоже не хватает, потому что у него не все хорошо с нативными запросами, с джоинами по несвязанным таблицам и выборкой отдельных полей. В результате мы стали использовать QueryDSL который позволяет делать типобезопасные, универсальные запросы к JDBC,JPA и collections.
norguhtar
Посмотрю. Вот только у Pebble не понятно как сделана интеграция форм в spring.
Если начинается что-то сложнее в него добавить сортировку, переделываем на метод
Не совсем понял. Не хватает подстановки запросов?
Вполне дает. Не надо писать свою реализацию репозитория, не требуется писать на каждый DAO свою реализацию, интерфейсов вполне хватает. Весьма часто можно просто использовать его DSL для получения необходимого результата.
vayho
> Не совсем понял. Не хватает подстановки запросов?
Не хватает динамики в запросах.
В одном случае мне нужен такой запрос:
Query(«SELECT u FROM User u JOIN FETCH u.profile»)
В другом случае:
Query(«SELECT u FROM User u»)
А в третьем:
Query(«SELECT u FROM User u JOIN FETCH u.profile JOIN FETCH u.role»)
А к этому мне иногда еще нужна постраничная разбивка с сортировкой, иногда просто сортировка.
У меня тут два варианта решения проблемы:
1. Написать три разных метода и сверху каждого сделать аннотации Query.
2. Сделать один метод в котором динамически строить запрос на основе входных параметров(нужно ли вытаскивать профиль или нет, нужна ли роль или нет).
Второй вариант для меня предпочтительней потому что в рантайме я могу изменить запросы. Поэтому что бы так сделать мне приходится отдельно писать метод и в нем с помощью QueryDSL описывать логику выборки данных.
norguhtar
Обычно такое делают вообще через ORM. Ну тут уже каждый страдает как ему кажется удобным.
MaximChistov Автор
по умолчанию вторичные сущности грузятся лениво в момент когда были запрошены :)
vayho
Я понимаю как работает меппинг в Hibernate, только в данном случае мне нужно за один запрос вытащить всех юзеров с их профилями(сразу) а не делать дополнительно кучу LAZY запросов.
MaximChistov Автор
Ну да, аннотации в таком плане не помощник, ими ленивость можно только в положение вкл/выкл ставить
norguhtar
Для этого или можно прописать тянуть все сразу ну или вешать хинты. Сильно зависит от ситуации.