📜 ⬆️ ⬇️

Spring MVC / Security, REST, Hibernate, Liquibase run in two lines


Modern build systems allow you to fully automate the process of compiling and running an application from source. On the target machine, only the JDK is needed, everything else, including the assembler itself, will be loaded on the fly. It is necessary only to build the build process correctly and to get two commands to launch, for example, the following: starting the database, executing SQL scripts, compiling Java, Javascript and CSS files, launching the servlet container. This is implemented using Gradle, HSQLDB, Liquibase, Google closure compile and Gretty. More in the article.

Content



Introduction


Having tried in the recent past to deal with the promising collector Gradle, I found an unpleasant feature. They write a lot of places about how wonderful, simple and quickly you can make separate things. But you have to collect the whole picture yourself. I will try to eliminate this deficiency in the materials on Gradle, using the example of a small application. In the application itself, I will try to consider the current Java EE basis.
Before we start writing and launching an application, we’ll define our requirements. They will be small - three pages: one root, accessible to all, the other available only to an authorized user. The last login page, it’s a page for creating a new user. Nothing beyond the complex and not obvious. Typical "Hello, world!". But even such a small example is enough to show the power and enormity of Java EE. With typical development, the number of configuration files needed to run a project will be scary large.
Fortunately, this problem occurs only at the start. And it's not just the choice of Java as the server language. By itself, the server application is not fully. It needs external resources: a database or at least a servlet container. The database must first be prepared, create tables and fill with data. The latter is necessary even in the case of using non-relational databases with the absence of rigid schemas.
Setting the logging policy, working with the database, automating the processing of JavaScript and CSS. There are many things that need to be set up before using the application. We will try to do it.

Used applications, plugins and libraries



The list is large, but for the successful launch and modification of the application, only the first item and access to the Internet is required. Additionally, I recommend to put all the same development environment, but in principle you can do without it, running the assembly through the console.

The first launch of the project


Let's create a new project that is going to be created using Gradle. The structure of the created project.

As you can see, even the new project contains a lot of files. Conventionally, they are divided into two groups. Some must be stored in a version control system (VCS), others are not.
The following files should be stored in VCS.

The following files are part of the project, but should not be stored in the VCS.

Each version system has its own file that describes which folders / files should be stored in the VCS. It must also be stored in the VCS itself, in order to maintain unity with all developers. In the case of git, the contents of .gitignore are as follows.
.gitignore
*.iml .idea/ /out/ /gradle.properties 


To launch a new project, we use the Gretty plugin for Gradle, which can download and deploy a servlet container. Plugin in the file build.gradle is connected.
 plugins { id "org.akhikhl.gretty" version "1.2.4" } 

Using the plugin, downloading, installing and starting the server is reduced to one command.
 gradlew jettyStart 

You can also run directly from Idea by selecting 'jettyStart'. In the list of tasks for gradle. This command will download version 9 of jetty and automatically deploy it. The project will be available at localhost: 8080 / gull / . To stop the server server is used 'jettyStop'.
Do not use jettyRun, as it does not work correctly when launched from Idea (it terminates immediately after launch).
To start the server in debug mode, use 'jettyStartDebug'
More
When launched via the 'jettyStartDebug' command, the server will wait until the debugger is connected to it. To do this in Idea you need to create a new configuration through the menu run-> Edit Configuration


Add a new 'remote' configuration. The default settings do not change.

Now you can select a new configuration and join the server.

Spring MVC


By default, the project can display only one 'root' page. To extend the functionality, we use Spring MVC, which implements the model-view-controller pattern. In our case, the view will be the JSP page, and the controller of the Java class will fill in the data model.

Connecting libraries.
build.gradle.
 dependencies { compile group: 'org.springframework', name: 'spring-webmvc', version: gradle.springVersion } 

settings.gradle
 gradle.ext.springVersion = '4.2.2.RELEASE' 


Connecting to the project library spring-webmvc, implicitly adds the core of Spring, as the library itself depends on it. If desired, the kernel can be added explicitly by specifying the following libraries.

We can say that it is the Spring kernel that collects the disparate classes and files into a single application, through dependency injection .
In modern versions of Spring, it is permissible to specify the entire configuration directly in the code . This approach seems to me too radical, after all, the configuration of connecting to the database looks more appropriate in the form of an xml file, not a code. Therefore, I use the mixed approach part through xml, part through annotations.
A description of how to deploy the application is stored in web.xml. It contains three elements.

web.xml
<? xml version = "1.0" encoding = "UTF-8"?>
<web-app xmlns: xsi = " www.w3.org/2001/XMLSchema-instance " version = "2.4"
xmlns = " java.sun.com/xml/ns/j2ee "
xsi: schemaLocation = " java.sun.com/xml/ns/j2ee java.sun.com/xml/ns/j2ee/web-app_2_4.xsd ">


<servlet-name> dispatcher </ servlet-name>
<servlet-class> org.springframework.web.servlet.DispatcherServlet </ servlet-class>
<load-on-startup> 1 </ load-on-startup>
')

<servlet-mapping>
<servlet-name> dispatcher </ servlet-name>
<url-pattern> / </ url-pattern>
</ servlet-mapping>


<listener-class> org.springframework.web.context.ContextLoaderListener </ listener-class>


</ web-app>


applicationContext.xml - describes the beans common to the entire application.
applicationContext.xml
 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans> 


dispatcher-servlet.xml - contains bins for a specific servlet. Note The default configuration file for a servlet is named the same as a servlet with the addition of '-servlet.xml'. To maintain hierarchy in the servlet name, you can use '/'. For example the servlet ' admin / dispatcher ' corresponds to the file 'src / main / webapp / WEBINF / admin / dispatcher -servlet.xml'

dispatcher-servlet.xml
 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--     jsp  view --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:view-controller path="/" view-name="index"/> <!--  mvc --> <mvc:annotation-driven/> <!--      --> <context:component-scan base-package="com.intetm.web"/> </beans> 


In dispatcher-servlet.xml, a bin is defined that indicates where the JSPs for the view are. The root ("/") page is specified and the line <mvc: annotation-driven /> "is also added, including Spring-mvc annotations. By default, Spring will search for annotation data only in bins that are described in this context, therefore we will specify a link to the package which needs to be checked.
Spring searches all classes marked with Controller annotation, and inside them there are methods with the @RequestMapping annotation. As an input parameter, the method accepts a model that needs to be filled with data. The output parameter is the view name. Annotation parameter value processed address.
 package com.intetm.web.login; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class LoginController { private static final String HELLO_VIEW = "hello"; @RequestMapping(value = "/hello", method = RequestMethod.GET) public String hello(Model model) { model.addAttribute("subject", "world"); return HELLO_VIEW; } } 

Access to model variables inside the view is made through the construction of "$ {parametr_name}". An example of use in hello.jsp.
 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> Hello, ${subject}! </body> </html> 

On the default page, specify the link on the welcome page.
index.jsp
 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <a href="hello">Hello</a> </body> </html> 


The result of adding and editing files will be a two-page application. Run the project and see the result.

Setting server options


Although the default server settings may be suitable when launching a simple application, in the future they will have to be changed. Let's see how you can configure the ports, server context. Add the file to the classpath.

To configure the port, it is enough to specify gretty to use the corresponding port in build.gradle.
 def serverHttpPort = project.properties['serverHttpPort'] ?: gradle.serverHttpPort gretty { httpPort = serverHttpPort } 

and the default port in settings.gradle
 //default config gradle.ext.serverHttpPort = 8080 

Now if the variable 'serverHttpPort' is found in gradle.properties, then it will be used. Otherwise, use the default value from settings.gradle. Since settings.gradle is in git, and gradle.properties is excluded from it, it allows on the one hand to centrally update the default value and configure the value locally without conflicts with git.

For the server context, it is also preferable to have the default file stored in the VCS and a local freely changeable copy. Switching between local and shared copy is implemented by specifying the serverContextFile variable. By default, a copy of the VCS is used. In addition to training, we will do a task in Gradle, creating a local copy.
build.gradle.
 def serverResourcesPath = project.properties['serverResourcesPath'] ?: gradle.serverResourcesPath def serverContextFile = project.properties['serverContextFile'] ?: gradle.serverContextFile gretty { httpPort = serverHttpPort serverConfigFile = serverContextFile } task copyEnvironment(type: Copy) { from 'src/test/resources/environment' into serverResourcesPath } 

Default settings.gradle
 gradle.ext.serverResourcesPath = "dev/resources" gradle.ext.serverContextFile = "src/test/resources/environment/jetty-context.xml" 

Empty configuration file for jetty
src / test / resources / environment / jetty-context.xml
 <?xml version="1.0"?> <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> <Configure id="ExampleServer" class="org.eclipse.jetty.server.Server"> </Configure> 


A copy of the file is created in the dev / resources folder. In the future, the dev folder will also be used to store logs and databases. To exclude random commits, exclude the entire dev folder from VCS.

Similarly, you can configure the server classpath. For example, add the file 'logback.xml' with the log settings.
build.gradle.
 def serverClassPath = [project.properties['serverClassPath'] ?: gradle.serverClassPath] as Set gretty { … classPath = serverClassPath } 

settings.gradle
 gradle.ext.serverClassPath = gradle.serverResourcesPath + "/classpath" 

Logging



The history of logging systems in java is rather confusing and sad . The project uses a more or less relevant bundle of slf4j and logback. For this, two dependencies have been added to build.gradle.
 dependencies { compile group: 'org.slf4j', name: 'slf4j-api', version: gradle.slf4jVersion runtime group: 'ch.qos.logback', name: 'logback-classic', version: gradle.logbackVersion … } 

Used versions in settings.gradle
 gradle.ext.slf4jVersion = '1.7.13' gradle.ext.logbackVersion = '1.1.3' 

For logback, you need a logback.xml file that will describe how to write logs. A typical configuration file contains the following components.

Sample logback.xml file
 <!--suppress XmlUnboundNsPrefix --> <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.padual.com/java/logback.xsd" scan="true" scanPeriod="10 seconds"> <!--     --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <!--    error. Debug, info        . --> <level>ERROR</level> </filter> <encoder> <pattern> <!--   .  ,   ,   (   )  --> %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{128} - %msg%n </pattern> </encoder> </appender> <!--   ,    --> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--      --> <file>dev/logs/error.log</file> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <!--    error. Debug, info        . --> <level>ERROR</level> </filter> <!--   .         --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>dev/logs/old/%d{yyyy-MM-dd}.error.log</fileNamePattern> <maxHistory>7</maxHistory> </rollingPolicy> <encoder> <pattern> <!--   .  ,   ,   (   )  --> %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{128} - %msg%n </pattern> </encoder> </appender> <!--   ,    --> <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--      --> <file>dev/logs/debug.log</file> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>DEBUG</level> </filter> <!--   .         --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>dev/logs/old/%d{yyyy-MM-dd}.debug.log</fileNamePattern> <maxHistory>7</maxHistory> </rollingPolicy> <encoder> <pattern> <!--   .  ,   ,   (   )  --> %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{128} - %msg%n </pattern> </encoder> </appender> <!--   ,     --> <appender name="SQL_FILE" class="ch.qos.logback.core.FileAppender"> <!--      --> <file>dev/logs/sql.log</file> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>dev/logs/sql.lg</level> </filter> <!--    --> <append>false</append> <encoder> <pattern> <!--   .  ,   ,   (   )  --> %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{128} - %msg%n </pattern> </encoder> </appender> <!-- ,     DEBUG  ,       com.intetm    DEBUG_FILE --> <logger name="com.intetm" level="DEBUG"> <appender-ref ref="DEBUG_FILE"/> </logger> <!--   hibernate--> <logger name="org.hibernate.type" level="ALL"> <appender-ref ref="SQL_FILE"/> </logger> <logger name="org.hibernate" level="DEBUG"> <appender-ref ref="SQL_FILE"/> </logger> <!--           ERROR_FILE--> <root level="ERROR"> <appender-ref ref="STDOUT"/> <appender-ref ref="ERROR_FILE"/> </root> </configuration> 



Add a line with logging to the LoginController.
 logger.debug("hello page"); 

Complete file
 package com.intetm.web.login; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class LoginController { private static final Logger logger = LoggerFactory.getLogger(LoginController.class); private static final String HELLO_VIEW = "hello"; @RequestMapping(value = "/hello", method = RequestMethod.GET) public String hello(Model model) { logger.debug("hello page"); model.addAttribute("subject", "world"); return HELLO_VIEW; } } 


Run the project. Make sure that when entering localhost / gull / hello , an entry appears in the dev \ log \ debug.log file.

Database startup



To store information about users, passwords, roles, you will need a database. It is theoretically possible to integrate the database directly into the application itself, but there will be problems with scaling, connecting and an external editor, and so on. Therefore, the database will be external to the application.
Oracle and other 'heavy' databases require pre-installation. This is their fee for scalability and tremendous performance. Such databases are well suited for combat operations, with a load of tens of thousands of users. During the development of such loads is not expected, so we use a small database of HSQL .
HSQL does not require installation, it is launched directly from the jar file or when using a small wrapper and directly from Gradle. Unfortunately, I did not find a standard way to run HSQL from Gradle in server mode, so I wrote a small bicycle and rendered it into a separate file.
database.gradle
 apply plugin: 'java' task startDatabase() { group = 'develop' outputs.upToDateWhen { return !available() } doLast { def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbFile = project.properties['dbFile'] ?: gradle.dbFile def dbName = project.properties['dbName'] ?: gradle.dbName def className = "org.hsqldb.server.Server"; def filePath = "file:${projectDir}/${dbFile};user=${dbUser};password=${dbPassword}"; def process = buildProcess(className, filePath, dbName) wait(process) } } def buildProcess(className, filePath, dbName) { def javaHome = System.getProperty("java.home"); def javaBin = javaHome + File.separator + "bin" + File.separator + "java"; def classpath = project.buildscript.configurations.classpath.asPath; def builder = new ProcessBuilder(javaBin, "-cp", classpath, className, "-database.0", filePath, "-dbname.0", dbName); builder.redirectErrorStream(true) builder.directory(projectDir) def process = builder.start() process } def wait(Process process) { def ready = "From command line, use [Ctrl]+[C] to abort abruptly" def reader = new BufferedReader(new InputStreamReader(process.getInputStream())) def line; while ((line = reader.readLine()) != null) { logger.quiet line if (line.contains(ready)) { break; } } } import groovy.sql.Sql task stopDatabase() { group = 'develop' outputs.upToDateWhen { return available() } doLast { def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbUrl = project.properties['dbUrl'] ?: gradle.dbUrl def dbDrive = project.properties['dbDrive'] ?: gradle.dbDrive ClassLoader loader = Sql.class.classLoader project.buildscript.configurations.classpath.each { File file -> loader.addURL(file.toURI().toURL()) } //noinspection GroovyAssignabilityCheck Sql sql = Sql.newInstance(dbUrl, dbUser, dbPassword, dbDrive) as Sql sql.execute('SHUTDOWN;') sql.close() } } boolean available() { try { int dbPort = project.properties['dbPort'] ?: gradle.dbPort as int String dbHost = project.properties['dbHost'] ?: gradle.dbHost Socket ignored = new Socket(dbHost, dbPort); ignored.close(); return false; } catch (IOException ignored) { return true; } } 



In build.gradle you only need to connect the file and indicate the use of hsql.
 buildscript { repositories { mavenLocal() mavenCentral() jcenter() } dependencies { classpath group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } } … apply from: 'database.gradle' 

settings.gradle
 //lib version gradle.ext.hsqldbVersion = '2.3.2' //default database config gradle.ext.dbName = "xdb" gradle.ext.dbFile = "dev/database/devDB" gradle.ext.dbUser = "SA" gradle.ext.dbPassword = "password" gradle.ext.dbPort = 9001 gradle.ext.dbHost = "localhost" gradle.ext.dbUrl = "jdbc:hsqldb:hsql://${gradle.dbHost}:${gradle.dbPort}/${gradle.dbName}" gradle.ext.dbDrive = "org.hsqldb.jdbc.JDBCDriver" 


The database can be launched via the console or through the Gradle task menu in Idea.
 gradlew startDatabase 

After executing the command, you can connect to the database through any external editor, including Idea. Default user / password "SA" / "password". The address is jdbc: hsqldb: hsql: // localhost: 9001 / xdb
Shutting down the database is similar.
 gradlew stopDatabase 

Creating tables



Before you start using a relational database in your application, you need to create tables, indexes, and others. Non-relational database in the absence of a rigid scheme can be filled with data immediately. But the very filling of the data will have to produce in that and in another case.
Liquibase will be used to streamline the execution of SQL scripts. Liquibase is able to execute scripts in a given order, making sure that the same scripts are not executed twice. Warns of a dangerous situation when changing files with already executed scripts. Supports rollbacks to a given point or period. Using Liquibase greatly reduces the number of errors when working with several databases: combat, test, etc.
We connect liquibase, we describe where to get files, where to spill and create a task.
build.gradle
 plugins { id 'org.liquibase.gradle' version '1.1.1' } def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbUrl = project.properties['dbUrl'] ?: gradle.dbUrl liquibase { activities { //noinspection GroovyAssignabilityCheck main { changeLogFile 'src/sql/main/changelog.xml' url dbUrl username dbUser password dbPassword } } } task updateDbMain(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main' tasks.update.execute() } } 

Complete file
 buildscript { repositories { mavenLocal() mavenCentral() jcenter() } dependencies { classpath group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } } plugins { id "org.akhikhl.gretty" version "1.2.4" id 'org.liquibase.gradle' version '1.1.1' } group 'com.intetm' version '0.1' apply plugin: 'java' apply plugin: 'war' apply from: 'database.gradle' //noinspection GroovyUnusedAssignment sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile group: 'org.springframework', name: 'spring-webmvc', version: gradle.springVersion compile group: 'org.slf4j', name: 'slf4j-api', version: gradle.slf4jVersion runtime group: 'ch.qos.logback', name: 'logback-classic', version: gradle.logbackVersion } def serverHttpPort = project.properties['serverHttpPort'] ?: gradle.serverHttpPort def serverResourcesPath = project.properties['serverResourcesPath'] ?: gradle.serverResourcesPath def serverContextFile = project.properties['serverContextFile'] ?: gradle.serverContextFile def serverClassPath = [project.properties['serverClassPath'] ?: gradle.serverClassPath] as Set def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbUrl = project.properties['dbUrl'] ?: gradle.dbUrl gretty { httpPort = serverHttpPort serverConfigFile = serverContextFile classPath = serverClassPath } task copyEnvironment(type: Copy) { group = 'develop' from 'src/test/resources/environment' into serverResourcesPath } liquibase { activities { //noinspection GroovyAssignabilityCheck main { changeLogFile 'src/sql/main/changelog.xml' url dbUrl username dbUser password dbPassword } } } task updateDbMain(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main' tasks.update.execute() } } 


It remains only to fill in the file changelog.xml.
Following the advice from the article , the following script structure will be used.
 /src /sql /main /changelog.xml /v-1.0 /2015.11.28_01_Create_User_table.sql ... /changelog-v.1.0-cumulative.xml /v-2.0 ... /changelog-v.2.0-cumulative.xml 

Only cumulative scripts from each version are included in the main changelog.xml file.
changelog.xml
 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <include file="src/sql/main/V-1.0/changelog-v.1.0-cumulative.xml"/> </databaseChangeLog> 


The changelog-v.1.0-cumulative.xml file includes all scripts for version 1 of the application.
changelog-v.1.0-cumulative.xml
 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <changeSet id="Version 1 tag" author="Sivodedov Dmitry"> <tagDatabase tag="Version 1"/> </changeSet> <include file="src/sql/main/V-1.0/2015.11.28_01_Create_User_table.sql"/> </databaseChangeLog> 


Specific lists of changes are stored only at the lowest level.
2015.11.28_01_Create_User_table.sql
 --liquibase formatted sql --changeset Sivodedov Dmitry:CREATE_TABLE_Users CREATE TABLE Users ( id BINARY(16) NOT NULL PRIMARY KEY, username VARCHAR_IGNORECASE(50) NOT NULL, password VARCHAR(60) NOT NULL, enabled BOOLEAN NOT NULL ); --rollback drop table Users; --changeset Sivodedov Dmitry:CREATE_TRIGGER_TRIG_BI_DM_USERS splitStatements:false CREATE TRIGGER TRIG_BI_DM_USERS BEFORE INSERT ON Users REFERENCING NEW AS NEW FOR EACH ROW BEGIN ATOMIC IF NEW.id IS NULL THEN -- noinspection SqlResolve SET NEW.id = UUID(); END IF; END; --rollback drop TRIGGER TRIG_BI_DM_USERS on Users; --changeset Sivodedov Dmitry:CREATE_TABLE_Authorities CREATE TABLE Authorities ( id BIGINT IDENTITY NOT NULL PRIMARY KEY, userId BINARY(16) NOT NULL, authority VARCHAR_IGNORECASE(50) NOT NULL, CONSTRAINT fk_authorities_users FOREIGN KEY (userId) REFERENCES users (id) ); --rollback drop table Authorities; --changeset Sivodedov Dmitry:CREATE_INDEX_ix_auth_username CREATE UNIQUE INDEX ix_auth_username ON Authorities (userId, authority); --rollback drop INDEX ix_auth_username on Authorities; 


'updateDbMain', . .




, . , , SQL .
build.gradle
 liquibase { activities { … dev { changeLogFile 'src/sql/dev/changelog.xml' url dbUrl username dbUser password dbPassword } } } task updateDbDev(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main, dev' tasks.update.execute() } } 

 buildscript { repositories { mavenLocal() mavenCentral() jcenter() } dependencies { classpath group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } } plugins { id "org.akhikhl.gretty" version "1.2.4" id 'org.liquibase.gradle' version '1.1.1' } group 'com.intetm' version '0.1' apply plugin: 'java' apply plugin: 'war' apply from: 'database.gradle' //noinspection GroovyUnusedAssignment sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile group: 'org.springframework', name: 'spring-webmvc', version: gradle.springVersion compile group: 'org.slf4j', name: 'slf4j-api', version: gradle.slf4jVersion runtime group: 'ch.qos.logback', name: 'logback-classic', version: gradle.logbackVersion } def serverHttpPort = project.properties['serverHttpPort'] ?: gradle.serverHttpPort def serverResourcesPath = project.properties['serverResourcesPath'] ?: gradle.serverResourcesPath def serverContextFile = project.properties['serverContextFile'] ?: gradle.serverContextFile def serverClassPath = [project.properties['serverClassPath'] ?: gradle.serverClassPath] as Set def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbUrl = project.properties['dbUrl'] ?: gradle.dbUrl gretty { httpPort = serverHttpPort serverConfigFile = serverContextFile classPath = serverClassPath } task copyEnvironment(type: Copy) { group = 'develop' from 'src/test/resources/environment' into serverResourcesPath } liquibase { activities { //noinspection GroovyAssignabilityCheck main { changeLogFile 'src/sql/main/changelog.xml' url dbUrl username dbUser password dbPassword } dev { changeLogFile 'src/sql/dev/changelog.xml' url dbUrl username dbUser password dbPassword } } } task updateDbMain(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main' tasks.update.execute() } } task updateDbDev(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main, dev' tasks.update.execute() } } 



changelog.xml
src\sql\dev\changelog.xml
 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <include file="src/sql/dev/V-1.0/2015.11.28_01_Create_User.sql"/> </databaseChangeLog> 


.
changelog-v.1.0-cumulative.xml
src\sql\dev\V-1.0\changelog-v.1.0-cumulative.xml
 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <include file="src/sql/dev/V-1.0/2015.11.28_01_Create_User.sql"/> </databaseChangeLog> 


.
2015.11.28_01_Create_User.sql
sql\dev\V-1.0\2015.11.28_01_Create_User.sql
 --liquibase formatted sql --changeset Sivodedov Dmitry:Create_User INSERT INTO USERS VALUES ('8a59d9547e5b4d9ca0a30804e8a33a94', 'admin', '$2a$10$GZtUdy1Z7Hpk0lYYG92CQeiW1f2c4e3XgA8wunVTDFyQJ2DAmH.x.', TRUE); INSERT INTO AUTHORITIES VALUES (1, '8a59d9547e5b4d9ca0a30804e8a33a94', 'ROLE_ADMIN'); INSERT INTO AUTHORITIES VALUES (2, '8a59d9547e5b4d9ca0a30804e8a33a94', 'ROLE_USER'); --rollback delete from AUTHORITIES where userId = '8a59d9547e5b4d9ca0a30804e8a33a94'; --rollback delete from USERS where id = '8a59d9547e5b4d9ca0a30804e8a33a94'; 



User Authorization



, . Spring — Spring Security.
Spring Security
build.gradle
 dependencies { runtime group: 'org.springframework', name: 'spring-jdbc', version: gradle.springVersion runtime group: 'org.springframework.security', name: 'spring-security-web', version: gradle.springSecurityVersion runtime group: 'org.springframework.security', name: 'spring-security-config', version: gradle.springSecurityVersion //      ,     gretty. //     ,     classpath . gretty group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } 

 buildscript { repositories { mavenLocal() mavenCentral() jcenter() } dependencies { classpath group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } } plugins { id "org.akhikhl.gretty" version "1.2.4" id 'org.liquibase.gradle' version '1.1.1' } group 'com.intetm' version '0.1' apply plugin: 'java' apply plugin: 'war' apply from: 'database.gradle' //noinspection GroovyUnusedAssignment sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { runtime group: 'org.springframework', name: 'spring-jdbc', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-webmvc', version: gradle.springVersion runtime group: 'org.springframework.security', name: 'spring-security-web', version: gradle.springSecurityVersion runtime group: 'org.springframework.security', name: 'spring-security-config', version: gradle.springSecurityVersion compile group: 'org.slf4j', name: 'slf4j-api', version: gradle.slf4jVersion runtime group: 'ch.qos.logback', name: 'logback-classic', version: gradle.logbackVersion gretty group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } def serverHttpPort = project.properties['serverHttpPort'] ?: gradle.serverHttpPort def serverResourcesPath = project.properties['serverResourcesPath'] ?: gradle.serverResourcesPath def serverContextFile = project.properties['serverContextFile'] ?: gradle.serverContextFile def serverClassPath = [project.properties['serverClassPath'] ?: gradle.serverClassPath] as Set def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbUrl = project.properties['dbUrl'] ?: gradle.dbUrl gretty { httpPort = serverHttpPort serverConfigFile = serverContextFile classPath = serverClassPath } task copyEnvironment(type: Copy) { group = 'develop' from 'src/test/resources/environment' into serverResourcesPath } liquibase { activities { //noinspection GroovyAssignabilityCheck main { changeLogFile 'src/sql/main/changelog.xml' url dbUrl username dbUser password dbPassword } dev { changeLogFile 'src/sql/dev/changelog.xml' url dbUrl username dbUser password dbPassword } } } task updateDbMain(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main' tasks.update.execute() } } task updateDbDev(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main, dev' tasks.update.execute() } } 


settings.gradle
 gradle.ext.springSecurityVersion = '4.0.2.RELEASE' 



spring security authentication-manager. BCrypt. SQL , .
, 'ANONYMOUS'.
hello USER. Attention! , ROLE_USER, 'USER'.
security.xml
 <beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd"> <http> <intercept-url pattern="/hello**" access="hasRole('USER')"/> <form-login default-target-url="/"/> <logout logout-url="/logout" logout-success-url="/"/> <anonymous username="guest" granted-authority="ANONYMOUS"/> <http-basic/> <remember-me/> </http> <authentication-manager> <authentication-provider> <password-encoder ref="encoder"/> <jdbc-user-service data-source-ref="dbDataSource" users-by-username-query="SELECT username, password, enabled FROM Users WHERE username= ?" authorities-by-username-query="SELECT u1.username, u2.authority FROM Users u1, Authorities u2 WHERE u1.id = u2.userId AND u1.UserName = ?"/> </authentication-provider> </authentication-manager> <beans:bean id="encoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"> <beans:constructor-arg name="strength" value="10"/> </beans:bean> </beans:beans> 


, spring .
.
dbContext.xml
 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:jee="http://www.springframework.org/schema/jee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd"> <jee:jndi-lookup id="dbDataSource" jndi-name="jdbc/Database" expected-type="javax.sql.DataSource"/> </beans> 


spring securuty .
applicationContext.xml
 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <import resource="security.xml"/> <import resource="dbContext.xml"/> </beans> 


. , .
web.xml
 <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app.xsd"> <!--Security--> <!--   springSecurityFilterChain    --> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <!--      springSecurityFilterChain    ,    /*--> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--Security--> <!--DispatcherServlet--> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <!--DispatcherServlet--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!--   --> <resource-ref> <!--  --> <description>Datasource</description> <!--     --> <res-ref-name>jdbc/Database</res-ref-name> <!--  --> <res-type>javax.sql.DataSource</res-type> <!--     ,   --> <res-auth>Container</res-auth> </resource-ref> </web-app> 


.
jetty-context.xml
 <?xml version="1.0"?> <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> <Configure id="ExampleServer" class="org.eclipse.jetty.server.Server"> <New id="DS" class="org.eclipse.jetty.plus.jndi.Resource"> <Arg> <Ref refid="wac"/> </Arg> <Arg>jdbc/Database</Arg> <Arg> <New class="org.hsqldb.jdbc.JDBCDataSource"> <Set name="DatabaseName">jdbc:hsqldb:hsql://localhost:9001/xdb</Set> <Set name="User">SA</Set> <Set name="Password">password</Set> </New> </Arg> </New> </Configure> 



localhost:8080/gull/hello , . (admin/password) .



, . . . .
, security.xml
  <form-login login-page="/login" default-target-url="/"/> 

 <beans:beans xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd"> <http> <intercept-url pattern="/hello**" access="hasRole('USER')"/> <form-login login-page="/login" default-target-url="/"/> <logout logout-url="/logout" logout-success-url="/"/> <anonymous username="guest" granted-authority="ANONYMOUS"/> <http-basic/> <remember-me/> </http> <authentication-manager> <authentication-provider> <password-encoder ref="encoder"/> <jdbc-user-service data-source-ref="dbDataSource" users-by-username-query="SELECT username, password, enabled FROM Users WHERE username= ?" authorities-by-username-query="SELECT u1.username, u2.authority FROM Users u1, Authorities u2 WHERE u1.id = u2.userId AND u1.UserName = ?"/> </authentication-provider> </authentication-manager> <beans:bean id="encoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"> <beans:constructor-arg name="strength" value="10"/> </beans:bean> </beans:beans> 



view dispatcher-servlet.xml
  <mvc:view-controller path="/login" view-name="login"/> 

 <?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--     jsp  view --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:view-controller path="/" view-name="index"/> <mvc:view-controller path="/login" view-name="login"/> <!--  mvc --> <mvc:annotation-driven/> <!--      --> <context:component-scan base-package="com.intetm.web"/> </beans> 



.
login.jsp
 <%@ page contentType="text/html" pageEncoding="UTF-8" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!--suppress HtmlFormInputWithoutLabel --> <html> <head> <title>Login Page</title> </head> <body> <div align="center"> <h3>Login with Username and Password</h3> <form:form id='formLogin' action='./login' method='POST'> <table> <tr> <td>username:</td> <td><input type='text' name='username' value='' autofocus></td> </tr> <tr> <td>password:</td> <td><input type='password' name='password'/></td> </tr> <tr> <td><input type='checkbox' name='remember-me'/></td> <td>remember-me</td> </tr> <tr> <td colspan='2'><input name="submit" type="submit" value="submit"/></td> </tr> </table> </form:form> </div> </body> </html> 


hello .
hello.jsp
 <%--suppress ELValidationInJSP --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>Title</title> </head> <body> <!--suppress XmlPathReference --> <c:url value="/logout" var="logoutUrl"/> <!-- csrt support --> <form action="${logoutUrl}" method="post" id="logoutForm"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> </form> <script> function formSubmit() { document.getElementById("logoutForm").submit(); } </script> <c:if test="${pageContext.request.userPrincipal.name != null}"> <h2> Welcome : ${pageContext.request.userPrincipal.name} | <a href="javascript:formSubmit()"> Logout</a> </h2> </c:if> Hello, ${subject}! </body> </html> 


JSP, .
jstl
build.gradle
 dependencies { runtime group: 'org.apache.taglibs', name: 'taglibs-standard-impl', version: gradle.jstlVersion } 

settings.gradle
 gradle.ext.jstlVersion = '1.2.5' 


.

javascript



Almost no one site can do without JavaScript. Execution of the code on the client side allows you to change the page, verify the entered data without reloading, and even create full-fledged analogues of desktop applications that load on the fly. And so that the download is not long, you need to take care of optimization.
JavaScript optimization will be done in two stages. First, we merge the disparate files into one, which will reduce the number of subqueries and have a positive effect on the download speed. After that, from the resulting file, all unnecessary ones are deleted - spaces, comments, the name of variables is shortened, and so on. Of course, optimization is not done manually, but is given to the compiler. I will use the Google Closure Compiler in a wrapper for the Gradle plugin.
Attention! . gooqle. .
gradle.
build.gradle
 plugins { id "com.eriwen.gradle.js" version "1.12.1" } 

.
login.js
 $(function () { $("#tabs").tabs(); }); 

login.jsp. , jquery google.
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css"> <script src="js/login.js"></script> 

 <%@ page contentType="text/html" pageEncoding="UTF-8" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!--suppress HtmlFormInputWithoutLabel --> <html> <head> <title>Login Page</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css"> <!--suppress HtmlUnknownTarget --> <script src="js/login.js"></script> </head> <body> <div align="center"> <div id="tabs"> <ul> <li><a href="#tabs-1">Sign in</a></li> <li><a href="#tabs-2">Create user</a></li> </ul> <div id="tabs-1"> <h3>Login with Username and Password</h3> <form:form id='formLogin' action='./login' method='POST'> <table> <tr> <td>username:</td> <td><input type='text' name='username' value='' autofocus></td> </tr> <tr> <td>password:</td> <td><input type='password' name='password'/></td> </tr> <tr> <td><input type='checkbox' name='remember-me'/></td> <td>remember-me</td> </tr> <tr> <td colspan='2'><input name="submit" type="submit" value="submit"/></td> </tr> </table> </form:form> </div> <div id="tabs-2"> <h3>Create user</h3> <form:form id='formCreate' action='./createUser' method='POST'> <table> <tr> <td>username:</td> <td><input type='text' name='username' value=''></td> </tr> <tr> <td>password:</td> <td><input type='password' name='password'/></td> </tr> <tr> <td colspan='2'><input name="submit" type="submit" value="submit"/></td> </tr> </table> </form:form> </div> </div> </div> </body> </html> 


dispatcher-servlet.xml , .
  <mvc:resources mapping="/js/**" location="/js/"/> 

 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--     jsp  view --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:view-controller path="/" view-name="index"/> <mvc:view-controller path="/login" view-name="login"/> <mvc:resources mapping="/js/**" location="/js/"/> <!--  mvc --> <mvc:annotation-driven/> <!--      --> <context:component-scan base-package="com.intetm.web"/> </beans> 


build.gradle , javascript . Google Closure Compiler sourcemap, . , , . — .
 javascript.source { login { js { srcDir "src/main/js/login" include "*.js" } } } combineJs { source = javascript.source.login.js.files //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/combine/login.js") } minifyJs { source = combineJs //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/min/login.js") //sourceMap = file("${buildDir}/js/min/login.sourcemap.json") closure { warningLevel = 'QUIET' } } def dev = true; task copyJs(type: Copy) { group = 'develop' from(dev ? combineJs : minifyJs) as String into "src/main/webapp/js" } compileJava.dependsOn.add(copyJs) 

 buildscript { repositories { mavenLocal() mavenCentral() jcenter() } dependencies { classpath group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } } plugins { id "org.akhikhl.gretty" version "1.2.4" id 'org.liquibase.gradle' version '1.1.1' id "com.eriwen.gradle.js" version "1.12.1" } group 'com.intetm' version '0.1' apply plugin: 'java' apply plugin: 'war' apply from: 'database.gradle' //noinspection GroovyUnusedAssignment sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { runtime group: 'org.springframework', name: 'spring-jdbc', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-webmvc', version: gradle.springVersion runtime group: 'org.springframework.security', name: 'spring-security-web', version: gradle.springSecurityVersion runtime group: 'org.springframework.security', name: 'spring-security-config', version: gradle.springSecurityVersion compile group: 'org.slf4j', name: 'slf4j-api', version: gradle.slf4jVersion runtime group: 'ch.qos.logback', name: 'logback-classic', version: gradle.logbackVersion runtime group: 'org.apache.taglibs', name: 'taglibs-standard-impl', version: gradle.jstlVersion gretty group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } def serverHttpPort = project.properties['serverHttpPort'] ?: gradle.serverHttpPort def serverResourcesPath = project.properties['serverResourcesPath'] ?: gradle.serverResourcesPath def serverContextFile = project.properties['serverContextFile'] ?: gradle.serverContextFile def serverClassPath = [project.properties['serverClassPath'] ?: gradle.serverClassPath] as Set def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbUrl = project.properties['dbUrl'] ?: gradle.dbUrl gretty { httpPort = serverHttpPort serverConfigFile = serverContextFile classPath = serverClassPath inplaceMode = "hard" } task copyEnvironment(type: Copy) { group = 'develop' from 'src/test/resources/environment' into serverResourcesPath } liquibase { activities { //noinspection GroovyAssignabilityCheck main { changeLogFile 'src/sql/main/changelog.xml' url dbUrl username dbUser password dbPassword } dev { changeLogFile 'src/sql/dev/changelog.xml' url dbUrl username dbUser password dbPassword } } } task updateDbMain(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main' tasks.update.execute() } } task updateDbDev(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main, dev' tasks.update.execute() } } javascript.source { login { js { srcDir "src/main/js/login" include "*.js" } } } combineJs { source = javascript.source.login.js.files //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/combine/login.js") } minifyJs { source = combineJs //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/min/login.js") //sourceMap = file("${buildDir}/js/min/login.sourcemap.json") closure { warningLevel = 'QUIET' } } def dev = true; task copyJs(type: Copy) { group = 'develop' from(dev ? combineJs : minifyJs) as String into "src/main/webapp/js" } compileJava.dependsOn.add(copyJs) 


copyJs, . , Java , .

css



Working with CSS is no different from JavaScript. Files also merge into one compressed. The name of the plugin and functions is different, only the letters CSS instead of JavaScript. Add a plugin and tasks to build.gradle.
 plugins { id "com.eriwen.gradle.css" version "1.11.1" } css.source { login { css { srcDir "src/main/css/login" include "*.css" } } } combineCss { source = css.source.login.css.files //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/css/combine/login.css") } minifyCss { source = combineCss dest = file("${buildDir}/css/min/login.css") yuicompressor { // Optional lineBreakPos = -1 } } task copyCss(type: Copy) { group = 'develop' from(dev ? combineCss : minifyCss) as String into "src/main/webapp/css" } compileJava.dependsOn.add(copyCss) 

Complete file
 buildscript { repositories { mavenLocal() mavenCentral() jcenter() } dependencies { classpath group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } } plugins { id "org.akhikhl.gretty" version "1.2.4" id 'org.liquibase.gradle' version '1.1.1' id "com.eriwen.gradle.js" version "1.12.1" id "com.eriwen.gradle.css" version "1.11.1" } group 'com.intetm' version '0.1' apply plugin: 'java' apply plugin: 'war' apply from: 'database.gradle' //noinspection GroovyUnusedAssignment sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { runtime group: 'org.springframework', name: 'spring-jdbc', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-webmvc', version: gradle.springVersion runtime group: 'org.springframework.security', name: 'spring-security-web', version: gradle.springSecurityVersion runtime group: 'org.springframework.security', name: 'spring-security-config', version: gradle.springSecurityVersion compile group: 'org.slf4j', name: 'slf4j-api', version: gradle.slf4jVersion runtime group: 'ch.qos.logback', name: 'logback-classic', version: gradle.logbackVersion runtime group: 'org.apache.taglibs', name: 'taglibs-standard-impl', version: gradle.jstlVersion gretty group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } def serverHttpPort = project.properties['serverHttpPort'] ?: gradle.serverHttpPort def serverResourcesPath = project.properties['serverResourcesPath'] ?: gradle.serverResourcesPath def serverContextFile = project.properties['serverContextFile'] ?: gradle.serverContextFile def serverClassPath = [project.properties['serverClassPath'] ?: gradle.serverClassPath] as Set def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbUrl = project.properties['dbUrl'] ?: gradle.dbUrl gretty { httpPort = serverHttpPort serverConfigFile = serverContextFile classPath = serverClassPath inplaceMode = "hard" } task copyEnvironment(type: Copy) { group = 'develop' from 'src/test/resources/environment' into serverResourcesPath } liquibase { activities { //noinspection GroovyAssignabilityCheck main { changeLogFile 'src/sql/main/changelog.xml' url dbUrl username dbUser password dbPassword } dev { changeLogFile 'src/sql/dev/changelog.xml' url dbUrl username dbUser password dbPassword } } } task updateDbMain(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main' tasks.update.execute() } } task updateDbDev(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main, dev' tasks.update.execute() } } javascript.source { login { js { srcDir "src/main/js/login" include "*.js" } } } combineJs { source = javascript.source.login.js.files //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/combine/login.js") } minifyJs { source = combineJs //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/min/login.js") //sourceMap = file("${buildDir}/js/min/login.sourcemap.json") closure { warningLevel = 'QUIET' } } css.source { login { css { srcDir "src/main/css/login" include "*.css" } } } combineCss { source = css.source.login.css.files //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/css/combine/login.css") } minifyCss { source = combineCss dest = file("${buildDir}/css/min/login.css") yuicompressor { // Optional lineBreakPos = -1 } } def dev = true; task copyJs(type: Copy) { group = 'develop' from(dev ? combineJs : minifyJs) as String into "src/main/webapp/js" } task copyCss(type: Copy) { group = 'develop' from(dev ? combineCss : minifyCss) as String into "src/main/webapp/css" } compileJava.dependsOn.add(copyJs) compileJava.dependsOn.add(copyCss) 


Create login.css
login.css
 .tab-centered { width: 500px; } .tab-centered .ui-tabs-nav { height: 2.35em; text-align: center; } .tab-centered .ui-tabs-nav li { display: inline-block; float: none; margin: 0; } 


Add CSS to the page.
 <link rel="stylesheet" href="css/login.css"> ... <div id="tabs" class="tab-centered"> 

Complete file
 <%@ page contentType="text/html" pageEncoding="UTF-8" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!--suppress HtmlFormInputWithoutLabel --> <html> <head> <title>Login Page</title> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css"> <!--suppress HtmlUnknownTarget --> <script src="js/login.js"></script> <!--suppress HtmlUnknownTarget --> <link rel="stylesheet" href="css/login.css"> </head> <body> <div align="center"> <div id="tabs" class="tab-centered"> <ul> <li><a href="#tabs-1">Sign in</a></li> <li><a href="#tabs-2">Create user</a></li> </ul> <div id="tabs-1"> <h3>Login with Username and Password</h3> <form:form id='formLogin' action='./login' method='POST'> <table> <tr> <td>username:</td> <td><input type='text' name='username' value='' autofocus></td> </tr> <tr> <td>password:</td> <td><input type='password' name='password'/></td> </tr> <tr> <td><input type='checkbox' name='remember-me'/></td> <td>remember-me</td> </tr> <tr> <td colspan='2'><input name="submit" type="submit" value="submit"/></td> </tr> </table> </form:form> </div> <div id="tabs-2"> <h3>Create user</h3> <form:form id='formCreate' action='./createUser' method='POST'> <table> <tr> <td>username:</td> <td><input type='text' name='username' value=''></td> </tr> <tr> <td>password:</td> <td><input type='password' name='password'/></td> </tr> <tr> <td colspan='2'><input name="submit" type="submit" value="submit"/></td> </tr> </table> </form:form> </div> </div> </div> </body> </html> 


Specify spring-mvc where to get the css files.
  <mvc:resources mapping="/css/**" location="/css/"/> 

dispatcher-servlet.xml
 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--     jsp  view --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:view-controller path="/" view-name="index"/> <mvc:view-controller path="/login" view-name="login"/> <mvc:resources mapping="/js/**" location="/js/"/> <mvc:resources mapping="/css/**" location="/css/"/> <!--  mvc --> <mvc:annotation-driven/> <!--      --> <context:component-scan base-package="com.intetm.web"/> </beans> 


Now when compiling, both Java and JavaScript and CSS files will be compiled simultaneously. The result is available on the localhost: 8080 / gull / login page.
ORM

Before creating the rest service, we will add a layer when working with a database in the form of an ORM. In order not to depend on the specific implementation of ORM, we will use the common JPA interface, which is part of the javaee-api library. The specific implementation (hibernate) connect only at startup time.
build.gradle
  compile group: 'javax', name: 'javaee-api', version: gradle.javaxVersion compile group: 'org.springframework', name: 'spring-orm', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-tx', version: gradle.springVersion runtime group: 'org.hibernate.javax.persistence', name: 'hibernate-jpa-2.1-api', version: gradle.hibernateJpaVersion runtime group: 'org.hibernate', name: 'hibernate-core', version: gradle.hibernateVersion runtime group: 'org.hibernate', name: 'hibernate-entitymanager', version: gradle.hibernateVersion 

Complete file
 buildscript { repositories { mavenLocal() mavenCentral() jcenter() } dependencies { classpath group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } } plugins { id "org.akhikhl.gretty" version "1.2.4" id 'org.liquibase.gradle' version '1.1.1' id "com.eriwen.gradle.js" version "1.12.1" id "com.eriwen.gradle.css" version "1.11.1" } group 'com.intetm' version '0.1' apply plugin: 'java' apply plugin: 'war' apply from: 'database.gradle' //noinspection GroovyUnusedAssignment sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile group: 'javax', name: 'javaee-api', version: gradle.javaxVersion runtime group: 'org.springframework', name: 'spring-jdbc', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-webmvc', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-orm', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-tx', version: gradle.springVersion runtime group: 'org.springframework.security', name: 'spring-security-web', version: gradle.springSecurityVersion runtime group: 'org.springframework.security', name: 'spring-security-config', version: gradle.springSecurityVersion runtime group: 'org.hibernate.javax.persistence', name: 'hibernate-jpa-2.1-api', version: gradle.hibernateJpaVersion runtime group: 'org.hibernate', name: 'hibernate-core', version: gradle.hibernateVersion runtime group: 'org.hibernate', name: 'hibernate-entitymanager', version: gradle.hibernateVersion compile group: 'org.slf4j', name: 'slf4j-api', version: gradle.slf4jVersion runtime group: 'ch.qos.logback', name: 'logback-classic', version: gradle.logbackVersion runtime group: 'org.apache.taglibs', name: 'taglibs-standard-impl', version: gradle.jstlVersion gretty group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } def serverHttpPort = project.properties['serverHttpPort'] ?: gradle.serverHttpPort def serverResourcesPath = project.properties['serverResourcesPath'] ?: gradle.serverResourcesPath def serverContextFile = project.properties['serverContextFile'] ?: gradle.serverContextFile def serverClassPath = [project.properties['serverClassPath'] ?: gradle.serverClassPath] as Set def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbUrl = project.properties['dbUrl'] ?: gradle.dbUrl gretty { httpPort = serverHttpPort serverConfigFile = serverContextFile classPath = serverClassPath inplaceMode = "hard" } task copyEnvironment(type: Copy) { group = 'develop' from 'src/test/resources/environment' into serverResourcesPath } liquibase { activities { //noinspection GroovyAssignabilityCheck main { changeLogFile 'src/sql/main/changelog.xml' url dbUrl username dbUser password dbPassword } dev { changeLogFile 'src/sql/dev/changelog.xml' url dbUrl username dbUser password dbPassword } } } task updateDbMain(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main' tasks.update.execute() } } task updateDbDev(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main, dev' tasks.update.execute() } } javascript.source { login { js { srcDir "src/main/js/login" include "*.js" } } } combineJs { source = javascript.source.login.js.files //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/combine/login.js") } minifyJs { source = combineJs //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/min/login.js") //sourceMap = file("${buildDir}/js/min/login.sourcemap.json") closure { warningLevel = 'QUIET' } } css.source { login { css { srcDir "src/main/css/login" include "*.css" } } } combineCss { source = css.source.login.css.files //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/css/combine/login.css") } minifyCss { source = combineCss dest = file("${buildDir}/css/min/login.css") yuicompressor { // Optional lineBreakPos = -1 } } def dev = true; task copyJs(type: Copy) { group = 'develop' from(dev ? combineJs : minifyJs) as String into "src/main/webapp/js" } task copyCss(type: Copy) { group = 'develop' from(dev ? combineCss : minifyCss) as String into "src/main/webapp/css" } compileJava.dependsOn.add(copyJs) compileJava.dependsOn.add(copyCss) 


settings.gradle.
 gradle.ext.javaxVersion = '7.0' gradle.ext.hibernateVersion = '5.0.2.Final' gradle.ext.hibernateJpaVersion = '1.0.0.Final' 

Complete file
 rootProject.name = 'gull' //lib version gradle.ext.springVersion = '4.2.2.RELEASE' gradle.ext.springSecurityVersion = '4.0.2.RELEASE' gradle.ext.javaxVersion = '7.0' gradle.ext.hibernateVersion = '5.0.2.Final' gradle.ext.hibernateJpaVersion = '1.0.0.Final' gradle.ext.slf4jVersion = '1.7.13' gradle.ext.logbackVersion = '1.1.3' gradle.ext.hsqldbVersion = '2.3.2' gradle.ext.jstlVersion = '1.2.5' //default server config gradle.ext.serverHttpPort = 8080 gradle.ext.serverResourcesPath = "dev/resources" gradle.ext.serverContextFile = "src/test/resources/environment/jetty-context.xml" gradle.ext.serverClassPath = "src/test/resources/environment/classpath" //default database config gradle.ext.dbName = "xdb" gradle.ext.dbFile = "dev/database/devDB" gradle.ext.dbUser = "SA" gradle.ext.dbPassword = "password" gradle.ext.dbPort = 9001 gradle.ext.dbHost = "localhost" gradle.ext.dbUrl = "jdbc:hsqldb:hsql://${gradle.dbHost}:${gradle.dbPort}/${gradle.dbName}" gradle.ext.dbDrive = "org.hsqldb.jdbc.JDBCDriver" 


For JPA to work, you need to create two beans.

dbContext.xml
 <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceXmlLocation" value="classpath:persistence.xml"/> <property name="persistenceUnitName" value="defaultUnit"/> <property name="dataSource" ref="dbDataSource"/> <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/> <property name="jpaDialect" ref="jpaDialect"/> </bean> <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/> <bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <context:component-scan base-package="com.intetm.db.dao"/> 

Complete file
 <?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:jee="http://www.springframework.org/schema/jee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <jee:jndi-lookup id="dbDataSource" jndi-name="jdbc/Database" expected-type="javax.sql.DataSource"/> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceXmlLocation" value="classpath:persistence.xml"/> <property name="persistenceUnitName" value="defaultUnit"/> <property name="dataSource" ref="dbDataSource"/> <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/> <property name="jpaDialect" ref="jpaDialect"/> </bean> <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/> <bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <context:component-scan base-package="com.intetm.db.dao"/> </beans> 


Already separately for hibernate, you need to create a file with the settings - persistence.xml. It contains a logging policy, a database type, a database structure update policy, and other server-specific data. We take out the file from the project and only we connect in server class class.
 <?xml version="1.0" encoding="UTF-8"?> <persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence.xsd" version="2.1"> <persistence-unit name="defaultUnit" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <jta-data-source>jdbc/Database</jta-data-source> <class>com.intetm.db.entity.User</class> <!-- Hibernate properties --> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/> <property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.ImprovedNamingStrategy"/> <property name="hibernate.connection.charSet" value="UTF-8"/> <property name="hibernate.validator.apply_to_ddl" value="false"/> <property name="hibernate.validator.autoregister_listeners" value="false"/> <property name="hibernate.show_sql" value="true"/> <property name="hibernate.hbm2ddl.auto" value="validate"/> </properties> </persistence-unit> </persistence> 


, «hibernate.hbm2ddl.auto» «update» «validate», hibernate . , sql ,
) hibernate , .
) .
.

DAO. - .
User.java- . Entity . Table. The class must have a constructor without parameters.
 @Entity @Table(name = User.TABLE) public class User { … } 

Displayed on the table columns, class fields are annotated with @Column with the column name. For these fields, standard get and set methods must exist.
 @Column(name = COLUMN_USER_NAME) private String userName; 

User.java
 package com.intetm.db.entity; import javax.persistence.*; import java.util.ArrayList; import java.util.List; import java.util.UUID; @Entity @Table(name = User.TABLE) public class User { public static final String TABLE = "Users"; public static final String COLUMN_ID = "id"; public static final String COLUMN_USER_NAME = "userName"; public static final String COLUMN_PASSWORD = "password"; public static final String COLUMN_ENABLED = "enabled"; @Id @Column(name = COLUMN_ID, columnDefinition = "BINARY(16)") private UUID id; @Column(name = COLUMN_USER_NAME) private String userName; @Column(name = COLUMN_PASSWORD) private String password; @Column(name = COLUMN_ENABLED) private boolean enabled; @ElementCollection(targetClass = Authority.class) @Enumerated(EnumType.STRING) @CollectionTable(name = Authority.TABLE, joinColumns = @JoinColumn(name = Authority.COLUMN_USERID, referencedColumnName = COLUMN_ID)) @Column(name = Authority.COLUMN_AUTHORITY) private List<Authority> authorities; public User() { } public User(String userName, String password, Authority authority) { this.userName = userName; this.password = password; this.enabled = true; this.authorities = new ArrayList<>(); this.authorities.add(authority); } public UUID getId() { return id; } public void setId(UUID id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public List<Authority> getAuthorities() { return authorities; } public void setAuthorities(List<Authority> authorities) { this.authorities = authorities; } } 



Authority.java - Simple enum, without annotations. All annotations pointing to a link to a table are listed in User.java. In this class made only the name of the tables and columns.
Authority.java
 package com.intetm.db.entity; public enum Authority { ROLE_ADMIN, ROLE_USER, ROLE_ANONYMOUS; public static final String TABLE = "authorities"; public static final String COLUMN_USERID = "userid"; public static final String COLUMN_AUTHORITY = "authority"; } 



AbstractDao is a generic ancestor for all dao. Contains two annotations @PersistenceContext and @PersistenceUnit, by which Spring will determine where to substitute the entityManager and entityManagerFactory. There are also general methods for loading, saving and searching objects in the database.
  @PersistenceContext private EntityManager entityManager; @PersistenceUnit private EntityManagerFactory entityManagerFactory; 


AbstractDao.java
 package com.intetm.db.dao; import javax.persistence.*; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Path; import javax.persistence.criteria.Root; import java.util.List; import java.util.Map; public abstract class AbstractDao<Entity, ID> { private final Class entryClass; @PersistenceContext private EntityManager entityManager; @PersistenceUnit private EntityManagerFactory entityManagerFactory; public AbstractDao(Class entryClass) { this.entryClass = entryClass; } public void persist(Entity entity) { entityManager.persist(entity); } public void merge(Entity entity) { entityManager.merge(entity); } public void delete(Entity entity) { entityManager.remove(entity); } @SuppressWarnings("unchecked") public CriteriaQuery<Entity> createCriteriaQuery() { return this.getCriteriaBuilder().createQuery(entryClass); } @SuppressWarnings("unchecked") public Entity find(ID id) { return (Entity) entityManager.find(entryClass, id); } public List<Entity> find(CriteriaQuery<Entity> criteriaQuery) { TypedQuery<Entity> query = entityManager.createQuery(criteriaQuery); return query.getResultList(); } public List<Entity> find(Object... keysAndValues) { CriteriaBuilder criteriaBuilder = this.getCriteriaBuilder(); CriteriaQuery<Entity> criteriaQuery = this.createCriteriaQuery(); Root root = criteriaQuery.from(entryClass); fillQuery(criteriaQuery, keysAndValues, root, criteriaBuilder); return find(criteriaQuery); } public List<Entity> find(Map<String, Object> parameters) { Object[] array = toArray(parameters); return find(array); } @SuppressWarnings("unchecked") public long count(Object... keysAndValues) { CriteriaBuilder criteriaBuilder = this.getCriteriaBuilder(); CriteriaQuery<Long> criteriaQuery = criteriaBuilder.createQuery(Long.class); Root<Entity> root = criteriaQuery.from(entryClass); criteriaQuery.select(criteriaBuilder.count(root)); fillQuery(criteriaQuery, keysAndValues, root, criteriaBuilder); return getEntityManager().createQuery(criteriaQuery).getSingleResult(); } public long count(Map<String, Object> parameters) { Object[] array = toArray(parameters); return count(array); } private void fillQuery(CriteriaQuery criteriaQuery, Object[] keysAndValues, Root root, CriteriaBuilder criteriaBuilder) { if (keysAndValues.length % 2 != 0) { throw new IllegalArgumentException("Expected even count argument, receive odd"); } for (int i = 0; i < keysAndValues.length; i += 2) { Path parameterPath = root.get((String) keysAndValues[i]); Object parameterValue = keysAndValues[i + 1]; criteriaQuery.where(criteriaBuilder.equal(parameterPath, parameterValue)); } } private Object[] toArray(Map<String, Object> parameters) { Object[] array = new Object[parameters.size() * 2]; int i = 0; for (Map.Entry<String, Object> parameter : parameters.entrySet()) { array[i] = parameter.getKey(); i++; array[i] = parameter.getValue(); i++; } return array; } public List<Entity> selectAll() { CriteriaQuery<Entity> criteriaQuery = createCriteriaQuery(); criteriaQuery.from(entryClass); return find(criteriaQuery); } public EntityManager getEntityManager() { return entityManager; } public void setEntityManager(EntityManager entityManager) { this.entityManager = entityManager; } public Object getEntityManagerFactory() { return entityManagerFactory; } public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) { this.entityManagerFactory = entityManagerFactory; } public CriteriaBuilder getCriteriaBuilder() { return this.entityManager.getCriteriaBuilder(); } } 


UserDao — Dao User. @ Repository, Spring . Spring @PersistenceContext @PersistenceUnit, AbstractDao .
 @Repository("userDao") public class UserDao extends AbstractDao<User, UUID> 

UserDao.java
 package com.intetm.db.dao; import com.intetm.db.entity.User; import org.springframework.stereotype.Repository; import java.util.UUID; @Repository("userDao") public class UserDao extends AbstractDao<User, UUID> { public UserDao() { super(User.class); } @Override public void persist(User user) { if (user.getId() == null) { user.setId(UUID.randomUUID()); } super.persist(user); } public boolean isUserExsist(String userName) { return count(User.COLUMN_USER_NAME, userName) != 0; } } 




REST



Rest service is a point of entry and exit into the application, with all the ensuing consequences. The input data stream is not controlled by the application, and may contain obviously incorrect data. This imposes additional error handling requirements on the service. In case of an error during the processing of the request, it should not uncontrollably change the database and give the client stacktrace. The correct behavior at an error is a rollback of all applied changes, error logging and return of the correct message to the client.
We will try to implement this behavior. First, create a service that creates a user. First, it checks to see if the user exists. If yes, then throws an error. In another case, hashes the password and saves the user to the database.
LoginService
 package com.intetm.service.login; import com.intetm.db.dao.UserDao; import com.intetm.db.entity.Authority; import com.intetm.db.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; public class LoginService { @Autowired private UserDao userDao; @Autowired private PasswordEncoder encoder; @Transactional public User createUser(String userName, String password, Authority authority) throws UserExistsException { if (userDao.isUserExsist(userName)) { throw new UserExistsException(userName); } String hash = encoder.encode(password); User user = new User(userName, hash, authority); userDao.persist(user); return user; } public UserDao getUserDao() { return userDao; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } public PasswordEncoder getEncoder() { return encoder; } public void setEncoder(PasswordEncoder encoder) { this.encoder = encoder; } } 


The exception thrown by it.
UserExistsException
 package com.intetm.service.login; public class UserExistsException extends Exception { public UserExistsException(String userName) { super("User " + userName + " already exists!"); } } 


LoginController . (, ) . Stacktrace , , .
  @RequestMapping(value = "/createUser", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST) @ResponseBody public UserDetails createUser(@RequestParam String username, @RequestParam String password) throws ServiceException { try { User user = loginService.createUser(username, password, ROLE_USER); return new UserDetails(user); } catch (UserExistsException exception) { throw new ServiceException(exception.getMessage()); } } 

LoginController
 package com.intetm.web.login; import com.intetm.db.entity.User; import com.intetm.service.login.LoginService; import com.intetm.service.login.UserExistsException; import com.intetm.web.exception.ServiceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import static com.intetm.db.entity.Authority.ROLE_USER; @Controller public class LoginController { private static final Logger logger = LoggerFactory.getLogger(LoginController.class); private static final String HELLO_VIEW = "hello"; @Autowired private LoginService loginService; @RequestMapping(value = "/hello", method = RequestMethod.GET) public String hello(Model model) { logger.debug("hello page"); model.addAttribute("subject", "world"); return HELLO_VIEW; } @RequestMapping(value = "/createUser", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST) @ResponseBody public UserDetails createUser(@RequestParam String username, @RequestParam String password) throws ServiceException { try { User user = loginService.createUser(username, password, ROLE_USER); return new UserDetails(user); } catch (UserExistsException exception) { throw new ServiceException(exception.getMessage()); } } } 


, json.
UserDetails
 package com.intetm.web.login; import com.intetm.db.entity.Authority; import com.intetm.db.entity.User; import java.util.List; class UserDetails { private String userName; private List<Authority> authorities; public UserDetails(User user) { this.userName = user.getUserName(); this.authorities = user.getAuthorities(); } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public List<Authority> getAuthorities() { return authorities; } public void setAuthorities(List<Authority> authorities) { this.authorities = authorities; } } 


. . @ControllerAdvice
.If errors occur during the processing of the request, Spring will look for methods in the original controller and helper with the ExceptionHandler annotation. If the error class matches, this method will be called and the result of the method will be returned to the client. This allows you to return a meaningful result even in case of errors and correctly log.
ExceptionController
 package com.intetm.web.exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @ControllerAdvice public class ExceptionController extends ResponseEntityExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(ExceptionController.class); @ExceptionHandler(ServiceException.class) @ResponseBody @ResponseStatus(code = HttpStatus.BAD_REQUEST) public String handleServiceException(ServiceException ex) { if (ex.isNeedLogging()) { logger.error(ex.getMessage(), ex); } return ex.getMessage(); } @ExceptionHandler(RuntimeException.class) @ResponseBody @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) public String handleException(RuntimeException ex) { logger.error(ex.getMessage(), ex); return ex.getMessage(); } } 


The exception thrown by the controller. By default, it is considered that it is not necessary to log it, since this is a data error.
ServiceException
 package com.intetm.web.exception; public class ServiceException extends Exception { private boolean needLogging = false; public ServiceException() { super(); } public ServiceException(String message) { super(message); } public ServiceException(boolean needLogging) { super(); this.needLogging = needLogging; } public ServiceException(String message, boolean needLogging) { super(message); this.needLogging = needLogging; } public boolean isNeedLogging() { return needLogging; } public void setNeedLogging(boolean needLogging) { this.needLogging = needLogging; } } 


In javascript add the send code.
login.js
 $(function () { $("#tabs").tabs(); }); $(document).ready(function () { var frm = $("#formCreate") frm.submit(function (event) { event.preventDefault(); $.ajax({ type: frm.attr('method'), url: frm.attr('action'), data: frm.serialize(), success: function (data) { alert('Username:' + data.userName + "\nrole:" + data.authorities[0]); }, error: function (xhr, str) { alert('User exist!'); } }); return false; }); }); 


It remains to make a few little things. Add a bean to convert Java objects to json.
 <bean id="jacksonMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="messageConverters"> <list> <ref bean="jacksonMessageConverter"/> </list> </property> </bean> 

Library connection
build.gradle java json.
  compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: gradle.jacksonVersion compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: gradle.jacksonVersion 

 buildscript { repositories { mavenLocal() mavenCentral() jcenter() } dependencies { classpath group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } } plugins { id "org.akhikhl.gretty" version "1.2.4" id 'org.liquibase.gradle' version '1.1.1' id "com.eriwen.gradle.js" version "1.12.1" id "com.eriwen.gradle.css" version "1.11.1" } group 'com.intetm' version '0.1' apply plugin: 'java' apply plugin: 'war' apply from: 'database.gradle' //noinspection GroovyUnusedAssignment sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile group: 'javax', name: 'javaee-api', version: gradle.javaxVersion runtime group: 'org.springframework', name: 'spring-jdbc', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-webmvc', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-orm', version: gradle.springVersion compile group: 'org.springframework', name: 'spring-tx', version: gradle.springVersion compile group: 'org.springframework.security', name: 'spring-security-web', version: gradle.springSecurityVersion runtime group: 'org.springframework.security', name: 'spring-security-config', version: gradle.springSecurityVersion runtime group: 'org.hibernate.javax.persistence', name: 'hibernate-jpa-2.1-api', version: gradle.hibernateJpaVersion runtime group: 'org.hibernate', name: 'hibernate-core', version: gradle.hibernateVersion runtime group: 'org.hibernate', name: 'hibernate-entitymanager', version: gradle.hibernateVersion compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: gradle.jacksonVersion compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: gradle.jacksonVersion compile group: 'org.slf4j', name: 'slf4j-api', version: gradle.slf4jVersion runtime group: 'ch.qos.logback', name: 'logback-classic', version: gradle.logbackVersion runtime group: 'org.apache.taglibs', name: 'taglibs-standard-impl', version: gradle.jstlVersion gretty group: 'org.hsqldb', name: 'hsqldb', version: gradle.hsqldbVersion } def serverHttpPort = project.properties['serverHttpPort'] ?: gradle.serverHttpPort def serverResourcesPath = project.properties['serverResourcesPath'] ?: gradle.serverResourcesPath def serverContextFile = project.properties['serverContextFile'] ?: gradle.serverContextFile def serverClassPath = [project.properties['serverClassPath'] ?: gradle.serverClassPath] as Set def dbUser = project.properties['dbUser'] ?: gradle.dbUser def dbPassword = project.properties['dbPassword'] ?: gradle.dbPassword def dbUrl = project.properties['dbUrl'] ?: gradle.dbUrl gretty { httpPort = serverHttpPort serverConfigFile = serverContextFile classPath = serverClassPath inplaceMode = "hard" } task copyEnvironment(type: Copy) { group = 'develop' from 'src/test/resources/environment' into serverResourcesPath } liquibase { activities { //noinspection GroovyAssignabilityCheck main { changeLogFile 'src/sql/main/changelog.xml' url dbUrl username dbUser password dbPassword } dev { changeLogFile 'src/sql/dev/changelog.xml' url dbUrl username dbUser password dbPassword } } } task updateDbMain(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main' tasks.update.execute() } } task updateDbDev(dependsOn: startDatabase) { group = 'develop' doLast { liquibase.runList = 'main, dev' tasks.update.execute() } } javascript.source { login { js { srcDir "src/main/js/login" include "*.js" } } } combineJs { source = javascript.source.login.js.files //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/combine/login.js") } minifyJs { source = combineJs //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/js/min/login.js") //sourceMap = file("${buildDir}/js/min/login.sourcemap.json") closure { warningLevel = 'QUIET' } } css.source { login { css { srcDir "src/main/css/login" include "*.css" } } } combineCss { source = css.source.login.css.files //noinspection GrReassignedInClosureLocalVar dest = file("${buildDir}/css/combine/login.css") } minifyCss { source = combineCss dest = file("${buildDir}/css/min/login.css") yuicompressor { // Optional lineBreakPos = -1 } } def dev = true; task copyJs(type: Copy) { group = 'develop' from(dev ? combineJs : minifyJs) as String into "src/main/webapp/js" } task copyCss(type: Copy) { group = 'develop' from(dev ? combineCss : minifyCss) as String into "src/main/webapp/css" } compileJava.dependsOn.add(copyJs) compileJava.dependsOn.add(copyCss) 



settings.gradle
 gradle.ext.jacksonVersion = '2.3.0' 

 rootProject.name = 'gull' //lib version gradle.ext.springVersion = '4.2.2.RELEASE' gradle.ext.springSecurityVersion = '4.0.2.RELEASE' gradle.ext.javaxVersion = '7.0' gradle.ext.hibernateVersion = '5.0.2.Final' gradle.ext.hibernateJpaVersion = '1.0.0.Final' gradle.ext.slf4jVersion = '1.7.13' gradle.ext.logbackVersion = '1.1.3' gradle.ext.hsqldbVersion = '2.3.2' gradle.ext.jacksonVersion = '2.3.0' gradle.ext.jstlVersion = '1.2.5' //default server config gradle.ext.serverHttpPort = 8080 gradle.ext.serverResourcesPath = "dev/resources" gradle.ext.serverContextFile = "src/test/resources/environment/jetty-context.xml" gradle.ext.serverClassPath = "src/test/resources/environment/classpath" //default database config gradle.ext.dbName = "xdb" gradle.ext.dbFile = "dev/database/devDB" gradle.ext.dbUser = "SA" gradle.ext.dbPassword = "password" gradle.ext.dbPort = 9001 gradle.ext.dbHost = "localhost" gradle.ext.dbUrl = "jdbc:hsqldb:hsql://${gradle.dbHost}:${gradle.dbPort}/${gradle.dbName}" gradle.ext.dbDrive = "org.hsqldb.jdbc.JDBCDriver" 




Enable transaction annotation.
  <tx:annotation-driven transaction-manager="transactionManager"/> 

dispatcher-servlet.xml
 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!--     jsp  view --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:view-controller path="/" view-name="index"/> <mvc:view-controller path="/login" view-name="login"/> <mvc:resources mapping="/js/**" location="/js/"/> <mvc:resources mapping="/css/**" location="/css/"/> <!--  mvc --> <mvc:annotation-driven/> <!--      --> <context:component-scan base-package="com.intetm.web"/> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="loginService" class="com.intetm.service.login.LoginService"/> <bean id="jacksonMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="messageConverters"> <list> <ref bean="jacksonMessageConverter"/> </list> </property> </bean> </beans> 



You can see that a lot of classes have been created. This is an attempt to divide the duties and responsibilities of the classes. This separation is not perfect, and only an example is given. The implied contract of classes is as follows.
1) Dao - relatively low-level work with the database. Saving, loading, updating data. At runtime, it can throw out Runtime Exception, completely interrupting the processing of the request.
2) Service - the main logic of work. Public methods that are entry points are marked as transactional. During the processing of a request, Dao methods can be invoked repeatedly, but calls occur within a single transaction. A single transaction for the entire processing of the request, will easily roll back all the changes made when an error occurs.
, . . transactional Spring .
 public void someMethod(){ transaction.open(); try{ subject.someMethod(); } catch(Exception exception){ transaction.setRollbackOnly(true); } finally { transaction.close(); } } 

, . , , , . .
Upon completion of the execution of the method marked with the annotation, the transaction is automatically closed and the changes cannot be rolled back. Therefore, the Service methods should open and close a transaction. In some teaching examples, transactions open at the Dao level, which is fundamentally wrong. If inside the Service method you make two consecutive Dao calls, then in case of an error in the second call, the result of the first one will remain in the database! After all, by the time of the second call, the first transaction has already been successfully closed!
The second conclusion is more trivial - since a proxy is used to open a transaction, the call must come from another bean. The proxy is embedded in calls between different bins and does not have the ability to intercept method calls within a class.
, Service . , . Dao , .
, Service . , . , . , -. , .
Attention!By default, rollback is performed only in cases of Runtime Exception. In order for the changes to roll back and for checked exceptions, they must be additionally specified using the rollbackFor parameter. Code example:
  @Transactional(rollbackFor = Exception.class) public void someMethod() throws Exception { ... } 

Then the rollback changes will be made with any exception.
3) Controller - Accepts the request. The minimum processes it and sends it to the Service. In the case of receiving a successful response from the service - forms the answer. The checked errors repackages, if necessary, into a suitable container, the rest is being forwarded to the next level. Although his role seems insignificant, you should not try to merge him with the Service. At the initial stage, the entry point will be one, but then their number can grow. For example, add mobile clients with their own API.
4) Controller advice - an additional class to the controller, helps to work with errors. It intercepts all Runtime Exception, necessarily makes an entry in the log and gives the client only the minimum necessary information. For checked exceptions, the situation is different. They, too, are all intercepted, but only errors flagged with a special flag get into the log. This difference is due to the nature of the error. The former are indeed application errors, and must be logged. Controlled exceptions only reflect the incorrectness of the data entered by the user and do not require mention in the log.

Conclusion

- 'Hello, world!', . , . .
github . .
 gradlew updateDbDev gradlew jettyStart 

JDK. . , . , , VCS.

Source: https://habr.com/ru/post/271719/


All Articles