Change Java to Scala. Base application

  • Tutorial
Hello, Habr.

Summer is in the yard, vacation is coming soon, and there is a bit of free time to share my achievements, some experience in writing Web applications on the Java platform. As the main language, I will use Scala. It will be like a small guide, how a person with Java experience will gradually start using Scala and not abandon his existing achievements.

This is the first part of a series of articles in which we will pay attention to the basic structure of the application. It is aimed at people who know Java, working with Spring, Hibernate, JPA, JSP and other 3-4 letter abbreviations. I will try to tell how to start using Scala in your projects as quickly and painlessly as possible and design your new application in a different way. All this will be around the project, which must fulfill a number of requirements:
1. The application is completely closed, we work only after authorization
2. The availability of a convenient API (we forget REST (it's history) and write something like the Google AdWords API, with our SQL like query)
3. The ability to run applications on the server without he
4. the i18n
5. Migrating database
6. The environment for development should unfold through Vagrant
7. and on the little things, logging, deployment ...

All this must be done so as to maintain and develop our application was very easy, that there was no such situation, when a programmer evaluates when adding a new directory it is valid for 2 days. If I am of interest to you, I ask for a cat.



To start


You should familiarize yourself with the Scala syntax, for example, flipping through Horstman's book Scala for the Impatient . To roughly imagine how the language works and to know what is in it. I advise you not to go straight into the jungle, start simple and remember where you saw any complex and interesting designs. After a while, return to them and watch how they are implemented, try to do such things. The language is large and immediately using all the features can be problematic.

What we will use


For Scala, there are many sharpened things, for example, it's worth looking at the Play Framework , SBT , Slick , Lift . But we will start with those things that we have already worked with. We will do the assembly through Maven. Take Spring, Spring MVC, Spring Security as a basis. For the database, let's take Squeryl (I do not like Hibernate because of its heaviness, specific features, and the always problematic Lazy). We will have the front entirely on Angular, for styles it will be SASS, instead of JS we will take CoffeeScript (I will show how to use it, but you can refuse Coffee with the same ease). Well, of course, we will write tests, both integration and modular, on ScalaTest. We will omit testing the front, since this is a separate volume conversation with its own characteristics. The API will be interesting for us. It will have the concept of a service, the service will have methods, and we will also support SQL like query queries. For instance:
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, we create a Maven project and immediately plug in a plug-in for compiling Scala.
pom.xml
2.10.4org.scala-langscala-library${scala-version}net.alchim31.mavenscala-maven-plugin3.1.6org.apache.maven.pluginsmaven-compiler-plugin2.0.2net.alchim31.mavenscala-maven-pluginscala-compile-firstprocess-resourcesadd-sourcecompilescala-test-compileprocess-test-resourcestestCompileorg.apache.maven.pluginsmaven-compiler-plugincompilecompile


All our sources will be in the src / main / scala directory , you can also write some things in Java, putting them in src / main / java . Actually, Scala classes can be used in Java classes and vice versa, if such an opportunity is needed. We will also need Spring, Spring MVC, Spring Security, Spring OAuth, I believe that connecting all this will not be difficult, so I will not describe it. Of the nuances, we will also need Jetty (during development, we will run our application through it). More Scala Config, ScalaTest. In order for the tests to run through Maven, you need to turn off the Maven Surefire Plugin and use the Scalatest Maven Plugin
pom.xml
org.apache.maven.pluginsmaven-surefire-plugin2.7trueorg.scalatestscalatest-maven-plugin1.0${project.build.directory}/surefire-reports.WDF TestSuite.txttesttest


In order not to write logger initialization in each class, we will connect a library that will provide us with trait LazyLogging .
com.typesafe.scala-loggingscala-logging-slf4j_2.102.1.2


Database migration

Now it's time to think about our database. For migration we will use Liquibase . First, create a file that will describe the links to all changeset.
resources / changelog / db.changelog-master.xml


And we will describe our first changeset in which there will be all tables for authorization and OAuth
db.changelog-0.1.xml
1ROLE_ADMIN2ROLE_USER3ROLE_POWER_USER1admindd28a28446b96db4c2207c3488a8f93fbb843af1eeb7db5d2044e64581145341c4f1f25de48be21b
            true111213simple-clientsimple-client-secret-keypassword


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

Now we need to ensure that when the application starts, liquibase brings our database “up 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 are specific depending on the environment. We will also create the AppConfig class, which will be responsible for configuring 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 determine the environment in which the application is running, it can be -Dspring.profiles.active , or export ENV . We load the necessary branch of the config and merge with the default settings. Create a database connection pool. Here you can still make the pool size in the settings, for example, everything is optional. Well, create a liquibase that supports the complete removal of the entire structure in the database for certain runtimes, for example, deleting everything can come in handy if you use CI for your application. Now you can register DataSource and Liquibase as a Bean in Spring
root.xml



Running from under 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 number 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 won’t describe how to configure Spring Security, the only thing I’ll say is that for authorization we will use /login.html , as a nice url - /index.html , we will have all the APIs in the / api branch .
Let's make a simple User model, make a Repository for it, in which for now there will be one method, it will have to return the user by name. Let's create 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 the model to the circuit.
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 only clutters up the code once again. If you suddenly need to change the implementation or use AOP, then selecting the interface from the class will not be difficult, but now we do not need it and such a need is not expected in the near future. Let's not complicate our lives.
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()
    }
  }
}


It is worth mentioning that for serialization in JSON we use Jackson. There is a library for it that allows you to work in Scala classes and collections, 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
  }



Tests

Now you need to fix the authorization behavior through 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 we will 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 a 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 will stop here for now. In the next article we will make the front, with the assembly of SASS, CoffeeScript, minimization and other convenient things. We will make friends with Yeoman , Bower , Grunt , and also we will deploy the environment for the programmer through Vagrant .

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

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

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

Also popular now: