Привет, хабрахабр!

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

Итак, если вам хочется попробовать Spring MVC с сохранением в базе и 0(нулем) файлов xml-конфигураций, прошу под кат!



Конечно хотелось бы сразу запустить приложение, но сначала немного подготовимся.

1. Подготовка к запуску


1.1 IDE


Вся разработка будет вестись на Intellij IDEA, но не думаю что реализация в другой IDE будет сильно сложнее.

1.2 Структура папок


Сначала создадим папку проекта, назовем ее ForHabrahabr
Для нашего проекта в корне нужно создать вот такое дерево папок:

directories tree
(можно же просто сделать по инструкции для остальных в следующем разделе)

1.3 Gradle & Git


Для самостоятельных
Итак, каркас приложения мы получили.
Теперь добавим в него контроль версий и сборщик.
Для этого в 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'
}


После чего в консольке в той же директории где 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).

image

В этом окошке просто жмете ок, если его нет(или проблемы с Gradle JVM) — пишите в лс, буду разбираться что не так.

В итоге должен получиться такой проект:

image

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/ для шаблонизатора.

Папка ресурсов будет выглядеть вот так:

image

Не обращайте внимание на файлы .gitkeep, для работы программы они не нужны, можно их спокойно удалять/не создавать.

Готово, можете впервые запустить ваше приложение без падения.

Для запуска нужно запустить таску bootRun (двойной клик по ней):

image

Если нет такой панельки, идем в 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
    image
  • Нету панельки Gradle? Открываем ее тут
    image
  • Запускаем bootRun
    image
  • На этом шаге уже должно все работать


Многое пришлось опустить/не объяснять чтобы не запутать окончательно, но если считаете необходимым добавить что-то уже сейчас — пишите в лс.

Осталось материала еще минимум на одну часть, если, конечно, тема актуальна. (Controllers, EntityToEntity(ManyToOne OneToOne etc), User Roles, Testing etc)

Комментарии к первой части


В самой статье пришлось некоторые момоменты пропустить, постараюсь про максимальное их количество написать здесь. Эта часть не нужна для запуска приложения, но может пригодиться в выяснении непонятных моментов.
Читать...

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
Полезна ли статья?

Проголосовало 134 человека. Воздержалось 49 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (33)


  1. norguhtar
    13.07.2015 07:42
    -2

    У меня два вопроса.
    Зачем gradle? Чтобы еще требовалось ставить groovy для сборки? :)
    Второй вопрос касается mvc. Сейчас для добавления новых view и security каждый раз нужно добавлять код и пересобирать. Зачем?


    1. tolkkv
      13.07.2015 08:31
      +3

      «Чтобы еще требовалось ставить groovy для сборки? :)» в данном случае похоже на «Чтобы еще требовалось ставить maven(заменить на нужное — ant/bazel) для сборки»
      groovy не требуется ставить для сборки, требуется запустить лишь ./gradlew build — он сам скачает бинарник gradle (а он в себе уже содержит все необходимое).
      При этом скачиваться будет только один раз для всех проектов.
      Впрочем, можно поставить и gradle в систему отдельно, но gradlew более гибко. Всегда знаешь, что собираешь нужной версией :)


      1. norguhtar
        13.07.2015 08:36
        -1

        Maven как правило из коробки есть практически везде. Ну это уже кадлый выбирает то что хочет. А что с mvc и security? Это как бы не очень хорошее решение.


        1. tolkkv
          13.07.2015 08:42
          +1

          Вопрос времени, может скоро gradle из коробки будет везде :) Хотя конечно вопрос «коробки». apt-get install gradle /apt-get install maven3 — разница не большая) Зато с ./gradlew точно знаешь что не нужно делать даже этого, просто запускай скрипт сборки, все само поставиться в случае отсутствия.
          Про второй вопрос: я не автор статьи, но тут у всех подробности разные. Если автору не нужно пересобирать динамическое изменение security, то почему бы и нет? Gradle просто захотелось ответить.


          1. norguhtar
            13.07.2015 09:00

            Ага и mvc тоже? На каждый контроллер ходить вписывать. Зачем? Уже давно через через указание сканировать и аннотации controller можно это не вписывать.


    1. MaximChistov Автор
      13.07.2015 10:04
      +1

      1) Потому что у меня на работе он :) Думаю те, кто задаются таким вопросом, легко переделают Gradle конфиг в maven. Да и ставить ничего не нужно — все уже прилагается к проекту, а недостатющее оно дотянет само.
      2) Ну это же не для CMS, изменение security редкое событие, а view обычно идут вместе с контроллером для которого созданы, статичных практически нету :)


      1. norguhtar
        13.07.2015 10:13

        а view обычно идут вместе с контроллером для которого созданы, статичных практически нету :)

        Тогда зачем их такое количество плодить? Особенно если указать view можно прямо в контроллере, а сам контроллер можно объявить через аннотацию? Вот будет у меня десятка два контроллеров, это уже будет портянка.


        1. MaximChistov Автор
          13.07.2015 10:36

          1) посмотрите вторую часть, там вьюха из контроллера отдается :) или такой вариант тоже не устраивает?
          2) не будет два десятка)) это же обучающий пример для новичков, а не полноценный проект


          1. norguhtar
            13.07.2015 11:01

            1) посмотрите вторую часть, там вьюха из контроллера отдается :) или такой вариант тоже не устраивает?

            Уже давно можно делать return «add». Т.е. просто возвращать строку. Как у вас было принято делать в spring 2.5.

            2) не будет два десятка)) это же обучающий пример для новичков, а не полноценный проект

            Угу новички как раз и будут делать портянку. «В том учебном проекте было сделано именно так!» В примерах должны быть лучшие практики, а не худшие.


            1. MaximChistov Автор
              13.07.2015 11:15

              Уже давно можно делать return «add»

              У меня вообще-то @RestController, а не Controller, в нем у всех методов по умолчанию @ResponseBody :)
              Угу новички как раз и будут делать портянку. «В том учебном проекте было сделано именно так!» В примерах должны быть лучшие практики, а не худшие.

              Ну у нас разные взгляды на то, что надо давать новичкам. Никто не мешает вам написать статью согласующуюся с вашим взглядом, или написать мне в лс что конкретно стоит поправить в моем.


              1. norguhtar
                13.07.2015 11:19

                У меня вообще-то @RestController, а не Controller, в нем у всех методов по умолчанию @ResponseBody :)

                В add действии то?


                1. MaximChistov Автор
                  13.07.2015 11:28

                      @RequestMapping(value = "/add",method = RequestMethod.GET)
                      public String getUserForm()
                      {
                          return "add";
                      }
                  

                  Вернет просто строку add :) А не add.html


                  1. norguhtar
                    13.07.2015 11:31

                    Вернет контроллер add.html. Настраивать надо уметь. Я это еще 5 лет назад делал.

                    habrahabr.ru/post/101546

                    Для tymleaf:

                    <beans xmlns="http://www.springframework.org/schema/beans"
                            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                            xmlns:mvc="http://www.springframework.org/schema/mvc"
                            xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
                                    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
                    
                            <!-- Configures support for @Controllers -->
                            <mvc:annotation-driven conversion-service="conversionService"/>
                    
                            <mvc:resources mapping="/favicon.ico" location="/favicon.ico"/>
                        <mvc:resources mapping="/img/**" location="/img/"/>
                            <mvc:resources mapping="/js/**" location="/js/"/>
                            <mvc:resources mapping="/css/**" location="/css/"/>
                            <mvc:resources mapping="/fonts/**" location="/fonts/"/>
                        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                            <property name="order" value="2"/>
                            <property name="prefix" value="/WEB-INF/views/"/>
                            <property name="suffix" value=".jsp"/>
                        </bean>
                    
                            <bean class="org.thymeleaf.spring3.view.ThymeleafViewResolver">
                                    <property name="templateEngine" ref="templateEngine"/>
                                    <property name="characterEncoding" value="UTF-8"/>
                                    <property name="order" value="1"/>
                            </bean>
                    </beans>
                    


                    1. MaximChistov Автор
                      13.07.2015 11:35

                      В том виде, в котором оно во второй части — не вернет :) А добавлять эти конфигурации в код я не хочу, на мой взгляд нельзя в таких статьях грузить тем, без чего можно обойтись.
                      Да и потом для контроллеров объявленных с аннотацией Controller оно и без этой кофигурации работает) Как раз стоит лишь вернуть строку. Это в 3 части будет


                      1. norguhtar
                        13.07.2015 11:54

                        Для tymleaf насколько помню надо было крутить.


                        1. MaximChistov Автор
                          13.07.2015 12:12

                          Добавьте в controllers такой класс:

                          ShowsViewController.java
                          package habraspring.controllers;
                          
                          import org.springframework.stereotype.Controller;
                          import org.springframework.web.bind.annotation.RequestMapping;
                          import org.springframework.web.bind.annotation.RequestMethod;
                          
                          @Controller
                          @RequestMapping("/showsview")
                          public class ShowsViewController {
                              @RequestMapping(method = RequestMethod.GET)
                              public String showView()
                              {
                                  return "add";
                              }
                          }
                          


                          1. norguhtar
                            13.07.2015 12:21

                            Вы уж определитесь вернет или нет.


                            1. MaximChistov Автор
                              13.07.2015 12:25

                              Попробуйте быть внимательнее. Пятый раз уже одно и то же объясняю))
                              Если контроллер объявлен как

                              @RestController
                              

                              как, например, Users контроллер, он просто так View возвращать не будет.
                              Но если он объявлен как
                              @Controller
                              

                              то если вернуть строку он будет возвращать view с таким названием.


                              1. norguhtar
                                13.07.2015 12:28

                                Эм. Теперь внимаааательно почитайте что я писал в начале. Я писал про Controller просто. И у вас там было

                                return new ModelView("add");
                                


                                Причем тут RestController? А Thymeleaf насколько помню в обычном контроллере через возврат строки не подхватывался.


                                1. MaximChistov Автор
                                  13.07.2015 12:33

                                  Потому что это RestContoller:
                                  github.com/MaxPovver/ForHabrahabr/blob/withcontroller/src/main/java/habraspring/controllers/UsersController.java
                                  Собственно только поэтому я и не возвращал там просто «add»
                                  (шестой раз)

                                  А Thymeleaf насколько помню в обычном контроллере через возврат строки не подхватывался.

                                  В текущей версии — подхватывается.


                                  1. norguhtar
                                    13.07.2015 12:39

                                    Потому что это RestContoller

                                    Тфу ты. Но вообще надо отделять тогда в отдельный контроллер. Опять же во избежание.

                                    В текущей версии — подхватывается.

                                    Значит меньше писанины.


  1. vayho
    13.07.2015 11:57
    +1

    Я напишу несколько замечаний/дополнений которые могут быть полезны юзерам:
    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.


    1. trix
      13.07.2015 22:51

      >Я бы не советовал использовать Thymeleaf, он довольно медленный

      довольно медленный — это сколько в попугаях? что именно медленное?

      >Иначе в определенный момент вы получите кашу из методов-запросов, запросов Query, нативных запросов в SQL или с помощью JPA Criteria API

      собсно, спринг-дата для того и создан, чтобы забыть про criteria api
      никаких проблем в смешении методов-запросов и Query не вижу.
      нативных запросов вообще должен быть крайний минимум, или зачем там вообще хибер появился? по крайней мере, нативных запросов будет не больше, чем без спринг-даты )


      1. norguhtar
        14.07.2015 07:16

        Ниже есть бенчмарк, типа весьма тормоз :)


  1. norguhtar
    13.07.2015 12:21

    2. Я бы не советовал использовать Thymeleaf, он довольно медленный и у него КРАЙНЕ неудобный синтаксис для шаблонизатора.

    Предложите другой вариант, который хорошо интегрируется с Spring MVC быстрый и у которого удобный синтаксис. JSTL не предлагать :)

    Я бы не советовал использовать Spring Data JPA. Ваш код 100% может обойтись без этого модуля, лучше возьмите QueryDSL и напишите всю прослойку Repository самостоятельно. Иначе в определенный момент вы получите кашу из методов-запросов, запросов Query, нативных запросов в SQL или с помощью JPA Criteria API.

    Я посмотрел QueryDSL выглядит не очень. Опять же делать свой репозиторий это возврат к временам до Spring Data. И да с чего у вас каша будет? Вот определен у вас интерфейсы ваших репозиториев, ну размещайте туда все вызовы через аннотации. Ну все же можно. В итоге кода получается меньше и каши меньше.


    1. vayho
      13.07.2015 14:09

      > Предложите другой вариант, который хорошо интегрируется с 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.


      1. norguhtar
        13.07.2015 14:33

        Варианты есть: jtwig.org, www.mitchellbosecke.com/pebble/home, github.com/greenlaw110/Rythm, github.com/jreijn/spring-comparing-template-engines.

        Мы используем Pebble engine.

        Посмотрю. Вот только у Pebble не понятно как сделана интеграция форм в spring.

        1. Короткие методы — не нужны, да первые ощущения от findAll положительные, но со временем понимаешь что тут у тебя метод findAll а вот тут Query, а там еще Specifications. А нужно чтобы все выглядело одинаково(или хочется).

        Если начинается что-то сложнее в него добавить сортировку, переделываем на метод

        2. Query не гибкий метод потому что иногда нужны динамические запросы. По итогу используем Criteria API, потому что Specifications нам явно не хватает.

        Не совсем понял. Не хватает подстановки запросов?

        В результате я смотрю на Spring Data JPA и думаю, а зачем мне весь этот сахар если он покрывается стандартным JPA полностью, а плюсов никаких не дает.

        Вполне дает. Не надо писать свою реализацию репозитория, не требуется писать на каждый DAO свою реализацию, интерфейсов вполне хватает. Весьма часто можно просто использовать его DSL для получения необходимого результата.


        1. vayho
          13.07.2015 15:03

          > Не совсем понял. Не хватает подстановки запросов?

          Не хватает динамики в запросах.
          В одном случае мне нужен такой запрос:
          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 описывать логику выборки данных.


          1. norguhtar
            13.07.2015 15:14

            Обычно такое делают вообще через ORM. Ну тут уже каждый страдает как ему кажется удобным.


          1. MaximChistov Автор
            13.07.2015 15:35

            по умолчанию вторичные сущности грузятся лениво в момент когда были запрошены :)


            1. vayho
              13.07.2015 15:57

              Я понимаю как работает меппинг в Hibernate, только в данном случае мне нужно за один запрос вытащить всех юзеров с их профилями(сразу) а не делать дополнительно кучу LAZY запросов.


              1. MaximChistov Автор
                13.07.2015 16:05

                Ну да, аннотации в таком плане не помощник, ими ленивость можно только в положение вкл/выкл ставить


              1. norguhtar
                14.07.2015 07:17

                Для этого или можно прописать тянуть все сразу ну или вешать хинты. Сильно зависит от ситуации.