Ändern Sie Java in Scala. Basisanwendung

  • Tutorial
Hallo habr

Der Sommer steht vor der Tür, der Urlaub steht vor der Tür und einige freie Stunden scheinen meine Erfolge mitzuteilen, einige Erfahrungen beim Schreiben von Webanwendungen auf der Java-Plattform. Als Hauptsprache verwende ich Scala. Es wird wie ein kleiner Leitfaden sein, wie eine Person mit Java-Erfahrung nach und nach mit der Verwendung von Scala beginnt und seine vorhandenen Erfolge nicht aufgibt.

Dies ist der erste Teil einer Artikelserie, in der wir uns mit der Grundstruktur der Anwendung befassen. Es richtet sich an Personen, die Java kennen und mit Spring, Hibernate, JPA, JSP und anderen Abkürzungen für 3-4 Buchstaben arbeiten. Ich werde versuchen, Ihnen zu erklären, wie Sie Scala in Ihren Projekten möglichst schnell und problemlos einsetzen und Ihre neue Anwendung auf eine andere Weise gestalten können. Dies alles dreht sich um das Projekt, das eine Reihe von Anforderungen erfüllen muss:
1. Приложение полностью закрыто, работаем только после авторизации
2. Наличие удобного API (REST мы забудем (он уже история) и напишем что-то вроде Google AdWords API, со своим SQL like запросником)
3. Возможность запуска на сервере приложений так и без него
4. i18n
5. Миграция БД
6. Среда для разработки должна разворачиваться через Vagrant
7. И, по мелочи, логирование, развертывание…

Все это нужно сделать так, чтобы сопровождать и развивать наше приложение было очень легко, чтобы не возникло такой ситуации, когда при добавление нового справочника программист оценивает это сроком в 2 дня. Если я вас заинтересовал, прошу под кат.



Для начала


Sie sollten sich mit der Scala-Syntax vertraut machen, indem Sie beispielsweise Horstmans Buch Scala for the Impatient durchblättern . Sich grob vorstellen, wie die Sprache funktioniert und wissen, was drin ist. Ich rate dir, nicht direkt in den Dschungel zu gehen, einfach zu beginnen und dich daran zu erinnern, wo du komplexe und interessante Designs gesehen hast. Kehren Sie nach einer Weile zu ihnen zurück und beobachten Sie, wie sie implementiert werden. Versuchen Sie, solche Dinge zu tun. Die Sprache ist groß und die sofortige Verwendung aller Funktionen kann problematisch sein.

Was wir verwenden werden


Für Scala gibt es viele grundlegende Dinge. Sehen Sie sich zum Beispiel das Play Framework , SBT , Slick , Lift an . Aber wir werden mit den Dingen beginnen, mit denen wir bereits gearbeitet haben. Wir werden die Montage durch Maven machen. Nehmen Sie Spring, Spring MVC, Spring Security als Basis. Nehmen wir für die Datenbank Squeryl (ich mag Hibernate nicht wegen seiner Schwere, spezifischen Funktionen und dem immer problematischen Lazy). Wir werden die Front komplett auf Angular haben, für Styles wird es SASS sein, anstelle von JS werden wir CoffeeScript nehmen (ich werde zeigen, wie man es benutzt, aber Sie können Coffee mit der gleichen Leichtigkeit ablehnen). Natürlich werden wir sowohl Integrationstests als auch Modultests für ScalaTest schreiben. Wir werden das Testen der Vorderseite weglassen, da dies ein separates Volumengespräch mit seinen eigenen Merkmalen ist. Die API wird für uns interessant sein. Es wird das Konzept eines Dienstes haben, der Dienst wird Methoden haben und wir werden auch SQL-ähnliche Abfragen unterstützen. Zum Beispiel:
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'}}]


Zum Geschäft



Struktur und Abhängigkeiten

Erstellen Sie zunächst ein Maven-Projekt und stecken Sie sofort ein Plug-In zum Kompilieren von Scala ein.
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


Alle unsere Quellen befinden sich im Verzeichnis src / main / scala . Sie können einige Dinge auch in Java schreiben und in src / main / java ablegen . Tatsächlich können Scala-Klassen in Java-Klassen verwendet werden und umgekehrt, wenn eine solche Gelegenheit benötigt wird. Wir werden auch Spring, Spring MVC, Spring Security und Spring OAuth brauchen. Ich glaube, dass es nicht schwierig sein wird, all dies miteinander zu verbinden, deshalb werde ich es nicht beschreiben. Von den Nuancen werden wir auch Jetty benötigen (während der Entwicklung werden wir unsere Anwendung durchlaufen lassen). Mehr Scala Config, ScalaTest. Damit die Tests durch Maven laufen, müssen Sie das Maven Surefire Plugin ausschalten und das Scalatest Maven Plugin verwenden
pom.xml
org.apache.maven.pluginsmaven-surefire-plugin2.7trueorg.scalatestscalatest-maven-plugin1.0${project.build.directory}/surefire-reports.WDF TestSuite.txttesttest


Um nicht in jeder Klasse eine Logger-Initialisierung zu schreiben, verbinden wir eine Bibliothek, die uns das Merkmal LazyLogging liefert .
com.typesafe.scala-loggingscala-logging-slf4j_2.102.1.2


Datenbankmigration

Jetzt ist es Zeit, über unsere Datenbank nachzudenken. Für die Migration verwenden wir Liquibase . Erstellen Sie zunächst eine Datei, die die Links zu allen Änderungssätzen beschreibt.
resources / changelog / db.changelog-master.xml


Und wir werden unser erstes Änderungsset beschreiben, in dem alle Tabellen für Authorization und OAuth enthalten sind
db.changelog-0.1.xml
1ROLE_ADMIN2ROLE_USER3ROLE_POWER_USER1admindd28a28446b96db4c2207c3488a8f93fbb843af1eeb7db5d2044e64581145341c4f1f25de48be21b
            true111213simple-clientsimple-client-secret-keypassword


Hierbei ist zu beachten, dass beim Start unserer Anwendung in einer Testumgebung der Admin-Benutzer mit dem Admin-Passwort, das alle möglichen Rechte besitzt, im System registriert und ein Client für OAuth angelegt wird. Es ist auch erwähnenswert, dass, wenn Sie beabsichtigen, nur ein DBMS zu verwenden, ich empfehlen würde, Changeset in SQL zu schreiben (dies ist in der Liquibase-Dokumentation zu finden ).

Jetzt müssen wir sicherstellen, dass beim Starten der Anwendung liquibase unsere Datenbank "auf den neuesten Stand" bringt, aber dazu später mehr.

Anwendungseinstellungen

Zuerst müssen wir resources / application.conf erstellen
habr.template = {
  default = {
    db.url = "jdbc:postgresql://localhost/habr"
    db.user = "habr"
    db.password = "habr"
  }
  test = {
    db.url = "jdbc:postgresql://localhost/test-habr"
  }
  dev = {
  }
}

Hier erstellen wir mehrere Abschnitte, in denen standardmäßig alle Standardeinstellungen festgelegt sind. In dev sind die Tests abhängig von der Umgebung. Wir werden auch die AppConfig-Klasse erstellen, die für die Konfiguration unserer Anwendung verantwortlich ist
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
  }
}


Wir bestimmen die Umgebung, in der die Anwendung ausgeführt wird. Sie kann -Dspring.profiles.active sein oder ENV exportieren . Wir laden den notwendigen Zweig der Konfiguration und merjim mit den Standardeinstellungen. Erstellen Sie einen Datenbankverbindungspool. Hier können Sie noch die Poolgröße in den Einstellungen vornehmen, zum Beispiel ist alles optional. Erstellen Sie eine Liquibase, die das vollständige Entfernen der gesamten Struktur in der Datenbank für bestimmte Laufzeiten unterstützt. Wenn Sie beispielsweise CI für Ihre Anwendung verwenden, ist es praktisch, alles zu löschen. Jetzt können Sie DataSource und Liquibase im Frühjahr als Bean registrieren
root.xml



Laufen unter dem Steg hervor

Ich verwende Jetty immer für die Entwicklung, da das lange Warten vor jedem Start auf dem Anwendungsserver entfällt. Wenn Sie über eine große Anzahl von Ressourcen verfügen, kann dieser Vorgang bis zu 30 Sekunden dauern, was äußerst ärgerlich ist. Erstellen Sie einen Einstiegspunkt für unsere Anwendung:
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()
}


Sicherheit

Ich werde nicht beschreiben, wie Spring Security konfiguriert wird. Das einzige, was ich sagen werde, ist, dass wir für die Autorisierung /login.html verwenden . Als URL /index.html werden wir alle APIs im / api- Zweig haben .
Lassen Sie uns ein einfaches Benutzermodell erstellen, ein Repository dafür erstellen, in dem es für den Moment eine Methode gibt, die den Benutzer namentlich zurückgeben muss. Erstellen wir einen Controller, der den Namen des aktuellen Benutzers zurückgibt:
Benutzerentität
case class User(username: String, password: String, enabled: Boolean, @Column("user_id") override val id: Int) extends BaseEntity {
  def this() = this("", "", false, 0)
}


Fügen Sie das Modell zur Schaltung hinzu.
Kernschema
object CoreSchema extends Schema {
  val users = table[User]("users")
  on(users)(user => declare(
    user.id is autoIncremented,
    user.username is unique
  ))
}


Und schreibe ein einfaches Repository. Ich werde die Schnittstelle mit der Implementierung nicht herstellen, ich werde die Implementierung sofort schreiben, da dies in den meisten Fällen nicht erforderlich ist, sondern nur den Code erneut unübersichtlich macht. Wenn Sie plötzlich die Implementierung ändern oder AOP verwenden müssen, ist die Auswahl des Interfaces aus der Klasse nicht schwierig, aber jetzt brauchen wir es nicht und ein solcher Bedarf wird in naher Zukunft nicht erwartet. Lassen Sie uns unser Leben nicht komplizieren.
Benutzer-Repository
@Repository
class UserRepository {
  def findOne(username: String) = inTransaction {
    CoreSchema.users.where(_.username === username).singleOption
  }
}



Nun, eine einfache Steuerung
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()
    }
  }
}


Es ist erwähnenswert, dass wir für die Serialisierung in JSON Jackson verwenden. Es gibt dafür eine Bibliothek, mit der Sie in Scala-Klassen und -Sammlungen arbeiten können. Dazu definieren wir den richtigen Mapper für Spring
def converter() = {
    val messageConverter = new MappingJackson2HttpMessageConverter()
    val objectMapper = new ObjectMapper() with ScalaObjectMapper
    objectMapper.registerModule(DefaultScalaModule)
    messageConverter.setObjectMapper(objectMapper)
    messageConverter
  }



Tests

Jetzt müssen Sie das Autorisierungsverhalten durch Tests korrigieren. Wir garantieren, dass sich der Kunde über das Anmeldeformular und über OAuth anmelden kann. Lassen Sie uns dazu ein paar Tests schreiben.
Lassen Sie uns zunächst eine Basisklasse für alle Tests mit Spring MVC erstellen
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)
}


Und wir werden unseren ersten Test für die Autorisierung schreiben
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))
  }

Und ein Test für die Autorisierung über das Formular
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"))
  }


Wir werden hier vorerst aufhören. Im nächsten Artikel befassen wir uns mit der Montage von SASS, CoffeeScript, Minimierung und anderen praktischen Dingen. Wir werden uns mit Yeoman , Bower und Grunt anfreunden und über Vagrant die Umgebung für den Programmierer bereitstellen .

All dies kann auf Bitbucket https://bitbucket.org/andy-inc/scala-habr-template eingesehen werden .

Wenn Sie einen Tippfehler oder einen Fehler finden, schreiben Sie an die PM. Vielen Dank im Voraus für Ihr Verständnis.

Vielen Dank für Ihre Aufmerksamkeit, teilen Sie Ihre Meinung und lassen Sie sich nicht im Stich.

Jetzt auch beliebt: