📜 ⬆️ ⬇️

Change Java to Scala. Basic application

Hello, Habr.

Summer is in the courtyard, a vacation is coming, and there is some free time to share experiences, some experience in writing Web applications on the Java platform. As a primary language, I will use Scala. It will be like a small guide, as a person with Java experience will gradually begin to use Scala and not abandon his existing developments.

This is the first part of a series of articles in which we will focus on the basic structure of the application. Focuses on people who know Java, who worked with Spring, Hibernate, JPA, JSP and other 3-4 letter letters. I will try to tell you how quickly and painlessly you can start using Scala in your projects and design your new application differently. All this will be around a project that must fulfill a number of requirements:
1. The application is completely closed, we work only after authorization
2. Availability of a convenient API (we will forget REST (it is already a story) and write something like Google AdWords API, with our SQL like questionnaire)
3. Ability to run on the application server without it
4. i18n
5. Database migration
6. Development environment should be deployed via Vagrant
7. And, on trivia, logging, deployment ...
')
All this needs to be done in such a way that it is very easy to maintain and develop our application, so that a situation does not arise when the programmer assesses a 2-day period when adding a new directory. If I am interested in you, please under the cat.



To start


You should familiarize yourself with the Scala syntax, for example, scrolling through Horstman's book “Scala for the Impatient” . To roughly imagine how the language is arranged and to know what is in it. I advise you not to go straight into the jungle, start with a simple one and remember where you have seen some complex and interesting constructions. After a while, return to them and see how they are implemented, try to do these things. The language is large and immediately use all the features can be problematic.

What will we use


For Scala there are a lot of sharpened things, for example, it is worth looking at the Play Framework , SBT , Slick , Lift . But we will start with the things that we have already worked with. We will do assembly through Maven. Take Spring, Spring MVC, Spring Security as a basis. For the database, take Squeryl (I do not like Hibernate because of its heavy weight, specific features, and the always problematic Lazy). We will have a front completely on Angular, for styles there will be SASS, instead of JS we will take CoffeeScript (I will show how to use it, but you can just as well refuse Coffee). Well and, by itself, we will write tests, both integration, and modular, on ScalaTest . We will omit the testing of the front, since this is a volumetric conversation with its own characteristics. The API will be interesting. It will have the concept of a service, the service will have methods, and we will also support SQL like query queries. For example:
select id, name, bank from Organization where bank.id = :id // => [{id: 1, name: 'name', bank: {id: 1, name: 'bankname', node: 'Note'}}] select name, bank.id, bank.name from Organization order by bank.name // => [{name: 'name', bank: {id: 1, name: 'bankname'}}] 


To business



Structure and dependencies

First of all, create a Maven project and immediately connect the plugin to compile Scala.
pom.xml
  <properties> <scala-version>2.10.4</scala-version> </properties> <dependencies> <dependency> <groupId>org.scala-lang</groupId> <artifactId>scala-library</artifactId> <version>${scala-version}</version> </dependency> </dependencies> <build> <pluginManagement> <plugins> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <version>3.1.6</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> </plugin> </plugins> </pluginManagement> <plugins> <plugin> <groupId>net.alchim31.maven</groupId> <artifactId>scala-maven-plugin</artifactId> <executions> <execution> <id>scala-compile-first</id> <phase>process-resources</phase> <goals> <goal>add-source</goal> <goal>compile</goal> </goals> </execution> <execution> <id>scala-test-compile</id> <phase>process-test-resources</phase> <goals> <goal>testCompile</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <executions> <execution> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build> 


All our sources will be in the src / main / scala directory, you can also write some things in Java, putting them into src / main / java . Actually Scala classes can be used in Java classes and vice versa, if such an opportunity is needed. We also need Spring, Spring MVC, Spring Security, Spring OAuth, I believe that connecting all this will not be difficult, so I will not describe. From the nuances we will need more Jetty (in development, we will run our application through it). Another Scala Config, ScalaTest. In order for tests to run through Maven, you need to turn off the Maven Surefire Plugin and use the Scalatest Maven Plugin
pom.xml
  <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.7</version> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <plugin> <groupId>org.scalatest</groupId> <artifactId>scalatest-maven-plugin</artifactId> <version>1.0</version> <configuration> <reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory> <junitxml>.</junitxml> <filereports>WDF TestSuite.txt</filereports> </configuration> <executions> <execution> <id>test</id> <goals> <goal>test</goal> </goals> </execution> </executions> </plugin> 


In order not to write logger initialization in each class, we will connect the library, which will provide us with a trait LazyLogging .
 <dependency> <groupId>com.typesafe.scala-logging</groupId> <artifactId>scala-logging-slf4j_2.10</artifactId> <version>2.1.2</version> </dependency> 


DB Migration

Now it's time to think about our database. For migration we will use Liquibase . To begin with, we will create a file in which links to the entire changeset will be described.
resources / changelog / db.changelog-master.xml
 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <include file="classpath:changelog/db.changelog-0.1.xml"/> </databaseChangeLog> 


And we describe our first changeset, in which there will be all tables for authorization and OAuth
db.changelog-0.1.xml
 <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <changeSet id="0.1-auth" author="andy.sumskoy@gmail.com"> <createTable tableName="users"> <column name="user_id" type="int" autoIncrement="true"> <constraints primaryKey="true" nullable="false"/> </column> <column name="username" type="varchar(255)"> <constraints unique="true" nullable="false"/> </column> <column name="password" type="varchar(255)"> <constraints nullable="false"/> </column> <column name="enabled" type="boolean" defaultValueBoolean="true"> <constraints nullable="false"/> </column> </createTable> <createTable tableName="authority"> <column name="authority_id" type="int" autoIncrement="true"> <constraints primaryKey="true" nullable="false"/> </column> <column name="name" type="varchar(255)"> <constraints unique="true" nullable="false"/> </column> </createTable> <createTable tableName="user_authorities"> <column name="user_id" type="int"> <constraints foreignKeyName="fk_user_authorities_users" referencedTableName="users" referencedColumnNames="user_id"/> </column> <column name="authority_id" type="int"> <constraints foreignKeyName="fk_user_authorities_authority" referencedTableName="authority" referencedColumnNames="authority_id"/> </column> </createTable> <addPrimaryKey columnNames="user_id, authority_id" constraintName="pk_user_authorities" tableName="user_authorities"/> <insert tableName="authority"> <column name="authority_id">1</column> <column name="name">ROLE_ADMIN</column> </insert> <insert tableName="authority"> <column name="authority_id">2</column> <column name="name">ROLE_USER</column> </insert> <insert tableName="authority"> <column name="authority_id">3</column> <column name="name">ROLE_POWER_USER</column> </insert> <createTable tableName="persistent_logins"> <column name="username" type="varchar(64)"> <constraints nullable="false"/> </column> <column name="series" type="varchar(64)"> <constraints nullable="false" primaryKey="true"/> </column> <column name="token" type="varchar(64)"> <constraints nullable="false"/> </column> <column name="last_used" type="timestamp"> <constraints nullable="false"/> </column> </createTable> <createTable tableName="oauth_client_details"> <column name="client_id" type="varchar(256)"> <constraints primaryKey="true" nullable="false"/> </column> <column name="resource_ids" type="varchar(256)"/> <column name="client_secret" type="varchar(256)"/> <column name="scope" type="varchar(256)"/> <column name="authorized_grant_types" type="varchar(256)"/> <column name="web_server_redirect_uri" type="varchar(256)"/> <column name="authorities" type="varchar(256)"/> <column name="access_token_validity" type="int"/> <column name="refresh_token_validity" type="int"/> <column name="additional_information" type="text"/> <column name="autoapprove" type="varchar(256)"/> </createTable> <createTable tableName="oauth_access_token"> <column name="token_id" type="varchar(256)"/> <column name="token" type="blob"/> <column name="authentication_id" type="varchar(256)"/> <column name="user_name" type="varchar(256)"/> <column name="client_id" type="varchar(256)"/> <column name="authentication" type="blob"/> <column name="refresh_token" type="varchar(256)"/> </createTable> <createTable tableName="oauth_refresh_token"> <column name="token_id" type="varchar(256)"/> <column name="token" type="blob"/> <column name="authentication" type="blob"/> </createTable> </changeSet> <changeSet id="0.1-auth-data" author="andy.sumskoy@gmail.com" context="test"> <insert tableName="users"> <column name="user_id">1</column> <column name="username">admin</column> <column name="password">dd28a28446b96db4c2207c3488a8f93fbb843af1eeb7db5d2044e64581145341c4f1f25de48be21b </column> <column name="enabled">true</column> </insert> <insert tableName="user_authorities"> <column name="user_id">1</column> <column name="authority_id">1</column> </insert> <insert tableName="user_authorities"> <column name="user_id">1</column> <column name="authority_id">2</column> </insert> <insert tableName="user_authorities"> <column name="user_id">1</column> <column name="authority_id">3</column> </insert> <insert tableName="oauth_client_details"> <column name="client_id">simple-client</column> <column name="client_secret">simple-client-secret-key</column> <column name="authorized_grant_types">password</column> </insert> </changeSet> </databaseChangeLog> 


Here it is worth paying attention to the fact that if our application is launched in a test environment, the admin user with the password admin who has all possible rights will be registered in the system and a client for OAuth will be created. It is also worth noting that if you intend to use only one DBMS, then I would recommend writing a changeset to SQL (you can read about this in the documentation for liquibase ).

Now we need to ensure that when starting the liquibase application, we bring our database “to standard”, but more on that later.

Application settings

First we need to create resources / application.conf
 habr.template = { default = { db.url = "jdbc:postgresql://localhost/habr" db.user = "habr" db.password = "habr" } test = { db.url = "jdbc:postgresql://localhost/test-habr" } dev = { } } 

Here we create several sections, in default all the default settings are set, in dev, test specific depending on the environment. Let's also create an AppConfig class, which will be responsible for setting up our application.
Appconfig
 class AppConfig { val env = scala.util.Properties.propOrElse("spring.profiles.active", scala.util.Properties.envOrElse("ENV", "test")) val conf = ConfigFactory.load() val default = conf.getConfig("habr.template.default") val config = conf.getConfig("habr.template." + env).withFallback(default) def dataSource = { val ds = new BasicDataSource ds.setDriverClassName("org.postgresql.Driver") ds.setUsername(config.getString("db.user")) ds.setPassword(config.getString("db.password")) ds.setMaxActive(20) ds.setMaxIdle(10) ds.setInitialSize(10) ds.setUrl(config.getString("db.url")) ds } def liquibase(dataSource: DataSource) = { val liquibase = new LiquibaseDropAllSupport() liquibase.setDataSource(dataSource) liquibase.setChangeLog("classpath:changelog/db.changelog-master.xml") liquibase.setContexts(env) liquibase.setShouldRun(true) liquibase.dropAllContexts += "test" liquibase } } 


We define the environment in which the application is running, it can be -Dspring.profiles.active , or export ENV . We load the necessary config branch and merdjim with default settings. We create pool of connections from a DB. Here you can also make the size of the pool in the settings, for example, everything at will. Well, create a liquibase that supports the complete removal of the entire structure in the database for certain runtimes, for example, deleting everything can be useful if you use CI for your application. Now you can register DataSource and Liquibase as Bean in Spring
root.xml
 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <bean id="config" class="com.sumskoy.habr.template.AppConfig"/> <bean id="dataSource" factory-bean="config" factory-method="dataSource"/> <bean id="liquibase" factory-bean="config" factory-method="liquibase"> <constructor-arg ref="dataSource"/> </bean> </beans> 



Running from under the jetty

I always use Jetty for development, it eliminates the long wait before each launch on the application server, and if you have a large amount of resources, this process can take up to 30 seconds, which is extremely annoying. Create an entry point to our application:
Main
 object Main extends App { val server = new Server(8080) val webAppContext = new WebAppContext() webAppContext.setResourceBase("src/main/webapp") webAppContext.setContextPath("/") webAppContext.setParentLoaderPriority(true) webAppContext.setConfigurations(Array( new WebXmlConfiguration() )) server.setHandler(webAppContext) server.start() server.join() } 


Security

I will not describe how to configure Spring Security, the only thing I will state is that for authorization we will use /login.html , as the fresh url is /index.html , we will have all the API in the / api branch.
Let's make a simple User model, make a Repository for it, in which while there is one method, it will have to return the user by name. Make a controller that returns the name of the current user:
User Entity
 case class User(username: String, password: String, enabled: Boolean, @Column("user_id") override val id: Int) extends BaseEntity { def this() = this("", "", false, 0) } 


Add a model to the scheme
Core schema
 object CoreSchema extends Schema { val users = table[User]("users") on(users)(user => declare( user.id is autoIncremented, user.username is unique )) } 


And write a simple Repository. I will not do the interface with the implementation, I will immediately write the implementation, since in most cases there is no need for this, it just clutters the code once again. If you suddenly need to change the implementation or use the AOP, then select the interface from the class is not difficult, but now we do not need it and this need is not foreseen in the near future. We will not complicate your life.
User Repository
 @Repository class UserRepository { def findOne(username: String) = inTransaction { CoreSchema.users.where(_.username === username).singleOption } } 



Well, a simple controller
Authcontroller
 @Controller @RequestMapping(Array("api/auth")) class AuthController @Autowired()(private val userRepository: UserRepository) { @RequestMapping(Array("check")) @ResponseBody def checkTokenValid(principal: Principal): Map[String, Any] = { userRepository.findOne(principal.getName) match { case Some(user) => Map[String, Any]("username" -> user.username, "enabled" -> user.enabled) case _ => throw new ObjectNotFound() } } } 


Here it is worth mentioning that for serialization in JSON we use Jackson. To him there is a library that allows you to work in classes and collections of Scala, for this we define the correct mapper for Spring
 def converter() = { val messageConverter = new MappingJackson2HttpMessageConverter() val objectMapper = new ObjectMapper() with ScalaObjectMapper objectMapper.registerModule(DefaultScalaModule) messageConverter.setObjectMapper(objectMapper) messageConverter } 

 <beans:bean id="converter" factory-bean="config" factory-method="converter"/> <mvc:annotation-driven> <message-converters register-defaults="true"> <beans:ref bean="converter"/> </message-converters> </mvc:annotation-driven> 


Tests

Now you need to fix the behavior of the authorization through the tests. We guarantee that the client can log in through the login form and through OAuth. Let's write a couple of tests for this.
First, let's make a base class for all tests using Spring MVC
IntegrationTestSpec
 @ContextConfiguration(value = Array("classpath:context/root.xml", "classpath:context/mvc.xml")) @WebAppConfiguration abstract class IntegrationTestSpec extends FlatSpec with ShouldMatchers with ScalaFutures { @Resource private val springSecurityFilterChain: java.util.List[FilterChainProxy] = new util.ArrayList[FilterChainProxy]() @Autowired private val wac: WebApplicationContext = null new TestContextManager(this.getClass).prepareTestInstance(this) var builder = MockMvcBuilders.webAppContextSetup(this.wac) for(filter <- springSecurityFilterChain.asScala) builder = builder.addFilters(filter) val mockMvc = builder.build() val md = MediaType.parseMediaType("application/json;charset=UTF-8") val objectMapper = new ObjectMapper() with ScalaObjectMapper objectMapper.registerModule(DefaultScalaModule) } 


And write our first test for authorization
 it should "Login as admin through oauth with default password" in { val resultActions = mockMvc.perform( get("/oauth/token"). accept(md). param("grant_type", "password"). param("client_id", "simple-client"). param("client_secret", "simple-client-secret-key"). param("username", "admin"). param("password", "admin")). andExpect(status.isOk). andExpect(content.contentType(md)). andExpect(jsonPath("$.access_token").exists). andExpect(jsonPath("$.token_type").exists). andExpect(jsonPath("$.expires_in").exists) val contentAsString = resultActions.andReturn.getResponse.getContentAsString val map: Map[String, String] = objectMapper.readValue(contentAsString, new TypeReference[Map[String, String]] {}) val access_token = map.get("access_token").get val token_type = map.get("token_type").get mockMvc.perform( get("/api/auth/check"). accept(md). header("Authorization", token_type + " " + access_token)). andExpect(status.isOk). andExpect(content.contentType(md)). andExpect(jsonPath("$.username").value("admin")). andExpect(jsonPath("$.enabled").value(true)) } 

And the test for authorization through the form
 it should "Login as admin through user form with default password" in { mockMvc.perform( post("/auth/j_spring_security_check"). contentType(MediaType.APPLICATION_FORM_URLENCODED). param("j_username", "admin"). param("j_password", "admin")). andExpect(status.is3xxRedirection()). andExpect(header().string("location", "/index.html")) } 


We’ll stop on this for now. In the next article we will make the front, with the assembly of SASS, CoffeeScript, minimization and other convenient things. We will be friends with Yeoman , Bower , Grunt , and also we will make an environment deployment for a programmer through Vagrant .

All this can be viewed on Bitbucket https://bitbucket.org/andy-inc/scala-habr-template .

If you find a typo or error, write to the LAN. Thank you in advance for your understanding.

Thank you for your attention, share your views and not silently minus.

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


All Articles