Upload
others
View
2
Download
0
Embed Size (px)
Citation preview
BuildingApplicationswithScala
TableofContents
BuildingApplicationswithScalaCreditsAbouttheAuthorAcknowledgmentsAbouttheReviewerwww.PacktPub.com
Whysubscribe?Preface
WhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport
DownloadingtheexamplecodeDownloadingthecolorimagesofthisbookErrataPiracyQuestions
1.IntroductiontoFP,Reactive,andScalaFunctionalprogrammingPrinciplesoffunctionalprogramming
ImmutabilityDisciplinedstatePurefunctionsandnosideeffectsFirst-classfunctionsandhigher-orderfunctionsTypesystemsReferentialtransparency
InstallingJava8andScala2.11ReadEvalPrintandLoop-REPLScalaHelloWorldusingtheREPL
ScalaREPLHelloWorldprogramScalaobject-orientedHelloWorldprogramScalaHelloWorldAppintheScalaREPLJavaHelloWorldapplication
Scalalanguage-thebasicsScalavariables-varandval
ScalaREPLvarusageScalavalusageattheScalaREPL
CreatingimmutablevariablesScalavariabletypeintheScalaREPL
ScalavariableswithexplicittypingattheScalaREPLScalaconditionalandloopsstatements
IfstatementsinScalaREPLIfstatementsinreturnstatementsinScalaREPLBasicforloopinScalaREPLForwithListinScalaREPLForwithifstatementsforfiltering-ScalaREPLJavacodeforfilteringevennumbers
ForcomprehensionsForcomprehensioninScalaREPLJavacodeforperformingfilteringwithcollections
ScalacollectionsCreating,removing,andgettinganitemfromamutablelistinScalaREPLScalatuplesScalaimmutableMapinScalaREPLScalamutableMapsatScalaREPL
MonadsScalaMapfunctioninScalaREPLOptionMonadinScalaAlistofallmethodsusingtheScalaREPL
Scalaclass,traits,andOOprogrammingAsimpleScalaclassinScalaREPLScalaplainoldJavaobjectinScalaREPLPersonclassinJava
TraitsandinheritanceScalainheritancecodeinScalaREPLScalatraitssamplecodeinScalaREPLScalatraitsusingvariablemixingtechniqueatScalaREPLScalatypealiassampleinScalaREPL
CaseclassesScalacaseclassesfeatureinScalaREPL
PatternMatcherSimplePatternMatcherinScalaAdvancedpatternmatcherinScalaREPLAdvancedcomplexpatternmatcherinScalaREPL
PartialfunctionsSimplePartialfunctioninScalaREPLScalaPartialFunctionwithoutOOusingcasestatementsinScalaREPLPartialFunctioncompositioninScalaREPL
Packageobjectspackage.scalaMainApp.scala
FunctionsPartialapplication
PartialfunctioninScalaREPLCurriedfunctions
Curriedfunctions-ScalaREPLCurriedtransformationinScalaREPL
OperatoroverloadingScalaoperatoroverloadinginScalaREPL
ImplicitsScalaImplicitsinSCALAREPLImplicitParameteratScalaREPL
FuturesSimpleFuturecodeinScalaREPLAcompleteFuturesampleatScalaREPL
ReactiveProgramingandRxScalaSimpleObservablesScalawithRxScalaSimpleObservablesScalawithRxScala-ExecutionintheconsoleComplexScalawithRxScalaObservables
Summary2.CreatingYourAppArchitectureandBootstrappingwithSBT
IntroducingSBTInstallingSBTonUbuntuLinuxGettingstartedwithSBTAddingdependenciesGeneratingEclipseprojectfilesfromSBTApplicationdistributionHelloworldSBT/ScalaAppBootstrappingourPlayframeworkappwithActivatorActivatorshellActivator-compiling,testing,andrunningSummary
3.DevelopingtheUIwithPlayFrameworkGettingstartedCreatingourmodelsCreatingroutesCreatingourcontrollersWorkingwithservicesConfiguringtheGuicemoduleWorkingwithviews(UI)Summary
4.DevelopingReactiveBackingServicesGettingstartedwithreactiveprogramming
IPriceService-ScalatraitPriceService-RxScalaPriceServiceimplementationGuiceInjection-Module.scalaNGServiceEndpoint
PlayframeworkandhighCPUusageRndDoubleGeneratorControllerIRndService.scala-ScalatraitRndService.scala-RndServiceimplementationModule.scala-GuiceInjectionsmain.scala.htmlproduct_details.scala.html
Summary5.TestingYourApplication
UnittestingprinciplesMakingcodetestableIsolationandself-containedtestsEffectivenamingLevelsoftesting
TestingwithJunitBehavior-DrivenDevelopment-BDDMyFirstPlaySpec.scala-FirstBDDwithScalaTestandthePlayframeworkTestingwithPlayframeworksupport
ProductService.scala-FIXthecodeissueImageServiceTestSpec.scala-ImageServiceTestReviewServiceTestSpec.scala-ReviewServicetest
TestingroutesRoutesTestingSpec.scala-Playframeworkroutetesting
ControllertestingRndDoubleGeneratorControllerTestSpec.scala-RndDoubleGeneratorControllertestsIntegrationSpec.scalaProductControllerTestSpec.scalaproduct_index.scala.htmlImageControllerTestSpec.scalaimage_index.scala.htmlReviewControllerTestSpec.scalareview_index.scala.htmlApplicationSpec.scalaNGServiceImplTestSpec.scalaNGServiceEndpointControllerTest.scala
Summary6.PersistencewithSlick
IntroducingtheSlickframeworkMySQLsetupConfiguringSlickinourPlayframeworkapp
ConfigurethedatabaseconnectionFPMMapping
ProductDaoReviewDAO
ImageDaoSlickevolutionsRefactoringservicesRefactoringcontrollersConfiguringDAOpackagesinGuiceRefactoringtestsGenericmocksServicetestsControllertestsRunningtheapplicationSummary
7.CreatingReportsIntroducingJasperReportsJasperReportsworkflowJaspersessionsInstallingJaspersoftStudio6ConfiguringMySQLDataAdapterinJaspersoftStudioCreatingaproductreportCreatingareviewreportCreatinganimagereportIntegratingJasperReportswithPlayframework
build.sbtGenericreportbuilderAddingthereporttotheproductcontrollerAddingthereporttothereviewcontrollerAddingthereporttotheimagecontroller
Routes-addingnewreportroutesNewcentralizedreportsUI
AddingthereportbuttonforeachviewSummary
8.DevelopingaChatwithAkkaAddingthenewUIintroductiontoAkkaIntroductiontotheActormodel
WhatisanActor?MessageexchangeandmailboxesCodingactorswithAkkaActorroutingPersistenceCreatingourchatapplicationThechatprotocolThechatcontroller
ImplementingthechatcontrollerConfiguringtheroutesWorkingontheUI
AddingAkkatestsScalatestforAkkaActorChatroomActortestChatBotAdminActortest
Summary9.DesignYourRESTAPI
IntroductiontoRESTRESTAPIdesign
HTTPverbsdesignUniformAPIResponsewithHTTPstatuscodesRESTAPIpatternsAPIversioningSomeanti-patternstobeavoided
CreatingourAPIwithRESTandJSONRestApiContoller
RESTAPIFrontControllerimplementationJSONmappingConfiguringnewroutesTestingtheAPIusingthebrowser
CreatingaScalaclientConfiguringplugins.sbtConfiguringbuild.sbtScalaclientcode
CreatingourRESTclientproxiesCreatingScalaTesttestsfortheproxiesAddingbackpressure
TheleakybucketalgorithmScalaleakybucketimplementationTestingbackpressure
AddingSwaggersupportSwaggerUI
BuildandinstallSwaggerStandaloneSummary
10.ScalingupStandalonedeployReportsfolderChangingreportbuilderDefiningthesecretRunningthestandalonedeployArchitectureprinciples
Serviceorientation(SOA/microservices)PerformanceScalability/Resiliency
ScalabilityprinciplesVerticalandhorizontalscaling(upandout)CachingLoadbalancerThrottlingDatabaseclusterCloudcomputing/containersAutoScalingAnoteaboutautomationDon'tforgetabouttelemetryReactiveDriversanddiscoverability
Mid-Tierloadbalancer,timeouts,Backpressure,andcachingScalingupmicroserviceswithanAkkaclusterScalinguptheinfrastructurewithDockerandAWScloudSummary
BuildingApplicationswithScala
BuildingApplicationswithScalaCopyright©2016PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:December2016
Productionreference:1021216
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
Birmingham
B32PB,UK.
ISBN978-1-78646-148-3
www.packtpub.com
Credits
Author
DiegoPacheco
CopyEditor
SoniaMathur
Reviewer
YuanhangWang
ProjectCoordinator
SuzanneCoutinho
CommissioningEditor
KunalParikh
Proofreader
SafisEditing
AcquisitionEditor
DenimPinto
Indexer
TejalDaruwaleSoni
ContentDevelopmentEditor
RohitSingh
Graphics
JasonMonteiro
TechnicalEditor
JijoMaliyekal
ProductionCoordinator
MelwynDsa
AbouttheAuthorDiegoPachecoisanexperiencedsoftwarearchitectandDevOpspractitionerwithover10yearsofsolidexperience.HehasledarchitectureteamsusingopensourcesolutionssuchasJava,Scala,AmazonWebServices(AWS),Akka,ApacheCassandra,Redis,ActiveMQ,NetflixOSSStack-SimianArmy,RxJava,Karyon,Eureka,andRibbononcigcustomersinBrazil,London,Barcelona,India,andtheUSA.Diegohasapassionforfunctionalprogrammingandiscurrentlyworkingasasoftwarearchitect/agilecoachwithScala,Akka,andNetflixOSS.Duringhisfreetime,heenjoysgaming,blogging,andplayingwickedtunesonhisguitar.Youcancheckouthisblogathttp://diego-pacheco.blogspot.in/.
Someofhiscoreskillsincludethefollowing:
ArchitecturedesignandarchitecturecodingforhighscalablesystemsDistributedsystemsusingSOAandmicroservicesprinciples,tools,andtechniquesPerformancetuningandDevOpsengineeringFunctionalprogrammingandScalaAgilecoachingandservantleadershipforarchitectureteamsConsultancyondevelopmentpracticeswithXP/Kanban
Moreabouthimcanbefoundatthefollowing:
Linkedin:https://www.linkedin.com/in/diegopachecorsBlog:http://diego-pacheco.blogspot.in/Github:https://github.com/diegopachecoSlideshare:http://www.slideshare.net/diego.pacheco/presentationsPresentations:https://gist.github.com/diegopacheco/ad3e3804a5071ef219d1
HisrecentlecturesincludeNetflix(https://www.youtube.com/watch?v=Z4_rzsZd70o&feature=youtu.be),QCon(http://qconsp.com/sp2016/speaker/diego-pacheco),andAmazon(http://www.meetup.com/Sao-Paulo-Amazon-Web-Services-AWS-Meetup/events/229283010/).
AcknowledgmentsFirstofall,I'mverythankfulforeverythingGodhasgiventomeinlife.So,IneedtosaythankyoutoGodatleastthreetimes.ThankyouGod,thankyouGod,thankyouGod.I’mverygladtohavefinishedthisbook,andIalsoneedtosayabigthankyoutoallmyfamilyandsupportivefriends,especiallyAndressaBicca,mytruelove;mymother,DeniseMaris;mygrandmother,Walkyria;andmydearfriends,MargaridaAvila,AdãoAvila,IsraelPrestes,andTaisdaRosa,foralltheirloveandsupport.IneedtosaythankstoPackt,especiallytoKirkD'costaandRohitKumarSinghforbeinggreateditors.Also,Ineedtosaythankyoutoilegra.comandespeciallytoIvãBoesingandRomuloDornellesforallthespace,trust,andsupport.Also,Icannotforgettosaythankyoutomycoworkers,customers,andfriends,whoaregreatpeopletoworkwith,andwhoI’velearnedalotfrom:SamSgro,DanielWildt,AnibalRojas,AlexandrePoletto,JefersonMachado,NilseuPadilha,JacksonSantos,ChristopheMarchal,JoelCorrea,andRafaelSouza.Finally,thankyoutoallofyouwhoboughtthisbookandhavereadit--youareawesome!
AbouttheReviewerYuanhangWangdescribeshimselfasanenthusiastofpurelyfunctionalprogrammingandneuralnetworks,withaprimaryfocusonDomainSpecificLanguage(DSL)design,andhehasdabbledinseveralfunctionalprogramminglanguages.HeiscurrentlyadatascientistatChinaMobileResearchCenter,workingonatypeddataprocessingengineandoptimizerbuiltontopofseveralbigdataplatforms.
www.PacktPub.comForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusatservice@packtpub.comformoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www.packtpub.com/mapt
Getthemostin-demandsoftwareskillswithMapt.MaptgivesyoufullaccesstoallPacktbooksandvideocourses,aswellasindustry-leadingtoolstohelpyouplanyourpersonaldevelopmentandadvanceyourcareer.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
PrefaceFunctionalprogrammingstartedinacademiaandendedupintheITindustry.TheScalalanguageisamulti-paradigmlanguageusedbybigplayersandlargeorganizationsthathelpsyougetthecorrect(inthesenseofpurefunctionalprogramming)softwareand,atthesametime,softwarethatispracticalandscalable.
Scalahasaveryrichecosystem,includingPlayFramework,Akka,Slick,Gatling,Finable,andmore.Inthisbook,wewillstartrightfromthebasicprinciplesandideasonfunctionalandReactiveXprogramming,andthroughpracticalexamples,youwilllearnhowtocodewiththemostimportantframeworksoftheScalaecosystem,suchasPlay,Akka,andSlick.
YouwilllearnhowtobootstrapaScalaapplicationwithSBTandActivator,howtobuildaPlayandAkkaapplicationstepbystep,andwecoverthetheoryofhowtoscalemassiveScalaapplicationswithcloudandtheNetflixOSSstack.ThisbookwillhelpyoutogofromthebasicsubjectstothemostadvancedonesinordertomakeyouaScalaexpert.
WhatthisbookcoversChapter1,IntroductiontoFP,Reactive,andScala,looksathowtosetupaScaladevelopmentenvironment,thedifferencebetweenfunctionalprogrammingandobject-orientedprogramming,andtheconceptsoffunctionalprogramming.
Chapter2,CreatingYourAppArchitectureandBootstrappingwithSBT,discussestheoverallarchitecture,SBTbasics,andhowtocreateyourownapplication.
Chapter3,DevelopingtheUIwithPlayFramework,coverstheprinciplesofwebdevelopmentinScala,creatingourmodels,creatingourviews,andaddingvalidations.
Chapter4,DevelopingReactiveBackingServices,introducesyoutoreactiveprogrammingprinciples,refactoringourcontrollers,andaddingRxScalatoourservices.
Chapter5,TestingYourApplication,looksintotestingprincipleswithScalaandJUnit,behavior-drivendevelopmentprinciples,usingScalaTestspecsandDSLinourtests,andrunningourtestswithSBT.
Chapter6,PersistencewithSlick,coversprinciplesofdatabasepersistencewithSlick,workingwithFunctionalRelationalMappinginyourapplication,creatingthequeriesyouneedwithSQLsupport,andimprovingthecodewithasyncdatabaseoperations.
Chapter7,CreatingReports,helpsyouunderstandJasperreportsandadddatabasereportstoyourapplication.
Chapter8,DevelopingaChatwithAkka,discussestheactormodel,actorsystems,actorrouting,anddispatchers.
Chapter9,DesignYourRESTAPI,looksintoRESTandAPIdesign,creatingourAPIwithRESTandJSON,addingvalidations,addingbackpressure,andcreatingaScalaclient.
Chapter10,ScalingUp,touchesuponthearchitectureprinciplesandscalinguptheUI,reactivedrivers,anddiscoverability.Italsocoversmiddle-tierloadbalancers,timeouts,backpressure,andcaching,andguidesyouthroughscalingupmicroserviceswithanAkkaclusterandscalinguptheinfrastructurewithDockerandAWScloud.
WhatyouneedforthisbookForthisbook,youwillneedthefollowing:
UbuntuLinux14orsuperiorJava8update48orsuperiorScala2.11.7TypesafeActivator1.3.9JasperReportsDesignerWindowsfontsforLinuxEclipseIDE
WhothisbookisforThisbookisforprofessionalswhowantlearnScala,aswellasfunctionalandreactivetechniques.Thisbookismainlyfocusedonsoftwaredevelopers,engineers,andarchitects.Thisisapracticalbookwithpracticalcode;however,wealsohavetheoryaboutfunctionalandreactiveprogramming.
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"ThenextstepistocreatetheenvironmentvariablecalledSCALA_HOME,andtoputtheScalabinariesinthePATHvariable."
Ablockofcodeissetasfollows:
packagescalabook.javacode.chap1;
publicclassHelloWorld{
publicstaticvoidmain(Stringargs[]){
System.out.println("HellowWorld");
}
}
Anycommand-lineinputoroutputiswrittenasfollows:
exportJAVA_HOME=~/jdk1.8.0_77
exportPATH=$PATH:$JAVA_HOME/bin
Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:"TheActorbehavioristhecodeyouwillhaveinsideyourActor."
Note
Warningsorimportantnotesappearinaboxlikethis.
Tip
Tipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook--whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,[email protected],andmentionthebook'stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORTtabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou'relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
ThecodebundleforthebookisalsohostedonGitHubathttps://github.com/PacktPublishing/Building-Applications-with-Scala.Wealsohaveothercodebundlesfromourrichcatalogofbooksandvideosavailableathttps://github.com/PacktPublishing/.Checkthemout!
DownloadingthecolorimagesofthisbookWealsoprovideyouwithaPDFfilethathascolorimagesofthescreenshots/diagramsusedinthisbook.Thecolorimageswillhelpyoubetterunderstandthechangesintheoutput.Youcandownloadthisfilefromhttps://www.packtpub.com/sites/default/files/downloads/BuildingApplicationswithScala_ColorImages.pdf
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks--maybeamistakeinthetextorthecode--wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusatcopyright@packtpub.comwithalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,[email protected],andwewilldoourbesttoaddresstheproblem.
Chapter1.IntroductiontoFP,Reactive,andScalaInourfirstchapter,wewilllearnthebasicconceptsofFunctionalPrograming(FP),reactiveprogramming,andtheScalalanguage.Theseconceptsarelistedasfollows:
SettingupaScaladevelopmentenvironmentwithEclipseScalaIDE.Basicconstructsofthelanguagelikevar,val,for,if,switch,andoperatoroverload.ThedifferencebetweenFPandobject-orientedprogramming.PrinciplesofpureFP:immutability,nosideeffects,statediscipline,composition,andhigherorderfunctions.ConceptsofFPsuchaslambda,recursion,forcomprehensions,partialfunctions,Monads,currying,andfunctions.PatternMatcher,recursion,reflection,packageobjects,andconcurrency.
Let'sgetgoing!
FunctionalprogrammingFPisnotnewatall.TheveryfirstimplementationofFPisLispandisdatedfromthe1950s.Currently,wearelivinginapost-functionalprogrammingera,wherewehavethestrongmathprinciplesandideasfromthe50smixedwiththemostmodernandbeautifulpieceofengineering,alsoknowastheJavaVirtualMachine(JVM).Scalaisapost-functionalprogramminglanguagebuiltontopoftheJVM.BeingontopoftheJVMgivesusalotofbenefitssuchasthefollowing:
Scalaisapost-functionalprogramminglanguagebuiltontopoftheJVM.BeingontopoftheJVMgivesusalotofbenefitssuchasthefollowing:
Reliabilityandperformance:Javaisusedby10outof10topwebsiteswehavecurrently,likeNetflix,Apple,Uber,Twitter,Yahoo,eBay,Yelp,LinkedIn,Google,Amazon,andmanyothers.JVMisthebestsolutionatscaleandisbattle-testedbytheseweb-scalecompanies.NativeJVMeco-system:FullaccesstoalloftheJavaecosystemincludingframeworks,libraries,servers,andtools.Operationsleverage:YouroperationteamcanrunScalainthesamewaytheyrunJava.Legacycodeleverage:ScalaallowsyoutoeasilyintegrateScalacodewithJavacode.ThisfeatureisgreatbecauseitenablesJavalegacysystemintegrationinsidethebox.Javainteroperability:AcodewritteninScalacanbeaccessedinJava.
Scalawascreatedin2001atEPFLbyMartinOdersky.Scalaisastrongstatic-typedlanguage,andwasinspiredbyanotherfunctionallanguagecalledHaskell.ScalaaddressesseveralcriticismsoftheJavalanguage,anddeliversabetterdeveloperexperiencethroughlesscodeandmoreconciseprograms,withoutlosingperformance.
ScalaandJavasharethesameinfrastructureastheJVM,butintermsofdesign,ScalaisadifferentlanguageincomparisonwithJava.Javaisanimperativeobject-orientedlanguageandScalaisapost-functional,multiparadigmprograminglanguage.FPworkswithdifferentprinciplesthanobject-orientedprograming(OOP).OOPgotverypopularandwellestablishedinenterprisethankstolanguageslikeJava,C#,Ruby,andPython.However,languageslikeScala,Clojure,F#,andSwiftaregainingahugemomentum,andFPhasgrownalotinthelast10years.Mostofthenewlanguagesarepurefunctional,post-functional,orhybrid(likeJava8).Inthisbook,youwillseeScalacodecomparedwithJavacodesoyoucanseebyyourselfhowScalaiswaymorecompact,objective,anddirectthanJavaandimperativeOOPlanguages.
FPstartedatacademiaandspreadtotheworld;FPiseverywhere.BigDataandStreamprocessingsolutionslikeHadoopandSpark(builtontopofScalaandAkka)arebuiltontopofFPideasandprinciples.FPspreadtoUIwithRxJavaScript-youcanevenfindFPinadatabasewithDatomic(Clojure).LanguageslikeClojureandScalamadeFPmorepracticalandattractivetoenterpriseandprofessionaldevelopers.Inthisbook,wewillbeexploring
bothprinciplesandpracticalaspectsoftheScalalanguage.
PrinciplesoffunctionalprogrammingFPisawayofthinking,aspecificstyleofconstructingandbuildingprograms.HavinganFPlanguagehelpsalotintermsofsyntax,butattheendoftheday,it'sallaboutideasanddevelopermindset.FPfavorsdisciplinedstatemanagementandimmutabilityinadeclarativeprogrammingwayratherthantheimperativeprogrammingmostlyusedbyOOPlanguagessuchasJava,Python,andRuby.
FPhasrootsinmathbacktoLambdacalculus-aformalsystemdevelopedinthe1930s.Lambdacalculusisamathematicalabstractionandnotaprogramminglanguage,butitiseasytoseeitsconceptsinprogramminglanguagesnowadays.
Imperativeprogrammingusesstatementstochangetheprogramstate.Inotherwords,thismeansyougivecommandstotheprogramtoperformactions.Thiswayofthinkingdescribesasequenceofstepsonhowtheprogramneedstooperate.WhatyouneedtokeepinmindisthekindofstylefocusonhowFPworksinadifferentway,focusingonwhattheprogramshouldaccomplishwithouttellingtheprogramhowtodoit.WhenyouarecodinginFP,youtendtousefewervariables,forloops,andIFS,andwritemorefunctionsandmakefunctioncomposition.
ThefollowingaretheCOREprinciplesofFP:
ImmutabilityDisciplinedstatePurefunctionsandnosideeffects/disciplinedstatesFirstclassfunctionsandhighorderfunctionsTypesystemsReferentialtransparency
Let'sunderstandtheseprinciplesindetail.
ImmutabilityTheconceptofimmutabilityistheCOREofFP,anditmeansthatonceyouassignavaluetosomething,thatvaluewon'tchange.Thisisveryimportant,becauseiteliminatessideeffects(anythingoutsideofthelocalfunctionscope),forinstance,changingothervariablesoutsidethefunction.Immutabilitymakesiteasiertoreadcode,becauseyouknowthefunctionthatyouareusingisapurefunction.Sinceyourfunctionhasadisciplinedstateanddoesnotchangeothervariablesoutsideofthefunction,youdon'tneedtolookatthecodeoutsidethefunctiondefinition.Thissoundslikeyou'renotworkingwithstateatall,sohowwoulditbepossibletowriteprofessionalapplicationsthisway?Well,youwillchangestatebutinaverydisciplinedway.Youwillcreateanotherinstanceoranotherpointertothatinstance,butyouwon'tchangethatvariable'svalue.Havingimmutabilityisthekeytohavingbetter,faster,andmorecorrectprograms,becauseyoudon'tneedtouselocksandyourcodeisparallelbynature.
DisciplinedstateSharedmutablestateisevil,becauseitismuchhardertoscaleandtorunitconcurrently.Whatissharedmutablestate?Asimplewaytoseeitisasaglobalvariablethatallyourfunctionshaveaccessto.Whyisthisbad?Firstofall,becauseitishardtokeepthisstatecorrectsincetherearemanyfunctionsthathavedirectaccesstothisstate.Second,ifyouareperformingrefactoring,thiskindofcodeisoftenthehardesttorefactoraswell.It'salsohardtoreadthiscode.Thisisbecauseyoucannevertrustthelocalmethod,sinceyourlocalmethodisjustonepartoftheprogram.Andwithmutablestate,youneedtolookupforallthefunctionsthatusethatvariable,inordertounderstandthelogic.It'shardtodebugfortheverysamereason.WhenyouarecodingwithFPprinciplesinmind,youavoid,asmuchaspossible,havingasharedmutablestate.Ofcourseyoucanhavestate,butyoushouldkeepitlocal,whichmeansinsideyourfunction.Thisisthestatediscipline:youusestate,butinaverydisciplinedway.Thisissimple,butitcouldbehardespeciallyifyouareaprofessionaldeveloper,becausethisaspectisnowusualtoseeinenterpriselanguagessuchasJava,.NET,Ruby,andPython.
PurefunctionsandnosideeffectsPurefunctionsaretheoneswithnosideeffects.Sideeffectsarebad,becausetheyareunpredictableandmakeyoursoftwarehardtotest.Let'ssayyouhaveamethodthatreceivesnoparametersandreturnsnothing--thisisoneoftheworstthingswecouldhave,becausehowdoyoutestit?Howcanyoureusethiscode?Thisisnotwhatwecallapurefunction.Whatarethepossiblesideeffects?Databasecall,globalvariables,IOcall,andsoon.Thismakessense,butyoucannothaveaprogramwithjustpurefunctions,becauseitwon'tbepractical.
First-classfunctionsandhigher-orderfunctionsFirst-classmeansthatthelanguagetreatsfunctionsasfirst-classcitizens.Inotherwords,itmeanshavinglanguagesupporttopassfunctionsasargumentstootherfunctionsandtoreturnvaluesasfunctions.First-classfunctionalsoimpliesthatthelanguageallowsyoutostorefunctionsasvariablesoranyotherdatastructure.
Higher-orderfunctionsarerelatedtoFirst-classfunctions,buttheyarenotthesamething.Higher-orderfunctionsoftenmeanslanguagesupportforpartialfunctionalapplicationandCurrying.Higher-orderfunctionsareamathematicalconceptwherefunctionsoperatewithotherfunctions.
Partialfunctionsarewhenyoucanfixavalue(argument)toaparticularfunction,whichyoumayormaynotchangelateron.Thisisgreatforfunctioncomposition.
Curryingisatechniquetotransformafunctionwithmultipleparametersinasequenceoffunctionswitheachfunctionhavingasingleargument.Scalalanguagedoesnotforcecurrying,however,languageslikeMLandHaskellalmostalwaysusethiskindoftechnique.
TypesystemsTypesystemisallaboutthecompiler.Theideaissimple:youcreateatypesystem,andbydoingso,youleveragethecompilertoavoidallkindsofmistakesanderrors.Thisisbecausethecompilerhelpsinmakingsurethatyouonlyhavetherighttypesasarguments,turnstatements,functioncomposition,andsoon.Thecompilerwillnotallowyoudomakeanybasicmistakes.ScalaandHaskellareexamplesoflanguagesthatareStrong-type.Meanwhile,CommonLisp,Scheme,andClojurearedynamiclanguagesthatmayacceptwrongvaluesduringcompilationtime.Oneofthebiggestbenefitsofthestrongtypesystemisthatyouhavetowritefewertests,becausethecompilerwilltakecareofseveralissuesforyou.Forinstance,ifyouhaveafunctionthatreceivesastring,itcouldbedangerous,becauseyoucanpassprettymuchanythinginastring.However,ifyouhaveafunctionthatreceivesatypecalledsalesman,thenyoudon'twriteavalidationtocheckifitisasalesman.Allthismaysoundsilly,butinarealapplication,thissaveslotsoflinesofcodeandmakesyouprogrambetter.Anothergreatbenefitofstrongtypingisthatyouhavebetterdocumentation,asyourcodebecomesyourdocumentation,andit'swaymoreclearwhatyoucanorcan'tdo.
ReferentialtransparencyReferentialtransparencyisaconceptwhichworksclosewithpurefunctionsandimmutabilitysinceyourprogramhasfewerassignmentstatements,andoftenwhenyouhaveit,youtendtoneverchangethatvalue.Thisisgreatbecauseyoueliminatesideeffectswiththistechnique.Duringprogramexecution,anyvariablecanbereplacedsincetherearenosideeffects,andtheprogrambecomesreferentiallytransparent.Scalalanguagemakesthisconceptveryclearthemomentyoudeclareavariable.
InstallingJava8andScala2.11ScalarequiresJVMtowork,soweneedgettheJDK8beforeinstallingScala.GototheOraclewebsite,anddownloadandinstallJDK8fromhttp://www.oracle.com/technetwork/pt/java/javase/downloads/index.html.
Onceyou'vedownloadedJava,weneedtoaddJavatothePATHvariable;otherwise,youcanusetheterminal.Wedothisasfollows:
$cd~/
$wget--no-cookies--no-check-certificate--header"Cookie:
gpw_e24=http%3A%2F%2Fwww.oracle.com%2F;oraclelicense=accept-securebackup-
cookie""
http://download.oracle.com/otn-pub/java/jdk/8u77-b03/jdk-8u77-linux-
i586.tar.gz"
$tar-xzvf$jdk-8u77-linux-x64.tar.gz
$rm-fjdk-8u77-linux-x64.tar.gz
ThenextstepistocreatetheenvironmentvariablecalledJAVA_HOME,andtoputtheJava8binariesinthePATHvariable.InLinux,weneedtoeditthe~/.bashrcfile,andexportthevariablesweneed,likeinthefollowing:
exportJAVA_HOME=~/jdk1.8.0_77
exportPATH=$PATH:$JAVA_HOME/bin
Savethefile,andthenonthesameterminalweneedtosourcethefilevia$source~/.bashrc
NowwecantestourJava8installation.Justtypein$java-version.Youshouldseesomethinglikethefollowing:
$java-version
javaversion"1.8.0_77"
Java(TM)SERuntimeEnvironment(build1.8.0_77-b03)
JavaHotSpot(TM)ServerVM(build25.77-b03,mixedmode)
Let'sgetstarted.WewillbeusingthelatestScalaversion2.11.8.However,thecodeinsidethisbookshouldworkwithanyScala2.11.xversion.Firstofall,let'sdownloadScalafromhttp://www.scala-lang.org/.
ScalaworksonWindows,Mac,andLinux.Forthisbook,IwillshowhowtouseScalaonUbuntuLinux(Debian-based).Openyourbrowserandgotohttp://www.scala-lang.org/download/.
Downloadscala2.11.8:itwillbeaTGZfile.Extractitandaddittoyourpath;otherwise,youcanusetheterminal.Dothisasfollows:
$cd~/
$wgethttp://downloads.lightbend.com/scala/2.11.8/scala-2.11.8.tgz
$tar-xzvfscala-2.11.8.tgz
$rm-rfscala-2.11.8.tgz
ThenextstepistocreatetheenvironmentvariablecalledSCALA_HOME,andtoputtheScalabinariesinthePATHvariable.InLinux,weneedtoeditthe~/.bashrcfileandexportthevariablesweneed,likeinthefollowing:
exportSCALA_HOME=~/scala-2.11.8/
exportPATH=$PATH:$SCALA_HOME/bin
Savethefile,andthen,onthesameterminal,weneedtosourcethefilevia$source~/.bashrc.
NowwecantestourScalainstallation.Justtypein$scala-version.Youshouldseesomethinglikethefollowing:
$scala-version
Scalacoderunnerversion2.11.8--Copyright2002-2016,LAMP/EPFL
YouhavesuccessfullyinstalledJava8andScala2.11.NowwearereadytostartlearningtheFPprinciplesinScala.Forthis,wewillbeusingtheScalaREPLinthebeginning.ScalaREPLisbundledwiththedefaultScalainstallation,andyoujustneedtotype$scalainyourterminalasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)ServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>
ScalaREPL
Congratulations!YouhaveinstalledJava8andScala2.11successfully.
ReadEvalPrintandLoop-REPLReadEvalPrintandLoop(REPL)isalsoknowasalanguageshell.Manyotherlanguageshaveshells,likeLisp,Python,andRubyforinstance.TheREPLisasimpleenvironmenttoexperimentthelanguagein.It'spossibletowriteverycomplexprogramsusingREPL,butthisisnottheREPLgoal.UsingREPLdoesnotinvalidatetheusageofanIDElikeEclipseorIntelliJIDEA.REPLisidealfortestingsimplecommandsandprogramswithouthavingtospendmuchtimeconfiguringprojectslikeyoudowithanIDE.TheScalaREPLallowsyoutocreateavariable,functions,classes,andcomplexfunctionsaswell.Thereisahistoryofeverycommandyouperform;thereissomelevelofautocompletetoo.AsaREPLuser,youcanprintvariablevaluesandcallfunctions.
ScalaHelloWorldusingtheREPLLet'sgetstarted.Goahead,openyourterminal,andtype$scalainordertoopentheScalaREPL.OncetheREPLisopen,youcanjusttype"HelloWorld".Bydoingthis,youperformtwooperations:evalandprint.TheScalaREPLwillcreateavariablecalledres0,andstoreyourStringthere.Thenitwillprintthecontentoftheres0variable.
ScalaREPLHelloWorldprogramWewillseehowtocreateHelloWorldprograminScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>"HelloWorld"
res0:String=HelloWorld
scala>
Scalaisahybridlanguage,whichmeansitisobject-orientedandfunctionalaswell.YoucancreateclassesandobjectsinScala.NextwewillcreateacompleteHelloWorldapplicationusingclasses.
Scalaobject-orientedHelloWorldprogramWewillseehowtocreateobject-orientedHelloWorldprograminScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>objectHelloWorld{
|defmain(args:Array[String])=println("HelloWorld")
|}
definedobjectHelloWorld
scala>HelloWorld.main(null)
HelloWorld
scala>
Thefirstthingyouneedtorealizeisthatweusethewordobjectinsteadofclass.TheScalalanguagehasdifferentconstructscomparedtoJava.ObjectisasingletoninScala.It'sthesameascodingthesingletonpatterninJava.
NextweseetheworddefthatisusedinScalatocreatefunctions.Intheprecedingprogram,wecreatethemainfunctionsimilartothewaywedoitinJava,andwecallthebuilt-infunctionprintlninordertoprinttheStringHelloWorld.ScalaimportssomeJavaobjectsandpackagesbydefault.CodinginScaladoesnotrequireyoutotype,forinstance,System.out.println("HelloWorld"),butyoucanifyouwant.Let'stakealookatitinthefollowingcode:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>System.out.println("HelloWorld")
HelloWorld
scala>
Wecanandwewilldobetter.Scalahassomeabstractionsforaconsoleapplication,sowecanwritethiscodewithalessernumberoflinesofcode.Toaccomplishthisgoal,weneedtoextendtheScalaclassApp.WhenweextendfromApp,weperforminheritanceandwedon'tneedtodefinethemainfunction.Wecanjustputallthecodeinthebodyoftheclass,whichisveryconvenientandmakesthecodecleanandsimpletoread.
ScalaHelloWorldAppintheScalaREPLWewillseehowtocreateScalaHelloWorldAppinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>objectHelloWorldextendsApp{
|println("HelloWorld")
|}
definedobjectHelloWorld
scala>HelloWorld
objectHelloWorld
scala>HelloWorld.main(null)
HelloWorld
scala>
AftercodingtheHelloWorldobjectintheScalaREPLwecanasktheREPLwhatHelloWorldis,andasyoumightrealize,theREPLwillanswerthatHelloWorldisanobject.ThisisaveryconvenientScalawaytocodeconsoleapplications,becausewecanhaveaHelloWorldapplicationwithjustthreelinesofcode.Sadly,tohavethesameprograminJava,itrequiredwaymorecode.Javaisagreatlanguageforperformance,butitisaverboselanguagecomparedwithScala,forinstance.
JavaHelloWorldapplicationWewillseehowtocreateJavaHelloWorldapplicationasfollows:
packagescalabook.javacode.chap1;
publicclassHelloWorld{
publicstaticvoidmain(Stringargs[]){
System.out.println("HellowWorld");
}
}
TheJavaapprequiredsixlinesofcode,whileinScala,wewereabletodothesamewith50%lesscode(threelinesofcode).Thisisaverysimpleapplication.Whenwearecodingcomplexapplications,thisdifferencegetsbigger,asaScalaapplicationendsupwithwaylesscodethanJava.
Remember,weuseanobjectinScalainordertohaveaSingleton(DesignPatternthatmakessureyouhavejustoneinstanceofaclass),andifwewantthesameinJava,thecodewouldbesomethinglikethefollowing:
packagescalabook.javacode.chap1;
publicclassHelloWorldSingleton{
privateHelloWorldSingleton(){}
privatestaticclassSingletonHelper{
privatestaticfinalHelloWorldSingletonINSTANCE=
newHelloWorldSingleton();
}
publicstaticHelloWorldSingletongetInstance(){
returnSingletonHelper.INSTANCE;
}
publicvoidsayHello(){
System.out.println("HelloWorld");
}
publicstaticvoidmain(String[]args){
getInstance().sayHello();
}
}
It'snotjustaboutthesizeofthecode,butalsoaboutconsistencyandthelanguageprovidingmoreabstractionsforyou.Ifyouwritelesscode,youwillhavefewerbugsinyoursoftwareattheendoftheday.
Scalalanguage-thebasicsScalaisastaticallytypedlanguagewithaveryexpressivetypesystemwhichenforcesabstractionsinasafeyetcoherentmanner.AllvaluesinScalaareJavaobjects(primitiveswhichareunboxedatruntime),becauseattheendoftheday,ScalarunsontheJavaJVM.ScalaenforcesimmutabilityasacoreFPprinciple.ThisenforcementhappensinmultipleaspectsoftheScalalanguage,forinstance,whenyoucreateavariable,youdoitinanimmutableway,whenyouuseancollection,youwilluseaimmutablecollection.Scalaalsoletsyouusemutablevariablesandmutablestructures,butbydesign,itfavorsimmutableones.
Scalavariables-varandvalWhenyouarecodinginScala,youcreatevariablesusingtheoperatorvar,oryoucanusetheoperatorval.Theoperatorvarallowsyoutocreateamutablestate,whichisfineaslongasyoumakeitlocal,followtheCORE-FPprinciplesandavoidamutablesharedstate.
ScalaREPLvarusageWewillseehowtousevarinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>varx=10
x:Int=10
scala>x
res0:Int=10
scala>x=11
x:Int=11
scala>x
res1:Int=11
scala>
However,Scalahasamoreinterestingconstructcalledval.Usingthevaloperatormakesyourvariablesimmutable,andthismeansyoucan'tchangethevalueonceyou'vesetit.IfyoutrytochangethevalueofthevalvariableinScala,thecompilerwillgiveyouanerror.AsaScaladeveloper,youshouldusethevariablevalasmuchaspossible,becausethat'sagoodFPmindset,anditwillmakeyourprogramsbetter.InScala,everythingisanobject;therearenoprimitives--thevarandvalrulesapplyforeverythingitcouldbutanIntorStringorevenaclass.
ScalavalusageattheScalaREPLWewillseehowtousevalinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valx=10
x:Int=10
scala>x
res0:Int=10
scala>x=11
<console>:12:error:reassignmenttoval
x=11
^
scala>x
res1:Int=10
scala>
CreatingimmutablevariablesRightnow,let'sseehowwedefinethemostcommontypesinScalasuchasInt,Double,Boolean,andString.Remember,youcancreatethesevariablesusingvalorvardependingonyourneeds.
ScalavariabletypeintheScalaREPLWewillseeScalavariabletypeinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valx=10
x:Int=10
scala>valy=11.1
y:Double=11.1
scala>valb=true
b:Boolean=true
scala>valf=false
f:Boolean=false
scala>vals="ASimpleString"
s:String=ASimpleString
scala>
Forthevariablesintheprecedingcode,wedidnotdefinethetype.Scalalanguagefiguresitoutforus.However,itispossibletospecifythetypeifyouwant.InScala,thetypecomesafterthenameofthevariable.
ScalavariableswithexplicittypingattheScalaREPLWewillseeScalavariableswithexplicittypingatScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valx:Int=10
x:Int=10
scala>valy:Double=11.1
y:Double=11.1
scala>vals:String="MyString"
s:String="MyString"
scala>valb:Boolean=true
b:Boolean=true
scala>
ScalaconditionalandloopsstatementsLikeanyotherlanguage,Scalahassupportforconditionalstatementslikeifandelse.WhileJavahasaswitchstatement,ScalahasamorepowerfulandfunctionalstructurecalledPatternMatcher,whichwewillcoverlaterinthischapter.Scalaallowsyoutouseifstatementsduringvariableassignments,whichisverypracticalaswellasuseful.
IfstatementsinScalaREPLWewillseehowtouseifstatementsinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valx=10
x:Int=10
scala>if(x==10)
|println("Xis10")
Xis10
scala>valy=if(x==10)11
y:AnyVal=11
scala>y
res1:AnyVal=11
scala>
Intheprecedingcode,youcanseethatwesetthevariableybasedonanifcondition.Scalaifconditionsareverypowerful,andtheyalsocanbeusedinreturnstatements.
IfstatementsinreturnstatementsinScalaREPLWewillseehowtouseifstatementsinreturnstatementsinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valx=10
x:Int=10
scala>defsomeFunction=if(x==10)"Xis10"
someFunction:Any
scala>someFunction
res0:Any=Xis10
scala>
Scalasupportselsestatementstoo,andyoualsocanusetheminvariablesandreturnstatementsaswellasfollows:
~$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valx=10
x:Int=10
scala>if(x==10){
|println("Xis10")
|}else{
|println("Xissomethingelse")
|}
Xis10
scala>
NowyouwilllearnhowtouseforloopsinScala.ForloopsareverypowerfulinScala.Wewillstartwiththebasicsandlaterwewillmoveontofunctionalloopsusedforcomprehensions,alsoknowasListcomprehensions.
InScala,forloopsworkwithranges,whichisanotherScaladatastructurethatrepresentsnumbersfromastartingpointtoanendpoint.Therangeiscreatedusingtheleftarrowoperator(<-).Scalaallowsyoutohavemultiplerangesinthesameforloopaslongasyouusethesemicolon(;).
Youalsocanuseifstatementsinordertofilterdatainsideforloops,andworksmoothlywithListstructures.Scalaallowsyoutocreatevariablesinsideaforloopaswell.Rightnow,let'sseesomecodewhichillustratesthevariousforloopusagesinScalalanguage.
BasicforloopinScalaREPLWewillseehowtousebasicforloopinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>for(i<-1to10)
|println("i*"+i+"="+i*10)
i*1=10
i*2=20
i*3=30
i*4=40
i*5=50
i*6=60
i*7=70
i*8=80
i*9=90
i*10=100
scala>
Rightnow,wewillcreateaforloopusingaScaladatastructurecalledList.Thisisveryuseful,becauseinthefirstlineofcode,youcandefineaListaswellassetitsvaluesinthesameline.SinceweareusingtheListstructure,youdon'tneedtopassanyotherargumentbesidestheListitself.
ForwithListinScalaREPLWewillseehowtouseforwithListinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>vallistOfValues=List(1,2,3,4,5,6,7,8,9,10)
listOfValues:List[Int]=List(1,2,3,4,5,6,7,8,9,10)
scala>for(i<-listOfValues)println(i)
1
2
3
4
5
6
7
8
9
10
scala>
Next,wecanuseforloopswithifstatementsinordertoapplysomefiltering.Laterinthisbook,wewillapproachamorefunctionalwaytoapproachfilteringusingfunctions.Forthiscode,let'ssaywewanttogetjusttheevennumbersonthelistandprintthem.
Forwithifstatementsforfiltering-ScalaREPLWewillseehowtouseforwithifstatementsinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>vallistOfValues=List(1,2,3,4,5,6,7,8,9,10)
listOfValues:List[Int]=List(1,2,3,4,5,6,7,8,9,10)
scala>for(i<-listOfValues)if(i%2==0)println(i)
2
4
6
8
10
scala>
JavacodeforfilteringevennumbersInScalalanguage,wejustneedtwolinesofcodetoperformthisfiltering,whereasinJavaitwouldhaverequiredatleastelevenlinesofcodeasyouseeinthefollowingcode:
packagescalabook.javacode.chap1;
importjava.util.Arrays;
importjava.util.List;
publicclassForLoopsEvenNumberFiltering{
publicstaticvoidmain(String[]args){
List<Integer>listOfValues=Arrays.asList(
newInteger[]{1,2,3,4,5,6,7,8,9,10});
for(Integeri:listOfValues){
if(i%2==0)System.out.println(i);
}
}
}
ForcomprehensionsAlsoknownaslistorsequencecomprehensions,forcomprehensionsareoneoftheFPwaystoperformloops.ThisisalanguagesupporttocreateListstructureorcollectionsbasedonothercollections.ThistaskisperformedinaSetBuildernotation.AnotherwaytoaccomplishthesamegoalwouldbebyusingtheMapandfilterfunctions,whichwewillcoverlaterinthischapter.Forcomprehensionscanbeusedinageneratorform,whichwouldintroducenewvariablesandvalues,orinareductionistway,whichwouldfiltervaluesresultingintoanewcollectionorsequence.Thesyntaxis:for(expt)yielde,wheretheyieldoperatorwilladdnewvaluestoanewcollection/sequencethatwillbecreatedfromtheoriginalsequence.
ForcomprehensioninScalaREPLWewillseehowtouseforcomprehensioninScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valnames=Set("Diego","James","John","Sam","Christophe")
names:scala.collection.immutable.Set[String]=Set(John,Sam,Diego,James,
Christophe)
scala>
scala>valbrazilians=for{
|name<-names
|initial<-name.substring(0,1)
|}yieldif(name.contains("Die"))name
brazillians:scala.collection.immutable.Set[Any]=Set((),Diego)
scala>
Intheprecedingcode,wecreateasetofnames.Asyoucansee,Scala,bydefault,prefersimmutabledatastructuresandusesimmutable.Set.Whenweapplytheforloop,wearesimplyfilteringonlythenameswhichcontainaspecificsubstring,andthen,usingtheyieldoperator,wearecreatinganewSetstructure.Theyieldoperatorwillkeepthestructureyouareusing.Forinstance,ifweuseListstructure,itwouldcreateaListinsteadofaSetstructure,theyieldoperatorwillalwayskeepthesamedatacollectionyouhaveonthevariable.AnotherinterestingaspectoftheprecedingcodeisthefactthatweareholdingtheresultoftheforcomprehensioninavariablecalledBrazilians.Javadoesnothaveforcomprehensions,butwecouldusesimilarcodealthoughitwouldrequirewaymorelinesofcode.
JavacodeforperformingfilteringwithcollectionsWewillseehowtouseJavacodeforperformingfilteringwithcollectionsasfollows:
packagescalabook.javacode.chap1;
importjava.util.LinkedHashSet;
importjava.util.Set;
publicclassJavaNoForComprehension{
publicstaticvoidmain(String[]args){
Set<String>names=newLinkedHashSet<>();
Set<String>brazillians=newLinkedHashSet<>();
names.add("Diego");
names.add("James");
names.add("John");
names.add("Sam");
names.add("Christophe");
for(Stringname:names){
if(name.contains("Die"))brazillians.add(name);
}
System.out.println(brazillians);
}
}
ScalacollectionsIntheprevioussection,wesawhowtocreatetheListandSetstructuresinScalainanimmutableway.NowwewilllearntoworkwiththeListandSetstructuresinamutableway,andalsowithothercollectionssuchassequences,tuples,andMaps.Let'stakealookattheScalacollectionshierarchytree,asshowninthefollowingdiagram:
Nowlet'stakealookattheScalaSeqclasshierarchy.Asyoucansee,Seqistraversableaswell.
Scalacollectionsextendfromtraversable,whichisthemaintraitofallcollection'sdescends.Liststructures,forinstance,extendfromSeqclasshierarchy,whichmeanssequence-Listisakindofsequence.AllthesetreesareimmutableormutabledependingontheScalapackageyouendupusing.
Let'sseehowtoperformbasicmutableoperationswithListstructuresinScala.Inordertohavefilterandremovaloperations,weneeduseaBuffersequenceasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>varms=scala.collection.mutable.ListBuffer(1,2,3)
ms:scala.collection.mutable.ListBuffer[Int]=ListBuffer(1,2,3)
scala>ms+=4
res0:scala.collection.mutable.ListBuffer[Int]=ListBuffer(1,2,3,4)
scala>ms+=5
res1:scala.collection.mutable.ListBuffer[Int]=ListBuffer(1,2,3,4,5)
scala>ms+=6
res2:scala.collection.mutable.ListBuffer[Int]=ListBuffer(1,2,3,4,5,6)
scala>ms(1)
res3:Int=2
scala>ms(5)
res4:Int=6
scala>ms-=5
res5:scala.collection.mutable.ListBuffer[Int]=ListBuffer(1,2,3,4,6)
scala>ms-=6
res6:scala.collection.mutable.ListBuffer[Int]=ListBuffer(1,2,3,4)
scala>
Let'sseethenextsetofcode.
Creating,removing,andgettinganitemfromamutablelistinScalaREPLWewillseehowtocreate,remove,andgetanitemfromamutablelistinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>varnames=scala.collection.mutable.SortedSet[String]("Diego",
"Poletto","Jackson")
names:scala.collection.mutable.SortedSet[String]=TreeSet(Diego,Jackson,
Poletto)
scala>names+="Sam"
res2:scala.collection.mutable.SortedSet[String]=TreeSet(Diego,Jackson,
Poletto,Sam)
scala>names("Diego")
res4:Boolean=true
scala>names-="Jackson"
res5:scala.collection.mutable.SortedSet[String]=TreeSet(Diego,Poletto,
Sam)
scala>
Haveyoueverwantedtoreturnmultiplevaluesinamethod?Well,inJavayouhavetocreateaclass,butinScala,thereisamoreconvenientwaytoperformthistask,andyouwon'tneedtocreatenewclasseseachtime.Tuplesallowyoutoreturnorsimplyholdmultiplevaluesinmethodswithouthavingtocreateaspecifictype.
ScalatuplesWewillseeScalatuplesasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valconfig=("localhost",8080)
config:(String,Int)=(localhost,8080)
scala>config._1
res0:String=localhost
scala>config._2
res1:Int=8080
scala>
Scalahasspecialmethodscalled_1and_2whichyoucanusetoretrieveatuple'svalues.Theonlythingyouhavetokeepinmindisthefactthatvaluesarekeptintheorderofinsertioninthetuple.
Scalahasaverypracticalandusefulcollectionlibrary.AMap,forinstance,isakey/valuepairthatcanberetrievedbasedonthekey,whichisunique.However,Mapvaluesdonotneedtobeunique.LikeotherScalacollections,youhavemutableandimmutableMapcollections.KeepinmindthatScalafavorsimmutablecollectionsovermutableones.
ScalaimmutableMapinScalaREPLWewillseeScalaimmutableMapinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valnumbers=Map("one"->1,
|"two"->2,
|"three"->3,
|"four"->4,
|"five"->5,
|"six"->6,
|"seven"->7,
|"eight"->8,
|"nine"->9,
|"ten"->10)
numbers:scala.collection.immutable.Map[String,Int]=Map(four->4,three->
3,two->2,six->6,seven->7,ten->10,five->5,nine->9,one->1,
eight->8)
scala>
scala>numbers.keys
res0:Iterable[String]=Set(four,three,two,six,seven,ten,five,nine,
one,eight)
scala>
scala>numbers.values
res1:Iterable[Int]=MapLike(4,3,2,6,7,10,5,9,1,8)
scala>
scala>numbers("one")
res2:Int=1
scala>
Asyoucansee,Scalausesscala.collection.immutable.MapwhenyoucreateaMapusingMap().Bothkeysandvaluesareiterable,andyoucanhaveaccesstoallthekeyswiththekeysmethodortoallthevaluesusingthevaluesmethod.
ScalamutableMapsatScalaREPLWewillseeScalamutableMapinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valmap=scala.collection.mutable.HashMap.empty[Int,String]
map:scala.collection.mutable.HashMap[Int,String]=Map()
scala>map+=(1->"one")
res0:map.type=Map(1->one)
scala>map+=(2->"two")
res1:map.type=Map(2->two,1->one)
scala>map+=(3->"three")
res2:map.type=Map(2->two,1->one,3->three)
scala>map+=(4->"mutable")
res3:map.type=Map(2->two,4->mutable,1->one,3->three)
scala>
Ifyouaredealingwithmutablestate,youhavetobeexplicitandthisisgreatinScala,becauseitincreasesdevelopers'awarenessandavoidsmutablesharedstatebydefault.So,inordertohaveamutableMap,weneedtoexplicitlycreatetheMapwithscala.collection.mutable.HashMap.
MonadsMonadsarecombinableparametrizedcontainertypeswhichhavesupportforhigher-orderfunctions.Rememberhigher-orderfunctionsarefunctionswhichreceivefunctionsasparametersandreturnfunctionsasresults.OneofthemostusedfunctionsinFPisMap.Maptakesafunction,appliesittoeachelementinthecontainer,andreturnsanewcontainer.
ScalaMapfunctioninScalaREPLWewillseeMapfunctioninScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>
scala>valnumbers=List(1,2,3,4,5,6)
numbers:List[Int]=List(1,2,3,4,5,6)
scala>defdoubleIt(i:Int):Double=i*2
doubleIt:(i:Int)Double
scala>valdoubled=numbers.map(doubleIt_)
doubled:List[Double]=List(2.0,4.0,6.0,8.0,10.0,12.0)
scala>valdoubled=numbers.map(2.0*_)
doubled:List[Int]=List(2.0,4.0,6.0,8.0,10.0,12.0)
scala>
Intheprecedingcode,wecreatedalistofnumberscontaining1,2,3,4,5,and6.WealsodefinedaScalafunctioncalleddoubleIt,whichreceivesanintegerandmultipliesitby2.0resultinginadoublenumber.ThemapfunctioncallsdoubleItforeachelementintheList(1,2,3,4,5,6),andtheresultisanewcontainer,abrandnewListinstancecontainingthenewvalues.
Scalahassomesyntacticalsugarwhichhelpsustobemoreproductive.Forinstance,youmayrealizethatinthepreviouscode,wealsodid-2.0*_.Theunderscoreisaspecialoperatorforthisspecificcase--itmeansthecurrentvalueisbeingiteratedintothecollection.Scalawillcreateafunctionfromthisexpressionforus.
Asyoumighthaverealized,mapfunctionsareprettyusefulforlotsofreasons:onereasonisthatyoucandocomplexcomputationswithoutusingtheforloopexplicitly,andthismakesyourcodefunctional.Secondly,wecanuseamapfunctiontoconvertelementtypesfromonetypetoanother.That'swhatwedidinthepreviouscode:wetransformedalistofintegersintoalistofdoubles.Lookatthefollowing:
scala>valone=Some(1)
one:Some[Int]=Some(1)
scala>valoneString=one.map(_.toString)
oneString:Option[String]=Some(1)
Themapfunctionoperatesoverseveraldatastructuresandnotonlycollections,asyoucanseeinthepreviouscode.YoucanusethemapfunctiononprettymucheverythinginScalalanguage.
Themapfunctionisgreat,butyoucanendupwithnestedstructures.That'swhy,whenweareworkingwithMonads,weuseaslightlydifferentversionofthemapfunctioncalledflatMap,whichworksinaverysimilarwaytothemapfunction,butreturnsthevaluesinaflatforminsteadofnestedvalues.
Inordertohaveamonad,youneedtohaveamethodcalledflatMap.OtherfunctionlanguagessuchasHaskellcallflatMapasbind,andusetheoperator>>=.Thesyntaxchangeswiththelanguage,buttheconceptisthesame.
Monadscanbebuiltindifferentways.InScala,weneedasingleargumentconstructorwhichwillworkasamonadfactory.Basically,theconstructorreceivesonetype,A,andreturnsMonad[A]orjustM[A].Forinstance,unit(A)foraListwillbe==List[A]andunit(A),whereaisanOption==Option[A].InScala,youdon'tneedtohaveunit;thisisoptional.TohaveamonadinScala,youneedtohavemapandflatMapimplemented.
WorkingwithMonadswillmakeyouwritealittlebitmorecodethanbefore.However,youwillgetawaybetterAPI,whichwillbeeasiertoreuseandyourpotentialcomplexitywillbemanaged,becauseyouwon'tneedtowriteacomplexcodefullofifandforloops.Thepossibilitiesareexpressedthroughthetypes,andthecompilercancheckitforyou.LetusseeasimplemonadexampleinScalalanguage:
OptionMonadinScalaWewillseeoptionMonadinScalaasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>vala:Option[Int]=Some(1)
a:Option[Int]=Some(1)
scala>a.get
res0:Int=1
scala>valb:Option[Int]=None
b:Option[Int]=None
scala>b.get
java.util.NoSuchElementException:None.get
atscala.None$.get(Option.scala:347)
atscala.None$.get(Option.scala:345)
...32elided
scala>b.getOrElse(0)
res2:Int=0
scala>a==b
res3:Boolean=false
scala>
InHaskell,thisalsoknownastheMaybemonad.Optionmeansoptionalvalue,becausewearenot100%sureifthevaluewillbepresent.Inordertoexpressavalue,weusetheSometype,andinordertoexpressthelackofvalue,weusenone.OptionMonadsaregreat,theymakeyourcodemoreexplicit,becauseamethodmightreceiveorreturnanoption,whichmeansyouareexplicitlysayingthiscouldbenull.However,thistechniqueisnotonlymoreexpressivebutalsosafer,sinceyouwon'tgetanullpointer,becauseyouhaveacontaineraroundthevalue.Although,ifyoucallthemethodgetinOptionanditisnone,youwillgetaNoSuchelementException.Inordertofixthis,youcanusethemethodgetOrElse,andyoucansupplyafallbackvaluewhichwillbeusedinthecaseofnone.Alright,butyoumightbewonderingwheretheflatMapmethodis.Don'tworry,ScalaimplementsthismethodforusintotheOptionabstraction,soyoucanuseitwithnoissues.
scala>valc=Some("one")
c:Some[String]=Some(one)
scala>c.flatMap(s=>Some(s.toUpperCase))
res6:Option[String]=Some(ONE)
TheScalaREPLcanperformautocompleteforyou.IfyoutypeC+Tab,youwillseealltheavailablemethodsfortheSomeclass.Themapfunctionisavailableforyoutouse,andasIsaidbefore,thereisnounitfunctioninScalawhatsoever.However,itisnotwrongifyouaddinyourAPIs.
AlistofallmethodsusingtheScalaREPLFollowingarethelistofallmethodsusingtheScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valc=Some("one")
c:Some[String]=Some(one)
scala>c.
++countforeachiteratorproductArity
seqtoBufferunzip
++:dropgenericBuilderlastproductElement
sizetoIndexedSequnzip3
/:dropRightgetlastOptionproductIterator
slicetoIterableview
:\dropWhilegetOrElsemapproductPrefix
slidingtoIteratorwithFilter
WithFilterequalsgroupBymax
reducespantoLeftx
addStringexistsgroupedmaxBy
reduceLeftsplitAttoListzip
aggregatefilterhasDefiniteSizeminreduceLeftOption
stringPrefixtoMapzipAll
canEqualfilterNothashCodeminByreduceOption
sumtoRightzipWithIndex
collectfindheadmkStringreduceRight
tailtoSeq
collectFirst
flatMapheadOptionnonEmptyreduceRightOptiontails
toSet
companionflatten
initorElsereprtake
toStream
containsfoldinitsorNull
sameElementstakeRighttoString
copyfoldLeftisDefinedpar
scantakeWhiletoTraversable
copyToArrayfoldRightisEmptypartitionscanLeft
totoVector
copyToBufferforallisTraversableAgainproductscanRight
toArray
transpose
scala>c
Scalaclass,traits,andOOprogrammingAsahybridpost-functionallanguage,ScalaallowsyoutowriteOOcodeandcreateclassesaswell.Rightnowwewilllearnhowtocreateclassesandfunctionsinsideclasses,andalsohowtoworkwithtraits,whicharesimilartoJavainterfacesinconceptbutwaymorepowerfulinpractice.
AsimpleScalaclassinScalaREPLWewillseeasimpleScalaclassinScalaREPLasfollows:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>classCalculator{
|defadd(a:Int,b:Int):Int=a+b
|defmultiply(n:Int,f:Int):Int=n*f
|}
definedclassCalculator
scala>
scala>valc=newCalculator
c:Calculator=Calculator@380fb434
scala>c.add(1,2)
res0:Int=3
scala>c.multiply(3,2)
res1:Int=6
scala>
Atfirstglance,theprecedingcodelookslikeJava.Butlet'saddconstructors,getters,andsetters,andthenyoucanseehowmuchwecanaccomplishwithjustafewlinesofcode.
ScalaplainoldJavaobjectinScalaREPLFollowingisaScalaplainoldJavaobjectinScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>classPerson(
|@scala.beans.BeanPropertyvarname:String="",
|@scala.beans.BeanPropertyvarage:Int=0
|){
|name=name.toUpperCase
|overridedeftoString="name:"+name+"age:"+age
|}
definedclassPerson
scala>
scala>valp=newPerson("Diego",31)
p:Person=name:DIEGOage:31
scala>valp1=newPerson(age=31,name="Diego")
p1:Person=name:DIEGOage:31
scala>p.getAge
res0:Int=31
scala>p1.getName
res1:String=DIEGO
scala>
ConstructorsinScalaarejustlinesofcode.Youmightrealizethatwegetthenamevariable,andapplyafunctiontochangethegivennametouppercaseintheprecedingexample.Ifyouwant,youcanputasmanylinesasyouwant,andyoucanperformasmanycomputationsasyouwish.
Onthissamecode,weperformmethodoverridingaswell,becauseweoverridethetoStringmethod.InScala,inordertodoanoverride,youneedtousetheoverrideoperatorinfrontofthefunctiondefinition.
WejustwroteaPlainOldJavaObject(POJO)withveryfewlinesofcodeinScala.Scalahasaspecialannotationcalled@scala.beans.BeanProperty,whichgeneratesthegetterandsettermethodforyou.Thisisveryuseful,andsaveslotsoflinesofcode.However,thetargetneedstobepublic;youcan'taapplyBeanPropertyannotationontopofaprivatevarorvalobject.
PersonclassinJavaFollowingisaPersonclassinJava:
packagescalabook.javacode.chap1;
publicclassJavaPerson{
privateStringname;
privateIntegerage;
publicJavaPerson(){}
publicJavaPerson(Stringname,Integerage){
super();
this.name=name;
this.age=age;
}
publicJavaPerson(Stringname){
super();
this.name=name;
}
publicJavaPerson(Integerage){
super();
this.age=age;
}
publicIntegergetAge(){
returnage;
}
publicvoidsetAge(Integerage){
this.age=age;
}
publicStringgetName(){
returnname;
}
publicvoidsetName(Stringname){
this.name=name;
}
}
TraitsandinheritanceIt'spossibletodoinheritanceinScalaaswell.Forsuchatask,youusetheoperatorextendaftertheclassdefinition.Scalajustallowsyoutoextendoneclass,justlikeJava.JavadoesnotallowmultipleinheritancelikeC++.However,ScalaallowsitbyusingtheMixingtechniquewithtraits.ScalatraitsarelikeJavainterface,butyoucanalsoaddconcretecode,andyouareallowedtohaveasmanytraitsasyouwantinyourcode.
ScalainheritancecodeinScalaREPLFollowingisaScalainheritancecodeinScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>classPerson(
|@scala.beans.BeanPropertyvarname:String="",
|@scala.beans.BeanPropertyvarage:Int=0
|){
|name=name.toUpperCase
|overridedeftoString="name:"+name+"age:"+age
|}
definedclassPerson
scala>
scala>classLowerCasePerson(name:String,age:Int)extendsPerson(name,age){
|setName(name.toLowerCase)
|}
definedclassLowerCasePerson
scala>
scala>valp=newLowerCasePerson("DIEGOPACHECO",31)
p:LowerCasePerson=name:diegopachecoage:31
scala>p.getName
res0:String=diegopacheco
scala>
ScaladoesnotmakeconstructorsinheritancelikeJava.Soyouneedtorewritetheconstructorsandpassthevaluesthroughasuperclass.Allcodeinsidetheclasswillbethesecondaryconstructor.Allcodeinsideparentheses()intheclassdefinitionwillbetheprimaryconstructor.It'spossibletohavemultipleconstructorsusingthethisoperator.Forthisparticularimplementation,wechangedthedefaultbehaviorandaddednewconstructorcodeinordertomakethegivennamelowercase,insteadofthedefaultuppercasedefinedbythePersonsuperclass.
ScalatraitssamplecodeinScalaREPLFollowingisaScalatraitssamplecodeinScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>traitCar
definedtraitCar
scala>
scala>traitSportCar{
|valbrand:String
|defrun():String="RghhhhhRghhhhhRghhhhh...."
|}
definedtraitSportCar
scala>
scala>traitPrintable{
|defprintIt:Unit
|}
definedtraitPrintable
scala>
scala>classBMWextendsCarwithSportCarwithPrintable{
|overridevalbrand="BMW"
|overridedefprintIt:Unit=println(brand+"does"+run())
|}
definedclassBMW
scala>
scala>valx1=newBMW
x1:BMW=BMW@22a71081
scala>x1.printIt
BMWdoesRghhhhhRghhhhhRghhhhh....
scala>
Intheprecedingcode,wecreatedmultipletraits.OneiscalledCar,whichisthemothertrait.Traitssupportinheritanceaswell,andwehaveitwiththeSportCartraitwhichextendsfromtheCartrait.TheSportCartraitdemandsavariablecalledbrand,anddefinesaconcreteimplementationofthefunctionrun.Finally,wehaveaclasscalledBMWwhichextendsfrommultipletraits--thistechniqueiscalledmixing.
ScalatraitsusingvariablemixingtechniqueatScalaREPLFollowingisaScalatraitsusingvariablemixingtechniqueatScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>traitSportCar{
|defrun():String="RghhhhhRghhhhhRghhhhh...."
|}
definedtraitSportCar
scala>
scala>valbmw=newObjectwithSportCar
bmw:SportCar=$anon$1@ed17bee
scala>bmw.run
res0:String=RghhhhhRghhhhhRghhhhh....
scala>
Scalaisaverypowerfullanguageindeed.It'spossibletoaddtraitstoavariableatruntime.Whenyoudefineavariable,youcanusethewithoperatoraftertheassignment.Thisisaveryusefulfeature,becauseitmakesiteasiertomakefunctioncomposition.Youcanhavemultiplespecializedtraitsandjustaddtheminyourvariablesasyouneedthem.
Scalaallowsyoutocreatethetypealiasaswell,thisisaverysimpletechniquewhichwillincreasethereadabilityofyourcode.It'sjustasimplealias.
ScalatypealiassampleinScalaREPLFollowingisaScalatypealiassampleinScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>typeEmail=String
definedtypealiasEmail
scala>
scala>vale=newEmail("[email protected]")
scala>
WhenyouarecodingwithScala,itishighlyrecommendedthatyouusethetypealiasandtraitsforeverything,becausethatwayyouwillgetmoreadvantageswithyourcompiler,andyouwillavoidwritingunnecessarycodeandunnecessaryunittests.
CaseclassesWearenotdoneyetintermsoftheOOfeaturesinScala;thereisanotherveryinterestingwaytoworkwithclassesinScala:theso-calledcaseclasses.CaseclassesaregreatbecauseyoucanhaveaclasswithwaylessnumberoflinesofcodeandcaseclassescanbepartofaPatternMatcher.
ScalacaseclassesfeatureinScalaREPLFollowingisaScalacaseclassesfeatureinScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>caseclassPerson(name:String,age:Int)
definedclassPerson
scala>valp=Person("Diego",31)
p:Person=Person(Diego,31)
scala>valp2=Person("Diego",32)
p2:Person=Person(Diego,32)
scala>p.name
res0:String=Diego
scala>p.age
res1:Int=31
scala>p==p
res2:Boolean=true
scala>p.toString
res3:String=Person(Diego,31)
scala>p.hashCode
res4:Int=668670772
scala>p.equals(p2)
res5:Boolean=false
scala>p.equals(p)
res6:Boolean=true
scala>
ThisistheScalawaytoworkwithclasses.Becausethisissomucheasierandcompact,youprettymuchcreateaclasswithonelineofcode,andyoucanhavetheequalsandhashcodemethodsforfree.
PatternMatcherWhenyoucodeinJava,youcanuseaSwitchstatement.However,inScala,wehaveamorepowerfulfeaturecalledPatternMatcher,whichisakindofswitchbutonsteroids.
SimplePatternMatcherinScalaFollowingisaSimplePatternMatcherinScala:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>defresolve(choice:Int):String=choicematch{
|case1=>"yes"
|case0=>"no"
|case_=>thrownewIllegalArgumentException("Validargumentsare:
0or1.Yourargis:
"+choice)
|}
resolve:(choice:Int)String
scala>println(resolve(0))
no
scala>println(resolve(1))
yes
scala>try{
|println(resolve(33))
|}catch{
|casee:Exception=>println("SomethingWentWorng.EX:"+e)
|}
SomethingWentWorng.EX:java.lang.IllegalArgumentException:Validarguments
are:0or1.Yourargis:33
scala>
ScalausesPatternMatcherforerrorhandling.JavadoesnothavePatternMatcherlikeScala.It'ssimilartoaswitchstatement;however,PatternMatchercanbeusedinamethodreturnstatementasyoucanseeintheprecedingcode.Scaladeveloperscanspecifyaspecialoperatorcalled_(Underscore),whichallowsyoutospecifyanythinginthePatternMatcherscope.Thisbehaviorissimilartoelseinanifconditional.However,inScala,youcanuse_inseveralplaces,andnotonlyastheotherwiseclause,likeinJavaswitch.
ErrorhandlinginScalaissimilartoerrorhandlinginJava.Weusetry...catchblocks.ThemaindifferenceisthatyouhavetousePatternMatcherinScala,whichisgreatbecauseitaddsmoreflexibilitytoyourcode.PatternMatcherinScalacanoperateagainstmanydatastructureslikecaseclasses,collections,integers,andstrings.
Theprecedingcodeisprettysimpleandstraightforward.NextwewillseeamorecomplexandadvancedcodeusingtheScalaPatternMatcherfeature.
AdvancedpatternmatcherinScalaREPLFollowingisanAdvancedPatternMatcherusingScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>deffactorial(n:Int):Int=nmatch{
|case0=>1
|casen=>n*factorial(n-1)
|}
factorial:(n:Int)Int
scala>
scala>println(factorial(3))
6
scala>println(factorial(6))
720
scala>
PatternMatchercanbeusedinaveryfunctionalway.Forinstance,intheprecedingcode,weusethePatternMatcherforrecursion.Thereisnoneedtocreateavariabletostoretheresult,wecanputthePatternMatcherstraighttothefunctionreturn,whichisveryconvenientandsaveslotsoflinesofcode.
AdvancedcomplexpatternmatcherinScalaREPLFollowingisanAdvancedcomplexPatternMatcherusingScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>traitColor
definedtraitColor
scala>caseclassRed(saturation:Int)extendsColor
definedclassRed
scala>caseclassGreen(saturation:Int)extendsColor
definedclassGreen
scala>caseclassBlue(saturation:Int)extendsColor
definedclassBlue
scala>defmatcher(arg:Any):String=argmatch{
|case"Scala"=>"AAwesomeLanguage"
|casex:Int=>"AnIntwithvalue"+x
|caseRed(100)=>"Redsat100"
|caseRed(_)=>"AnykindofREDsat"
|caseGreen(s)ifs==233=>"Greensat233"
|caseGreen(s)=>"Greensat"+s
|casec:Color=>"SomeColor:"+c
|casew:Any=>"Whatever:"+w
|}
matcher:(arg:Any)String
scala>println(matcher("Scala"))
AAwesomeLanguage
scala>println(matcher(1))
AnIntwithvalue1
scala>println(matcher(Red(100)))
Redsat100
scala>println(matcher(Red(160)))
AnykindofREDsat
scala>println(matcher(Green(160)))
Greensat160
scala>println(matcher(Green(233)))
Greensat233
scala>println(matcher(Blue(111)))
SomeColor:Blue(111)
scala>println(matcher(false))
Whatever:false
scala>println(matcher(newObject))
Whatever:java.lang.Object@b56c222
scala>
TheScalaPatternMatcherisreallyamazing.WejustusedanifstatementinthemiddleofthePatternMatcher,andalso_tospecifyamatchforanykindofredvalue.WealsousedcaseclassesinthemiddleofthePatternMatcherexpressions.
PartialfunctionsPartialfunctionsaregreatforfunctioncomposition.TheycanoperatewithcasestatementsaswejustlearnedfromPatternMatcher.Partialfunctionsaregreatinthesenseoffunctioncomposition.Theyallowustodefineafunctioninsteps.Scalaframeworksandlibrariesusethisfeaturealottocreateabstractionsandcallbackmechanisms.It'salsopossibletocheckifapartialfunctionisbeingsuppliedornot.
Partialfunctionsarepredictable,becausethecallercancheckbeforehandifthevaluewillbeappliedtothepartialfunctionornot.Partialfunctioncanbecodedwithorwithoutcase-likestatements.
SimplePartialfunctioninScalaREPLFollowingisasimplePartialfunctionusingScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valpositiveNumber=newPartialFunction[Int,Int]{
|defapply(n:Int)=n/n
|defisDefinedAt(n:Int)=n!=0
|}
positiveNumber:PartialFunction[Int,Int]=<function1>
scala>
scala>println(positiveNumber.isDefinedAt(6))
true
scala>println(positiveNumber.isDefinedAt(0))
false
scala>
scala>println(positiveNumber(6))
1
scala>println(positiveNumber(0))
java.lang.ArithmeticException:/byzero
at$anon$1.apply$mcII$sp(<console>:12)
...32elided
scala>
PartialfunctionsareScalaclasses.Theyhavesomemethodsyouneedtoprovide,forinstance,applyandisDefinedAt.ThefunctionisDefinedAtisusedbythecallertocheckifthePartialFunctionwillacceptandoperatewiththevaluesupplied.TheapplyfunctionwilldotheworkwhenthePartialFunctionisexecutedbyScala.
ScalaPartialFunctionwithoutOOusingcasestatementsinScalaREPLFollowingisaScalaPartialFunctionwithoutOOusingcasestatementsinScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valpositiveNumber:PartialFunction[Int,Int]={
|casen:Intifn!=0=>n/n
|}
positiveNumber:PartialFunction[Int,Int]=<function1>
scala>
scala>println(positiveNumber.isDefinedAt(6))
true
scala>println(positiveNumber.isDefinedAt(0))
false
scala>
scala>println(positiveNumber(6))
1
scala>println(positiveNumber(0))
scala.MatchError:0(ofclassjava.lang.Integer)
atscala.PartialFunction$$anon$1.apply(PartialFunction.scala:253)
atscala.PartialFunction$$anon$1.apply(PartialFunction.scala:251)
at$anonfun$1.applyOrElse(<console>:11)
at$anonfun$1.applyOrElse(<console>:11)
atscala.runtime.AbstractPartialFunction$mcII$sp.apply$mcII$sp
(AbstractPartialFunction.scala:36)
...32elided
scala>
ScalawasamorefluentwaytoworkwithPartialFunctionusingthecasestatements.Whenyouusethecasestatements,youdon'tneedtosupplytheapplyandisDefinedAtfunctions,sincethePatternMatchertakescareofthat.
PartialFunctioncompositioninScalaREPLFollowingisaPartialFunctioncompositioninScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>valeven:PartialFunction[Int,String]={
|caseiifi%2==0=>"even"
|}
even:PartialFunction[Int,String]=<function1>
scala>
scala>valodd:PartialFunction[Int,String]={case_=>"odd"}
odd:PartialFunction[Int,String]=<function1>
scala>
scala>valevenOrOdd:(Int=>String)=evenorElseodd
evenOrOdd:Int=>String=<function1>
scala>
scala>println(evenOrOdd(1)=="odd")
true
scala>println(evenOrOdd(2)=="even")
true
scala>
ScalaallowsustocomposeasmanyPartialFunctionsaswewant.PartialFunctioncompositionhappenswiththeorElsefunction.Intheprecedingcode,wedefinedanimmutablevariablecalledeven,whichverifiesevennumbers.Secondly,wecreatedasecondimmutablevariablecalledodd,whichchecksforoddnumbers.Thenwedidthecomposition,andcreatedathirdPartialFunctioncalledevenOrOddwithcomposeevenandoddusingtheorElseoperator.
PackageobjectsScalahaspackageslikeJava.However,Scalapackagesarealsoobjects,andyoucanhavecodeinsideapackage.JavadoesnothavethesamepowerasScalaintermsofpackages.Ifyouaddcodetoapackage,itwillbeavailabletoallclassesandfunctionswithinthatpackage.
package.scalaYourpackage.scalafileshouldcontainthefollowingcode
packagecom.packait.scala.book
packageobjectcommons{
valPI=3.1415926
objectconstraintsHolder{
valODD="Odd"
valEVEN="Even"
}
defisOdd(n:Int):String=if(n%2==0)constraintsHolder.ODDelse
null
defisEven(n:Int):String=if(n%2!=0)constraintsHolder.EVEN
elsenull
defshow(s:String)=println(s)
}
ThisistheScalapackageobject.Thereisthisspecialtokencalledpackageobjectwhichyouusetodefinecommoncodetoallclasses,objects,andfunctionsthataredefinedinsidethispackageorsub-package.Forthiscase,wedefineavalueofPIasaconstantandalsooneobjectholdercontainingtheStringvaluesforOddandEven.Therearealsothreehelperfunctions,whichcanandwillbeusedbytheclassesinsidethispackage.
MainApp.scalaYourMainApp.scalafileshouldcontainthefollowingcode
packagecom.packait.scala.book.commons
objectMainAppextendsApp{
show("PIis:"+PI)
show(constraintsHolder.getClass.toString())
show(isOdd(2))
show(isOdd(6))
show(isEven(3))
show(isEven(7))
}
Asyoucanseeintheprecedingcode,thisnewobjectisplacedinthepackage:com.packait.scala.book.commons.Anotherinterestingthingisthefactthatwedon'thaveanyimportstatementherebecauseofthepackageobjectfeature.Whenyoucompileandrunthisprogram,youwillseethefollowingoutput:
PIis:3.1415926
classcom.packait.scala.book.commons.package$constraintsHolder$
Odd
Odd
Even
Even
ScalausesthePackageobjectagreatdealprovidinglotsofshortcutsandconvenienceforallScaladevelopers.ThefollowingistheScalapackageobjectdefinition:
/*__
*\
**___________//___ScalaAPI
**
**/__/__//_|///_|(c)2003-2013,LAMP/EPFL
**
**__\\//__/__|//__/__|http://scala-lang.org/
**
**/____/\___/_/|_/____/_/||
**
**|/
**
\*
*/
/**
*CoreScalatypes.Theyarealwaysavailablewithoutanexplicit
import.
*@contentDiagramhideNodes"scala.Serializable"
*/
packageobjectscala{
typeThrowable=java.lang.Throwable
typeException=java.lang.Exception
typeError=java.lang.Error
typeRuntimeException=java.lang.RuntimeException
typeNullPointerException=
java.lang.NullPointerException
typeClassCastException=
java.lang.ClassCastException
typeIndexOutOfBoundsException=
java.lang.IndexOutOfBoundsException
typeArrayIndexOutOfBoundsException=
java.lang.ArrayIndexOutOfBoundsException
typeStringIndexOutOfBoundsException=
java.lang.StringIndexOutOfBoundsException
typeUnsupportedOperationException=
java.lang.UnsupportedOperationException
typeIllegalArgumentException=
java.lang.IllegalArgumentException
typeNoSuchElementException=
java.util.NoSuchElementException
typeNumberFormatException=
java.lang.NumberFormatException
typeAbstractMethodError=
java.lang.AbstractMethodError
typeInterruptedException=
java.lang.InterruptedException
//Adummyusedbythespecializationannotation.
valAnyRef=newSpecializable{
overridedeftoString="objectAnyRef"
}
typeTraversableOnce[+A]=scala.collection.TraversableOnce[A]
typeTraversable[+A]=scala.collection.Traversable[A]
valTraversable=scala.collection.Traversable
typeIterable[+A]=scala.collection.Iterable[A]
valIterable=scala.collection.Iterable
typeSeq[+A]=scala.collection.Seq[A]
valSeq=scala.collection.Seq
typeIndexedSeq[+A]=scala.collection.IndexedSeq[A]
valIndexedSeq=scala.collection.IndexedSeq
typeIterator[+A]=scala.collection.Iterator[A]
valIterator=scala.collection.Iterator
typeBufferedIterator[+A]=scala.collection.BufferedIterator[A]
typeList[+A]=scala.collection.immutable.List[A]
valList=scala.collection.immutable.List
valNil=scala.collection.immutable.Nil
type::[A]=scala.collection.immutable.::[A]
val::=scala.collection.immutable.::
val+:=scala.collection.+:
val:+=scala.collection.:+
typeStream[+A]=scala.collection.immutable.Stream[A]
valStream=scala.collection.immutable.Stream
val#::=scala.collection.immutable.Stream.#::
typeVector[+A]=scala.collection.immutable.Vector[A]
valVector=scala.collection.immutable.Vector
typeStringBuilder=scala.collection.mutable.StringBuilder
valStringBuilder=scala.collection.mutable.StringBuilder
typeRange=scala.collection.immutable.Range
valRange=scala.collection.immutable.Range
//Numerictypeswhichweremovedintoscala.math.*
typeBigDecimal=scala.math.BigDecimal
valBigDecimal=scala.math.BigDecimal
typeBigInt=scala.math.BigInt
valBigInt=scala.math.BigInt
typeEquiv[T]=scala.math.Equiv[T]
valEquiv=scala.math.Equiv
typeFractional[T]=scala.math.Fractional[T]
valFractional=scala.math.Fractional
typeIntegral[T]=scala.math.Integral[T]
valIntegral=scala.math.Integral
typeNumeric[T]=scala.math.Numeric[T]
valNumeric=scala.math.Numeric
typeOrdered[T]=scala.math.Ordered[T]
valOrdered=scala.math.Ordered
typeOrdering[T]=scala.math.Ordering[T]
valOrdering=scala.math.Ordering
typePartialOrdering[T]=scala.math.PartialOrdering[T]
typePartiallyOrdered[T]=scala.math.PartiallyOrdered[T]
typeEither[+A,+B]=scala.util.Either[A,B]
valEither=scala.util.Either
typeLeft[+A,+B]=scala.util.Left[A,B]
valLeft=scala.util.Left
typeRight[+A,+B]=scala.util.Right[A,B]
valRight=scala.util.Right
//Annotationswhichwemightmovetoannotation.*
/*
typeSerialVersionUID=annotation.SerialVersionUID
typedeprecated=annotation.deprecated
typedeprecatedName=annotation.deprecatedName
typeinline=annotation.inline
typenative=annotation.native
typenoinline=annotation.noinline
typeremote=annotation.remote
typespecialized=annotation.specialized
typetransient=annotation.transient
typethrows=annotation.throws
typeunchecked=annotation.unchecked.unchecked
typevolatile=annotation.volatile
*/
}
FunctionsLikeanygreatFPlanguage,Scalahaslotsofbuilt-infunctions.Thesefunctionsmakeourcodemorefluentandfunctional;nowit'stimetolearnsomeofthesefunctions:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>//Createsthenumbers1,2,3,4,5andthemmultiplytheyby2and
createsanewVector
scala>println((1to5).map(_*2))
Vector(2,4,6,8,10)
scala>
scala>//Creates1,2,3andsumthemallwitheachorherandreturnthetotal
scala>println((1to3).reduceLeft(_+_))
6
scala>
scala>//Creates1,2,3andmultiplyeachnumberbyitselfandreturna
Vector
scala>println((1to3).map(x=>x*x))
Vector(1,4,9)
scala>
scala>//Createsnumbers1,2,3,4ans5filteronlyOddnumbersthemmultiply
themoddsby2andreturnaVector
scala>println((1to5)filter{_%2==0}map{_*2})
Vector(4,8)
scala>
scala>//CreatesaListwith1to5andthemprinteachelementbeing
multiplyedby2
scala>List(1,2,3,4,5).foreach((i:Int)=>println(i*2))
2
4
6
8
10
scala>
scala>//CreatesaListwith1to5andthenprinteachelementbeing
multipliedby2
scala>List(1,2,3,4,5).foreach(i=>println(i*2))
2
4
6
8
10
scala>
scala>//Drops3elementsfromthelists
scala>println(List(2,3,4,5,6).drop(3))
List(5,6)
scala>println(List(2,3,4,5,6)drop3)
List(5,6)
scala>
scala>//Zip2listsintoasingleone:Itwilltake1elementofeachlist
andcreateapairList
scala>println(List(1,2,3,4).zip(List(6,7,8)))
List((1,6),(2,7),(3,8))
scala>
scala>//Takenestedlistsandcreateasinglelistwithflatelements
scala>println(List(List(1,2),List(3,4)).flatten)
List(1,2,3,4)
scala>
scala>//FindsapersoninaListbyAge
scala>caseclassPerson(age:Int,name:String)
definedclassPerson
scala>println(List(Person(31,"Diego"),Person(40,"Nilseu")).find((p:Person)
=>p.age<=33))
Some(Person(31,Diego))
scala>
PartialapplicationInScala,theunderscore(_)meansdifferentthingsindifferentcontexts.Theunderscorecanbeusedtopartiallyapplyafunction.Itmeansavaluewillbesuppliedlater.Thisfeatureisusefulforfunctioncompositionandallowsyoutoreusefunctions.Let'sseesomecode.
PartialfunctioninScalaREPLFollowingisanexampleusingPartialfunctioninScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>defsum(a:Int,b:Int)=a+b
sum:(a:Int,b:Int)Int
scala>
scala>valadd6=sum(6,_:Int)
add6:Int=>Int=<function1>
scala>
scala>println(add6(1))
7
scala>
Intheprecedingcode,first,wedefineafunctioncalledsum,whichtakestwoIntparametersandcalculatesasumofthesetwoparameters.Later,wedefineafunctionandholditasavariablecalledadd6.Fortheadd6functiondefinition,wejustcallthesumfunctionpassing6and_.Scalawillgettheparameterpassedthroughadd6,andpassitthroughthesumfunction.
CurriedfunctionsThisfeatureisverypopularinfunctionlanguageslikeHaskell.Curriedfunctionsaresimilartopartialapplications,becausetheyallowsomeargumentstopassnowandotherslater.However,theyarealittlebitdifferent.
Curriedfunctions-ScalaREPLFollowingisanexampleusingcurriedfunctioninScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>//FunctionDefinition
scala>defsum(x:Int)(y:Int):Int=x+y
sum:(x:Int)(y:Int)Int
scala>
scala>//Functioncall-Callingacurriedfunction
scala>sum(2)(3)
res0:Int=5
scala>
scala>//DoingpartialwithCurriedfunctions
scala>valadd3=sum(3)_
add3:Int=>Int=<function1>
scala>
scala>//Supplythelastargumentnow
scala>add3(3)
res1:Int=6
scala>
Fortheprecedingcode,wecreateacurriedfunctioninthefunctiondefinition.Scalaallowsustotransformregular/normalfunctionsintocurriedfunctions.Thefollowingcodeshowstheusageofthecurriedfunction.
CurriedtransformationinScalaREPLFollowingisanexampleusingcurriedtransformationinScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>defnormalSum(x:Int,y:Int):Int=x+y
normalSum:(x:Int,y:Int)Int
scala>
scala>valcurriedSum=(normalSum_).curried
curriedSum:Int=>(Int=>Int)=<function1>
scala>
scala>valadd3=curriedSum(3)
add3:Int=>Int=<function1>
scala>
scala>println(add3(3))
6
scala>
OperatoroverloadingLikeC++,Scalapermitsoperatoroverload.ThisfeatureisgreatforcreatingcustomDomainSpecificLanguages(DSL),whichcanbeusefultocreatebettersoftwareabstractionsoreveninternalorexternalAPIsfordevelopers,orforbusinesspeople.Youshouldusethisfeaturewithwisdom--imagineifallframeworksdecidetooverloadthesameoperatorswithimplicits!Youmightrunintotrouble.ScalaisaveryflexiblelanguagecomparedtoJava.However,youneedtobecareful,otherwiseyoucouldcreatecodethat'shardtomaintainorevenincompatiblewithotherScalaapplications,libraries,orfunctions.
ScalaoperatoroverloadinginScalaREPLFollowingisanexampleusingScalaoperatoroverloadinginScalaREPL:
$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>caseclassMyNumber(value:Int){
|def+(that:MyNumber):MyNumber=newMyNumber(that.value+this.value)
|def+(that:Int):MyNumber=newMyNumber(that+this.value)
|}
definedclassMyNumber
scala>valv=newMyNumber(5)
v:MyNumber=MyNumber(5)
scala>
scala>println(v)
MyNumber(5)
scala>println(v+v)
MyNumber(10)
scala>println(v+newMyNumber(4))
MyNumber(9)
scala>println(v+8)
MyNumber(13)
scala>
Asyoucansee,wehavetwofunctionscalled+.OneofthisfunctionsreceivesaMyNumbercaseclass,andtheotherreceivesaIntvalue.YoucanuseOOinScalawithregularclassesandfunctionsaswellifyouwish.We'realsofavoringimmutabilityherebecausewealwayscreateanewinstanceofMyNumberwhentheoperation+happens.
ImplicitsImplicitsallowyoutodomagicinScala.Withgreatpowercomesgreatresponsibility.ImplicitsallowtoyoucreateverypowerfulDSL,buttheyalsoallowyoutogetcrazy,sodoitwithwisdom.Youareallowedtohaveimplicitfunctions,classes,andobjects.TheScalalanguageandothercoreframeworksfromtheScalaecosystemlikeAkkaandPlayFrameworkuseimplicitsmanytimes.
ScalaImplicitsinSCALAREPL$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>implicitdeftransformStringtoInt(n:String)=n.toInt
warning:therewasonefeaturewarning;re-runwith-featurefordetails
transformStringtoInt:(n:String)Int
scala>
scala>vals:String="123456"
s:String=123456
scala>println(s)
123456
scala>
scala>vali:Int=s
i:Int=123456
scala>println(i)
123456
scala>
Touseimplicits,youneedtousethekeywordimplicitbeforeafunction.Scalawillimplicitlycallthatfunctionwhenitisappropriate.Forthiscase,itwillcalltoconverttheStringtypetoInttypeaswecansee.
ImplicitParameteratScalaREPL$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>implicitvalyValue:Int=6
yValue:Int=6
scala>defsum(x:Int)(implicityValue:Int)=x+yValue
sum:(x:Int)(implicityValue:Int)Int
scala>valresult=sum(10)
result:Int=16
scala>println(result)
16
scala>
Forthisothercase,giveninthelastcode,weuseanimplicitparameterinthefunctionsum.Wealsousedacurriedfunctionhere.Wedefinedtheimplicitfunctionfirst,andthencalledthesumfunction.Thistechniqueisgoodforexternalizedfunctionsconfigurationandvaluesyouwouldletithardcode.Italsosaveslinesofcode,becauseyoudon'tneedtopassaparametertoallfunctionsallthetime,soit'squitehandy.
FuturesFuturesenableanefficientwaytowriteparalleloperationsinanonblockingIOfashion.Futuresareplaceholderobjectsforvaluesthatmightnotexistyet.Futuresarecomposable,andtheyworkwithcallbacksinsteadoftraditionalblockingcode.
SimpleFuturecodeinScalaREPL$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>importconcurrent.Future
importconcurrent.Future
scala>importconcurrent.ExecutionContext.Implicits.global
importconcurrent.ExecutionContext.Implicits.global
scala>
scala>valf:Future[String]=Future{"Helloworld!"}
f:scala.concurrent.Future[String]=Success(Helloworld!)
scala>
scala>println("Result:"+f.value.get.get)
Result:Helloworld!
scala>
scala>println("Result:"+f)
Result:Success(Helloworld!)
scala>
InordertoworkwithfuturesinScala,wehavetoimportconcurrent.Future.Wealsoneedanexecutor,whichisawaytoworkwiththreads.Scalahasadefaultsetofexecutionservices.Youcantweakitifyoulike,however,fornowwecanjustusethedefaults;todothat,wejustimportconcurrent.ExecutionContext.Implicits.global.
It'spossibletoretrievetheFuturevalue.ScalahasaveryexplicitAPI,whichmakesthedeveloper'slifeeasier,andalsogivesgoodsamplesforhowweshouldcodeourownAPIs.Futurehasamethodcalledvalue,whichreturnsOption[scala.util.Try[A]]whereAisthegenerictypeyouareusingforthefuture;forourcase,it'saStringA.Tryisadifferentwaytodoatry...catch,andthisissafer,becausethecallerknowsbeforehandthatthecodetheyarecallingmayfail.Try[Optional]meansthatScalawilltrytorunsomecodeandthecodemayfail--evenifitdoesnotfail,youmightreceiveNoneorSome.Thistypeofsystemmakeseverybody'slivesbetter,becauseyoucanhaveSomeorNoneastheOptionreturn.Futuresareakindofcallback.Forourprevioussamplecode,theresultwasobtainedquitequickly,however,weoftenusefuturestocallexternalAPIs,RESTservices,Microservices,SOAPWebservices,oranycodethattakestimetorunandmightnotgetcompleted.FuturesalsoworkwithPatternMatcher.Let'sseeanothersamplecode.
AcompleteFuturesampleatScalaREPL$scala
WelcometoScala2.11.8(JavaHotSpot(TM)64-BitServerVM,Java1.8.0_77).
Typeinexpressionsforevaluation.Ortry:help.
scala>importconcurrent.Future
importconcurrent.Future
scala>importconcurrent.ExecutionContext.Implicits.global
importconcurrent.ExecutionContext.Implicits.global
scala>importscala.util.{Success,Failure}
importscala.util.{Success,Failure}
scala>defcreateFuture():Future[Int]={
|Future{
|valr=scala.util.Random
|if(r.nextInt(100)%2==0)0elsethrownewRuntimeException("ODDnumbers
arenotgoodhere:(")
|}
|}
createFuture:()scala.concurrent.Future[Int]
scala>defevaluateFuture(f:Future[_]){
|f.onComplete{
|caseSuccess(i)=>println(s"ASuccess$i")
|caseFailure(e)=>println(s"Somethingwentwrong.Ex:
${e.getMessage}")
|}
|}
evaluateFuture:(f:scala.concurrent.Future[_])Unit
scala>evaluateFuture(createFuture)
scala>Somethingwentwrong.Ex:ODDnumbersarenotgoodhere:(
evaluateFuture(createFuture)
ASuccess0
scala>evaluateFuture(createFuture)
Somethingwentwrong.Ex:ODDnumbersarenotgoodhere:(
scala>evaluateFuture(createFuture)
Somethingwentwrong.Ex:ODDnumbersarenotgoodhere:(
scala>evaluateFuture(createFuture)
ASuccess0
scala>evaluateFuture(createFuture)
ASuccess0
scala>
ThereisafunctioncalledcreateFuture,whichcreatesFuture[Int]eachtimeyoucallit.Intheprecedingcode,weusescala.util.Randomtogeneraterandomnumbersbetween0and99.Ifthenumberiseven,wereturna0,whichmeanssuccess.However,ifthenumberisodd,wereturnaRuntimeException,whichwillmeanafailure.
ThereisasecondfunctioncalledevaluateFuture,whichreceivesanyFuture.Weallowaresultofanykindofgenericparameterizedtypeoffunction,becauseweusedthemagicunderscore_.ThenweapplyPatternMatcherwithtwocaseclasses:SuccessandFailure.Inboththecases,wejustprintonstdin.WealsouseanotherinterestingandhandyScalafeaturecalledStringinterpolation.WeneedtowestarttheStringwithsbefore"".Thisallowsustouseexpressionswith$and${}toevaluateanyvariableinthecontext.Thisisadifferent
approachforStringconcatenationfromwhatwehavedonesofar.Later,wemade6callsfortheevaluteFuturefunction,passinganewFutureeachtime,createdbythefunctioncreateFuture.
ReactiveProgramingandRxScalaReactiveprogrammingisbetter,scalable,andafasterwaytobuildapplications.ReactiveProgramingcanbedonewithOOlanguages,however,theymakealotofsensewithFPlanguages.WhenFPismarriedtoReactivePrograming,wegetsomethingcalledFunctionalReactivePrograming(FRP).ScalaFRPcanbeusedformanypurposeslikeGUI,Robotics,andMusic,becauseitgivesyouabettermodeltomodeltime.Reactiveprogrammingisanewtechnique,whichworkswithStreams(alsoknownasDataFlows).Streamsisawaytothinkandcodeapplicationsinawaywhichcanexpressdatatransformationsandflow.Themainideaistopropagatechangesthroughacircuitorflow.Yes,wearetalkingaboutanewwaytodoasyncprogramming.
ThemainlibraryforReactiveProgramingiscalledReactiveExtensions(Rx)-http://reactivex.io/),originallybuiltfor.NETbyEricMeijer.ItcombinesthebestideasfromtheObserverandIteratorPatterns,andFP.RxhasimplementationsformanylanguageslikeScala,Java,Python,.NET,PHP,andothers(https://github.com/ReactiveX).CodingwithRxiseasy,andyoucancreateStreams,combinewithquery-likeoperators,andalsolisten(subscribe)toanyobservableStreamstoperformdatatransformations.RxisusedbymanysuccessfulcompaniestodaylikeNetflix,GitHub,Microsoft,SoundCloud,Couchbase,Airbnb,Trello,andseveralothers.Inthisbook,wewilluseRxScala,whichistheScalaimplementationoftheReactiveStreams.
Thefollowingtableshowsthemainclass/conceptsyouneedtoknowinordertoworkwithRx.
Term/Class Concept
Observable CreateasynccomposableStreamsfromsources.
Observer Acallbackfunctiontype.
Subscription TheboundbetweentheSubscriberandtheObservable.ReceivesnotificationsfromObservables.
ReactiveStreamsisalsothenameofacommonspecificationtryingtoconsolidateandstandardizethereactivestreamprocessing,ThereareseveralimplementationssuchasRxJava/RxScala,Reactor,Akka,Slick,andVert.x.Youcanfindmoreathttps://github.com/reactive-streams/reactive-streams-jvm.
BacktotheObservables--wecanperformallkindsofoperationswithobservables.For
instance,wecanfilter,select,aggregate,compose,performtime-basedoperations,andapplybackpressure.TherearetwobigwinswithObservablesinsteadofcallbacks.Firstofall,Observablesarenotopinionatedabouthowlow-levelI/Oandthreadinghappens,andsecondly,whenyouaredoingcomplexcode,callbackstendtobenested,andthatiswhenthingsgetuglyandhardtoread.ObservableshaveasimplewaytodocompositionthankstoFP.
Observablespushvaluestoconsumerswhenevervaluesareavailable,whichisgreatbecausethenthevaluescanarriveinsyncorasyncfashion.Rxprovidesaseriesofcollectionoperatorstodoallsortsofdatatransformationsyoumayneed.Let'sseesomecodenow.WewilluseRxScalaversion0.26.1,whichiscompatiblewithRxJavaversion1.1.1+.RxScalaisjustawrapperforRxJava(CreatedbyNetflix).WhynotuseRxJavastraight?Becausethesyntaxwon'tbepleasant;withRxScala,wecanhaveafluentScalaexperience.RxJavaisgreat,however,Javasyntaxforthisisnotpleasant-asScalais,infact,prettyugly.
SimpleObservablesScalawithRxScalapackagescalabook.rx.chap1
importrx.lang.scala.Observable
importscala.concurrent.duration._
objectSimpleRXextendsApp{
valo=Observable.
interval(100millis).
take(5)
o.subscribe(x=>println(s"Gotit:$x"))
Thread.sleep(1000)
Observable.
just(1,2,3,4).
reduce(_+_).
subscribe(r=>println(s"Sum1,2,3,4is$rinaRxWay"))
}
IfyourunthisprecedingScalaprogram,youwillseethefollowingoutput:
SimpleObservablesScalawithRxScala-ExecutionintheconsoleGotit:0
Gotit:1
Gotit:2
Gotit:3
Gotit:4
Sum1,2,3,4is10inaRxWay
IfyoutrytorunthiscodeintheScalaREPL,itwillfail,becauseweneedtheRxScalaandRxJavadependencies.Forthis,wewillneedSBTanddependencymanagement.Donotworry,wewillcoverhowtoworkwithSBTinourScalaapplicationinthenextchapter.
Goingbacktotheobservables,weneedtoimporttheScalaObservable.MakesureyougetitfromtheScalapackage,becauseifyougettheJavaone,youwillhaveissues:intheveryfirstpartofthecode,wewillgetnumbersstartingfrom0each100milliseconds,andthiscodewouldrunforever.Toavoidthis,weusethetakefunctiontoputalimitintothecollection,sowewillgetthefirstfivevalues.Then,later,wesubscribetotheobserver,andwhendataisready,ourcodewillrun.Forthefirstsample,it'sprettyeasy,wearejustprintingthevalueswehavegot.Thereisathreadsleepinthisprogram,otherwise,theprogramwouldterminate,andyouwouldnotseeanyvalueontheconsole.
Thesecondpartofthecodedoessomethingmoreinteresting.Firstofall,itcreatesanObservablefromastaticlistofvalues,whichare1,2,3,and4.Weapplyareducefunctionintotheelements,whichwillsumalltheelementswitheachother,andthenwesubscribeandprinttheresult.
ComplexScalawithRxScalaObservablespackagescalabook.rx.chap1
importrx.lang.scala.Observable
objectComplexRxScalaextendsApp{
Observable.
just(1,2,3,4,5,6,7,8,9,10).//1,2,3,4,5,6,7,8,9,10
filter(x=>x%2==0).//2,4,6,8,10
take(2).//2,4
reduce(_+_).//6
subscribe(r=>println(s"#1$r"))
valo1=Observable.
just(1,2,3,4,5,6,7,8,9,10).//1,2,3,4,5,6,7,8,9,10
filter(x=>x%2==0).//2,4,6,8,10
take(3).//2,4,6
map(n=>n*n)//4,16,36
valo2=Observable.
just(1,2,3,4,5,6,7,8,9,10).//1,2,3,4,5,6,7,8,9,10
filter(x=>x%2!=0).//1,3,5,7,9
take(3).//1,3,5
map(n=>n*n)//1,9,25
valo3=o1.
merge(o2).//2,16,36,1,9,25
subscribe(r=>println(s"#2$r"))
}
TheprecedingfirstpartofthecodecreatesanObservablewithnumbersfrom1to10,andthenappliesafilterfunction,whichwillgetonlytheevennumbers.Itthenreducesthem,calculatestheirsum,andlastly,printsthesolution.Youcanvisualizeitasdepictedinthefollowingimage:
Forthesecondpartofthecode,wecreatetwodifferentobservables.Thefirstoneiswithevennumbersandthesecondoneiswithoddnumbers.Thesetwoobservablesaredecoupledfromeachother;youcancontrolasmanyobservablesyouwant.Lateron,thecodeusesamergefunctiontojointhesetwoobservablesintoathirdandnewobservablecontainingthecontentofthefirstandsecondobservables.
Merging2Observables
Therearemanyfunctionsandoptions,andyoucanseethewholelistathttp://rxmarbles.com/andhttps://github.com/ReactiveX/RxScala.Forthesakeofsimplicity,fornow,wearejustworkingwithnumbers.Later,wewillusethistodomoreadvancecompositionsincludingdatabasecallsandexternalwebservicescalls.
SummaryInthischapter,welearnedthebasicconceptsofFP,ReactivePrograming,andtheScalalanguage.WelearnedaboutthebasicconstructsoftheScalalanguageandFunctionalProgramming,functions,collections,andOOinScala,andconcurrentprogrammingwithFutures.
Next,wewillseehowtouseSBTtobuildScalaprojects.WewilllearnhowtocompileandrunScalaapplicationsusingSBT.
Chapter2.CreatingYourAppArchitectureandBootstrappingwithSBTInthepreviouschapter,welearnedaboutFunctionalProgramingandScala.ThischapterwillbefocusedonSimpleBuildTool(SBT)andActivatorinordertoBootstrapcomplexScalaandPlayframeworkprojects.UsingSBTandActivator,wecanperformseveraldevelopmenttaskssuchasbuilding,runningtests,anddeployingtheapplication(whichwillbecoveredindetailinChapter10,ScalingUp).Let'sgetstarted.
Inthischapter,wewillseethefollowingtopics:
SBTbasics--installation,structure,anddependenciesActivatorbasics--creatingprojectsOverallarchitectureofourapplication
IntroducingSBTSBTistheultimateScalasolutionforbuildingandpackingScalaapplications.SBThaslotsofplugins,suchasEclipseandIntelliJIDEAprojectsgeneration,whichhelpagreatdealwhenwearedoingScaladevelopment.SBTisbuiltinScalainordertohelpyoubuildyourScalaapplications.However,SBTcanstillbeusedtobuildJavaapplicationsifyouwish.
ThecorefeaturesofSBTareasfollows:
Scala-basedbuilddefinitionIncrementalcompilationContinuouscompilationandtestingGreatsupportfortestinglibrariessuchasScalaCheck,Specs,ScalaTest,andJUnitREPLintegrationParallelTaskexecution
WewilluseSBTwiththeTypesafeActivatortoBootstrapourapplicationlaterinthisverychapter.Beforedoingso,wewillplaywithSBTtolearnthekeyconceptsofsettingupabuildprojectforaScalaapplication.Inthisbook,wewillbeusingSBTversion0.13.11.
InstallingSBTonUbuntuLinuxKeepinmindthatweneedtohaveJavaandScalainstalledbeforeinstallingSBT.Ifyoudon'thaveJavaandScalainstalled,gobacktoChapter1,IntroductiontoFP,Reactive,andScalaandfollowtheinstallationinstructions.Openaterminalwindow,andrunthefollowingcommandsinordertodownloadandinstallSBT:
$cd/tmp
$wgethttps://repo.typesafe.com/typesafe/ivy-releases/org.scala-
sbt/sbt-launch/0.13.11/sbt-launch.jar?
_ga=1.44294116.1153786209.1462636319-Osbt-launch.jar
$chmod+xsbt-launch.jar
$mkdir~/bin/&&mkdir~/bin/sbt/
$mvsbt-launch.jar~/bin/sbt/
$cd~/bin/sbt/
$touchsbt
Addthefollowingcontenttothe~/bin/sbt/sbtfile:
#!/bin/bash
w
Aftersavingthe~/bin/sbt/sbtfile,weneedtogivepermissiontoexecutethefilewiththefollowingcommand:
$chmodu+x~/bin/sbt/sbt
NowweneedtoputSBTintotheoperationalsystempathinordertobeabletoexecuteanywhereintheLinuxterminal.WeneedtoexportSBTthroughthePATHcommandintothe~/.bashrcfile.Openthe~/.bashrcfileinyourfavoriteeditor,andaddthefollowingcontent:
exportSBT_HOME=~/bin/sbt/
exportPATH=$PATH:$SBT_HOME
Weneedtosourcethefileusing$source~/.bashrc.
NowwecanrunSBTandmoveonwiththeinstallation.Whenyounowtype$sbtonyourconsole,SBTwilldownloadallthedependenciesrequiredforusetorunitself.
GettingstartedwithSBTLet'screatefoldernamedhello-world-sbt,andaddthefollowingprojectstructure:
Forbuild.properties,youneedtohavethefollowingcontent:
build.properties
sbt.version=0.13.11
Forhello_world.scala,wewillusethefollowingcode:
hello_world.scala
objectSbtScalaMainAppextendsApp{
println("HelloworldSBT/ScalaApp")
}
FornowwewilluseanSBTDSL.However,sinceSBTiswrittenScala,wecanusethebuild.scalaformatifwewish.Thisishandyinsomecases,becausewecanuseanykindofScalacodeinordertomakethebuildmoredynamicandtoreusecodeandtasks.
Wewillsetsomepredefinedvariables,however,youcancreateyourownvariables,whichcanbeusedtoavoidduplicatecode.Finally,let'sseethebuild.sbtfilecontentasfollows:
build.scala
name:="hello-world-sbt"
version:="1.0"
scalaVersion:="2.11.8"
scalaVersioninThisBuild:="2.11.8"
Intheprecedingcode,wehavethenameoftheapplication,theversionwhichwillbeusedinthegeneratedJARfile,andalsotheScalaversionusedontheapplicationandtheoneusedinthebuildprocess.Wearereadytobuildthisproject,soopenyourterminalandtype$sbtcompile.
ThisinstructionwillmakeSBTcompileourScalacode,andyoushouldseesomethinglikethisfollowingscreen:
$sbtcompile
Congratulations!SBTjustcompiledourScalaapplication.NowwecanruntheapplicationusingSBT.Inordertodothis,wejustneedtotype$sbtrunasfollows:
$sbtrun
SBTmakesiteasiertotestandplaywithyourScalaapplication,becauseSBThasaREPLliketheScalaREPLwewereplayingwithinChapter1,IntroductiontoFP,Reactive,andScala.TheSBTREPLmakesalltheScalacodethatyoumighthaveundertheprojectavailableattheREPL.
Executethecommand$sbtconsole.
OnceyouareintotheREPL,youcantypeanyScalacode.Asyoumust'verealized,IjustcalledthemainScalaapplicationdirectlyvia$SbtScalaMainApp.main(null).
AddingdependenciesLikeanybuildtool,SBTallowsyoutoresolvedependencies.SBTusestheIvy/Maven2patternstoresolvedependencies.So,ifyouarefamiliarwithMaven2,Gradle,orAnt/Ivy,youwillrealizethatsettingSBTdependenciesisthesame,althoughwithadifferentsyntax.Dependenciesaredefinedinthebuild.sbtfile.ThereisnoScaladevelopmentwithoutunittests.OneofthemostpopulartestinglibrariesisJUnit(http://junit.org/junit4/).JUnitworkswithJavaandScalaprojects.SBTwilldownloadandaddJUnittoyourScalaapplicationclasspathparameter.Weneedtoeditthebuild.sbtfiletoaddJUnitasadependencyasfollows:
build.sbt
name:="hello-world-sbt"
version:="1.0"
scalaVersion:="2.11.7"
scalaVersioninThisBuild:="2.11.7"
libraryDependencies+="junit"%"junit"%"4.12"%Test
libraryDependencies+="com.novocode"%"junit-interface"%"0.11"
%"test"
testOptions+=Tests.Argument(TestFrameworks.JUnit,"-q","-v")
AsImentionedbefore,SBTusesthesamepatternasMaven2/Ivywith:groupID+artifactid+version.Ifyoudon'tknowthepatternforthelibraryyouwanttoadd,youcancheckouttheMavenRepositorywebsite(theygenerateSBTconfigsaswell)atthefollowinglink:http://mvnrepository.com/artifact/junit/junit/4.12.
SBThasscopefordependencies.Wedon'twanttoshipJUnitaspartofthesourcecodedependency.That'swhywehavethe%Testafterthedependencydefinition.
Onceyouhavesavedthefilewiththenewcontent,youcanrun$sbtcompile.SBTwilldownloadJUnitforyouandstorethejarintothelocalIvyrepofileslocatedat/home/YOUR_USER/.ivy2/cache.Withthedependencyinplace,wecanaddmorecodeandalsouseSBTtorunourtestsasfollows:
src/main/scala/calc.scala
classCalculator{
defsum(a:Int,b:Int):Int={
returna+b
}
defmultiply(a:Int,b:Int):Int={
returna*b
}
}
Intheprecedingcode,wejustcreatedasimpleandstraightforwardcalculatorinScala,whichcanaddtwointegernumbersandalsoperformmultiplicationoftwointegersnumbers.NowwecanmoveontotheunittestsusingJUnit.Testsneedtobelocatedinthesrc/test/scala/folder.Lookatthefollowingcode:
src/test/scala/calcTest.scala
importorg.junit.Test
importorg.junit.Assert._
classCalcTest{
@Test
deftestSumOK():Unit={
valc:Calculator=newCalculator()
valresult:Int=c.sum(1,5)
assertNotNull(c)
assertEquals(6,result)
}
@Test
deftestSum0():Unit={
valc:Calculator=newCalculator()
valresult:Int=c.sum(0,0)
assertNotNull(c)
assertEquals(0,result)
}
@Test
deftestMultiplyOk():Unit={
valc:Calculator=newCalculator()
valresult:Int=c.multiply(2,3)
assertNotNull(c)
assertEquals(6,result)
}
@Test
deftestMultiply0():Unit={
valc:Calculator=newCalculator()
valresult:Int=c.multiply(5,0)
assertNotNull(c)
assertEquals(4,result)
}
}
Okay,nowwecanjustrunthetestswiththecommand$sbttestasfollows:
Asyoucanseeinthepreviousscreenshot,allthetestsarerunning.AtestiscreatedwhenweaddtheJavaannotation@Test,anditneedstobeapublicfunctionaswell.Thereisonetest,
calledtestMultiply0,whichfails,becauseitexpectstheresult4,but5multipliedby0iszero,sothetestiswrong.Let'sfixthismethodbychangingassertiontoacceptzero,likeinthefollowingcode,andrerunthe$sbttestasfollows:
@Test
deftestMultiply0():Unit={
valc:Calculator=newCalculator()
valresult:Int=c.multiply(5,0)
assertNotNull(c)
assertEquals(0,result)
}
$sbttestgivesyouthefollowingresult:
Hooray!Allthetestspassed.Bydefault,SBTrunsallyourtestsinparallel,whichisgreatforspeedingupbuildtime-nobodylikestowaitwhendoingbuilds,andScalaisnotthefastesttechtobuild.However,youcandisableparalleltestsifyouwantbyaddingthefollowinglineintothebuild.sbt:
parallelExecutioninTest:=false
GeneratingEclipseprojectfilesfromSBTSBTviapluginscangenerateEclipsefiles.It'spossibletoaddthesepluginsdirectlyintoyourbuild.sbtfile.However,thereisabettersolution.Youcandefineglobalconfigurations,whichareideal,becauseyoudon'tneedtoaddineverysimplebuild.sbtfileyouhave.Thisalsomakesalotofsenseifyouareworkingwithmultipleprojectsand/oryouareworkingwithopensourceprojectsbecause,asitisamatterofpreference,peopleoftendonotversionateIDEfiles.
Gotothefollowingdirectoryifitexists,otherwisepleasecreatethefollowingdirectory:/home/YOUR_USER/.sbt/0.13/plugins.
Nowcreatethefilebuild.sbtwiththefollowingcontent:
/home/YOUR_USER/.sbt/0.13/plugins/build.sbtGlobalconfigfile
resolvers+=Classpaths.typesafeResolver
addSbtPlugin("com.typesafe.sbteclipse"%"sbteclipse-plugin"%
"4.0.0")
Onceyousavethefilewiththiscontent,wecanreloadourSBTapplicationbyexecuting$sbtreload,orquittheSBTconsole(Ctrl+D)andopensbtagainusing$sbt.
$sbtreload
NowwecangenerateEclipsefilesbyusingthecommand$eclipse.
Oncethegenerationisdone,youcanimportthe.projectfilegeneratedintoEclipse.
Bydefault,EclipsedoesnotattachsourcefolderswhengeneratingtheEclipseproject.Ifyouwantthesourcecode(ofthird-partydepslikeJunit),youneedtoaddanextralineintoyourbuild.sbtproject.Addingsourcefoldersisoftenagoodidea,otherwise,youcan'tdoproperdebuggingwithoutthesourcecode.
build.sbt
EclipseKeys.withSource:=true
TheSBTScalaapplicationimportedintoEclipseisshowninthefollowingscreenshot:
ApplicationdistributionForthissection,wewillplaywiththreedifferentpackagingsolutions,whichareasfollows:
ThedefaultSBTpackagersSBTassemblypluginSBTnativepackager
SBTcangeneratejarsbydefault.ItisalsopossibletogenerateRPMs,DEBs,andevendockerimagesviaSBTplugins.Firstofall,let'sgenerateanexecutablejar.ThisisdonebythetaskpackageinSBT.OpenyourSBTconsole,andrunthe$sbtpackage.However,wewanttogenerateaFATjar,whichisajarwithallotherdependencies(jars)oftheapplication.Inordertodothat,weneedtouseanotherplugincalledassembly.
TheSBTpackagecangenerateajar,butitdoesnotshipthedependencies.Inordertousetheassemblyplugin,createthefileproject/assembly.sbt,andaddthecontentasfollows:
$project/assembly.sbt
addSbtPlugin("com.eed3si9n"%"sbt-assembly"%"0.11.2")
Inourbuild.sbt,weneedtoimporttheassemblyplugin,likethis:
$build.sbt(putintothetopofthefile)
importAssemblyKeys._
assemblySettings
Nowwecanrun$sbtassemblytogenerateourFATjar.
Therewego.NowwecanrunthisasanormalJavaapplicationjustusingthecommandjava-jarasfollows:
$java-jarhello-world-sbt/target/scala-2.11/hello-world-sbt-
assembly-1.0.jar
HelloworldSBT/ScalaAppThereisanotherusefulpluginforpackingtheScalaapplication,whichissbt-native-packager.sbt-native-packagercangeneratepackagesforLinuxOSlikeDEBandRPMfiles.Sincethisisanewplugin,weneedtocreateafilecalledplugins.sbtinproject/asfollows:
resolvers+="Typesaferepository"at
"http://repo.typesafe.com/typesafe/releases/"
addSbtPlugin("com.typesafe.sbt"%%"sbt-native-packager"%"1.0.4")
Attheveryendofyourbuild.sbt,youneedtoaddthisline:
enablePlugins(JavaAppPackaging)
Nowwecangeneratepackageswithsbt-native-packagerusing$sbtuniversal:packageBinor$sbtuniversal:packageZipTarball.
NowwehaveaZIPandaTGZfilewithyourapplicationinthefolderhello-world-sbt/target/universal/.InsidethisZIP/TGZfile,wehaveourapplicationinajarformatwithallthedependencies;fornowwejusthaveScala,butifwehadmore,theywouldbethereaswell.ThereareSHandBATscriptstorunthisapplicationeasilyinLinux(SH)andWindows(BAT)respectively.
sbt-native-packagercanalsocookdockerimages.Thisisgreat,becausethatmakesiteasiertodeployapplicationsintoproductionenvironments.Ourprojectisfullyreadytobakedockerimages.WeneedtohavedockerinstalledonLinux;youcandosobyrunningthefollowingcommands:
sudoapt-getupdate
sudoapt-getinstalldocker-engine
sudoservicedockerstart
sudodockerrunhello-world
Youshouldseesomethinglikethefollowingscreenshotifyou'vesuccessfullyinstalledDocker:
Nowyoucanrun$sbt,andthengenerateyourdockerimagesbyusingthecommand$docker:publishLocal.Youwillseeanoutputsimilartothefollowing:
>docker:publishLocal
[info]Wrote
/home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_Applic
ations/hello-world-sbt/target/scala-2.11/hello-world-sbt_2.11-1.0.pom
[info]SendingbuildcontexttoDockerdaemon5.769MB
[info]Step1:FROMjava:latest
[info]Pullingrepositorydocker.io/library/java
[info]31ae46664586:Pullingimage(latest)fromdocker.io/library/java
[info]31ae46664586:Pullingimage(latest)fromdocker.io/library/java,
endpoint:https://registry-1.docker.io/v1/
[info]31ae46664586:Pullingdependentlayers
[info]e9fa146e2b2b:Pullingmetadata
[info]Status:Downloadednewerimageforjava:latest
[info]docker.io/library/java:thisimagewaspulledfromalegacyregistry.
Important:Thisregistryversionwillnotbesupportedinfutureversionsof
docker.
[info]--->31ae46664586
[info]Step2:WORKDIR/opt/docker
[info]--->Runningin74c3e354e9fd
[info]--->d67542bcaa1c
[info]Removingintermediatecontainer74c3e354e9fd
[info]Step3:ADDopt/opt
[info]--->f6cec2a2779f
[info]Removingintermediatecontainer0180e167ae2d
[info]Step4:RUNchown-Rdaemon:daemon.
[info]--->Runningin837ecff2ffcc
[info]--->8a261bd9d88a
[info]Removingintermediatecontainer837ecff2ffcc
[info]Step5:USERdaemon
[info]--->Runningin6101bd5b482b
[info]--->e03f5fa23bdf
[info]Removingintermediatecontainer6101bd5b482b
[info]Step6:ENTRYPOINTbin/hello-world-sbt
[info]--->Runningin43de9335129c
[info]--->eb3961f1e26b
[info]Removingintermediatecontainer43de9335129c
[info]Step7:CMD
[info]--->Runningin302e1fcd0a3d
[info]--->04e7872e85fa
[info]Removingintermediatecontainer302e1fcd0a3d
[info]Successfullybuilt04e7872e85fa
[info]Builtimagehello-world-sbt:1.0
[success]Totaltime:447s,completed07/05/201617:41:47
>
Youcanconfirmthatthereisanewdockerimageinyoursystemjustbyrunningthecommand$dockerps:
Theveryfirstimageisourdockerimagegeneratedbythesbt-native-packagerpluginwithourScalaapplication.Congratulations!YouhaveadockercontainerrunningwithyourScalaapplication.SBTNativePackagerisreallypowerful,yetsimpletouse.Youcangetmoredetailsontheofficialdocumentationsite(http://www.scala-sbt.org/sbt-native-packager/gettingstarted.html).
ThesearethebasicthingsweneedtoknowaboutSBTtobuildprofessionalScalaapplications.SBThasmanyotherfeaturesandpossibilities,whichyoucancheckitoutathttp://www.scala-sbt.org/0.13/docs/index.html.Next,wewilllearnaboutTypesafeActivator,whichisawrapperaroundSBTthatmakesiteasytousewithPlayframeworkapplications.
BootstrappingourPlayframeworkappwithActivatorLightband(formerTypesafe)hasanothertoolcalledActivator(https://www.lightbend.com/community/core-tools/activator-and-sbt),whichisawrapperontopofSBT.ActivatormakesiteasiertocreateReactiveapplicationsusingScala,Akka,andthePlayframework.Don'tworryaboutthePlayframeworkrightnow,becausewewillcoverthatingreaterdetailinChapter3,DevelopingtheUIwithPlayFramework.AkkawillbecoveredindetailinChapter8,DevelopingachatwithAkka.
Let'sdownloadandinstallActivator,andBootstrapourarchitecture.Remember,weneedtohaveJava8andScala2.11alreadyinstalled.Ifyoudon'thaveJava8orScala2.11,gobacktoChapter1,IntroductiontoFP,Reactive,andScalaandinstallthem.
Firstofall,youneedtodownloadactivatorfromhere:https://www.lightbend.com/activator/download
Irecommendthatyoudownloadtheminimalpackage,andletActivatordownloadandinstalltherestoftheotherdependenciesforyou.Youcandownloadtheminimalpackagehere:https://downloads.typesafe.com/typesafe-activator/1.3.10/typesafe-activator-1.3.10-minimal.zip.
Forthisbook,wewillbeusingversion1.3.10.Weneedtoputtheactivator/binfolderintheOSPATH.Ifyouwant,youcaninstallActivatorusingtheterminal,likethis:
Ifyouwant,youcaninstallActivatorusingtheterminal,likethis:
$cd/usr/local/
$wgethttps://downloads.typesafe.com/typesafe-
activator/1.3.10/typesafe-activator-1.3.10-minimal.zip
$tar-xzftypesafe-activator-1.3.10-minimal.zip
$rm-rftypesafe-activator-1.3.10-minimal.zip
$sudoecho'exportPATH=$PATH:/usr/local/typesafe-activator-
1.3.10-minimal/bin'>>~/.bashrc
$source>>~/.bashrc
Inordertotestyourinstallation,executethiscommand:
$activatornewReactiveWebStore
TheprecedingcommandwillBootstrapanarchitectureforyouwithScala,Akka,Playframework,andSBT.
Activatorwillaskyouaseriesofquestionslikesuchaswhattemplatesyoumightliketouse.ThereareacoupleoftemplatesforJavaapplications,Scalaapplications,Akkaapplications,andPlayapplications.Fornow,wewillpickoption6)play-scala.
ThefirsttimeyourunActivator,itcouldtakesometime,becauseitwilldownloadallthedependenciesfromtheweb.WhenActivatorfinishes,youshouldseeafoldercalledReactiveWebStoreinyourfilesystem.
Thecommand$activatornewReactiveWebStoreshowsthefollowingresult:
YoushouldentertheReactiveWebStorefolderifyoutype$llintotheconsole,andyoushouldalsoseethefollowingstructure:
diego@4winds:~/bin/activator-1.3.10-minimal/bin/ReactiveWebStore$
ll
total52
drwxrwxr-x9diegodiego4096Mai1419:03./
drwxr-xr-x3diegodiego4096Mai1419:03../
drwxrwxr-x6diegodiego4096Mai1419:03app/
drwxrwxr-x2diegodiego4096Mai1419:03bin/
-rw-rw-r--1diegodiego346Mai1419:03build.sbt
drwxrwxr-x2diegodiego4096Mai1419:03conf/
-rw-rw-r--1diegodiego80Mai1419:03.gitignore
drwxrwxr-x2diegodiego4096Mai1419:03libexec/
-rw-rw-r--1diegodiego591Mai1419:03LICENSE
drwxrwxr-x2diegodiego4096Mai1419:03project/
drwxrwxr-x5diegodiego4096Mai1419:03public/
-rw-rw-r--1diegodiego1063Mai1419:03README
drwxrwxr-x2diegodiego4096Mai1419:03test/
Thecontentisexplainedasfollows:
app:ThisisthePlayframeworkapplicationfolderwherewewilldotheScalawebdevelopment.build.sbt:Thisisthebuildfile;asyoucansee,ActivatorhasgeneratedtheSBTbuildconfigforus.conf:ThisholdstheapplicationconfigfilessuchasloggingandScala/Playappconfig.project:ThisistheSBTprojectfolderwherewedefineSBTpluginsandSBTversion.test:Thisholdsthetestsourcecodeforourapplication.public:ThisholdsstaticHTMLassetslikeImages,CSS,andJavaScriptcode.bin:ThisholdsacopyoftheactivatorscriptforLinux/MacandWindows.libexec:ThisholdstheActivatorjar.Thisisprettyuseful,becauseActivatorhaspackeditselfwithourapplication.So,let'ssayyoupushthisapplicationforGitHub-whensomeoneneedstoaccessthisappanddownloaditfromGitHub,theSBTfilewillbethere,sotheywon'tneedtodownloaditfromtheInternet.Thisisespeciallyusefulwhenyouareprovisioninganddeployingapplicationsinproduction,whichthisbookwillcoverindetailinChapter10,ScalingUp.
ActivatorshellActivatorallowsyoutorunREPLlikewedidinScalaandSBT.InordertogetREPLaccess,youneedtotypethefollowingontheconsole:
$activatorshell
Activatorhasplentyoftasksthatyoucanuse.Inordertoknowalltheavailablecommands,youcantype$activatorhelpontheconsole.
Activator-compiling,testing,andrunningLet'sgettobusinessnow.Wewillcompile,runtests,andrunourwebapplicationusingActivatorandSBT.Firstofall,let'scompile.Type$activatorcompileontheconsoleasfollows:
$activatorcompile
diego@4winds:~/bin/activator-1.3.10-minimal/bin/ReactiveWebStore$
activatorcompile
[info]Loadingglobalpluginsfrom/home/diego/.sbt/0.13/plugins
[info]Loadingprojectdefinitionfrom/home/diego/bin/activator-
1.3.10-minimal/bin/ReactiveWebStore/project
[info]SetcurrentprojecttoReactiveWebStore(inbuild
file:/home/diego/bin/activator-1.3.10-
minimal/bin/ReactiveWebStore/)
[info]Updating{file:/home/diego/bin/activator-1.3.10-
minimal/bin/ReactiveWebStore/}root...
[info]Compiling14Scalasourcesand1Javasourceto
/home/diego/bin/activator-1.3.10-
minimal/bin/ReactiveWebStore/target/scala-2.11/classes...
[success]Totaltime:154s,completed14/05/201619:28:03
diego@4winds:~/bin/activator-1.3.10-minimal/bin/ReactiveWebStore$
Let'srunourtestsnowwiththecommand$activatortest.
Finally,it'stimetorunyourapplication.Type$activatorrunontheconsole.
Openyourwebbrowser,andgototheURL:http://localhost:9000.
Youshouldseeascreenlikethefollowing:
SummaryCongratulations!You'vejustBootstrappedyourfirstScala/Playframeworkfirst.Activatormakesourlifeeasier.Asyoucansee,withthreecommands,wewereabletogetasiteupandrunning.YoucoulddothesamewithjustSBT,however,itwouldtakemoretime,becausewewouldneedtogetallthedependencies,configureallthesourcecodestructure,andaddsomesampleHTMLandScalacode.ThankstoActivator,wedon'tneedtodoanyofthat.However,wecanstillchangealltheSBTfilesandconfigsasperourwish.ActivatorisnottightwithScalaorourapplicationcode,sinceitismorelikeatemplate-basedcodegenerator.
Inthenextchapter,wewillbeimprovingtheapplicationbyaddingvalidations,databasepersistence,ReactiveMicroservicescallingusingRxScalaandScala,andmuchmore.
Chapter3.DevelopingtheUIwithPlayFrameworkInthepreviouschapter,weperformedbootstrappingonourapplicationusingActivator.Inthischapter,wewillcontinuedevelopingourwebapplicationusingScalaandPlayframework.Playframeworkisgreatforwebdevelopmentbecauseitissimpletouse,andatthesametime,verypowerful.Thisisbecauseitusestop-notchreactivesolutionslikespray,Akka,andAkkaStreamunderthehood.Forthischapter,wewillcreatethebasicUIforsomepartsofourreactivewebsolutionbyaddingvalidationandanin-memorystoresoyoucanfeeltheapplicationworking.WewillusealittlebitofCSSforstyling,andJavaScriptforsomesimplevisualizations.
Inthischapter,wewillcoverthefollowingtopics:
BasicsofwebdevelopmentwithScalaandPlayframeworksCreatingyourmodelsWorkingwithviewsandvalidationsWorkingwithsessionscopes
GettingstartedLet'shavealookatthepreviewofReactiveWebStore--theapplicationthatwewillbuild.
Fornow,wewillbuildthreesimpleoperations--Create,Retrieve,Update,andDelete(CRUD)inordertomanageproducts,productreviews,andproductimages.Wewillcreatemodels,controllers,views,androutesforeachCRUD.
Let'sgetstarted.Firstofall,weneedtodefineourmodels.ThemodelsneedtobelocatedatReactiveWebStore/app/models.ModelsaretheCOREofthesystemandtheyrepresenttheentity.WewillusethisentitylatertostoreandretrievedatafromadatabaselateroninChapter6,PersistencewithSlick.OurmodelsshouldnothaveanyUIlogic,sinceweshouldusecontrollersforUIlogic.
CreatingourmodelsForourproductmodel,wehaveasimpleScalacaseclassinProduct.scalaasfollows:
packagemodels
caseclassProduct
(varid:Option[Long],
varname:String,
vardetails:String,
varprice:BigDecimal)
{
overridedeftoString:String=
{
"Product{id:"+id.getOrElse(0)+",name:"+name+",
details:"+details+",price:"+price+"}"
}
}
AproductcanhaveanoptionalID,aname,details,andaprice.WealsooverridethetoStringmethodjustforthesakeofsimplicityforlogging.Wealsoneedtodefinemodelsforimageandreview.
ThefollowingisthereviewmodelfromReview.scala:
packagemodels
caseclassReview
(varid:Option[Long],
varproductId:Option[Long],
varauthor:String,
varcomment:String)
{
overridedeftoString:String={
"Review{id:"+id+",productId:"+
productId.getOrElse(0)+",author:"+author+",comment:
"+comment+"}"
}
}
Forareviewmodel,wehaveanoptionalID,anoptionalproductId,oneauthor,andacomment.Validationswillbedoneontheviews.Nowlet'sgofortheimagemodel.
TheimagemodelcanbefoundinImage.scalaasfollows:
packagemodels
caseclassImage
(varid:Option[Long],
varproductId:Option[Long],
varurl:String){
overridedeftoString:String={
"Image{productId:"+productId.getOrElse(0)+",url:"
+url+"}"
}
}
Foranimagemodel,wehaveanoptionalID,anoptionalproductId,andtheimageURL.
ThePlayframeworkdoestherouting,andweneedtodefinetheroutesatReactiveWebStore/conf/routes.KeepinmindthatthePlayframeworkwillvalidatealltheroutes,soyouneedtospecifyvalidpackagesandclasses.Playalsocreatessomethingcalledreversecontroller,whichwewilluselaterinthechapter.Fornow,let'sdefinetheroutes.ReversecontrollerisgeneratedbythePlayframeworkwithanactionmethodwhichisthesameasthatoftheoriginalcontrollerwiththesamesignature,butitreturnsplay.api.mvc.Callinsteadofplay.api.mvc.Action.
CreatingroutesThePlayframeworkCRUDoperations'routesforproduct,image,andreviewareasfollows:
#Routes
#Thisfiledefinesallapplicationroutes(Higherpriorityroutesfirst)
#~~~~
GET/controllers.HomeController.index
GET/assets/*filecontrollers.Assets.at(path="/public",file)
#
#CompleteCRUDforProduct
#
GET/productcontrollers.ProductController.index
GET/product/addcontrollers.ProductController.blank
POST/product/controllers.ProductController.insert
POST/product/:idcontrollers.ProductController.update(id:Long)
POST/product:id/removecontrollers.ProductController.remove(id:Long)
GET/product/details/:idcontrollers.ProductController.details(id:Long)
#
#CompleteGRUDforReview
#
GET/reviewcontrollers.ReviewController.index
GET/review/addcontrollers.ReviewController.blank
POST/review/controllers.ReviewController.insert
POST/review/:idcontrollers.ReviewController.update(id:Long)
POST/review:id/removecontrollers.ReviewController.remove(id:Long)
GET/review/details/:idcontrollers.ReviewController.details(id:Long)
#
#CompleteCRUDforImage
#
GET/imagecontrollers.ImageController.index
GET/image/addcontrollers.ImageController.blank
POST/image/controllers.ImageController.insert
POST/image/:idcontrollers.ImageController.update(id:Long)
POST/image:id/removecontrollers.ImageController.remove(id:Long)
GET/image/details/:idcontrollers.ImageController.details(id:Long)
Theroutesworklikethis--FirstyouneedtodefinetherestverbsuchasGET,POST,PUT,DELETE,andthenyouputinaPATHlike/image.Finally,youspecifywhichcontrollerfunctionwillhandlethatroute.Nowwehavetheroutesinplace,wecanmovetothecontrollers.Wewilldefinethecontrollersforproduct,imageandreview.
Alltheroutesfollowthesamelogic.Firstwesendtheusertothewebpagewherewelistalltheitems(products,images,reviews)--thisisrepresentedbyGET/resource,wheretheresourcecanbeanimage,product,orreview,forinstance.Inordertogetaspecificresource,oftenbyID,wegivethecommandGET/resource(product,revieworimage)/ID.POST/resourceisusedtoperformtheUPDATE.
Inordertocreateanewitem(product,review,orimage),thepatternisGET/resource/addandPOST/resource/.Youmaywonderwhytherearetworoutestoperformaninsert.Well,that'sbecausefirstofallweneedtoloadthewebpage,andsecondly,whentheformissubmitted,weneedanewroutetohandlethevalues.Therearetworoutesforanupdateaswellforthesamereason.IfyouwanttoDELETEaresource,thepatternisPOST/resource/ID/remove.Lastly,wehavethedetailsoftheoperation,whichisusedtoshowdetailedinformationwithregardtoaspecificitem--thepatternisGET/resource/details/ID.Withsixroutes,wecandoacompleteCRUDforaresourcesuchasproduct,image,andreview,oranyotherfutureresourcethatyoumayaddtothisapplicationoryourownapplications.
CreatingourcontrollersNowlet'smovetothecontrollersusedonthepreviousroutes.ThecontrollersneedtobelocatedatReactiveWebStore/app/controllers.Controllersareboundbetweenviews(UI),modelsandservice,whichareresponsibleforbusinessoperations.It'salwaysimportanttoseparateUIlogic,whichtendstobespecific,frombusinesslogic,whichtendstobemoregeneric,andoften,waymoreimportant.
Let'shavealookattheproductcontrollerinProductController.scalainthefollowingcode:
@Singleton
classProductController@Inject()(valmessagesApi:MessagesApi,val
service:IProductService)extendsControllerwithI18nSupport{
valproductForm:Form[Product]=Form(
mapping(
"id"->optional(longNumber),
"name"->nonEmptyText,
"details"->text,
"price"->bigDecimal
)(models.Product.apply)(models.Product.unapply))
defindex=Action{implicitrequest=>
valproducts=service.findAll().getOrElse(Seq())
Logger.info("indexcalled.Products:"+products)
Ok(views.html.product_index(products))
}
defblank=Action{implicitrequest=>
Logger.info("blankcalled.")
Ok(views.html.product_details(None,productForm))
}
defdetails(id:Long)=Action{implicitrequest=>
Logger.info("detailscalled.id:"+id)
valproduct=service.findById(id).get
Ok(views.html.product_details(Some(id),
productForm.fill(product)))
}
definsert()=Action{implicitrequest=>
Logger.info("insertcalled.")
productForm.bindFromRequest.fold(
form=>{
BadRequest(views.html.product_details(None,form))
},
product=>{
valid=service.insert(product)
Redirect(routes.ProductController.index).flashing("success"
->Messages("success.insert",id))
})
}
defupdate(id:Long)=Action{implicitrequest=>
Logger.info("updatedcalled.id:"+id)
productForm.bindFromRequest.fold(
form=>{
Ok(views.html.product_details(Some(id),
form)).flashing("error"->"Fixtheerrors!")
},
product=>{
service.update(id,product)
Redirect(routes.ProductController.index).
flashing("success"->Messages("success.update",
product.name))
})
}
defremove(id:Long)=Action{
service.findById(id).map{product=>
service.remove(id)
Redirect(routes.ProductController.index).flashing("success"->
Messages("success.delete",product.name))
}.getOrElse(NotFound)
}
}
ThePlayframeworkusesdependencyinjectionandinversionofcontrolusingGoogleGuice.So,youcanseeatthetopofthecontrollerthatwehavetheannotations@Singletonand@Inject.SingletonmeansthatGuicewillcreateasingleinstanceoftheclasstohandleallrequests.Injectmeansweareinjectingotherdependenciesintoourcontroller,forinstance,weinjectMessagesApiinordertohavethePlayframeworkinternalizationsupportforstringmessages,andIProductService,thatis,theproductservicethatwewillcoverlaterinthischapter.
WealsoneedtoextendthePlayclass,play.api.mvc.Controller.Eachfunctioninacontrollerneedstoreturnanaction.Thisactioncouldbeaview.ThePlayframeworkcompilesalltheviewsintoScalaclasses,soyoucansafelyreferencethemintoyourcontrollerscode.
AllbusinessoperationsaredelegatedtoatraitcalledIProductService,whichwewillcoverlaterinthischapter.WealsologsomeinformationusingtheLoggerclass.PlayFrameworkusesLogbackasthedefaultloggingsolution.Let'stakeacloserlookateachcontrollerfunctionnow.
TheindexfunctioncallsIProductService,andfindsalltheavailableproducts.Iftherearenoproductsavailable,itreturnsanemptysequence,andthencallstheproductUIpassingthecollectionofproducts.
Theblankfunctionrendersablankproductform,sotheusercanhaveablankproductformontheUIinordertoadddata(insertoperation).Playframeworkworkswithformbinding.
So,ineachcontroller,youneedtodefinehowyourformlooksontheUI.Thatformmappingisdoneusingplay.api.data.Form.YoucanseethemappingontheimmutablevariablecalledproductForm.Themappingisbetweentheview(UI)andthemodelcalledproduct.KeepinmindthatthenamefieldismappedasNonEmptyText,whichmeansPlaywon'tacceptnullorblankvalues.Thisisagreatfuture,becausewecandovalidationsinadeclarativewaywithouthavingtowritecode.PriceisdefinedasBigDecimal,soPlaywon'taccepttext,butonlynumbers.
ThedetailsfunctionretrievesaproductusingIProductService,andredirectstotheview.However,beforedoingtheredirect,itbindsthedatawiththeformsotheUIwillloadwithallthedataintotheHTMLinputs.
Wealsohavetheinsertandupdatemethods.Theyareallconstructedwithafoldmethod.Thefoldmethodhasleftandright,whichmeanserrororok.Thefoldfunctioniscalledfromthemappedformandiftherearenovalidationerrors,itgoesright,butiftherearevalidationserrors,itgoesleft.That'saverysimpleandcleanwaytocodetheupdateandinsertflows.Withfold,wedon'tneedcodeforanifstatement.OncethevalidationisOK,wecallIProductServicetodoaninsertorupdate,andthenweperformaredirecttotheview.Messagesarepassedviascope.Playhasoptionsforscope--sessionorFlash.Sessionisformultiplerequests,andthevaluewillbestoredintheclientside.Flashisarequestscope,andmostofthetimesthatiswhatyouneedtouse.HereweareusingtheFlashscope,soitwillonlyexitduringthatspecificrequest.ThisfeatureisusedtopassInternationalizationmessages(i18n),whicharetheresultoftheaction.Allthei18nmessagesneedtobedefinedatReactiveWebStore/conf/messagesasfollows:
success.delete=OK'{0}'deleted!
success.insert=OK'{0}'created!
success.update=OK'{0}'updated!
error.notFound=NothingFoundwithID{0,number,0}
error.number=Notavalidnumber
error.required=Missingvaluehere
Lastly,wehavetheremovemethod.Firstofall,weneedtomakesuretheproductexists,sowedoafindByIdusingIProductService,andthenweapplyamapfunction.Iftheproductdoesn'texist,thePlayframeworkhasprebuiltHTTPerrorcodemessageslikeNotFound.Iftheproductexists,weremoveitusingIProductService,andthenweredirecttotheUIwithaflashingmessage.Nowlet'sseetheimageandreviewcontrollers.
Thereviewcontroller,ReviewController.scala,isasfollows:
@Singleton
classReviewController@Inject()
(valmessagesApi:MessagesApi,
valproductService:IProductService,
valservice:IReviewService)
extendsControllerwithI18nSupport{
valreviewForm:Form[Review]=Form(
mapping(
"id"->optional(longNumber),
"productId"->optional(longNumber),
"author"->nonEmptyText,
"comment"->nonEmptyText
)(models.Review.apply)(models.Review.unapply))
defindex=Action{implicitrequest=>
valreviews=service.findAll().getOrElse(Seq())
Logger.info("indexcalled.Reviews:"+reviews)
Ok(views.html.review_index(reviews))
}
defblank=Action{implicitrequest=>
Logger.info("blankcalled.")
Ok(views.html.review_details(None,
reviewForm,productService.findAllProducts))
}
defdetails(id:Long)=Action{implicitrequest=>
Logger.info("detailscalled.id:"+id)
valreview=service.findById(id).get
Ok(views.html.review_details(Some(id),
reviewForm.fill(review),productService.findAllProducts))
}
definsert()=Action{implicitrequest=>
Logger.info("insertcalled.")
reviewForm.bindFromRequest.fold(
form=>{
BadRequest(views.html.review_details(None,
form,productService.findAllProducts))
},
review=>{
if(review.productId==null||
review.productId.getOrElse(0)==0){
Redirect(routes.ReviewController.blank).flashing("error"->
"ProductIDCannotbeNull!")
}else{
Logger.info("Review:"+review)
if(review.productId==null||
review.productId.getOrElse(0)==0)thrownew
IllegalArgumentException("ProductIdCannotBeNull")
valid=service.insert(review)
Redirect(routes.ReviewController.index).flashing("success"-
>Messages("success.insert",id))
}
})
}
defupdate(id:Long)=Action{implicitrequest=>
Logger.info("updatedcalled.id:"+id)
reviewForm.bindFromRequest.fold(
form=>{
Ok(views.html.review_details(Some(id),
form,productService.findAllProducts)).flashing("error"->
"Fixtheerrors!")
},
review=>{
service.update(id,review)
Redirect(routes.ReviewController.index).flashing("success"-
>Messages("success.update",review.productId))
})
}
defremove(id:Long)=Action{
service.findById(id).map{review=>
service.remove(id)
Redirect(routes.ReviewController.index).flashing("success"-
>Messages("success.delete",review.productId))
}.getOrElse(NotFound)
}
}
Thereviewcontrollerfollowsthesameideasandstructureastheproductcontroller.TheonlymaindifferenceisthathereweneedtoInjectIProductService,becauseareviewneedstobelongtoaproduct.ThenweneedtouseIProductServiceinordertofindAllProduct,becauseinthereviewview,wewillhaveSelectBoxwithalltheavailableproducts.
Theimagecontroller,ImageController.scala,isasfollows:
@Singleton
classImageController@Inject()
(valmessagesApi:MessagesApi,
valproductService:IProductService,
valservice:IImageService)
extendsControllerwithI18nSupport{
valimageForm:Form[Image]=Form(
mapping(
"id"->optional(longNumber),
"productId"->optional(longNumber),
"url"->text
)(models.Image.apply)(models.Image.unapply))
defindex=Action{implicitrequest=>
valimages=service.findAll().getOrElse(Seq())
Logger.info("indexcalled.Images:"+images)
Ok(views.html.image_index(images))
}
defblank=Action{implicitrequest=>
Logger.info("blankcalled.")
Ok(views.html.image_details(None,
imageForm,productService.findAllProducts))
}
defdetails(id:Long)=Action{implicitrequest=>
Logger.info("detailscalled.id:"+id)
valimage=service.findById(id).get
Ok(views.html.image_details(Some(id),
imageForm.fill(image),productService.findAllProducts))
}
definsert()=Action{implicitrequest=>
Logger.info("insertcalled.")
imageForm.bindFromRequest.fold(
form=>{
BadRequest(views.html.image_details(None,form,
productService.findAllProducts))
},
image=>{
If(image.productId==null||
image.productId.getOrElse(0)==0){
Redirect(routes.ImageController.blank).
flashing("error"->"ProductIDCannotbeNull!")
}else{
if(image.url==null||"".equals(image.url))image.url
="/assets/images/default_product.png"
valid=service.insert(image)
Redirect(routes.ImageController.index).
flashing("success"->Messages("success.insert",id))
}
})
}
defupdate(id:Long)=Action{implicitrequest=>
Logger.info("updatedcalled.id:"+id)
imageForm.bindFromRequest.fold(
form=>{
Ok(views.html.image_details(Some(id),form,
null)).flashing("error"->"Fixtheerrors!")
},
image=>{
service.update(id,image)
Redirect(routes.ImageController.index).
flashing("success"->Messages("success.update",
image.id))
})
}
defremove(id:Long)=Action{
service.findById(id).map{image=>
service.remove(id)
Redirect(routes.ImageController.index).flashing("success"
->Messages("success.delete",image.id))
}.getOrElse(NotFound)
}
}
ImagereviewworksinasimilarwaytoReviewController.WeneedIProductServicetogetalltheservices.
WorkingwithservicesServicesarewhereweputthebusinesslogic.WewilllookatreactivepersistenceinChapter6,PersistencewithSlick.Rightnow,wedon'thaveadatabasetopersistinformation,so,fornow,wewilldoanin-memorypersistence.
Firstwewilldefinethecontractofourservices.ThisistheBaseAPIthatwewilluseinthecontrollers.Let'stakealookatthefollowingtraitinBaseService.scala:
packageservices
importjava.util.concurrent.atomic.AtomicLong
importscala.collection.mutable.HashMap
traitBaseService[A]{
varinMemoryDB=newHashMap[Long,A]
varidCounter=newAtomicLong(0)
definsert(a:A):Long
defupdate(id:Long,a:A):Boolean
defremove(id:Long):Boolean
deffindById(id:Long):Option[A]
deffindAll():Option[List[A]]
}
Intheprecedingcode,wehaveanin-memorymutableHashMap,whichisinourmemorydatabasewherewewillstoreproducts,images,andreviews.WealsohaveanatomiccounterwithwhichwecangenerateIDsforourmodels.ThisisatraitusingGenerics--asyoucansee,herewehavealltheoperationswithA,whichwillbespecifiedlater.Nowwecanmovetheserviceimplementationforproduct,review,andimage.
TheProductService.scalapackageisasfollows:
packageservices
importmodels.Product
importjavax.inject._
traitIProductServiceextendsBaseService[Product]{
definsert(product:Product):Long
defupdate(id:Long,product:Product):Boolean
defremove(id:Long):Boolean
deffindById(id:Long):Option[Product]
deffindAll():Option[List[Product]]
deffindAllProducts():Seq[(String,String)]
}
@Singleton
classProductServiceextendsIProductService{
definsert(product:Product):Long={
valid=idCounter.incrementAndGet()
product.id=Some(id)
inMemoryDB.put(id,product)
id
}
defupdate(id:Long,product:Product):Boolean={
validateId(id)
product.id=Some(id)
inMemoryDB.put(id,product)
true
}
defremove(id:Long):Boolean={
validateId(id)
inMemoryDB.remove(id)
true
}
deffindById(id:Long):Option[Product]={
inMemoryDB.get(id)
}
deffindAll():Option[List[Product]]={
if(inMemoryDB.values==Nil||
inMemoryDB.values.toList.size==0)returnNone
Some(inMemoryDB.values.toList)
}
privatedefvalidateId(id:Long):Unit={
valentry=inMemoryDB.get(id)
if(entry==null)thrownewRuntimeException("Couldnotfind
Product:"+id)
}
deffindAllProducts():Seq[(String,String)]={
valproducts:Seq[(String,String)]=this
.findAll()
.getOrElse(List(Product(Some(0),"","",0)))
.toSeq
.map{product=>(product.id.get.toString,product.name)}
returnproducts
}
}
Inthelastcode,wedefinedatraitcalledIProductService,whichextendsBaseServicewithagenericapplytoproduct.TheProductServicepackageimplementsIProductService.InScala,wecanhavemultipleclassesinthesameScalafile,sothereisnoneedtocreatedifferentfiles.
Thecodeisverystraightforward.ThereisautilitymethodherecalledfindAllProducts,whichisusedbyreviewandimagecontrollers.Herewegetalltheelementsonthein-
memoryhashmap.Iftherearenoelements,wereturnalistwithemptyproduct.ThenwemapthelisttoaSeqoftuple,whichisrequiredbytheSelectBoxcheckboxthatwewillhaveintheview(UI).Nowlet'sgofortheimageandreviewservicesasfollows:
packageservices
importjavax.inject._
importmodels.Image
importscala.collection.mutable.HashMap
importjava.util.concurrent.atomic.AtomicLong
traitIImageServiceextendsBaseService[Image]{
definsert(image:Image):Long
defupdate(id:Long,image:Image):Boolean
defremove(id:Long):Boolean
deffindById(id:Long):Option[Image]
deffindAll():Option[List[Image]]
}
@Singleton
classImageServiceextendsIImageService{
definsert(image:Image):Long={
valid=idCounter.incrementAndGet();
image.id=Some(id)
inMemoryDB.put(id,image)
id
}
defupdate(id:Long,image:Image):Boolean={
validateId(id)
image.id=Some(id)
inMemoryDB.put(id,image)
true
}
defremove(id:Long):Boolean={
validateId(id)
inMemoryDB.remove(id)
true
}
deffindById(id:Long):Option[Image]={
inMemoryDB.get(id)
}
deffindAll():Option[List[Image]]={
if(inMemoryDB.values.toList==null||
inMemoryDB.values.toList.size==0)returnNone
Some(inMemoryDB.values.toList)
}
privatedefvalidateId(id:Long):Unit={
valentry=inMemoryDB.get(id)
If(entry==null)thrownewRuntimeException("Couldnotfind
Image:"+id)
}
}
Intheprecedingcode,wehavesomethingprettysimilartothatofProductService.WehaveatraitcalledIImageServiceandtheImageServiceimplementation.Nowlet'sgoforthereviewserviceimplementationinReviewService.scalaasfollows:
packageservices
importjavax.inject._
importmodels.Review
importscala.collection.mutable.HashMap
importjava.util.concurrent.atomic.AtomicLong
traitIReviewServiceextendsBaseService[Review]{
definsert(review:Review):Long
defupdate(id:Long,review:Review):Boolean
defremove(id:Long):Boolean
deffindById(id:Long):Option[Review]
deffindAll():Option[List[Review]]
}
@Singleton
classReviewServiceextendsIReviewService{
definsert(review:Review):Long={
valid=idCounter.incrementAndGet();
review.id=Some(id)
inMemoryDB.put(id,review)
id
}
defupdate(id:Long,review:Review):Boolean={
validateId(id)
review.id=Some(id)
inMemoryDB.put(id,review)
true
}
defremove(id:Long):Boolean={
validateId(id)
inMemoryDB.remove(id)
true
}
deffindById(id:Long):Option[Review]={
inMemoryDB.get(id)
}
deffindAll():Option[List[Review]]={
if(inMemoryDB.values.toList==null||
inMemoryDB.values.toList.size==0)returnNone
Some(inMemoryDB.values.toList)
}
privatedefvalidateId(id:Long):Unit={
valentry=inMemoryDB.get(id)
If(entry==null)thrownewRuntimeException("Couldnotfind
Review:"+id)
}
}
Intheprecedingcode,wehavetheIReviewServicetraitandtheReviewServiceimplementation.Wehavevalidationsontheserviceaswellasagoodpractice.
ConfiguringtheGuicemoduleWeinjectedclassesusing@Injectinourcontrollers.Theinjectionhappensbasedonatrait;weneedtodefineaconcreteimplementationforthetraitsweinjected.ThePlayframeworklooksforGuiceinjectionsatthelocationReactiveWebStore/app/Module.scala.Okay,solet'sdefineourinjectionsforthethreecontrollerswejustcreated.
TheGuicemoduleisfoundinModule.scalaasfollows:
importcom.google.inject.AbstractModule
importjava.time.Clock
importservices.{ApplicationTimer}
importservices.IProductService
importservices.ProductService
importservices.ReviewService
importservices.IReviewService
importservices.ImageService
importservices.IImageService
/**
*ThisclassisaGuicemodulethattellsGuicehowtobindseveral
*differenttypes.ThisGuicemoduleiscreatedwhenthePlay
*applicationstarts.
*Playwillautomaticallyuseanyclasscalled`Module`thatisin
*therootpackage.Youcancreatemodulesinotherlocationsby
*adding`play.modules.enabled`settingstothe`application.conf`
*configurationfile.
*/
classModuleextendsAbstractModule{
overridedefconfigure()={
//UsethesystemclockasthedefaultimplementationofClock
bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
//AskGuicetocreateaninstanceofApplicationTimer
//whentheapplicationstarts.
bind(classOf[ApplicationTimer]).asEagerSingleton()
bind(classOf[IProductService]).to(classOf[ProductService]).
asEagerSingleton()
bind(classOf[IReviewService]).to(classOf[ReviewService]).
asEagerSingleton()
bind(classOf[IImageService]).to(classOf[ImageService]).
asEagerSingleton()
}
}
Sowejustneedtoaddbindwithourtraitsforthecontrollers,andthenpointtothecontrollerimplementation.Theyshouldalsobecreatedassingletons,asthePlayframeworkstartsourapplication.Hereyoualsocandefineanyotherconfigurationorinjectionthatourapplicationmayneed.Thelastcodedefinesthreeservices:productservice,IReviewService,andIImageService.
Workingwithviews(UI)ThePlayframeworkworkswithaScala-basedtemplatingenginecalledTwirl.TwirlwasinspiredbyASP.NETRazor.Twirliscompactandexpressive;youwillseewecandomorewithless.Twirltemplatefilesaresimpletextfiles,however,thePlayframeworkcompilesthetemplatesandturnsthemintoScalaclasses.YoucanmixHTMLwithScalasmoothlyinTwirl.
TheUIwillbecompiledintoaScalaclass,thatcanandwillbereferencedatourcontrollers,becausewecanroutetoaview.Thenicethingaboutitisthatthismakesourcodingwaysafer,sincewehavethecompilercheckingforus.ThebadnewsisthatyouneedtocompileyourUI,otherwise,yourcontrollerswon'tfindit.
Previouslyinthischapter,wedefinedcontrollersforproducts,images,andreviews,andwewrotethefollowingcode:
Ok(views.html.product_details(None,productForm))
Withtheprecedingcode,weredirecttheusertoablankpageforproductssothattheusercancreateanewproduct.WealsocanpassparameterstotheUI.SinceitisallScalacode,youareactuallyjustcallingafunctionasfollows:
valproduct=service.findById(id).get
Ok(views.html.product_details(Some(id),productForm.fill(product)))
Intheprecedingcode,wecalltheservicetoretrieveaproductbyID,andthenpasstheobjecttotheUIwiththeformbeingfilled.
Let'scontinuebuildingourapplicationandcreatetheUIfortheproducts,reviews,andimages.SincewearedoingaCRUD,wewillneedmorethanonetemplatefileperCRUD.Wewillneedthefollowingstructure:
IndexTemplateListallitemsLinktoeditoneitemLinktoremoveoneitemLinktocreateanewItem
DetailTemplateHTMLFormtocreateanewItemHTMLformtoeditanexistingitem(forupdate)
Havingsaidthat,wewillhavethefollowingfiles:
ForProducts:product_index.scala.htmlproduct_details.scala.html
ForImage:
image_index.scala.htmlimage_details.scala.html
ForReviews:review_index.scala.htmlreview_details.scala.html
Forthesakeofcodereuse,wewillcreateanotherfilecontainingthebasicstructureofourUI,likeCSSimports(CSSneedstobelocatedatReactiveWebStore\public\stylesheets),JavaScriptimports,andpagetitlesothatwedon'tneedtorepeatthatinallthetemplatesforeachCRUD.Thispagewillbecalled:main.scala.html.
AlltheUIcodeshouldbelocatedatReactiveWebStore/app/views.
ThemainScalawiththeUIindexforallCRUDoperationsisinmain.scala.html,asfollows:
@(title:String)(content:Html)(implicitflash:Flash)
<!DOCTYPEhtml>
<htmllang="en">
<head>
<title>@title</title>
<linkrel="shortcuticon"type="image/png"
href="@routes.Assets.at("images/favicon.png")">
<linkrel="stylesheet"media="screen"
href="@routes.Assets.at("stylesheets/main.css")">
<linkrel="stylesheet"media="screen"
href="@routes.Assets.at("stylesheets/bootstrap.min.css")">
<scriptsrc="@routes.Assets.at("javascripts/jquery-
1.9.0.min.js")"type="text/javascript"></script>
<scriptsrc="@routes.Assets.at("javascripts/bootstrap.js")"
type="text/javascript"></script>
<scriptsrc="@routes.Assets.at("javascripts/image.js")"
type="text/javascript"></script>
</head>
<body>
<center><ahref='/'><imgheight='42'width='42'
src='@routes.Assets.at("images/rws.png")'></a>
<h3>@title</h3></center>
<divclass="container">
@alert(alertType:String)={
@flash.get(alertType).map{message=>
<divclass="alertalert-@alertType">
<buttontype="button"class="close"data-
dismiss="alert">×</button>
@message
</div>
}
}
@alert("error")
@alert("success")
@content
<ahref="/"></a><BR>
<buttontype="submit"class="btnbtn-primary"
onclick="window.location.href='/';">
ReactiveWebStore-Home
</button>
</div>
</body>
</html>
Intheprecedingcode,firstofall,thereisthefollowinglineattheverytop:
@(title:String)(content:Html)(implicitflash:Flash)
ThismeansthatwedefinetheparametersthisUIcanreceive.Hereweexpectastringtitle,whichwillbethepagetitle,andtherearesomecurryingvariablesaswell.YoucangetmoredetailsaboutcurryinginChapter1,IntroductiontoFP,Reactive,andScala.Soincurrying,therearetwothings:FirstisHTML,whichmeansyoucanpassHTMLcodetothisfunction,andsecond,wehaveFlashwhichthePlayframeworkwillpassforus.Flashisusedtogetparametersbetweenrequests.
Asyoucansee,laterinthecodewehave@title,whichmeansweretrievethetitleoftheparameters,andaddthevaluetotheHTML.Wealsoprintanyerrormessageoranyvalidationsissues,ifpresent,[email protected],butwedonotputhard-codedpaths.Instead,weusetherouterslike@routes.Assets.at.TheJavascriptsstillneedtobelocatedatReactiveWebStore\public\javascripts.
Nowothertemplatescanworkwith@main(..),andtheydon'tneedanydeclarationofJavascriptorCSS.TheycanaddanextraHTMLcode,[email protected],fortheproducts,thecontentwillbeHTMLproductcontent,andsoonforreviews,andimages.NowwecanmovefortheproductsUI.
Productindex:UIindexforproductsproduct_index.scala.html
@(products:Seq[Product])(implicitflash:Flash)
@main("Products"){
@if(!products.isEmpty){
<tableclass="tabletable-striped">
<tr>
<th>Name</th>
<th>Details</th>
<th>Price</th>
<th></th>
</tr>
@for(product<-products){
<tr>
<td><ahref="@routes.ProductController.
details(product.id.get)">@product.name</a></td>
<td>@product.details</td>
<td>@product.price</td>
<td><formmethod="post"action=
"@routes.ProductController.remove(product.id.get)">
<buttonclass="btnbtn-link"type="submit">
<iclass="icon-trash"></i>Delete</button>
</form></td>
</tr>
}
</table>
}
<p><ahref="@routes.ProductController.blank"class=
"btnbtn-success"><iclass="icon-plusicon-white">
</i>AddProduct</a></p>
}
Asyoucansee,thereisHTMLmixedwithScalacode.EverytimeyouneedtorunHTML,youjustrunit,andwhenyouneedrunScalacode,youneeduseaspecialcharacter,@.Attheverytopofthetemplate,youcanseethefollowingcode:
@(products:Seq[Product])(implicitflash:Flash)
SincethisisScalacodeintheend,andwillbecompiled,weneedtodefinewhatparametersthisUItemplatecanreceive.Hereweexpectasequenceofproducts.ThereisalsoacurryingimplicitvariablecalledFlash,whichwillbeprovidedbythePlayframework,andwewilluseitforthemessagedisplay.Wealsohavethecode-@main("Products"){..}.ThismeansthatwecallthemainScalatemplateandaddextraHTML--theproductHTML.ForthisproductUI,welistalltheproductsbasedonthesequenceofproducts.Asyoucansee,wedefineanHTMLtable.Wealsovalidateifthesequenceisnotemptybeforelistingalltheproducts.
Nowwecangoforthedetailspageforproductsinproduct_details.scala.htmlasfollows:
@(id:Option[Long],product:Form[Product])(implicitflash:Flash)
@importplay.api.i18n.Messages.Implicits._
@importplay.api.Play.current
@main("Product:"+product("name").value.getOrElse("")){
@if(product.hasErrors){
<divclass="alertalert-error">
<buttontype="button"class="close"data-
dismiss="alert">×</button>
Sorry!Someinformationdoesnotlookright.Couldyou
reviewitpleaseandre-submit?
</div>
}
@helper.form(action=if(id.isDefined)
routes.ProductController.update(id.get)else
routes.ProductController.insert)
{
@helper.inputText(product("name"),'_label->"Product
Name")
@helper.inputText(product("details"),'_label->"Product
Details")
@helper.inputText(product("price"),'_label->"Price")
<divclass="form-actions">
<buttontype="submit"class="btnbtn-primary">
@if(id.isDefined){UpdateProduct}else{NewProduct}
</button>
</div>
}
}
ForthisprecedingUI,attheverytop,wehavethefollowingline:
@(id:Option[Long],product:Form[Product])(implicitflash:Flash)
ThismeansthatweexpectanIDwhichiscompletelyoptional.ThisIDisusedtoknowifwearedealingwithaninsertscenariooranupdatescenario,becauseweusethesameUIforbothinsertandupdate.Wealsogettheproductform,whichwillbepassedthroughProductController,andwereceiveFlash,whichwillbeprovidedbythePlayframework.
WeneedtodosomeimportsontheUIsothatwecangetaccesstothePlayframeworki18nsupport.Thisisdonebythefollowing:
@importplay.api.i18n.Messages.Implicits._
@importplay.api.Play.current
LikethepreviousUI,werenderthisUIwithinthemainScalaUI.Sowedon'tneedtospecifyJavaScriptandCSSagain.Thisisdonebythefollowingcode:
@main("Product:"+product("name").value.getOrElse("")){..}
Nextwecheckifthereareanyvalidationerrors.Ifthereare,theuserswillneedtofixtheerrorsbeforemovingon.Thisisdonebythefollowingcode:
@if(product.hasErrors){
<divclass="alertalert-error">
<buttontype="button"class="close"data-
dismiss="alert">×</button>
Sorry!Someinformationdoesnotlookright.Couldyoureview
itpleaseandre-submit?
</div>
}
Nowisthetimetocreatetheproductform,which,intheend,willbemappedtoHTMLinputboxes.Wedothiswiththefollowingcode:
@helper.form(action=if(id.isDefined)
routes.ProductController.update(id.get)else
routes.ProductController.insert){
@helper.inputText(product("name"),'_label->"ProductName")
@helper.inputText(product("details"),'_label->"Product
Details")
@helper.inputText(product("price"),'_label->"Price")
<divclass="form-actions">
<buttontype="submit"class="btnbtn-primary">
@if(id.isDefined){UpdateProduct}else{NewProduct}
</button>
</div>
}
@helper.formisaspecialhelperprovidedbythePlayframeworktocreateHTMLformseasily.So,theactionisthetargetwheretheformwillbesubmitted.Weneedtodoanifhere,sinceweneedtoknowifitisanupdateoraninsert.Thenwemapallthefieldswehaveforourproductmodelwiththiscode:
@helper.inputText(product("name"),'_label->"ProductName")
@helper.inputText(product("details"),'_label->"ProductDetails")
@helper.inputText(product("price"),'_label->"Price")
Remember,theproductformcomesfromtheproductcontroller.Forthehelper,wejustneedtotellitwhichproductfieldisforwhichHTMLlabel,andthat'sit.ThiswillproducethefollowingUIs.
ThefollowingimageshowstheblankproductindexUI:
TheinsertUIformforproductdetailslooksasfollows:
Withproductsadded,theproductindexUIappearsasfollows:
Nowwecanmovetoreviews.Let'sgofortheUIs.
ThereviewindexUIinreview_index.scala.htmlisasfollows:
@(reviews:Seq[Review])(implicitflash:Flash)
@main("Reviews"){
@if(!reviews.isEmpty){
<tableclass="tabletable-striped">
<tr>
<th>ProductId</th>
<th>Author</th>
<th>Comment</th>
<th></th>
</tr>
@for(review<-reviews){
<tr>
<td><ahref="@routes.ReviewController.details
(review.id.get)">@review.productId</a></td>
<td>@review.author</td>
<td>@review.comment</td>
<td>
<formmethod="post"action="@routes.ReviewController.
remove(review.id.get)">
<buttonclass="btnbtn-link"type="submit"><iclass=
"icon-trash"></i>Delete</button>
</form></td>
</tr>
}
</table>
}
<p><ahref="@routes.ReviewController.blank"class="btnbtn-
success"><iclass="icon-plusicon-white"></i>AddReview</a></p>
}
Soherewehavethesamethingsaswehadfortheproducts.Let'stakealookatthedetailspageforreviewnow.Youcanfinditinreview_details.scala.html.
@(id:Option[Long],review:Form[Review],products:
Seq[(String,String)])(implicitflash:Flash)
@importplay.api.i18n.Messages.Implicits._
@importplay.api.Play.current
@main("review:"+review("name").value.getOrElse("")){
@if(review.hasErrors){
<divclass="alertalert-error">
<buttontype="button"class="close"data-
dismiss="alert">×</button>
Sorry!Someinformationdoesnotlookright.Couldyou
reviewitpleaseandre-submit?
</div>
}
@helper.form(action=if(id.isDefined)
routes.ReviewController.update(id.get)else
routes.ReviewController.insert){
@helper.select(
field=review("productId"),
options=products,
'_label->"ProductName",
'_default->review("productId").value.getOrElse("Choose
One"))
@helper.inputText(review("author"),'_label->"Author")
@helper.inputText(review("comment"),'_label->
"Comment")
<divclass="form-actions">
<buttontype="submit"class="btnbtn-primary">
@if(id.isDefined){Updatereview}else{Newreview}
</button>
</div>
}
}
Here,inthislastcode,wehavealmosteverythingsimilartowhatwehadforproducts,however,thereisonebigdifference.ReviewneedstobeassociatedwithaproductID.That'swhy,weneedtohaveaselectfortheproducts,whichisfulfilledbyproducts:Seq[(String,String)].ThiscomesfromtheReviewControllercode.ThiscodeproducesthefollowingUIs.
TheblankreviewindexUIisshownasfollows:
TheinsertreviewdetailsUIlooksasfollows:
ThereviewindexUIwithreviewswilllooklikethefollowingimage:
Nowwecanmovetothelastone:theimageUI.TheimageUIisverysimilartothereviewUI,becauseitdependsontheproductIDtoo.Let'sgoforit.
TheimageindexUIhasthefollowingcodeinimage_index.scala.html:
@(images:Seq[Image])(implicitflash:Flash)
@main("Images"){
@if(!images.isEmpty){
<tableclass="tabletable-striped">
<tr>
<th>ProductID</th>
<th>URL</th>
<th></th>
</tr>
@for(image<-images){
<tr>
<td><ahref="@routes.ImageController.details
(image.id.get)">@image.id</a></td>
<td>@image.productId</td>
<td>@image.url</td>
<td><formmethod="post"action=
"@routes.ImageController.remove(image.id.get)">
<buttonclass="btnbtn-link"type="submit">
<iclass="icon-trash"></i>Delete</button>
</form></td>
</tr>
}
</table>
}
<p><ahref="@routes.ImageController.blank"class=
"btnbtn-success"><iclass="icon-plusicon-white">
</i>AddImage</a></p>
}
ImageDetailsUI[image_details.scala.html]
@(id:Option[Long],image:Form[Image],products:Seq[(String,String)])
(implicitflash:Flash)
@importplay.api.i18n.Messages.Implicits._
@importplay.api.Play.current
@main("Image:"+image("productId").value.getOrElse("")){
@if(image.hasErrors){
<divclass="alertalert-error">
<buttontype="button"class="close"data-
dismiss="alert">×</button>
Sorry!Someinformationdoesnotlookright.Couldyouimage
itpleaseandre-submit?
</div>
}
@helper.form(action=if(id.isDefined)
routes.ImageController.update(id.get)else
routes.ImageController.insert){
@helper.select(field=image("productId"),
options=products,
'_label->"ProductName",
'_default->image("productId").value.getOrElse("Choose
One")
)
@helper.inputText(
image("url"),
'_label->"URL",
'_placeholder->"/assets/images/default_product.png",
'onchange->"javascript:loadImage();"
)
Visualization<br>
<imgid="imgProduct"height="42"width="42"
src="@image("url").value"></img>
<divclass="form-actions">
<buttontype="submit"class="btnbtn-primary">
@if(id.isDefined){UpdateImage}else{NewImage}
</button>
</div>
}
}
ThisUItemplatewillcreatethefollowingHTMLPages:
TheblankimageindexUIisshowninthefollowingimage:
TheinsertUIforimagedetailslooksasfollows:
ThefollowingistheimageindexUIwithitems:
NowwehaveacompleteworkingUIapplication.Therearecontrollers,models,views,andsimpleservicesaswell.Wealsohaveallvalidationsinplace.
SummaryInthischapter,youlearnedhowtocreatecontrollers,models,services,views(usingTwirltemplating),Guiceinjections,androuting.WecoveredtheprinciplesofScalaWebDevelopmentusingthePlayframework.Bytheendofthechapter,wegottheapplicationwiththePlayframeworkupandrunning.
Inthenextchapter,wewilllearnmoreaboutservices.Asyoumayrealize,wedidsomesimpleservicesinthischapterforproducts,reviews,andimages,butnowwewillcontinueworkingwithservices.
Chapter4.DevelopingReactiveBackingServicesInthepreviouschapter,youlearnedhowtoBootstrapyourapplicationusingActivator,andwedevelopedourwebapplicationusingScalaandthePlayframework.NowwewillenterintothereactiveworldofRxJavaandRxScala.
Inthischapter,wewillcoverthefollowingtopics:
ReactiveprogrammingprinciplesandtheReactiveManifestoUnderstandingtheimportanceofnon-blockingIOObservables,functions,anderrorhandlingwithRxRefactoringourcontrollersandmodelstocallourservicesAddingRxScalatoourservicesAddinglogging
GettingstartedwithreactiveprogrammingBuildingapplicationstodayisharderthanitwasbefore.Everythingnowismorecomplex:wehavetousemorecoresinprocessors,andwehavecloud-nativeapplicationswithhundredsofmachinesforasingleservice.Concurrentprogramminghasalwaysbeenhard,anditwillalwaysbeso,becauseitisdifficulttomodeltime.Inordertoaddressthis,weneedtohaveareactivestyleofarchitecture.Inordertobeabletohandlemoreusersandscaleourapplications,weneedtoleverageAsyncandnon-blockingIO.Tohelpuswiththistask,wecanrelyonRxJavaandRxScala.Beingreactiveisnotonlyaboutcodebutalsoaboutarchitecturalprinciples.
TheReactiveManifestocapturestheseprinciplesverywell,andthereareacoupleoftechnologiesthatfollowtheseprinciplesinordertobefullyreactive.
TheReactiveManifestocanbeshownasinthefollowingdiagram:
Formoreinformation,youcanvisithttp://www.reactivemanifesto.org/.
TheReactiveManifestodescribeswhatthisreactivearchitecture/systemlookslike.Basically,therearethefollowingfourcoreprinciplesunderliningthereactiveidea:
Responsive:Thesystemshouldrespondinatimelymanner.Inotherwords,thesystemshoulddetectproblemsquickly,anddealwiththemeffectively,apartfromprovidingrapidandconsistentresponsetime.Resilient:Thesystemshouldstayresponsiveevenafterfailure.Thisisdoneviareplicationcontainment,isolation,anddelegation(https://en.wikipedia.org/wiki/Delegation_pattern).Containmentandisolationareideasthatcomefromthenavalindustry,andaredefinedbythebulkheadpattern(https://en.wikipedia.org/wiki/Bulkhead_(partition)).Failuresarecontainedateachcomponent.Doingsomakessurethatonesystem'sfailuredoesnotaffectothersystems.Recoveryisdelegatedtoanothersystem,andnottotheclient.
Elastic:Theabilitytoincreaseanddecreaseresourcesforthesystem.ThisrequiresyoudesignyoursystemwithoutSinglePointOfFailure(SPOF),anddesignusingshardsandreplication.Reactivesystemsarepredictiveandcost-effective.Message-driven:Reactivesystemsrelyonasynchronousmessagepassingtoensureloosecoupling,isolation,andlocationtransparency.Bydoingso,wecandelegatefailuresasmessages.Thisgivesuselasticity,loadmanagement,andflowcontrol.It'sevenpossibletoapplyback-pressure(alsoknownasthrottling)whenneeded.Allthisshouldbedonewithnon-blockingcommunicationforbetterresourceutilization.
Alright,let'susetheseprinciplespracticallyinourapplicationwithRxScala.RxScalaisjustaScalawrapperforRxJava,butitisbettertousebecauseitmakesthecodemorefunctional,andyoudon'tneedtocreateobjectssuchasAction1.
Inourapplication,wehavethreemajorresources:products,reviews,andimages.Allproductsmusthaveaprice,sowewillbuiltafullyreactivepricegeneratorwiththePlayframework,RxScala,andScalarightnow.
Sofirstofall,wewillplaywithRxScalainourPlayapplication,thenwewillcreateaseparatemicroservice,makereactivecallstothatmicroservice,andretrieveourpricesuggestionforthatservice.Alldataflowtransformationsareusingobservables.
Let'screatetheroutesforthiscontrolleratReactiveWebStore/conf/routes,asfollows:
#
#Services
#
GET/rx/pricescontrollers.RxController.prices
GET/rx/apricescontrollers.RxController.pricesAsync
Wehavetworouteshere:oneforaregularaction,andanotherforanAsyncactionthatwillreturnaScalaFuture.Let'screateanewcontrollercalledRxController.scala.ThiscontrollerneedstobelocatedatReactiveWebStore/app/controller.
Let'shavealookatRxController,whichisourreactiveRxScalasimplecontroller:
@Singleton
classRxController@Inject()(priceService:IPriceSerice)extends
Controller{
defprices=Action{implicitrequest=>
Logger.info("RXcalled.")
importExecutionContext.Implicits.global
valsourceObservable=priceService.generatePrices
valrxResult=Observable.create{sourceObservable.subscribe
}
.subscribeOn(IOScheduler())
.take(1)
.flatMap{x=>println(x);Observable.just(x)}
.toBlocking
.first
Ok("RxScalaPricesuggestedis="+rxResult)
}
defpricesAsync=Action.async{implicitrequest=>
Logger.info("RXAsynccalled.")
importplay.api.libs.concurrent.Execution.Implicits.
defaultContext
valsourceObservable=priceService.generatePrices
valrxResult=Observable.create{sourceObservable.subscribe
}
.subscribeOn(IOScheduler())
.take(1)
.flatMap{x=>println(x);Observable.just(x)}
.toBlocking
.first
Future{Ok("RxScalaPricesugestedis="+rxResult)}
}
}
So,intheveryfirstmethodcalledprices,wereturnaregularPlayframeworkAction.WereceiveIPriceServiceviadependencyinjection.ThisIPriceServiceisareactiveservice,becauseitusesobservables.Sowecallamethod,generatePrices,whichwillreturnObservable[Double].Thiswillbeoursourceobservable,thatis,thedatasourceofourcomputation.Movingforward,wecreateanewobservablesubscribingintothesourceobservable,andthenweapplysometransformation.Forinstance,wetakejustoneelement,andthenwecanperformtransformationusingflatMap.Forthiscase,wedonotreallyapplytransformations.WeuseflatMaptosimplyprintwhatwegot,andthencontinuethechain.ThenextstepistocalltoBlocking,whichwillblockthethreaduntilthedataisback.Oncethedataisback,wegetthefirstelement,whichwillbeadouble,andwereturnOk.
Blockingsoundsbadandwedon'twantthat.Alternatively,wecanusetheasynccontrollerinthePlayframework,whichwon'tblockthethreadandreturnaFuture.Sothat'sthesecondmethod,calledpricesAsync.Herewehavesimilarobservablecode.However,intheend,wereturnaFuturewhichisnotblocking.However,wecalltoBlockingfromtheobservablethatwillblockthecall,thusmakingitthesameasthepreviousmethod.Tobeclear,Actionisnotbad.Bydefault,everythingisAsyncinPlay,becauseevenifyoudon'treturnanexplicitFuture,thePlayframeworkcreatesapromiseforyandmakesyoucodeAsync.UsingHTTP,youwillblockthethreadatsomepoint.Ifyouwanttobe100%non-blockingfromendtoend,youneedtoconsideradifferentprotocolsuchaswebsockets.
Let'stakealookattheservicenow.Thisservice,andotherservices,needtobelocatedatReactiveWebStore/apps/services.Firstwewillcreatetraittodefinetheservicebehavior.
IPriceService-ScalatraitAsyoucanseeinthefollowingcode,wedefinedIPriceServicewithjustoneoperation,thatis,generatePrices,whichreturnsObservable[Double].Thenextstepnowistodefinetheserviceimplementation.Thiscodeneedstobelocatedinthesameservicesfolderastheprevioustrait:
traitIPriceSerice{
defgeneratePrices:Observable[Double]
}
PriceService-RxScalaPriceServiceimplementationFirstwecreatePublishSubject,whichisawaytogeneratedataintoobservables.ScalahasanicewayofgeneratinginfinitesequencesusingStream.continually.Sowepassafunctionwhichgeneratesdoublerandomnumbersfrom0to1,000.Thiswillhappenforeverand,becausethisisanexpensivecomputation,werunitintoaFuture.TherightthingtodowillbetouseamethodafterStream,becausethiswillfinishthecomputation.Forthesakeoftheexercise,wewillkeepitthiswayfornow.
EachdoublerandomnumberispublishedintoPublishSubjectbytheonNextmethod.Nowlet'smovetothegeneratePricesmethod,whichusesthreeobservablestogeneratethenumberforus.Tobeclear,ofcoursewecoulddoasimplersolutionhere.However,wearedoingitthiswaytoillustratethepowerofobservables,andhowyoucanusetheminpractice.
WehaveEvenandOddobservables,bothsubscribingtoPublishSubject,sotheywillreceiveinfinitedoublenumbers.ThereisaflatMapoperationtoadd10tothenumber.Keepinmindthateverythingyoudoneedstobeinanobservable.SowhenyoudotransformationswithflatMap,youalwaysneedtoreturnanobservable.
Finally,weapplythefilterfunctiontogetonlyevennumbersontheEvenobservableandonlyoddnumbersontheOddobservable.Allthishappensinparallel.EvenandOddobservablesdonotwaitforeachother.
Thenextstepistomergebothobservables.WecreateathirdobservablethatstartsemptyandthenmergestheinfinitedoublesfromtheEvenobservablewiththeinfinitedoublesfromtheOddobservable.Nowisthetimetolimitthecomputationtoonly10numbers.Wedon'tknowhowmanyoddorhowmanyevennumberswillbetherebecauseofAsync.Ifyouwishtocontrolthenumberofoddsandevens,youneedtoapplythetakefunctiononeachobservable.
Finally,weapplyfoldLefttosumallthenumbersandgetatotal.However,whenwedothat,wegetonly90%ofthenumbers.Thislastobservableiswhatisreturnedtothecontroller.Nothingisblockedhere,andit'sallAsyncandreactive.
YoumaybewonderingwhyStream.Continuouslygeneratesdifferentvaluesallthetime.ThathappensbecauseweuseaCall-by-NamefunctioninScala.WeimportthenextDoublefunction,andpassafunctioninsteadofthevalueofthefunction:
@Singleton
classPriceServiceextendsIPriceSerice{
vardoubleInfiniteStreamSubject=PublishSubject.apply[Double]()
Future{
Stream.continually(nextDouble*1000.0).foreach{
x=>Thread.sleep(1000);
doubleInfiniteStreamSubject.onNext(x)
}
}
overridedefgeneratePrices:Observable[Double]={
varobservableEven=Observable.create{
doubleInfiniteStreamSubject.subscribe}
.subscribeOn(IOScheduler())
.flatMap{x=>Observable.from(Iterable.fill(1)(x+10))
}
.filter{x=>x.toInt%2==0}
varobservableOdd=Observable.create{
doubleInfiniteStreamSubject.subscribe}
.subscribeOn(IOScheduler())
.flatMap{x=>Observable.from(Iterable.fill(1)(x+10))
}
.filter{x=>x.toInt%2!=0}
varmergeObservable=Observable
.empty
.subscribeOn(IOScheduler())
.merge(observableEven)
.merge(observableOdd)
.take(10)
.foldLeft(0.0)(_+_)
.flatMap{x=>Observable.just(x-(x*0.9))}
returnmergeObservable
}
}
WeneedtoregisterthisserviceinGuiceinModule.scalalocatedinthedefaultpackageatReactiveWebStore/app.
GuiceInjection-Module.scalaYourModule.scalafileshouldlooksomethinglikethis:
classModuleextendsAbstractModule{
overridedefconfigure()={
//UsethesystemclockasthedefaultimplementationofClock
bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
//AskGuicetocreateaninstanceofApplicationTimerwhen
//the
//applicationstarts.
bind(classOf[ApplicationTimer]).asEagerSingleton()
bind(classOf[IProductService]).to(classOf[ProductService]).
asEagerSingleton()
bind(classOf[IReviewService]).to(classOf[ReviewService]).
asEagerSingleton()
bind(classOf[IImageService]).to(classOf[ImageService]).
asEagerSingleton()
bind(classOf[IPriceSerice]).to(classOf[PriceService]).
asEagerSingleton()
}
}
Inordertocompileandruntheprecedingcode,weneedtoaddanextraSBTdependency.Openbuild.sbt,andaddRxScala.Wewillalsoaddanotherdependency,whichisws,aplaylibrarytomakewebservicecalls.Wewilluseitlaterinthischapter.
Yourbuild.sbtshouldlooksomethinglikethis:
name:="""ReactiveWebStore"""
version:="1.0-SNAPSHOT"
lazyvalroot=(projectinfile(".")).enablePlugins(PlayScala)
scalaVersion:="2.11.7"
libraryDependencies++=Seq(
jdbc,
cache,
ws,
"org.scalatestplus.play"%%"scalatestplus-play"%"1.5.1"%
Test,
"com.netflix.rxjava"%"rxjava-scala"%"0.20.7"
)
resolvers+="scalaz-bintray"at
"http://dl.bintray.com/scalaz/releases"
resolvers+=DefaultMavenRepository
Nowwecancompileandrunthiscodeusingactivatorrun.
WecancallthisnewroutenowusingaCURLcall.Ifyouprefer,youcanjustopenyourbrowseranddoitthere.
curl-vhttp://localhost:9000/rx/prices
curl-vhttp://localhost:9000/rx/aprices
Wewillseethefollowingresult:
Great!WehaveRxScalaworkingwiththePlayframework.Nowwewillrefactorourcodetomakeitevenmoreinteresting.SowewillcreateamicroserviceusingthePlayframework,andwewillexternalizethisrandomnumbergeneratortothemicroservice.
WeneedtocreateanewPlayframeworkapplication.Wewillpicktheoptionnumber6)play-scalaapplicationtemplate.Openyourconsole,andthentypethefollowingcommand:
$activatornewng-microservice
Youwillseethisresult:
Let'screatetheroutesonng-microservice.Wewon'thaveanyUIhere,sincethiswillbeamicroservice.Weneedtoaddarouteatng-microservice/conf/routes:
#Routes
#Thisfiledefinesallapplicationroutes(Higherpriorityroutes
first)
#~~~~
GET/doublecontrollers.NGServiceEndpoint.double
GET/doubles/:ncontrollers.NGServiceEndpoint.doubles(n:Int)
Nowlet'sdefinethecontroller.Thisisnotaregularcontroller,becausethisonewon'tserveUIviews.Instead,itwillserveJSONtothemicroserviceconsumers.Herewewillhavejustoneconsumer,whichwillbeReactiveWebStore.However,itispossibletohaveasmany
consumersasyouwishsuchasothermicroservicesorevenmobileapplications.
NGServiceEndpointForthiscontroller,wejusthavetworoutes.Theroutesaredoubleanddoubles.Thefirstroutereturnsadoublefromtheservice,andthesecondonereturnsalistofdoublesgeneratedinabatch.Forthesecondmethod,wegetalistofdoublesandtransformthatlistinJSONusingthePlayframeworkutilitylibrarycalledJson:
classNGServiceEndpoint@Inject()(service:NGContract)extends
Controller{
defdouble=Action{
Ok(service.generateDouble.toString())
}
defdoubles(n:Int)=Action{
valjson=Json.toJson(service.generateDoubleBatch(n))
Ok(json)
}
}
Thenextstepiscreatetraitforthemicroservice.ThistraitisalsotheservicecontractinServiceOrientedArchitecture(SOA)terms,thatis,thecapabilitiesthatthemicroserviceoffers.
TheNGContract.scalafileshouldlooksomethinglikethis:
traitNGContract{
defgenerateDouble:Double
defgenerateDoubleBatch(n:Int):List[Double]
}
Let'slookattheServiceimplementationofthismicroservice:
packageservices
importscala.util.Random
importscala.util.Random.nextDouble
classNGServiceImplextendsNGContract{
overridedefgenerateDouble:Double={
Stream.continually(nextDouble*1000.0)
.take(1)
}
overridedefgenerateDoubleBatch(n:Int):List[Double]={
require(n>=1,"Numbermustbebiggerthan0")
valnTimes:Option[Int]=Option(n)
nTimesmatch{
caseSome(number:Int)=>
Stream.continually(nextDouble*1000.0)
.take(n)
.toList
caseNone=>
thrownewIllegalArgumentException("Youneedprovideavalid
numberofdoublesyouwant.")
}
}
}
ThisserviceimplementationdoesnothaveanyRxScalacode.However,itisveryfunctional.Wehavetwomethodsimplementedhere.ThemethodsaregenerateDoubleandgenerateDoubleBatch,whichreceive,throughparameters,thenumberofdoublesyouwantittogenerateforyou.Forthefirstoperation(generateDouble),weuseStream.continuallytogenerateinfiniterandomdoubles,thenwemultiplythesenumbersby1,000,andthentakejust1andreturnit.
Thesecondoperationisverysimilar.However,wehavetoaddsomevalidationstomakesurethatthenumbersofdoublearepresent.Thereareacoupleofwaystodoit.OnewayusetheassertmethodinScala.Thesecondwayisthepatternmatcher,whichisnicebecausewedon'tneedtowriteanifstatement.
ThistechniqueisverycommonintheScalacommunity.Sowecreateanoptionthattakesthenumber,andthenwepattern-matchit.Ifthereisanumberpresent,thecaseSomemethodwillbetriggered,otherwise,thecaseNonewillbecalled.
Afterthesevalidations,wecanuseStreamtogenerateasmanynumbersasrequested.Beforewerunthecode,weneedtodefinetheGuiceinjections.Thisfileislocatedinthedefaultpackageatng-microservice/app/:
classModuleextendsAbstractModule{
overridedefconfigure()={
//UsethesystemclockasthedefaultimplementationofClock
bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
bind(classOf[NGContract]).to(classOf[NGServiceImpl]).
asEagerSingleton()
}
}
Nowitstimetocompileandrunourmicroservice.SincewealreadyhaveaplayapplicationcalledReactiveWebStorerunningonport9000,youwillhavetroubleifyousimplyrunthemicroservice.Tofixthis,weneedtorunitonadifferentport.Let'suse9090forthemicroservice.Opentheconsole,andexecutethecommand$activatorfollowedby$run9090:
ng-microservice$activator-Dsbt.task.forcegc=false
[info]Loadingglobalpluginsfrom/home/diego/.sbt/0.13/plugins
[info]Loadingprojectdefinitionfrom
/home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_Applic
ations/Chap4/ng-microservice/project
[info]Setcurrentprojecttong-microservice(inbuild
file:/home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_A
pplications/Chap4/ng-microservice/)
[ng-microservice]$run9090
---(Runningtheapplication,auto-reloadingisenabled)---
[info]p.c.s.NettyServer-ListeningforHTTPon/0:0:0:0:0:0:0:0:9090
(Serverstarted,useCtrl+Dtostopandgobacktotheconsole...)
Wecantestourmicroservicebycallingthetwooperationsthatwehave.Solet'sopenthewebbrowseranddoit.
Thedoublemicroservicecallathttp://localhost:9090/doublelooksasfollows:
Everytimeyoucallthisoperation,you'llseeadifferentrandomdoublenumber.Nowwecantryoutthenextoperation:theoneyoupassforthenumberofdoublesyouwant.ThisoperationwillreturnalistofdoublesintheJSONformat.
Thecallfordoubleinbatchesmicroserviceathttp://localhost:9090/doubles/100lookslikethefollowingscreenshot:
Itworks!Wehave100doubleshere.Nowthatwehaveamicroserviceworking,wecangobacktoourReactiveWebStoreandchangeourRxScalacode.Wewillcreatenewcontrollers.Wewillalsoupdatetheexistingcodeforproductstocallournewcode,andsuggestapricefortheuserontheUI,allinareactivemanner.Keepinmindthatyouneedtohaveng-microservicerunning;otherwise,ReactiveWebStorewon'tbeabletoretrievedoubles.
PlayframeworkandhighCPUusageIfyounoticeyourCPUusagegoinghigherthanitshould,donotworry;thereisafixforit.Actually,theissueisrelatedtoSBT.Justmakesurethat,whenyourunActivator,youpassthefollowingparameter:
$activator-Dsbt.task.forcegc=false.
GoingbacktoReactiveWebStore,let'screatenewroutes.OpenReactiveWebStore/conf/routes:
GET/rnd/double
controllers.RndDoubleGeneratorController.rndDouble
GET/rnd/callcontrollers.RndDoubleGeneratorController.rndCall
GET/rnd/rxcontrollers.RndDoubleGeneratorController.rxCall
GET/rnd/rxbat
controllers.RndDoubleGeneratorController.rxScalaCallBatch
Oncewehavethenewroutes,weneedtocreatethenewcontroller.ThiscontrollerneedstobelocatedwiththeothercontrollersatReactiveWebStore/app/controllers.
RndDoubleGeneratorControllerTheRndDoubleGeneratorControllerclassfileshouldlooklikethis:
@Singleton
classRndDoubleGeneratorController@Inject()(service:IRndService)
extendsController{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
defrndDouble=Action{implicitrequest=>
Ok(service.next().toString())
}
defrndCall=Action.async{implicitrequest=>
service.call().map{res=>Ok(res)}
}
defrxCall=Action{implicitrequest=>
Ok(service.rxScalaCall().toBlocking.first.toString())
}
defrxScalaCallBatch=Action{implicitrequest=>
Ok(service.rxScalaCallBatch().toBlocking.first.toString())
}
}
AllmethodsintheprecedingcontrollercalltheserviceIRndService.AlloperationsinRndServicecallng-microservice.Herewehavesomeflavorsofoperations,whichwillbecoveredingreatdetailnextwhenweexploretheserviceimplementation.
Therearesomeinterestingthingshere:forinstance,forthesecondoperationcalledrndCall,weseeAction.asyncinuse,whichmeansourcontrollerwillreturnaFuture,andthisFuturecomesfromtheservice.WealsoexecuteaMaptotransformtheresultintoanOk.
ThelastoperationcalledrxScalaCallBatchisthemostinteresting,andtheonewewillbeusingforourUI.However,youcanusetheotheroneifyouwish,sincetheyallreturndoubles,andthat'sgood.
IRndService.scala-ScalatraitLet'slookattheservicedefinition.Firstofall,weneedtodefineatraitfortheservicethatwilldefinetheoperationsweneed:
traitIRndService{
defnext():Double
defcall():Future[String]
defrxScalaCall():Observable[Double]
defrxScalaCallBatch():Observable[Double]
}
RndService.scala-RndServiceimplementationNowwecanmovetotherealserviceimplementation.ThisneedstobelocatedatReactiveWebStore/app/services:
@Singleton
classRndService@Inject()(ws:WSClient)extendsIRndService{
importplay.api.libs.concurrent.Execution.Implicits.
defaultContext
overridedefnext():Double={
valfuture=ws.url("http://localhost:9090/double").get().map
{res=>res.body.toDouble}
Await.result(future,5.seconds)
}
overridedefcall():Future[String]={
ws.url("http://localhost:9090/double").get().map
{res=>res.body}
}
overridedefrxScalaCall():Observable[Double]={
valdoubleFuture:Future[Double]=
ws.url("http://localhost:9090/double").get().map{x=>
x.body.toDouble}
Observable.from(doubleFuture)
}
//Continue...
Inordertocallourmicroservice(ng-microservice),weneedtoinjectaspecialPlayframeworklibrarycalledws,autilitylibrarytocallwebservices.Weinjectitbyaddingthecode(ws:WSClient)intotheclassdefinition.
Whenyoucallsomethingwithws,itreturnsaFuture.WeneedtohavetheFutureexecutorsinplace.That'swhytheimportdefaultContextisveryimportant,andyoucannotskipit.
Forthemethod,asyoucansee,wenextcallourmicroserviceathttp://localhost:9090/doubletogetasingledouble.Wemapthisresult,andgetthebodyoftheresult,whichwillbethedoubleitself.
Forthismethod,weuseAwait.result,whichwillblockandwaitfortheresult.Iftheresultisnotbackinfiveseconds,thiscodewillfail.
Thesecondmethodcalledcalldoesthesame,butthemaindifferenceisthatwearenotblockingtheservice;instead,wearereturningaFuturetothecontroller.
Finally,thelastmethodcalledrxScalaCalldoesthesame:itcallsourmicroserviceusingthewslibrary.However,wereturnanobservable.ObservablesaregreatbecausetheycanusedasaFuture.
Nowitistimetogocheckoutthefinaloperationandthemostinterestingone.Forthissameclass,weneedtoaddanothermethodsuchasthisone:
ThemethodrxScalaCallBatchinRndService.scalaisasfollows:
overridedefrxScalaCallBatch():Observable[Double]={
valdoubleInfiniteStreamSubject=PublishSubject.apply[Double]()
valfuture=ws.url("http://localhost:9090/doubles/10")
.get()
.map{x=>Json.parse(x.body).as[List[Double]]}
future.onComplete{
caseSuccess(l:List[Double])=>l.foreach{e=>
doubleInfiniteStreamSubject.onNext(e)}
caseFailure(e:Exception)=>
doubleInfiniteStreamSubject.onError(e)
}
varobservableEven=Observable.create{
doubleInfiniteStreamSubject.subscribe}
.onErrorReturn{x=>2.0}
.flatMap{x=>Observable.from(Iterable.fill(1)(x+10))}
.filter{x=>x.toInt%2==0}
.flatMap{x=>println("ODD:"+x);Observable.just(x)}
varobservableOdd=Observable.create{
doubleInfiniteStreamSubject.subscribe}
.onErrorReturn{x=>1.0}
.flatMap{x=>Observable.from(Iterable.fill(1)(x+10))}
.filter{x=>x.toInt%2!=0}
.flatMap{x=>println("EVEN:"+x);Observable.just(x)}
varmergeObservable=Observable
.empty
.merge(observableEven)
.merge(observableOdd)
.take(10)
.foldLeft(0.0)(_+_)
.flatMap{x=>Observable.just(x-(x*0.9))}
mergeObservable
}
So,firstwecreatePublishSubjectinordertobeabletoproducedatafortheobservables.Thenwemakethewscalltoourmicroservice.Themaindifferencenowisthatwecallthebatchesoperationandorder10doubles.Thiscodehappensinafuture,soitisnon-blocking.
WethenusetheMapfunctiontotransformtheresult.Theng-microservicefunctionwillreturnJSON,soweneedtodeserializethisJSONintoScalaobjects.Finally,werunapatternmatcherintheFutureresult.Iftheresultisasuccess,itmeanseverythingisgood.So,foreachdouble,wepublishintotheobservablesusingPublishSubject.Iftheserviceisdownorwehaveaproblem,wepublishanerrortotheobservablesdownstream.
Nextwecreatethreeobservables:oneforoddnumbers,oneforevennumbers,andathirdonewhichwillmergetheothertwoanddoextracomputation.ThewaywedidtheconversionbetweenFutureandObservableisideal,becauseitisnon-blocking.
HerewehavecodeverysimilartowhatwehadbeforefortheRxcontroller.Themaindifferenceisthatwehaveerrorhandling,becauseng-microservicemightneverreturn,asitmaybedownorjustnotworking.Soweneedtostartworkingwithfallbacks.Goodfallbacks
arekeytoerrorhandlingforReactiveapplications.Fallbacksshouldbesortofstatic;inotherwords,theyshouldnotfailatall.
Weprovidedtwofallbackmethods:onefortheOddObservableandtheotherfortheEvenObservable.ThesefallbacksaredonebysettingthemethodOnErrorReturn.Sofortheevenone,thefallbackisstaticandthevalueis2,andfortheoddonethevalueis1.Thisisgreat,becauseevenwithfailureourapplicationcontinuestowork.
Youmightrealizewearenotusingthetakefunctionthistime.Sowillthiscoderunforever?No,becauseng-microservicejustreturns10doubles.Finally,wemergetheobservablesintoasingleobservable,addallthenumbers,get90%ofthevalue,andreturnanobservable.
Module.scala-GuiceInjectionsNowhookthisnewserviceinGuice.Let'schangetheGuiceModule.scalalocatedatReactiveWebStore/apps/Module.scala:
classModuleextendsAbstractModule{
overridedefconfigure()={
bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
bind(classOf[ApplicationTimer]).asEagerSingleton()
bind(classOf[IProductService]).to(classOf[ProductService]).
asEagerSingleton()
bind(classOf[IReviewService]).to(classOf[ReviewService]).
asEagerSingleton()
bind(classOf[IImageService]).to(classOf[ImageService]).
asEagerSingleton()
bind(classOf[IPriceSerice]).to(classOf[PriceService]).
asEagerSingleton()
bind(classOf[IRndService]).to(classOf[RndService]).
asEagerSingleton()
}}
NextweneedtocreateaJQueryfunctioninJavaScripttocallournewcontroller.ThisfunctionneedstobelocatedatReactiveWebStore/public/javascripts.
Thefollowingisprice.js,theJQueryfunctionthatcallsourcontroller:
/**
*ThisfunctionsloadsthepriceintheHTMLcomponent.
*/
functionloadPrice(doc){
jQuery.get("http://localhost:9000/rnd/rxbat",function(
response){
doc.getElementById("price").value=parseFloat(response)
}).fail(function(e){
alert('Wops!Wewasnotabletocall
http://localhost:9000/rnd/rxba.Error:'+e.statusText);
});
}
WejusthaveasinglefunctioncalledloadPrice,whichreceivesadocument.WeusetheJQuery.getmethodtocallourcontroller,andparsetheresponse,addingtotheHTMLtextboxcalledprice.Ifsomethinggoeswrong,wealerttheuserthatitwasnotpossibletoloadaprice.
main.scala.htmlWeneedtochangeourmain.scalacodelocatedatReactiveWebStore/app/views/main.scala.htmlinordertoimportanewJavaScriptfunction:
@(title:String)(content:Html)(implicitflash:Flash)
<!DOCTYPEhtml>
<htmllang="en">
<head>
<title>@title</title>
<linkrel="shortcuticon"type="image/png"
href="@routes.Assets.at("images/favicon.png")">
<linkrel="stylesheet"media="screen"
href="@routes.Assets.at("stylesheets/main.css")">
<linkrel="stylesheet"media="screen"
href="@routes.Assets.at("stylesheets/bootstrap.min.css")">
<scriptsrc="@routes.Assets.at("javascripts/jquery-
1.9.0.min.js")"type="text/javascript"></script>
<scriptsrc="@routes.Assets.at("javascripts/bootstrap.js")"
type="text/javascript"></script>
<scriptsrc="@routes.Assets.at("javascripts/image.js")"
type="text/javascript"></script>
<scriptsrc="@routes.Assets.at("javascripts/price.js")"
type="text/javascript"></script>
</head>
<body>
<center><ahref='/'><imgheight='42'width='42'
src='@routes.Assets.at("images/rws.png")'></a>
<h3>@title</h3></center>
<divclass="container">
@alert(alertType:String)={
@flash.get(alertType).map{message=>
<divclass="alertalert-@alertType">
<buttontype="button"class="close"data-
dismiss="alert">×</button>
@message
</div>
}
}
@alert("error")
@alert("success")
@content
<ahref="/"></a><BR>
<buttontype="submit"class="btnbtn-primary"
onclick="window.location.href='/';">
ReactiveWebStore-Home
</button>
</div>
</body>
</html>
product_details.scala.htmlFinally,weneedtochangeourproductviewinordertoaddabuttontoloadthepricefromthecontroller.Let'schangetheproduct_detailsviewatReactiveWebStore/app/views/product_details.scala.html:
@(id:Option[Long],product:Form[Product])(implicitflash:Flash)
@importplay.api.i18n.Messages.Implicits._
@importplay.api.Play.current
@main("Product:"+product("name").value.getOrElse("")){
@if(product.hasErrors){
<divclass="alertalert-error">
<buttontype="button"class="close"data-
dismiss="alert">×</button>
Sorry!Someinformationdoesnotlookright.Couldyou
reviewitpleaseandre-submit?
</div>
}
@helper.form(action=if(id.isDefined)
routes.ProductController.update(id.get)else
routes.ProductController.insert){
@helper.inputText(product("name"),'_label->"ProductName")
@helper.inputText(product("details"),'_label->"Product
Details")
@helper.inputText(product("price"),'_label->"Price")
<divclass="form-actions">
<buttontype="button"class="btnbtn-primary"
onclick="javascript:loadPrice(document);">LoadRnd
Price</button>
<buttontype="submit"class="btnbtn-primary">
@if(id.isDefined){UpdateProduct}else{NewProduct}
</button>
</div>
}
}
Great!NowwehaveabuttonthatloadsthedatafromthecontrollerusingJQuery.YoucanrealizethatthebuttonLoadRndPricehasanonClickproperty,whichcallsourJavaScriptfunction.
Nowyouneedtoopenyourconsoleandtype$activatorruntocompileandrunthechangesaswedidtoReactiveWebStore.
Thiscommandwillgivethefollowingresult:
ReactiveWebStore$activator-Dsbt.task.forcegc=false
[info]Loadingglobalpluginsfrom/home/diego/.sbt/0.13/plugins
[info]Loadingprojectdefinitionfrom
/home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_Applic
ations/Chap4/ReactiveWebStore/project
[info]SetcurrentprojecttoReactiveWebStore(inbuild
file:/home/diego/github/diegopacheco/Book_Building_Reactive_Functional_Scala_A
pplications/Chap4/ReactiveWebStore/)
[ReactiveWebStore]$run
---(Runningtheapplication,auto-reloadingisenabled)---
[info]p.c.s.NettyServer-ListeningforHTTPon/0:0:0:0:0:0:0:0:9000
(Serverstarted,useCtrl+Dtostopandgobacktotheconsole...)
[info]application-ApplicationTimerdemo:Startingapplicationat2016-07-
03T02:35:54.479Z.
[info]play.api.Play-Applicationstarted(Dev)
Soopenyoubrowserathttp://localhost:9000,andgototheproductpagetoseeourneedfeatureintegratedandworkinglikeacharm.Keepinmindthatyouneedtohaveng-microserviceworkinginanotherconsolewindow;otherwise,ourapplicationwillusestaticfallbacks.
Thenewproductfeaturewillbeshownathttp://localhost:9000/product/addasseeninthefollowingscreenshot:
SoifyouclickontheLoadRndPricebutton,youwillseesomethinglikethis:
Ifyoutakealookattheapplicationlogintheactivatorconsole,youwillseesomethingsimilartothis:
[info]application-ApplicationTimerdemo:Startingapplicationat2016-07-
03T02:35:54.479Z.
[info]play.api.Play-Applicationstarted(Dev)
[info]application-indexcalled.Products:List()
[info]application-blankcalled.
ODD:722.8017048639501
EVEN:863.8229024202085
ODD:380.5549208988492
EVEN:947.6312814830953
ODD:362.2984794191124
ODD:676.978825910585
ODD:752.7412673916701
EVEN:505.3293481709368
EVEN:849.9768444508936
EVEN:99.56583617819769
Alright,that'sit.Wehaveeverythingworking!
SummaryInthischapter,youlearnedthecoreprinciplesofreactiveapplicationguidedbytheReactiveManifesto.YoualsolearnedhowtocreatereactiveapplicationsusingRxScala.Wethenexplainedhowtocallotherinternalandexternalwebservicesusingthewslibrary.ThenyoulearnedtoserializeanddeserializeJSONusingtheJsonLibrary.Movingon,youlearnedhowtocreatesimplemicroservicesusingthePlayframeworksupport.
Inthenextchapter,wewillcontinuebuildingourapplication,andlearnhowtotestourapplicationwithJUnitandScalaTest.
Chapter5.TestingYourApplicationInthechapterssofar,webootstrappedourapplicationusingActivator,developedourwebapplicationusingScalaandthePlayframework,andaddedaReactivemicroservicecallusingRxScalafordataflowcomputations.NowwewillgoaheadandentertheunittestandcontrollertestingusingtheBDDandPlayframework.
Inthischapter,wewillcoverthefollowingtopics:
UnittestingprinciplesTestingScalaapplicationswithJUnitBehavior-drivendevelopmentprinciplesTestingwithScalaTestTestingPlayframeworkapplicationswithScalaTestRunningtestsinActivator/SBT
TheneedfortestingTestisafundamentalandveryimportantpartofsoftwaredevelopment.Withouttests,wecannotbesurethatourcodeworks.Weshouldperformtestsonalmostallthecodeweproduce.Therearethingsthatdon'tmakesensefortesting,forinstance,caseclassesandclassesthatjustrepresentstructuralobjects,or,inotherwords,classeswithoutfunctions.Ifyouhavecodethatappliescomputations,transformations,andvalidations,youdefinitelywanttotestthiscodewithagoodcodecoverage,whichreferstofeaturesoranyimportantcodethatisnotjuststructural.
Testcoverageisimportant,becauseitallowsustodorefactoring(improveapplicationcodequalitybyreducingcode,creatinggenericcode,orevendeletingcode)withtrust.Thisisbecause,ifyouhavetestsandifyoudosomethingwrongbyaccident,yourtestswillletyouknow.Thisisallabouthavingshortcyclesoffeedback.Theearlierthebetter;youwanttoknowifyouhaveintroducedbugsassoon,andasclosetodevelopment,aspossible.Nobodylikestodiscoverbugsthatcouldbecaughtbyasimpletestinproduction.
UnittestingprinciplesUnittestingisthesmallestunitoftestingthatyoucouldpossiblyapply.Youneedtoapplyitattheclasslevel.Soaunittestwillcoveryourclasswithallthefunctionsyouhavethere.Butholdonaminute,aclassoftenhasdependencies,andthesedependenciesmighthaveotherdependencies,sohowdoyoutestthat?Weneedtohavemocks,simpledumbobjectsthatsimulateotherclasses'behavior.Thisisanimportanttechniquetoisolatecodeandallowunittesting.
MakingcodetestableUnittestingissimple:basically,wecallafunctionbypassingargumentstoit,andthenwechecktheoutputtoseeifitmatchesourexpectations.Thisisalsocalledassertsorassertions.So,unittestingisaboutasserts.Sometimes,yourcodemightnotbetestable.Forinstance,let'ssayyouhaveafunctionthatreturnsaunitandhasnoparameters.Thisisverytoughtotest,becauseitimpliesthatthefunctionisfullofside-effects.IfyourememberwhatwediscussedinChapter1,IntroductiontoFP,Reactive,andScala,thisisagainstFPprinciples.So,ifwehavethiscase,weneedtorefactorthecodetomakethefunctionreturnsomething,andthenwecantestit.
Isolationandself-containedtestsUnittestsshouldbeself-contained,whichmeansthataunittest'sclassesshouldnotdependonanyparticularorderofexecution.Let'ssayyouhaveaunittestclasswithtwotestfunctions.So,eachtestshouldtestjustonefunctionatatime,andbothfunctionsshouldbeabletoruninanyorderwhatsoever.Otherwise,thetestswillbefragileandhardtomaintaininthelongrun.
EffectivenamingEffectivenamingisessential.Thetestfunctionneedstosayexactlywhatthetestdoes.Thisisimportantbecause,whenthetestfails,itiseasiertofigureoutwhatwentwrongandwhy.Followingthesameidea,whenyoudoassertions,youshouldassertjustonethingatatime.ImaginethatyouneedtotestwhetherawebservicereturnsavalidJSON.ThisparticularJSONcouldhavetwofields:firstnameandlastname.So,youwillmakeoneassertforthenameandanotherforthelastname.Thisway,itwillbeeasiertounderstandwhatthetestdoes,andtotroubleshootwhenthetestfails.
LevelsoftestingWhenweruntests,weoftendoitinlayers.Unittestingisthebasiclevel;however,thereareotherlevelssuchascontrollertests,integrationtests,UItests,End-to-Endtests,stresstests,andsomanyothers.Forthisbook,wewillcoverunittests,controllertests,andUItestsusingJunitandScalaTest,Play'sframeworksupport.
TestingwithJunitIfyoucomefromaJavabackground,itishighlypossiblethatyouhavealreadyworkedwithJunit.It'spossibletotestwithJunitusingtheScalaandPlayframework.However,thisisnotthebestpracticewhenwearecreatingapplicationswiththePlayframework,sinceitfavorsBehaviorDrivenDevelopment(BDD)testingwithScalaSpec.Forthischapter,wewillcoverhowtoperformallsortsoftestusingBDDandPlay.Rightnow,let'stakealookathowwecandounittestingwithJunitbeforewemovetoBDD.
@RunWith(classOf[Suite])
@Suite.SuiteClasses(Array(classOf[JunitSimpleTest]))
classJunitSimpleSuiteTest
classJunitSimpleTestextendsPlaySpecwithAssertionsForJUnit{
@TestdeftestBaseService(){
vals=newProductService
valresult=s.findAll()
assertEquals(None,result)
assertTrue(result!=null)
println("AllgoodjunitworksfinewithScalaTestandPlay")
}
}
SowhatwehaveintheprecedingcodeisaclassthatextendsPlaySpec,andaddsatraitcalledAssertionForJunit.Whydon'twehavetheclassicalJunitclasshere?BecausethePlayframeworkissetuptorunScalatests,sothisbridgeallowsustorunJunitbyScalaTestPlayframeworkconstructs.
ThenwehaveatestfunctioncalledtestBaseServer,[email protected],wecreateaninstanceofProductService,andthenwecallthefunctionfindAll.
Finally,wehaveassertionsthatwillcheckiftheresultiswhatweareexpecting.Sowedon'thaveproducts,becausewedidnotinsertthemearlier.Hence,weexpecttohaveNoneastheresponse,andtheresultshouldalsonotbenull.
Youcanrunthisinyourconsoleusingthefollowingcommand:
$activator"test-onlyJunitSimpleTest"
Youwillseetheresultshowninthenextscreenshot:
Asyoucansee,ourtestwasexecutedwithoutanyissues.It'salsopossibletorunthistestandanormaltestinJunitusingtheEclipseIDE.Youjustright-clickonthefileandselectRunAs:ScalaJunitTest;refertothefollowingscreenshot:
Behavior-DrivenDevelopment-BDDBehavior-DrivenDevelopment(BDD)isanagiledevelopmenttechnique,thatfocusesontheengagementbetweendevelopersandnon-technicalpeoplesuchasproductownersfromthebusiness.Theideaisprettysimple:usethesamelanguageasthebusinessusesinordertoextractthereasonwhythecodeyouarebuildingexistsinthefirstplace.BDDendsupminimizingthetranslationbetweentechlanguageandbusinesslanguage,creatingmoresynergyandlessnoisebetweeninformationtechnologyandbusiness.
BDDtestsdescribewhattheapplicationneedstodo,andhowitbehaves.It'sverycommontowritedownthesetestsusingpairprogramingbetweenbusinesspeopleanddevelopers.ScalaTestisaBDDframework.PlayframeworkhasagreatintegrationwithScalaTest.Let'sgetstartedwithScalaTestandPlayrightnow.
MyFirstPlaySpec.scala-FirstBDDwithScalaTestandthePlayframeworkTheMyFirstPlaySpec.scalaclassshouldcontainthefollowingcode:
classMyFirstPlaySpecextendsPlaySpec{
"x+1"must{
"be2ifx=1"in{
valsum=1+1
summustBe2
}
"be0ifx=-1"in{
valsum=-1+1
summustBe0
}
}
}
So,youcreateaclasscalledMyFirstPlaySpec,andweextenditfromPlaySpecinordertogetPlayframeworkScalaTestsupport.Thenwecreatetwofunctionstotestthesumoftwonumbers.Inthefirsttest,1+1shouldbe2,andinthesecond,-1+1shouldbe0.WhenweexecutemustBe,itisthesamethingasdoinganassertinJunit.ThemaindifferencehereisthatthetesthasbehaviorexplicitlyontheSpec.Nowwecanrunthetestbytypingthefollowing:
$activator"test-onlyMyFirstPlaySpec"
Youwillseethefollowingresult:
TestingwithPlayframeworksupportNowwewillcontinuebuildingourapplication.Let'saddBDDtestsinourapplication.Wewillstartdoingtestsforyourservices.WehavetotestProductService,ImageService,andReviewService.
YourProductServiceTestSpec.scalafileshouldcontainthefollowingcode:
classProductServiceTestSpecextendsPlaySpec{
"ProductService"must{
valservice:IProductService=newProductService
"insertaproductproperly"in{
valproduct=newmodels.Product(Some(1),
"Ball","AwesomeBasketball",19.75)
service.insert(product)
}
"updateaproduct"in{
valproduct=newmodels.Product(Some(1),
"BlueBall","AwesomeBlueBasketball",19.99)
service.update(1,product)
}
"notupdatebecausedoesnotexit"in{
intercept[RuntimeException]{
service.update(333,null)
}
}
"findtheproduct1"in{
valproduct=service.findById(1)
product.get.idmustBeSome(1)
product.get.namemustBe"BlueBall"
product.get.detailsmustBe"AwesomeBlueBasketball"
product.get.pricemustBe19.99
}
"findall"in{
valproducts=service.findAll()
products.get.lengthmustBe1
products.get(0).idmustBeSome(1)
products.get(0).namemustBe"BlueBall"
products.get(0).detailsmustBe"AwesomeBlueBasketball"
products.get(0).pricemustBe19.99
}
"findallproducts"in{
valproducts=service.findAllProducts()
products.lengthmustBe1
products(0)._1mustBe"1"
products(0)._2mustBe"BlueBall"
}
"remove1product"in{
valproduct=service.remove(1)
productmustBetrue
valoldProduct=service.findById(1)
oldProductmustBeNone
}
"notremovebecausedoesnotexist"in{
intercept[RuntimeException]{
service.remove(-1)
}
}
}
}
Forthistest,wearetestingallpublicfunctionsavailableinProductService.Thetestsareprettystraightforward:wecallaspecificserviceoperation,suchasfindById,andthenwechecktheresulttomakesurethatallthedatathatissupposedtobethereispresent.
Therearescenarioswheretheserviceshouldreturnanexception,forinstance,ifyoutrytoremovesomethingthatdoesnotexist.Ifyoutakealookatthelasttestfunctioncalled"notremovebecausedoesnotexist",weshouldgetanexception.However,thereisabugintheservicecode.Runthetests,andthenyouwillseeit.
ProductService.scala-FIXthecodeissueThat'sthegreatthingabouttests:theyshowissuesinourcodesothatwecanfixthembeforethecodegoestoproductionandaffectstheuserexperience.Tofixthelasttest,weneedtogototheProductServiceclassandfixamethod.Thisishowwefixit:
privatedefvalidateId(id:Long):Unit={
valentry=inMemoryDB.get(id)
if(entry==null||entry.equals(None))thrownew
RuntimeException("CouldnotfindProduct:"+id)
}
Allsetnow,everythingisokay.ThePlayframeworksupportstestingforexpectedexceptionsusingtheinterceptfunctiontopasstheexpectedexception,Let'srunthetestintheconsoleusingtheactivatorcommand.
$activator"test-onlyProductServiceTestSpec"
Afterexecutingthiscommand,wegetthefollowing:
ImageServiceTestSpec.scala-ImageServiceTestAlright,NowwecanaddtestsforImageServiceasfollows:
classImageServiceTestSpecextendsPlaySpec{
"ImageService"must{
valservice:IImageService=newImageService
"insertaimageproperly"in{
valimage=newmodels.Image(Some(1),Some(1),
"http://www.google.com.br/myimage")
service.insert(image)
}
"updateaimage"in{
valimage=newmodels.Image(Some(2),Some(1),
"http://www.google.com.br/myimage")
service.update(1,image)
}
"notupdatebecausedoesnotexist"in{
intercept[RuntimeException]{
service.update(333,null)
}
}
"findtheimage1"in{
valimage=service.findById(1)
image.get.idmustBeSome(1)
image.get.productIdmustBeSome(1)
image.get.urlmustBe"http://www.google.com.br/myimage"
}
"findall"in{
valreviews=service.findAll()
reviews.get.lengthmustBe1
reviews.get(0).idmustBeSome(1)
reviews.get(0).productIdmustBeSome(1)
reviews.get(0).urlmustBe"http://www.google.com.br/myimage"
}
"remove1image"in{
valimage=service.remove(1)
imagemustBetrue
valoldImage=service.findById(1)
oldImagemustBeNone
}
"notremovebecausedoesnotexist"in{
intercept[RuntimeException]{
service.remove(-1)
}
}
}
}
SothesearetheBDDtestsforImageService.Wehavecoveredalltheavailablefunctionsontheservice.LikeintheProductServiceclass,wealsohavetestsforunfortunatescenarioswhereweexpectexceptionstohappen.
Sometimes,weneedtocallmorethanonefunctiontotestaspecificfunctionoraspecifictest
case.Forexample,in"remove1image",wefirstdeleteanimage.Ourtestcasechecksforanimagethatdoesnotexist.Let'srunthetestsontheActivatorconsole.
$activator"test-onlyImageServiceTestSpec"
Thefollowingresultwillbeobtained:
ReviewServiceTestSpec.scala-ReviewServicetestNext,weneedaddtestsforthereviewservice.Herewego.
classReviewServiceTestSpecextendsPlaySpec{
"ReviewService"must{
valservice:IReviewService=newReviewService
"insertareviewproperly"in{
valreview=new
models.Review(Some(1),Some(1),"diegopacheco",
"TestingisCool")
service.insert(review)
}
"updateareview"in{
valreview=newmodels.Review(Some(1),Some(1),
"diegopacheco","TestingsosoCool")
service.update(1,review)
}
"notupdatebecausedoesnotexist"in{
intercept[RuntimeException]{
service.update(333,null)
}
}
"findthereview1"in{
valreview=service.findById(1)
review.get.idmustBeSome(1)
review.get.authormustBe"diegopacheco"
review.get.commentmustBe"TestingsosoCool"
review.get.productIdmustBeSome(1)
}
"findall"in{
valreviews=service.findAll()
reviews.get.lengthmustBe1
reviews.get(0).idmustBeSome(1)
reviews.get(0).authormustBe"diegopacheco"
reviews.get(0).commentmustBe"TestingsosoCool"
reviews.get(0).productIdmustBeSome(1)
}
"remove1review"in{
valreview=service.remove(1)
reviewmustBetrue
valoldReview=service.findById(1)
oldReviewmustBeNone
}
"notremovebecausedoesnotexist"in{
intercept[RuntimeException]{
service.remove(-1)
}
}
}
}
Alright,wehavetestsforthereviewservice.Wecanrunthemnow.
$activator"test-onlyReviewServiceTestSpec"
Hereistheoutput:
TestingroutesThePlayframeworkallowsustotestroutesaswell.Thisisgood,becauseasourapplicationgrowsandwerefactorthecode,wecanbe100%surethatourroutesarefunctioning.Routetestingcouldbeeasilyconfusedwithcontrollertesting.Themaindifferenceisthat,withroutingtesting,weshouldtestifweareabletoreachtheroutesandthat'sit.Afterroutetesting,wewillcovercontrollertestingindetail.
RoutesTestingSpec.scala-PlayframeworkroutetestingYourRoutesTestingSpec.scalafileshouldcontainthefollowingcode:
classRoutesTestingSpecextendsPlaySpecwithOneAppPerTest{
"RootController"should{
"routetoindexpage"in{
valresult=route(app,FakeRequest(GET,"/")).get
status(result)mustBeOK
contentType(result)mustBeSome("text/html")
contentAsString(result)mustinclude("WelcometoReactive
WebStore")
}
}
"ProductController"should{
"routetoindexpage"in{
valresult=route(app,FakeRequest(GET,"/product")).get
status(result)mustBeOK
contentType(result)mustBeSome("text/html")
contentAsString(result)mustinclude("Product")
}
"routetonewproductpage"in{
valresult=route(app,FakeRequest(GET,
"/product/add")).get
status(result)mustBeOK
contentType(result)mustBeSome("text/html")
contentAsString(result)mustinclude("Product")
}
"routetoproduct1detailspagepage"in{
try{
route(app,FakeRequest(GET,"/product/details/1")).get
}catch{
casee:Exception=>Unit
}
}
}
"ReviewController"should{
"routetoindexpage"in{
valresult=route(app,FakeRequest(GET,"/review")).get
status(result)mustBeOK
contentType(result)mustBeSome("text/html")
contentAsString(result)mustinclude("Review")
}
"routetonewreviewpage"in{
valresult=route(app,FakeRequest(GET,
"/review/add")).get
status(result)mustBeOK
contentType(result)mustBeSome("text/html")
contentAsString(result)mustinclude("review")
}
"routetoreview1detailspagepage"in{
try{
route(app,FakeRequest(GET,"/review/details/1")).get
}catch{
casee:Exception=>Unit
}
}
}
"ImageController"should{
"routetoindexpage"in{
valresult=route(app,FakeRequest(GET,"/image")).get
status(result)mustBeOK
contentType(result)mustBeSome("text/html")
contentAsString(result)mustinclude("Image")
}
"routetonewimagepage"in{
valresult=route(app,FakeRequest
(GET,"/image/add")).get
status(result)mustBeOK
contentType(result)mustBeSome("text/html")
contentAsString(result)mustinclude("image")
}
"routetoimage1detailspagepage"in{
try{
route(app,FakeRequest(GET,"/image/details/1")).get
}catch{
casee:Exception=>Unit
}
}
}
}
Soherewehavetestsforallourmaincontrollers,whichareroot,product,review,andimage.RootControlleristhecontrollerofthemainpagewhenyouvisithttp://localhost:9000.
ThereisaspecialhelperfunctioncalledrouteinthePlayframework,whichhelpsustotestroutes.ThenweuseFakeRequestpassthepathtotheroute.It'spossibletotestthestatuscodeandcontenttypeofthepagetowhichtherouterroutedourrequest.
Forproduct,image,andreviewcontrollers,youcanseewearetryingtocallanitemthatdoesnotexist.That'swhywehavethetry...catch,becauseweexpecttohaveanexceptionhappeningthere.
$activator"test-onlyRoutesTestingSpec"
Executingthisprecedingcommandproducesthefollowingresult:
ControllertestingWedidunittests,wedidroutetests,andnowisthetimetoaddcontrollertests.Controllertestsaresimilartoroutestests,buttheyarenotthesame.Forinstance,ourcontrolleralwaysrespondtoUIpages,soweexpectedtocreatespecificHTMLpagesbasedoneachmethod.ThePlayframeworkhasintegrationwithSelenium,whichisatestingframeworkforUIs,andacontrollersthatallowsyoutosimulatewebbrowsers,andyoucandoprettymuchthesameasyouwouldbyclickingonthepageslikearealuser.
Solet'sgetstarted.First,wewillstartwithRndDoubleGeneratorControllerTestSpec.
RndDoubleGeneratorControllerTestSpec.scala-RndDoubleGeneratorControllertestsTheRndDoubleGeneratorControllerTestSpec.scalafileshouldcontainthefollowingcode:
classRndDoubleGeneratorControllerTestSpec
extendsPlaySpec
withOneServerPerSuitewithOneBrowserPerSuitewithHtmlUnitFactory
{
valinjector=newGuiceApplicationBuilder()
.injector
valws:WSClient=injector.instanceOf(classOf[WSClient])
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
"Assumingng-microserviceisdownrxnumbershouldbe"must{
"work"in{
valfuture=ws.url(s"http://localhost:${port}/rnd/rxbat")
.get().map{res=>res.body}
valresponse=Await.result(future,15.seconds)
responsemustBe"2.3000000000000007"
}
}
}
Thisclasshassomeinterestingthings.Forinstance,weinjectWSClientusingGoogleGuicewithGuiceApplicationBuilder.Secondly,weassumethattheng-microservicewecreatedinthepreviouschapterisdown,sowecanpredicttheresponsecomingfromthefallback.
WecallthecontrollerusingWSClient,andthenwemaptheresponsetoreturnthebodycontentasastring.SothiswillbeanAsyncFuture,andinordertogettheresult,weuseAwaittowaitfivesecondsfortheresponsetocomeback.Oncetheresponseisback,wemakesuretheresultis2.3.Iftheresultdoesnotcomebackin15s,thetestwillfail.Runthefollowingcommand:
$activator"test-onlyRndDoubleGeneratorControllerTestSpec"
Nowyouwillseethefollowingresult:
Allright,nowwehaveacontrollertestfullyworkingusingGuiceinjectionsandtheWSClientPlayframeworklibrary.Let'snowmakecontrollertestsfortheproduct,image,andreviewcontrollers.
IntegrationSpec.scalaWecantestourmainpagetocheckifitisokay.Thisisaverysimpletest,andgetsyoureadyforthenexttests.So,herewego.
classIntegrationSpecextendsPlaySpecwithOneServerPerTestwith
OneBrowserPerTestwithHtmlUnitFactory{
"Application"should{
"workfromwithinabrowser"in{
goto("http://localhost:"+port)
pageSourcemustinclude("WelcometoReactiveWebStore")
}
}
}
Thistestisveryeasy.Wejustcallthemainpage,andwecheckifitcontainsthetextWelcometoReactiveWebStore.Let'srunit.
$activator"test-onlyIntegrationSpec"
Theresultafterrunningthistestisshowninthefollowingimage:
ProductControllerTestSpec.scalaNowwewilllookattheproductcontrollertestspec:
classProductControllerTestSpecextendsPlaySpecwith
OneServerPerSuitewithOneBrowserPerSuitewithHtmlUnitFactory{
"ProductController"should{
"insertanewproductshouldbeok"in{
goTo(s"http://localhost:${port}/product/add")
clickonid("name")
enter("BlueBall")
clickonid("details")
enter("BlueBallisaAwesomeandsimpleproduct")
clickonid("price")
enter("17.55")
submit()
}
"detailsfromtheproduct1shouldbeok"in{
goTo(s"http://localhost:${port}/product/details/1")
textField("name").valuemustBe"BlueBall"
textField("details").valuemustBe"BlueBallisaAwesome
andsimpleproduct"
textField("price").valuemustBe"17.55"
}
"updateproduct1shouldbeok"in{
goTo(s"http://localhost:${port}/product/details/1")
textField("name").value="BlueBall2"
textField("details").value="BlueBallisaAwesomeand
simpleproduct2"
textField("price").value="17.66"
submit()
goTo(s"http://localhost:${port}/product/details/1")
textField("name").valuemustBe"BlueBall2"
textField("details").valuemustBe"BlueBallisaAwesome
andsimpleproduct2"
textField("price").valuemustBe"17.66"
}
"deleteaproductshouldbeok"in{
goTo(s"http://localhost:${port}/product/add")
clickonid("name")
enter("BlueBall")
clickonid("details")
enter("BlueBallisaAwesomeandsimpleproduct")
clickonid("price")
enter("17.55")
submit()
goTo(s"http://localhost:${port}/product")
clickonid("btnDelete")
}
}
}
So,fortheproductcontroller,wesimulateawebbrowserusingSeleniumPlay'sframeworksupport.Wetestbasiccontrollerfunctionalitysuchasinsertinganewproduct,detailsfora
specificproduct,andupdatingandremovingaproduct.
Forinsert,wegotothenewproductformusinggoTo.Weuse$portasavariable.WedothisbecausethePlayframeworkwillbootuptheapplicationforus,butwedon'tknowinwhichport.Soweneedtousethisvariableinordertogettotheproductcontroller.
Thenweclickoneachtextfieldusingtheclickfunction,andweentervaluesusingtheenterfunction.Afterfillinginthewholeform,wesubmititusingthesubmitfunction.
Fordetails,wejustgototheproductdetailspage,andwecheckifthetextfieldshavethevaluesthatweareexpecting.WedothatusingthetextField.valuefunction.
Inordertochecktheproductupdatefunction,weneedtofirstupdatetheproductdefinition,andthengotodetailstoseeifthevalueswechangedarethere.
Finally,wetestthedeletefunction.Forthisfunction,weneedtoclickonabutton.WeneedtosettheIDofthebuttoninordertohavethisworking.WeneedtodoasmallrefactoringinourUItohavetheIDthere.
product_index.scala.htmlYourproduct_index.scala.htmlfileshouldcontainthefollowingcode:
@(products:Seq[Product])(implicitflash:Flash)
@main("Products"){
@if(!products.isEmpty){
<tableclass="tabletable-striped">
<tr>
<th>Name</th>
<th>Details</th>
<th>Price</th>
<th></th>
</tr>
@for(product<-products){
<tr>
<td><ahref="@routes.ProductController.details
(product.id.get)">@product.name</a></td>
<td>@product.details</td>
<td>@product.price</td>
<td><formmethod="post"action=
"@routes.ProductController.remove(product.id.get)">
<buttonid="btnDelete"name="btnDelete"class="btn
btn-link"type="submit"><iclass="icon-
trash"></i>Delete</button>
</form></td>
</tr>
}
</table>
}
<p><ahref="@routes.ProductController.blank"
class="btnbtn-success"><iclass="icon-plusicon-white">
</i>AddProduct</a></p>
}
Allset.NowwecanrunourtestsonActivatorsusingtheconsole.
$activator"test-onlyProductControllerTestSpec"
Thisprecedingcommandshowstheresultinthefollowingscreenshot:
Sincethistestrunstheapplicationforrealandcallsthecontrollersimulatingthewebbrowser,thistestcouldtakesometime.Nowit'stimetomovetotheImageControllertests.
ImageControllerTestSpec.scalaYourproduct_index.scala.htmlshouldcontainthefollowingcode:
classImageControllerTestSpecextendsPlaySpecwithOneServerPerSuitewith
OneBrowserPerSuitewith
HtmlUnitFactory{
"ImageController"should{
"insertanewimageshouldbeok"in{
goTo(s"http://localhost:${port}/product/add")
clickonid("name")
enter("BlueBall")
clickonid("details")
enter("BlueBallisaAwesomeandsimpleproduct")
clickonid("price")
enter("17.55")
submit()
goTo(s"http://localhost:${port}/image/add")
singleSel("productId").value="1"
clickonid("url")
enter("http://myimage.com/img.jpg")
submit()
}
"detailsfromtheimage1shouldbeok"in{
goTo(s"http://localhost:${port}/image/details/1")
textField("url").valuemustBe"http://myimage.com/img.jpg"
}
"updateimage1shouldbeok"in{
goTo(s"http://localhost:${port}/image/details/1")
textField("url").value="http://myimage.com/img2.jpg"
submit()
goTo(s"http://localhost:${port}/image/details/1")
textField("url").valuemustBe"http://myimage.com/img2.jpg"
}
"deleteaimageshouldbeok"in{
goTo(s"http://localhost:${port}/image/add")
singleSel("productId").value="1"
clickonid("url")
enter("http://myimage.com/img.jpg")
submit()
goTo(s"http://localhost:${port}/image")
clickonid("btnDelete")
}
}
}
Firstofall,weneedtogototheproductcontrollertoinsertaproduct;otherwise,wecannotdoimageoperations,sincetheyallneedaproductID.
image_index.scala.htmlYourimage_index.scala.htmlfileshouldcontainthefollowingcode:
@(images:Seq[Image])(implicitflash:Flash)
@main("Images"){
@if(!images.isEmpty){
<tableclass="tabletable-striped">
<tr>
<th>ProductID</th>
<th>URL</th>
<th></th>
</tr>
@for(image<-images){
<tr>
<td><ahref="@routes.ImageController.details
(image.id.get)">@image.id</a></td>
<td>@image.productId</td>
<td>@image.url</td>
<td><formmethod="post"action=
"@routes.ImageController.remove(image.id.get)">
<buttonid="btnDelete"name="btnDelete"class="btn
btn-link"type="submit"><iclass="icon-
trash"></i>Delete</button></form>
</td>
</tr>
}
</table>
}
<p><ahref="@routes.ImageController.blank"
class="btnbtn-success"><iclass="icon-plusicon-white">
</i>AddImage</a></p>
}
Allset.NowwecanruntheImageControllertests.
$activator"test-onlyImageControllerTestSpec"
Theresultisshowninthefollowingscreenshot:
ImageControllerhaspassedallitstests.NowwewillmovetotheReviewControllertests.
ReviewControllerTestSpec.scalaYourReviewControllerTestSpec.scalafileshouldcontainthefollowingcode:
classReviewControllerTestSpecextendsPlaySpecwithOneServerPerSuitewith
OneBrowserPerSuite
withHtmlUnitFactory{
"ReviewController"should{
"insertanewreviewshouldbeok"in{
goTo(s"http://localhost:${port}/product/add")
clickonid("name")
enter("BlueBall")
clickonid("details")
enter("BlueBallisaAwesomeandsimpleproduct")
clickonid("price")
enter("17.55")
submit()
goTo(s"http://localhost:${port}/review/add")
singleSel("productId").value="1"
clickonid("author")
enter("diegopacheco")
clickonid("comment")
enter("Testsareamazing!")
submit()
}
"detailsfromthereview1shouldbeok"in{
goTo(s"http://localhost:${port}/review/details/1")
textField("author").valuemustBe"diegopacheco"
textField("comment").valuemustBe"Testsareamazing!"
}
"updatereview1shouldbeok"in{
goTo(s"http://localhost:${port}/review/details/1")
textField("author").value="diegopacheco2"
textField("comment").value="Testsareamazing2!"
submit()
goTo(s"http://localhost:${port}/review/details/1")
textField("author").valuemustBe"diegopacheco2"
textField("comment").valuemustBe"Testsareamazing2!"
}
"deleteareviewshouldbeok"in{
goTo(s"http://localhost:${port}/review/add")
singleSel("productId").value="1"
clickonid("author")
enter("diegopacheco")
clickonid("comment")
enter("Testsareamazing!")
submit()
goTo(s"http://localhost:${port}/review")
clickonid("btnDelete")
}
}
Firstofall,weneedtogototheproductcontrollertoinsertaproduct;otherwise,wecannotdoimageoperations,sincetheyallneedaproductID.
Forinsert,wegotothenewproductformusinggoto.Weuse$portasavariable.WedothisbecausethePlayframeworkwillbootuptheapplicationforus,butwedon'tknowonwhichport,soweneedtousethisvariableinordertogettotheproductcontroller.
Thenweclickoneachtextfieldusingtheclickfunction,andweentervaluesusingtheenterfunction.Afterfillinginthewholeform,wesubmititusingthesubmitfunction.
Fordetails,wejustgototheproductdetailspageandcheckifthetextfieldshavethevaluesthatweexpect.WedothatusingthetextField.valuefunction.
Inordertochecktheproductupdatefunction,weneedtofirstupdatetheproductdefinition,andthengotothedetailstoseeifthevalueswechangedarethere.
Finally,wetestthedeletefunction.Forthisfunction,weneedtoclickonabutton.WeneedtosettheIDofthebuttoninordertohavethisworking.ThenwedoasmallrefactoringinourUItohavetheIDthere.
review_index.scala.htmlYourreview_index.scala.htmlfileshouldcontainthefollowingcode:
@(reviews:Seq[Review])(implicitflash:Flash)
@main("Reviews"){
@if(!reviews.isEmpty){
<tableclass="tabletable-striped">
<tr>
<th>ProductId</th>
<th>Author</th>
<th>Comment</th>
<th></th>
</tr>
@for(review<-reviews){
<tr>
<td><ahref="@routes.ReviewController.
details(review.id.get)">@review.productId</a></td>
<td>@review.author</td>
<td>@review.comment</td>
<td>
<formmethod="post"action="@routes.
ReviewController.remove(review.id.get)">
<buttonid="btnDelete"name="btnDelete"
class="btnbtn-link"type="submit"><iclass="icon-
trash"></i>Delete</button>
</form>
</td>
</tr>
}
</table>
}
<p><ahref="@routes.ReviewController.blank"class="btnbtn-
success"><iclass="icon-plusicon-white"></i>AddReview</a></p>
}
Finally,wecanrunthetestsontheconsole.
$activator"test-onlyReviewControllerTestSpec"
Thetestwillshowthefollowingoutput:
Alright,ReviewControllerhaspassedallourtests.
It'saverygoodpracticetohavethetestsseparatedbytype.However,ifyouwant,youcouldmixallthetestssuchasunittesting,controllertesting,androutetestinginonesinglefile.
ApplicationSpec.scalaYourApplicationSpec.scalashouldhavethefollowingcode:
classApplicationSpecextendsPlaySpecwithOneAppPerTest{
"Routes"should{
"send404onabadrequest"in{
route(app,FakeRequest(GET,"/boum")).map(status(_))mustBe
Some(NOT_FOUND)
}
}
"HomeController"should{
"rendertheindexpage"in{
valhome=route(app,FakeRequest(GET,"/")).get
status(home)mustBeOK
contentType(home)mustBeSome("text/html")
contentAsString(home)mustinclude("WelcometoReactiveWeb
Store")
}
}
"RndController"should{
"returnarandomnumber"in{
//Assumingng-microserviceisdownotherwisewillfail.
contentAsString(route(app,FakeRequest(GET,
"/rnd/rxbat")).get)mustBe"2.3000000000000007"
}
}
}
Wecanrunthesemixedtests,andtheywillallpass.
$activator"test-onlyApplicationSpec"
Youwillseethisresult:
OK,wearealmostdone.Wejustneedtoaddsometestsforthemicroservicecalledng-
microservice,whichwecreatedinChapter4,DevelopingReactiveBackingServices.
NGServiceImplTestSpec.scalaYourNGServiceImplTestSpec.scalafileshouldhavethefollowingcode:
classNGServiceImplTestSpecextendsPlaySpec{
"TheNGServiceImpl"must{
"GenerateaRamdonnumber"in{
valservice:NGContract=newNGServiceImpl
valdouble=service.generateDouble
assert(double>=1)
}
"Generatealistof3Ramdonnumbers"in{
valservice:NGContract=newNGServiceImpl
valdoubles=service.generateDoubleBatch(3)
doubles.sizemustBe3
assert(doubles(0)>=1)
assert(doubles(1)>=1)
assert(doubles(2)>=1)
}
}
}
Sohere,intheprecedingcode,wehavetwomethodstotestthetwooperationsthatwehaveinourmicroservice.Firstwegenerateonedouble,andthenweaskforalistofthreedoubles.Asyoucansee,wejustcheckifwegetapositivedoublebackfromtheservice,andthat'sit.Sincetheresultisnotpredictable,thisisagoodwaytotestit.Sometimes,evenwhentheresultispredictable,youwanttestslikethis.Why?Becauseitmakesthetestsmorereliable,andoften,whenweusetoomanyhardcorevalues.Thevaluescouldbechangingandbreakingourtests,andthat'snotcool.Let'srunitontheconsole.
$activator"test-onlyNGServiceImplTestSpec"
Hereistheresultthatweget:
Nowlet'smoveontothecontroller,anddosomecontrollertesting.
NGServiceEndpointControllerTest.scalaYourNGServiceEndpointControllerTest.scalafileshouldcontainthefollowingcode:
classNGServiceEndpointControllerTestextendsPlaySpecwith
OneServerPerSuitewith
OneBrowserPerSuitewithHtmlUnitFactory{
valinjector=newGuiceApplicationBuilder()
.injector
valws:WSClient=injector.instanceOf(classOf[WSClient])
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
"NGServiceEndpointController"must{
"returnasingledouble"in{
valfuture=ws.url(s"http://localhost:${port}/double")
.get().map{res=>res.body}
valresponse=Await.result(future,15.seconds)
responsemustnotbeempty
assert(newjava.lang.Double(response)>=1)
}
"returnalistof3doubles"in{
valfuture=ws.url(s"http://localhost:${port}/doubles/3")
.get().map{res=>res.body}
valresponse=Await.result(future,15.seconds)
responsemust(notbeemptyandinclude("[")and
include("]"))
}
}
}
HerewehavetoinjecttheWSClientlibrarysowecancallthecontroller.Thiscontrollerhastwomethodsliketheservicewetestedbefore.ThesecondmethodreturnsaJSONstructure.Thenwecheckfor"["and"]"tomakesurethatthearrayispresent,sincethisisalistofthreenumbers.
Weusetheassertfunctiontochecktheresponsefromthecontroller,andtobe100%surethateverythingisokay.Let'srunit.
$activator"test-onlyNGServiceEndpointControllerTest"
Refertothefollowingscreenshottoseethetestresult:
Great!Wehavecoveredprettymucheverything.
Inthischapter,weranallkindsoftests.Wealwaysusedthecommand$activator"test-onlyxxx";thereasonforthisistosavetime.However,itisverycommontorunalltests.Youcandothatinbothprojects;wehavetojusttype$activatortest.
Whenrunningallthetestsintheng-microserviceproject,wegettheresultshowninthefollowingscreenshot:
Ontheotherhand,runningallthetestsintheReactiveWebStoreprojectgivestheresultshowninthenextscreenshot:
SummaryInthischapter,youlearnedhowtodotests.WeaddedseveraltestsforyourScalaandPlayframeworkprojects.Youalsolearnedaboutunittestingprinciples,testingScalaapplicationswithJUnit,BDDprinciples,testingwithScalaTest,testingPlayframeworkapplicationswithScalaTest,andrunningtestsinActivator/SBT.
Inthenextchapter,youwilllearnmoreaboutpersistenceusingSlick,whichisreactive.Wewillalsochangeourtestsalittlebitinordertoworkwithadatabase.
Chapter6.PersistencewithSlickInthepreviouschapters,youlearnedtobootstrapyourapplicationusingActivator,wedevelopedourwebapplicationusingScalaandPlayframework,andweaddedReactivemicroservicescallsusingRxScalafordataflowcomputations.WeperformedunittestsandcontrollertestingusingtheBDDandPlayframeworks.
Inthischapter,youwilllearnhowtoachieverelationaldatabasepersistence.Sofar,wehaveourapplicationupandrunning.However,weareusingin-memorypersistencewithHashMap.Nowwewillupgradeourapplicationtouseproperpersistence.Inordertoachievethis,wewilluseareactivedatabasepersistenceframeworkcalledSlick.
Inthischapter,wewillcoverthefollowingtopics:
PrinciplesofdatabasepersistencewithSlickWorkingwithFunctionalRelationalMappinginourapplicationCreatingQuerieswithSQLsupportImprovingcodewithAsyncdatabaseoperations
Wewilldosomerefactoringinourapplication.Step-by-step,wewillcreateallthetablesandpersistenceclassesthatweneedtohaveSlickworkingwithourPlayframeworkapplication.Testswillberefactoredaswellinordertotesttheapplicationlogic,andskipthedatabasepersistencepart.
IntroducingtheSlickframeworkScalaLanguageIntegratedConnectionKit(Slick)isaScalamodernframework,whichallowsworkingwithdatausingabstractionsthatareverysimilartoScalacollections.YoucanwritedatabasequeriesinbothSQLandScalacode.WritingScalacodeinsteadofSQLisbetter,becauseweleveragethecompiler,andhence,thisapproachislesserror-prone.Also,itbecomeseasiertomaintainthecode,sincethecompilerwillhelpyoubypointingoutwherethecodebreaks,ifithappens.
SlickisaFunctionalRelationalMapping(FRM)library.ThoseofyouwhocomefromaJavabackground,andarefamiliarwithObjectRelationalMapping(ORM)frameworkssuchasHibernate,willseethatSlickhassimilarconcepts.Basically,youcreateaScalaclass,whichwillexplicitlymaptoarelationaltable.SlickFRMideasareinspiredbyMicrosoft'sLINQframework.
Slickisreactivebydesign,andworksinanasynchronousnon-blockingIOmodel.UsingSlickyouhavethefollowingadvantages:
Resilience:AcommonissueisthataheavyloadontheDBandapplicationcreatesmorethreads,andmakesthesituationworse.Slickcanfixthisproblem,becauseitqueuesdatabaseoperationsintheDB.EfficientResourcesutilization:Slickcanbetunedforparallelismintermsofthenumberofactivejobsandsuspendeddatabasesessions.SlickalsohasacleandemarcationbetweenI/OandCPU-intensivecode.
MySQLsetupWewilluseSlickwithMySQL5.6.However,SlicksupportsotherrelationaldatabaseslikeOracle,SQLServer,DB2,andPostgres.Firstofall,weneedtoinstallMySQLinourmachine.Opentheconsole,andperformthefollowingsteps(forUbuntuLinux,otherOS(Windows/Mac),anddistros,checkouthttp://dev.mysql.com/downloads/mysql/):
$sudoapt-getinstallmysql-server-y
$mysql--version
$servicemysqlstatus
Afterinstallationwithapt-get,whenyouruntheothertwocommands,youshouldseeanoutputlikethis:
MySQLInstallation
OncetheinstallationisdoneandtheMySQLserverisupandrunning,wecanmoveonandcreatethedatabase.Inordertogetthis,wewillneedtoopentheMySQLconsole.Fordevelopmentreasons,Ididnotputapasswordforroot.However,forproduction,itisstronglyrecommendedthatyoudouseastrongpassword.
Executethefollowingcommand:
$mysql-uroot-p
Thiswillgiveoutputasfollows:
MySQLConsole
OnceyouentertheMySQLconsole,youcancreatethedatabase.WewillcreateadatabasenamedRWS_DBusingthefollowingcommand:
$CREATEDATABASERWS_DB;
Youwillseethefollowingresult:
Youcantype$SHOWDATABASES;inordertogetalistofalltheavailabledatabasesinMySQL.Allset,wehaveourdatabaseupandrunning.
ConfiguringSlickinourPlayframeworkappFirst,weneedtoadddependenciestothebuild.sbtfile.WewillneedtoremoveorcommentalibrarycalledJDBCandaddtheplay-slicklibrariesandMySQLjavadriver.
Thisisdoneasfollows:
name:="""ReactiveWebStore"""
version:="1.0-SNAPSHOT"
lazyvalroot=(projectinfile(".")).enablePlugins(PlayScala)
scalaVersion:="2.11.7"
libraryDependencies++=Seq(//jdbc,
cache,
ws,
"org.scalatestplus.play"%%"scalatestplus-play"%"1.5.1"%
Test,
"com.netflix.rxjava"%"rxjava-scala"%"0.20.7",
"com.typesafe.play"%%"play-slick"%"2.0.0",
"com.typesafe.play"%%"play-slick-evolutions"%"2.0.0",
"mysql"%"mysql-connector-java"%"6.0.3"
)
resolvers+="scalaz-bintray"at"http://dl.bintray.com/scalaz/releases"
resolvers+=DefaultMavenRepository
Asyoucanseeintheprecedingcode,wecommentouttheJDBClibrary,andaddthreenewdependencies:
"com.typesafe.play"%%"play-slick"%"2.0.0",
"com.typesafe.play"%%"play-slick-evolutions"%"2.0.0",
"mysql"%"mysql-connector-java"%"6.0.3"
Youcangototheconsoleandrunthecommands$activator,$reload,and$compileinordertogetSBTtodownloadallthenewdependencies.
ConfigurethedatabaseconnectionSlickneedstobeconfiguredtoaccesstheMySQLdatabasethatwecreated.UnderthefolderReactiveWebStore/conf,weneedtoedittheapplication.conffileandaddthedatabaseconnectionURLandsettingsasfollows:
#Defaultdatabaseconfiguration
slick.dbs.default.driver="slick.driver.MySQLDriver$"
slick.dbs.default.db.driver="com.mysql.cj.jdbc.Driver"
slick.dbs.default.db.url="jdbc:mysql://127.0.0.1:3306/RWS_DB?
useUnicode=true&useJDBCCompliantTimezoneShift=
true&useLegacyDatetimeCode=false&serverTimezone=UTC"
slick.dbs.default.db.user="root"
slick.dbs.default.db.password=""
FPMMappingThenextstepistocreateFPMmappingbetweenourScalacodeandMySQLtablesunderReactiveWebStore/app,wewillcreateanewpackagecalleddao.DAOstandsforDatabaseAccessObject(DAO),andisawell-knownOOpattern.SowewillcreatesomeDAOclasseshere.Firstwewilldefineabasetrait,whichwilldefinethebehaviorandcodecapabilityforeachofourdaopackages.
WewillstartwithBaseDao.scala:
packagedao
importslick.lifted.TableQuery
importscala.concurrent.Future
/**
*Definesbasedaostructureeverydaoshouldhave.
*/
traitBaseDao[T]{
deftoTable():TableQuery[_]
deffindAll():Future[Seq[T]]
defremove(id:Long):Future[Int]
definsert(o:T):Future[Unit]
defupdate(o:T):Future[Unit]
deffindById(id:Long):Future[Option[T]]
}
Wewillhavethreedaopackages:ProductDao,ImageDao,andReviewDao.Eachdaowillbeabletoperformanoperation,butoveradifferentMySQLtable.Accordingtothetraitwejustdefined,wewillbeabletodothefollowing:
findAll:Findalldataforaspecifictableremove:DeleteaniteminatablebyIDinsert:Addnewdatatoatableupdate:UpdatedatainatablefindbyId:Getaspecificrecordinatable,filteredbyIDtoTable:ReturnthetableFRMmappingforthatdao.
ProductDaoWewillstartwithhavingalookatProductDao.scala:
packagedao
traitIProductDaoextendsBaseDao[Product]{
deffindAll():Future[Seq[Product]]
deffindById(id:Long):Future[Option[Product]]
defremove(id:Long):Future[Int]
definsert(p:Product):Future[Unit]
defupdate(p2:Product):Future[Unit]
}
classProductDao@Inject()(protectedvaldbConfigProvider:
DatabaseConfigProvider)
extends
HasDatabaseConfigProvider[JdbcProfile]withIProductDao{
importdriver.api._
classProductTable(tag:Tag)extendsTable[Product](tag,
models.ProductDef.toTable){
defid=column[Option[Long]]("ID",O.PrimaryKey)
defname=column[String]("NAME")
defdetails=column[String]("DETAILS")
defprice=column[BigDecimal]("PRICE")
def*=(id,name,details,price)<>(Product.tupled,
Product.unapply_)
}
overridedeftoTable=TableQuery[ProductTable]
privatevalProducts=toTable()
overridedeffindAll():Future[Seq[Product]]=
db.run(Products.result)
overridedeffindById(id:Long):Future[Option[Product]]=
db.run(Products.filter(_.id===id).result.headOption)
overridedefremove(id:Long):Future[Int]=
db.run(Products.filter(_.id===id).delete)
overridedefinsert(p:Product):Future[Unit]=db.run(Products
+=p).map{_=>()}
overridedefupdate(p2:Product)=Future[Unit]{
db.run(
Products.filter(_.id===p2.id)
.map(p=>(p.name,p.details,p.price))
.update((p2.name,p2.details,p2.price))
)
}
}
ThisisthedaoimplementationforProduct.Wehavelotsofnewconceptshere,solet'stakealookateachstep,oneatatime.Asyoucansee,atraitcalledIProductDaowhichextendsfromBaseDaousinggenericstospecifythemodelProduct.
ThistraitisimportantfordependencyinjectionusingGuice.Wewillhavetwodaoimplementationsforeachmodel:oneimplementationusingSlickandMySQL,andtheotherusingourpreviousinMemorydatabasefortestingpurpose.
ThereisaclasstherecalledProductDaowhichisthedaoimplementationusingSlick.WeneedGuicetoinjectaclasshere,calledDatabaseConfigProvider,whichwillbeusedtoperformthedatabaseoperations.ProductDaoalsoneedstoextendHasDatabaseConfigProvider[JdbcProfile]toworkwiththedatabase.
Wealsoneedtoimportthedriverapiviathefollowingcommand:
Importdriver.api._
ThenextstepistocreateFRMmappingwithaclasscalledProductTable,whichextendstablepassingthemodel,whichinourcaseisaproduct.YoualsoneedtoannouncethenameoftheMySQLtable.Inordertogetthetablename,weuseacompanionobject,whichweneedtocreatearoundourmodels.WedoitthiswayinordertoavoidduplicatingtheMySQLtablenameeverywhere.
IntheProductTabletable,youcanseesomefunctionssuchasid,name,price,anddetails.Thesearetheexactnameofthefieldsofmodel.Product.However,wehavetoaddthemappingtotheMySQLtableontherightside.WedoitusingafunctioncalledcolumnwherewepassthetypeandtheexactMySQLfieldname.
Finally,weneedtorunaspecialprojectionfunctioncalled*topassallthefieldsonthemodel,whicharebeingmappedtotherelationaldatabase.
Nowwecanmovetothedaooperations.Asyoucansee,allthefunctionsusedb.runtoperformdataaccess.Thisisgreatbecause,asyoucanrealize,theyreturnaFuturesothedaowon'tbeblocking,andyoucandosomethingelse,forinstance,moredatabaseoperations,pre-optimizations,andvalidations.
OncewehaveaProductTabletable,wecancreateaSlickTableQuerywithittoperformdatabaseoperationsasiftheyareScalafunctions.Inordertolistalltheavailableproducts,wejustusethiscommand:
db.run(Products.result)
Itisassimpleasthat.ThiscodewillreturnaFuture[Seq[Products]].WecanalsofilterbyIDusingthis:
db.run(Products.filter(_.id===id).result.headOption)
So,first_.idistheidonthedatabase,andidistheonethatcomesbyparameter.Aftergettingtheresult,youcanseethatwecalledanotherfunctioncalledheadOption,whichmakessurethatwegettheresultasanoption.Thisisagreatpatterntorelyon,sincethedatamightnotbethereonthetable,andweavoidgettingNoSuchElementException.
Removingaproductisfairlytrivialaswell.Wejustusethefollowing:
db.run(Products.filter(_.id===id).delete)
ThisprecedingcodereturnsFuture[Int],countingthenumberofitemsthatweredeleted.IftherecordIDisnotfoundinthedatabase,theresultwillbe0.Weexpectittobealways1,sincewearegoingtodeletebyID.However,theAPIisgeneric,andif,let'ssay,youdeletebynameoranotherfield,youmighthavemultipledeletes.That'swhyitisanIntandnotaBoolean.
Insertingdataiseasytoo;wejustgivethefollowingcommand:
db.run(Products+=p).map{_=>()}
Asyoucansee,itisaverysimplemapfunctionasifwewereaddinganelementtoalist.Thiscodereturnsunit,whichmeansnothing.However,westillhaveaFuture,sothiscodeisnotblocking.
Toperformanupdate,thereisalittlebitmorecode,butitisstillsimpleattheendoftheday.
db.run(
Products.filter(_.id===p2.id)
.map(p=>(p.name,p.details,p.price))
.update((p2.name,p2.details,p2.price))
)
Firstweneedtoapplyafiltertoselecttherecordsthatwewillupdate.WepasstheID,becausewejustwanttoupdateasinglerecord.Thenweneedtoapplyamapfunctiontopickthefieldsthatwewanttoupdate;finally,weperformtheupdate,passingthenewvaluestotheupdatefunction.
Let'stakealookatthecompanionobjectfortheproductmodel.
HereisthecodeforModels.Product.scala:
objectProductDef{
deftoTable:String="Product"
}
Asyoucansee,thisisasimplehelpercompanionobjecttoholdtheMySQLtablename.
ReviewDAOWearedonewithProductDao,andnowweneedtomovetothereviewmodelandcreatedaoforreviews.Wewillperformstepssimilartotheoneswedidfortheproduct.
ReviewDao.scalaisasfollows:
packagedao
traitIReviewDaoextendsBaseDao[Review]{
deffindAll():Future[Seq[Review]]
deffindById(id:Long):Future[Option[Review]]
defremove(id:Long):Future[Int]
definsert(p:Review):Future[Unit]
defupdate(p2:Review):Future[Unit]
}
classReviewDao@Inject()(protectedvaldbConfigProvider:
DatabaseConfigProvider)
extendsHasDatabaseConfigProvider[JdbcProfile]withIReviewDao{
importdriver.api._
classReviewTable(tag:Tag)extendsTable[Review](tag,
models.ReviewDef.toTable){
defid=column[Option[Long]]("ID",O.PrimaryKey)
defproductId=column[Option[Long]]("PRODUCT_ID")
defauthor=column[String]("AUTHOR")
defcomment=column[String]("COMMENT")
def*=(id,productId,author,comment)<>(Review.tupled,
Review.unapply_)
}
overridedeftoTable=TableQuery[ReviewTable]
privatevalReviews=toTable()
overridedeffindAll():Future[Seq[Review]]=
db.run(Reviews.result)
overridedeffindById(id:Long):Future[Option[Review]]=
db.run(Reviews.filter(_.id===id).result.headOption)
overridedefremove(id:Long):Future[Int]=
db.run(Reviews.filter(_.id===id).delete)
overridedefinsert(r:Review):Future[Unit]=
db.run(Reviews+=r).map{_=>()}
overridedefupdate(r2:Review)=Future[Unit]{
db.run(
Reviews.filter(_.id===r2.id)
.map(i=>(i.productId,i.author,i.comment))
.update((r2.productId,r2.author,r2.comment))
)
}
}
Intheprecedingcode,wehavetheelementsthatwesawinProductDao.ThereisaninterfacefordaocalledIReviewDao,whichextendsBaseDaousingthereviewmodel.WehavetheReviewDaoimplementationwiththeReviewTableFRMmapping.Wealsohaveacompanionobjectforthereviewmodel.
Review.scalaisasfollows:
objectReviewDef{
deftoTable:String="Review"
}
ImageDaoNowweneedtomovetoourlastdao,ImageDao.LikeProductDaoandReviewDao,wewillgothroughthesameideasandconceptsasimplementationtoo.
WewillnowlookatImageDao.scala:
packagedao
traitIImageDaoextendsBaseDao[Image]{
deffindAll():Future[Seq[Image]]
deffindById(id:Long):Future[Option[Image]]
defremove(id:Long):Future[Int]
definsert(p:Image):Future[Unit]
defupdate(p2:Image):Future[Unit]
}
classImageDao@Inject()(protectedvaldbConfigProvider:
DatabaseConfigProvider)
extends
HasDatabaseConfigProvider[JdbcProfile]
withIImageDao{
importdriver.api._
classImageTable(tag:Tag)extendsTable[Image](tag,
models.ImageDef.toTable){
defid=column[Option[Long]]("ID",O.PrimaryKey)
defproductId=column[Option[Long]]("PRODUCT_ID")
defurl=column[String]("URL")
def*=(id,productId,url)<>(Image.tupled,Image.unapply
_)
}
overridedeftoTable=TableQuery[ImageTable]
privatevalImages=toTable()
overridedeffindAll():Future[Seq[Image]]=
db.run(Images.result)
overridedeffindById(id:Long):Future[Option[Image]]=
db.run(Images.filter(_.id===id).result.headOption)
overridedefremove(id:Long):Future[Int]=
db.run(Images.filter(_.id===id).delete)
overridedefinsert(i:Image):Future[Unit]=
db.run(Images+=i).map{_=>()}
overridedefupdate(i2:Image)=Future[Unit]
{db.run(
Images.filter(_.id===i2.id)
.map(i=>(i.productId,i.url))
.update((i2.productId,i2.url))
)}
}
Wealsoneedtohaveacompanionobjecthelperfortheimage.
Image.scalaisasfollows:
objectImageDef{
deftoTable:String="Image"
}
SlickevolutionsSlickwon'tcreatethetableforus,unlesswecreateanevolution.SlickkeepstrackofthedatabasestateandcreatesandappliesSQLcommandsforus.EvolutionsneedbelocatedatReactiveWebStore/conf/evolutions/default,wheredefaultisthenameofthedatabaseweconfiguredinapplication.conf.EvolutionsneedtobenamedinasequentialwaysothatwecanpreserveorderandSlickcankeeptrackofthechanges.Rightnow,wewillcreateanevolutionforProductDao,becauseweneedaproducttable.
Thecodewillbeasfollowswiththename1.sql:
#---!Ups
CREATETABLEProduct(IDINTNOTNULLAUTO_INCREMENT,NAMEVARCHAR(100)NOT
NULL,DETAILSVARCHAR(250),PRICEDOUBLENOTNULL,PRIMARYKEY(ID));
#---!Downs
#droptable"Product";
Weneedevolutionsforthreviewandimageaswell.Soweneedtocreate2.sqlfortheimageand3.sqlforthereview.
Thecodewillbeasfollowsfor2.sql:
#---!Ups
CREATETABLEImage(IDINTNOTNULLAUTO_INCREMENT,PRODUCT_IDINTNOTNULL,URL
VARCHAR(250),PRIMARYKEY(ID));
#---!Downs
#droptable"Product";
Thecodewillbeasfollowswiththename3.sql:
#---!Ups
CREATETABLEReview(IDINTNOTNULLAUTO_INCREMENT,PRODUCT_IDINTNOT
NULL,AUTHORVARCHAR(250),COMMENTVARCHAR(250),PRIMARYKEY(ID));
#---!Downs
#droptable"Review";
RefactoringservicesWeneedtochangethedefaultbasetraitforourdaopackagestoreturnFuturesnow.
Let'sstartwithBaseServices.scala:
packageservices
importscala.concurrent.Future
traitBaseService[A]{
definsert(a:A):Future[Unit]
defupdate(id:Long,a:A):Future[Unit]
defremove(id:Long):Future[Int]
deffindById(id:Long):Future[Option[A]]
deffindAll():Future[Option[Seq[A]]]
}
Thislastimplementationreflectswhat'shappeninginthedaopackages.Nowwecanmovetotheservicesimplementation,andproceedwithourrefactoring.
NextweseeProductService.scala:
packageservices
traitIProductServiceextendsBaseService[Product]{
definsert(product:Product):Future[Unit]
defupdate(id:Long,product:Product):Future[Unit]
defremove(id:Long):Future[Int]
deffindById(id:Long):Future[Option[Product]]
deffindAll():Future[Option[Seq[Product]]]
deffindAllProducts():Seq[(String,String)]
}
@Singleton
classProductService
@Inject()(dao:IProductDao)
extendsIProductService{
importplay.api.libs.concurrent.Execution.Implicits.
defaultContext
definsert(product:Product):Future[Unit]={
dao.insert(product);
}
defupdate(id:Long,product:Product):Future[Unit]={
product.id=Option(id.toInt)
dao.update(product)
}
defremove(id:Long):Future[Int]={
dao.remove(id)
}
deffindById(id:Long):Future[Option[Product]]={
dao.findById(id)
}
deffindAll():Future[Option[Seq[Product]]]={
dao.findAll().map{x=>Option(x)}
}
privatedefvalidateId(id:Long):Unit={
valfuture=findById(id)
valentry=Awaits.get(5,future)
if(entry==null||entry.equals(None))thrownew
RuntimeException("CouldnotfindProduct:"+id)
}
deffindAllProducts():Seq[(String,String)]={
valfuture=this.findAll()
valresult=Awaits.get(5,future)
valproducts:Seq[(String,String)]=result
.getOrElse(Seq(Product(Some(0),"","",0)))
.toSeq
.map{product=>(product.id.get.toString,product.name)}
returnproducts
}
}
Thereareacoupleofchangeshere.First,weinjectanIProductDao,andletGuicefigureouttherightinjectionthatweneedtobeabletotestwithouroldin-memoryHashMapimplementation,whichwillbecoveredlaterinthischapter.
Thechangesinvolvenewfunctionsignatures,usingAwaits,andusingSeqinsteadofList.
Let'smovetoonReviewService.scalanow.
packageservices
traitIReviewServiceextendsBaseService[Review]{
definsert(review:Review):Future[Unit]
defupdate(id:Long,review:Review):Future[Unit]
defremove(id:Long):Future[Int]
deffindById(id:Long):Future[Option[Review]]
deffindAll():Future[Option[Seq[Review]]]
}
@Singleton
classReviewService@Inject()(dao:IReviewDao)
extendsIReviewService{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
definsert(review:Review):Future[Unit]={
dao.insert(review);}
defupdate(id:Long,review:Review):Future[Unit]={
review.id=Option(id.toInt)
dao.update(review)
}
defremove(id:Long):Future[Int]={
dao.remove(id)
}
deffindById(id:Long):Future[Option[Review]]={
dao.findById(id)
}
deffindAll():Future[Option[Seq[Review]]]={
dao.findAll().map{x=>Option(x)}
}
privatedefvalidateId(id:Long):Unit={
valfuture=findById(id)
valentry=Awaits.get(5,future)
if(entry==null||entry.equals(None))thrownew
RuntimeException("CouldnotfindReview:"+id)
}
}
Intheprecedingcode,wehavethesamekindofchangesthatwemadefortheproduct.Let'smovetoImageService.scala,whichisourlastservice.
packageservices
traitIImageServiceextendsBaseService[Image]{
definsert(image:Image):Future[Unit]
defupdate(id:Long,image:Image):Future[Unit]
defremove(id:Long):Future[Int]
deffindById(id:Long):Future[Option[Image]]
deffindAll():Future[Option[Seq[Image]]]
}
@Singleton
classImageService@Inject()(dao:IImageDao)
extendsIImageService{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
definsert(image:Image):Future[Unit]={
dao.insert(image)
}
defupdate(id:Long,image:Image):Future[Unit]={
image.id=Option(id.toInt)
dao.update(image)
}
defremove(id:Long):Future[Int]={
dao.remove(id)
}
deffindById(id:Long):Future[Option[Image]]={
dao.findById(id)
}
deffindAll():Future[Option[Seq[Image]]]={
dao.findAll().map{x=>Option(x)}
}
privatedefvalidateId(id:Long):Unit={
valfuture=findById(id)
valentry=Awaits.get(5,future)
if(entry==null||entry.equals(None))thrownew
RuntimeException("CouldnotfindImage:"+id)
}
}
Wehaverefactoredallservicestousethenewdaopackagesimplementation.Nowthenextstepisthemovetothecontrollers.
RefactoringcontrollersNowwehaveallthedaopackagesimplementedwiththerespectivedatabaseevolutions.However,ourservicesexpectedadifferentcontract,sincewewereusinganin-memorydatabasebefore.Let'srefactortheproductcontroller:
packagecontrollers
@Singleton
classProductController@Inject()(valmessagesApi:MessagesApi,val
service:IProductService)
extendsControllerwithI18nSupport{
valproductForm:Form[Product]=Form(
mapping(
"id"->optional(longNumber),
"name"->nonEmptyText,
"details"->text,
"price"->bigDecimal
)(models.Product.apply)(models.Product.unapply)
)
defindex=Action{implicitrequest=>
valproducts=Awaits.get(5,service.findAll())
.getOrElse(Seq())
Logger.info("indexcalled.Products:"+products)
Ok(views.html.product_index(products))
}
defblank=Action{implicitrequest=>
Logger.info("blankcalled.")
Ok(views.html.product_details(None,productForm))
}
defdetails(id:Long)=Action{implicitrequest=>
Logger.info("detailscalled.id:"+id)
valproduct=Awaits.get(5,service.findById(id)).get
Ok(views.html.product_details(Some(id),
productForm.fill(product)))
}
definsert()=Action{implicitrequest=>
Logger.info("insertcalled.")
productForm.bindFromRequest.fold(
form=>{
BadRequest(views.html.product_details(None,form))
},
product=>{
service.insert(product)
Redirect(routes.ProductController.index).
flashing("success"->Messages("success.insert",
"newproductcreated"))
}
)
}
defupdate(id:Long)=Action{implicitrequest=>
Logger.info("updatedcalled.id:"+id)
productForm.bindFromRequest.fold(
form=>{
Ok(views.html.product_details(Some(id),
form)).flashing("error"->"Fixtheerrors!")
},
product=>{
service.update(id,product)
Redirect(routes.ProductController.index).flashing
("success"->Messages("success.update",product.name))
}
)
}
defremove(id:Long)=Action{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
valresult=Awaits.get(5,service.findById(id))
result.map{product=>
service.remove(id)
Redirect(routes.ProductController.index).flashing("success"
->Messages("success.delete",product.name))
}.getOrElse(NotFound)
}
}
Therearetwobigchangesintheprecedingcodedespitethenewfunctionsignatures.First,weuseautilityfunctioncalledgetfromaclasscalledAwaits.Thisisneededsothatwewaitfortheresulttocomebackfromthedatabase.Second,whenweflashtheresult,wenolongershowtheid,wejustdisplayatextmessage.Let'stakealookattheAwaitsimplementationinUtils.Awaits.scala,whichisasfollows:
packageutils
objectAwaits{
defget[T](sec:Int,f:Future[T]):T={
Await.result[T](f,secseconds)
}
}
AwaitsisjustasimpleutilityclassthatwaitsforaperiodoftimetogetaFutureresult.WeneedtoaddsometweaksinReviewControllerandImageControlleraswell.
WewillfirstexploreReviewController.scala:
packagecontrollers
@Singleton
classReviewController@Inject()
(valmessagesApi:MessagesApi,valproductService:IProductService,
valservice:IReviewService)
extendsControllerwithI18nSupport{
valreviewForm:Form[Review]=Form(
mapping(
"id"->optional(longNumber),
"productId"->optional(longNumber),
"author"->nonEmptyText,
"comment"->nonEmptyText
)(models.Review.apply)(models.Review.unapply)
)
defindex=Action{implicitrequest=>
valreviews=Awaits.get(5,service.findAll()).getOrElse(Seq())
Logger.info("indexcalled.Reviews:"+reviews)
Ok(views.html.review_index(reviews))
}
defblank=Action{implicitrequest=>
Logger.info("blankcalled.")
Ok(views.html.review_details(None,
reviewForm,productService.findAllProducts))
}
defdetails(id:Long)=Action{implicitrequest=>
Logger.info("detailscalled.id:"+id)
valreview=Awaits.get(5,service.findById(id)).get
Ok(views.html.review_details(Some(id),
reviewForm.fill(review),productService.findAllProducts))
}
definsert()=Action{implicitrequest=>
Logger.info("insertcalled.")
reviewForm.bindFromRequest.fold(
form=>{
BadRequest(views.html.review_details(None,
form,productService.findAllProducts))
},
review=>{
if(review.productId==null||review.productId.isEmpty){
Redirect(routes.ReviewController.blank).flashing("error"
->"ProductIDCannotbeNull!")
}else{
Logger.info("Review:"+review)
if(review.productId==null||
review.productId.getOrElse(0)==0)thrownew
IllegalArgumentException("ProductIdCannotBeNull")
service.insert(review)
Redirect(routes.ReviewController.index).
flashing("success"->Messages("success.insert",
"newReview"))
}
}
)
}
defupdate(id:Long)=Action{implicitrequest=>
Logger.info("updatedcalled.id:"+id)
reviewForm.bindFromRequest.fold(
form=>{
Ok(views.html.review_details(Some(id),
form,productService.findAllProducts)).
flashing("error"->"Fixtheerrors!")
},
review=>{
service.update(id,review)
Redirect(routes.ReviewController.index).
flashing("success"->Messages("success.update",
review.productId))
}
)
}
defremove(id:Long)=Action{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
valresult=Awaits.get(5,service.findById(id))
result.map{review=>
service.remove(id)
Redirect(routes.ReviewController.index).flashing("success"-
>Messages("success.delete",review.productId))
}.getOrElse(NotFound)
}
}
ForReviewController,wehavemadethesamechangesthatwedidfortheproduct,thatis,theuseofAwaitsandlabelsonflashreturns.
Let'smoveontothefinalcontroller:ImageController.scala.
packagecontrollers
@Singleton
classImageController@Inject()
(valmessagesApi:MessagesApi,
valproductService:IProductService,
valservice:IImageService)
extendsControllerwithI18nSupport{
valimageForm:Form[Image]=Form(
mapping(
"id"->optional(longNumber),
"productId"->optional(longNumber),
"url"->text
)(models.Image.apply)(models.Image.unapply)
)
defindex=Action{implicitrequest=>
valimages=Awaits.get(5,service.findAll()).getOrElse(Seq())
Logger.info("indexcalled.Images:"+images)
Ok(views.html.image_index(images))
}
defblank=Action{implicitrequest=>
Logger.info("blankcalled.")
Ok(views.html.image_details(None,
imageForm,productService.findAllProducts))
}
defdetails(id:Long)=Action{implicitrequest=>
Logger.info("detailscalled.id:"+id)
valimage=Awaits.get(5,service.findById(id)).get
Ok(views.html.image_details(Some(id),
imageForm.fill(image),productService.findAllProducts))
}
definsert()=Action{implicitrequest=>
Logger.info("insertcalled.")
imageForm.bindFromRequest.fold(
form=>{
BadRequest(views.html.image_details(None,form,
productService.findAllProducts))
},
image=>{
if(image.productId==null||
image.productId.getOrElse(0)==0){
Redirect(routes.ImageController.blank).flashing
("error"->"ProductIDCannotbeNull!")
}else{
if(image.url==null||"".equals(image.url))
image.url="/assets/images/default_product.png"
service.insert(image)
Redirect(routes.ImageController.index).
flashing("success"->Messages("success.insert",
"newimage"))
}
}
)
}
defupdate(id:Long)=Action{implicitrequest=>
Logger.info("updatedcalled.id:"+id)
imageForm.bindFromRequest.fold(
form=>{
Ok(views.html.image_details(Some(id),form,
null)).flashing("error"->"Fixtheerrors!")
},
image=>{
service.update(id,image)
Redirect(routes.ImageController.index).flashing
("success"->Messages("success.update",image.id))
}
)
}
defremove(id:Long)=Action{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
valresult=Awaits.get(5,service.findById(id))
result.map{image=>
service.remove(id)
Redirect(routes.ImageController.index).flashing("success"
->Messages("success.delete",image.id))
}.getOrElse(NotFound)
}
}
ConfiguringDAOpackagesinGuiceWeneedtoconfiguretheinjectionsforthethreenewdaopackagesthatwecreated.SowetoneedtoaddthreelinesinthefileModule.scala.PleaseopenthefileinyourIDE,andaddthefollowingcontent:
bind(classOf[IProductDao]).to(classOf[ProductDao]).asEagerSingleton()
bind(classOf[IImageDao]).to(classOf[ImageDao]).asEagerSingleton()
bind(classOf[IReviewDao]).to(classOf[ReviewDao]).asEagerSingleton
Thewholefile,Module.scala,shouldlooklikethis:
/**
*ThisclassisaGuicemodulethattellsGuicehowtobindseveral
*differenttypes.ThisGuicemoduleiscreatedwhenthePlay
*applicationstarts.
*Playwillautomaticallyuseanyclasscalled`Module`thatisin
*therootpackage.Youcancreatemodulesinotherlocationsby
*adding`play.modules.enabled`settingstothe`application.conf`
*configurationfile.
*/
classModuleextendsAbstractModule{
overridedefconfigure()={
//UsethesystemclockasthedefaultimplementationofClock
bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
//AskGuicetocreateaninstanceofApplicationTimerwhen
//theapplicationstarts.
bind(classOf[ApplicationTimer]).asEagerSingleton()
bind(classOf[IProductService]).
to(classOf[ProductService]).asEagerSingleton()
bind(classOf[IReviewService]).
to(classOf[ReviewService]).asEagerSingleton()
bind(classOf[IImageService]).
to(classOf[ImageService]).asEagerSingleton()
bind(classOf[IPriceSerice]).
to(classOf[PriceService]).asEagerSingleton()
bind(classOf[IRndService]).
to(classOf[RndService]).asEagerSingleton()
bind(classOf[IProductDao]).
to(classOf[ProductDao]).asEagerSingleton()
bind(classOf[IImageDao]).
to(classOf[ImageDao]).asEagerSingleton()
bind(classOf[IReviewDao]).
to(classOf[ReviewDao]).asEagerSingleton()
}
}
RefactoringtestsAsyoumightexpect,mosttestsarenolongerworking.Wewillneedtoperformsomerefactoringhereaswell.Wewillrefactorourformerdaotomakeitgeneric,anditwillbeusedinintegrationtests(end-to-endtests).
Sincewewillcreateagenericdaosysteminmemoryforend-to-endtestingpurposes,weneedtochangeourmodelsalittlebit.First,weneedtocreateabasetraitforallthemodels.Thisisneededsowecantreatourmodelsasequals.
Let'shavealookatmodels.BaseModel.scala:
packagemodels
traitBaseModel{
defgetId:Option[Long]
defsetId(id:Option[Long]):Unit
}
Wealsoneedtomakeallourmodelsimplementthisnewtrait.SowewillneedtochangetheScalacodefortheproduct,image,andreview.Thisisverytrivial:wejustaddagetterandasetterfortheidfield.Youcanalsousescala.bean.BeanPropertyinsteadofwritingonebyyourself.
Yourmodels.Product.scalafileshouldlooksomethinglikethis:
packagemodels
caseclassProduct
(varid:Option[Long],
varname:String,
vardetails:String,
varprice:BigDecimal)
extendsBaseModel{
overridedeftoString:String={
"Product{id:"+id.getOrElse(0)+",name:"+name+",
details:"+details+",price:"+price+"}"
}
overridedefgetId:Option[Long]=id
overridedefsetId(id:Option[Long]):Unit=this.id=id
}
objectProductDef{
deftoTable:String="Product"
}
Asyoucanseeintheprecedingcode,weextendtheBaseModelmethod,andimplementgetIdandsetId.Weneedtodothesameforthereviewandimagemodels.
Yourmodels.Review.scalafileshouldlooklikethis:
packagemodels
caseclassReview
(varid:Option[Long],
varproductId:Option[Long],
varauthor:String,
varcomment:String)
extendsBaseModel{
overridedeftoString:String={
"Review{id:"+id+",productId:"+
productId.getOrElse(0)+",author:"+author+",comment:"
+comment+"}"
}
overridedefgetId:Option[Long]=id
overridedefsetId(id:Option[Long]):Unit=this.id=id
}
objectReviewDef{
deftoTable:String="Review"
}
Nowwemoveontothelastmodel.WeneedtoimplementitinImage.scala.
Yourmodels.Image.scalafileshouldlooklikethis:
packagemodels
caseclassImage
(varid:Option[Long],
varproductId:Option[Long],
varurl:String)
extendsBaseModel{
overridedeftoString:String={
"Image{productId:"+productId.getOrElse(0)+",url:"+
url+"}"
}
overridedefgetId:Option[Long]=id
overridedefsetId(id:Option[Long]):Unit=this.id=id
}
objectImageDef{
deftoTable:String="Image"
}
GenericmocksNowwehaveallthatweneedtocreateagenericmockimplementationandmockallthedaopackages.UnderthelocationReactiveWebStore/test/,wewillcreateapackagecalledmocks,andcreateacall,GenericMockedDao.
YourGenericMockedDao.scalafileshouldlooklikethis:
packagemocks
importmodels.BaseModel
classGenericMockedDao[T<:BaseModel]{
importjava.util.concurrent.atomic.AtomicLong
importscala.collection.mutable.HashMap
importscala.concurrent._
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
varinMemoryDB=newHashMap[Long,T]
varidCounter=newAtomicLong(0)
deffindAll():Future[Seq[T]]={
Future{
if(inMemoryDB.isEmpty)Seq()
inMemoryDB.values.toSeq
}
}
deffindById(id:Long):Future[Option[T]]={
Future{
inMemoryDB.get(id)
}
}
defremove(id:Long):Future[Int]={
Future{
validateId(id)
inMemoryDB.remove(id)
1
}
}
definsert(t:T):Future[Unit]={
Future{
valid=idCounter.incrementAndGet();
t.setId(Some(id))
inMemoryDB.put(id,t)
Unit
}
}
defupdate(t:T):Future[Unit]={
Future{
validateId(t.getId.get)
inMemoryDB.put(t.getId.get,t)
Unit
}
}
privatedefvalidateId(id:Long):Unit={
valentry=inMemoryDB.get(id)
if(entry==null||entry.equals(None))thrownew
RuntimeException("CouldnotfindProduct:"+id)
}
}
SotheGenericMockedDaocallexpectstheGenericparameter,whichcouldbeanyclassextendingfromBaseModel.Thenweuseanin-memoryHashMapimplementationandacountertosimulatedatabaseoperations.WerunalltheoperationsinsideFutures,sowedon'tbreakthenewsignaturethecodeisexpecting.NowwecancreatethreeMockedDaosforeachmodelweneed:product,review,andimage.
Yourmocks.ProductMockedDao.scalafileshouldlooklikethis:
packagemocks
classProductMockedDaoextendsIProductDao{
valdao:GenericMockedDao[Product]=new
GenericMockedDao[Product]()
overridedeffindAll():Future[Seq[Product]]={
dao.findAll()
}
overridedeffindById(id:Long):Future[Option[Product]]={
dao.findById(id)
}
overridedefremove(id:Long):Future[Int]={
dao.remove(id)
}
overridedefinsert(p:Product):Future[Unit]={
dao.insert(p)
}
overridedefupdate(p2:Product):Future[Unit]={
dao.update(p2)
}
overridedeftoTable:TableQuery[_]={
null
}
}
Asyoucanseehere,weimplementtheIProdutDaotrait,andwedelegatealloperationstogenericMockedDao.Sinceeverythingisin-memory,wedon'tneedtoimplementthetoTablefunction.Weneedtodothesameforthereviewandimage.
Yourmocks.ReviewMockedDao.scalafileshouldlooklikethis:
packagemocks
classReviewMockedDaoextendsIReviewDao{
valdao:GenericMockedDao[Review]=new
GenericMockedDao[Review]()
overridedeffindAll():Future[Seq[Review]]={
dao.findAll()
}
overridedeffindById(id:Long):Future[Option[Review]]={
dao.findById(id)
}
overridedefremove(id:Long):Future[Int]={
dao.remove(id)
}
overridedefinsert(p:Review):Future[Unit]={
dao.insert(p)
}
overridedefupdate(p2:Review):Future[Unit]={
dao.update(p2)
}
overridedeftoTable:TableQuery[_]={
null
}
}
Exactlylikeproduct,wedelegatealloperationstoGenericMockedDao.Nowlet'smovetothelastone,theimage,andthenwecanfixthetests.
Yourmocks.ImageMockedDao.scalafileshouldlooklikethis:
packagemocks
classImageMockedDaoextendsIImageDao{
valdao:GenericMockedDao[Image]=newGenericMockedDao[Image]()
overridedeffindAll():Future[Seq[Image]]={
dao.findAll()
}
overridedeffindById(id:Long):Future[Option[Image]]={
dao.findById(id)
}
overridedefremove(id:Long):Future[Int]={
dao.remove(id)
}
overridedefinsert(p:Image):Future[Unit]={
dao.insert(p)
}
overridedefupdate(p2:Image):Future[Unit]={
dao.update(p2)
}
overridedeftoTable:TableQuery[_]={
null
}
}
Okay,wehaveallthemocksthatweneedfornow.Wecanmoveontofixthetestspecs.Weneedtofixservicestestsandcontrollertest.Servicestestswillusemocks.Controllerstests,however,willusetherealdatabaseimplementation.Weneedtouseotherutilityclassesforcontrollertests.Locatedinthetestsourcefolder,weneedtocreateapackagecalledutils.
Yourutils.DBCleaner.scalafileshouldlooklikethis:
packageutils
objectDBCleaner{
valpool=Executors.newCachedThreadPool()
implicitvalec=ExecutionContext.fromExecutorService(pool)
defcleanUp():Unit={
Class.forName("com.mysql.cj.jdbc.Driver")
valdb=Database.forURL
("jdbc:mysql://127.0.0.1:3306/RWS_DB?useUnicode=
true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=
false&serverTimezone=UTC",
"root","")
try{
Await.result(
db.run(
DBIO.seq(
sqlu"""DELETEFROMProduct;""",
sqlu"""DELETEFROMImage;""",
sqlu"""DELETEFROMReview;""",
sqlu"""ALTERTABLEProductAUTO_INCREMENT=1""",
sqlu"""ALTERTABLEImageAUTO_INCREMENT=1""",
sqlu"""ALTERTABLEReviewAUTO_INCREMENT=1"""
)
)
,20seconds)
}catch{
casee:Exception=>Unit
}
}
}
DBCleanerwillconnecttotherealdatabaseandperformdeletestatementstocleanupalltabledata.Afterdeletingalldatainthetables,wealsoresetthesequenceinthedatabase;otherwise,ourtestswillnothavethepredictabilityweneedtodoassertions.
Asyoucanseeindb.run,wecanuseDBIO.seq,whichallowsustoexecutemultipleinstructionsonthedatabase.HerewearenotusingScalacode.WeareusingpureSQLstatements,sinceweneedtouseveryspecificMySQLfunctionstoresetthesequences.
Ifyouneed,youcoulduseallthesefunctionsinyourapplication.Thisisusefulifyouneedtouseaspecificdatabasefunction,ifyouhaveaverycomplexquery,orsometimes,becausethereisaperformanceissue.
MostfixeswedonowcenteraroundusingAwaitstowaitfortheFutureresult,andalsousingournewmocks.Forthecontrollertest,weneedtocalltheDBCleanerfunctionaswell.
ServicetestsNowwewillcreatetestsforservicestotestthem.Solet'sgetstarted.
YourProductServiceTestSpec.scalafileshouldlooklikethis:
classProductServiceTestSpecextendsPlaySpec{
"ProductService"must{
valservice:IProductService=newProductService(new
ProductMockedDao)
"insertaproductproperly"in{
valproduct=newmodels.Product(Some(1),"Ball","Awesome
Basketball",19.75)
service.insert(product)
}
"updateaproduct"in{
valproduct=newmodels.Product(Some(1),"Blue
Ball","AwesomeBlueBasketball",19.99)
service.update(1,product)
}
"notupdatebecausedoesnotexit"in{
intercept[RuntimeException]{
service.update(333,null)
}
}
"findtheproduct1"in{
valproduct=Awaits.get(5,service.findById(1))
product.get.idmustBeSome(1)
product.get.namemustBe"BlueBall"
product.get.detailsmustBe"AwesomeBlueBasketball"
product.get.pricemustBe19.99
}
"findall"in{
valproducts=Awaits.get(5,service.findAll())
products.get.lengthmustBe1
products.get(0).idmustBeSome(1)
products.get(0).namemustBe"BlueBall"
products.get(0).detailsmustBe"AwesomeBlueBasketball"
products.get(0).pricemustBe19.99
}
"findallproducts"in{
valproducts=service.findAllProducts()
products.lengthmustBe1
products(0)._1mustBe"1"
products(0)._2mustBe"BlueBall"
}
"remove1product"in{
valproduct=Awaits.get(5,service.remove(1))
productmustBe1
valoldProduct=Awaits.get(5,service.findById(1))
oldProductmustBeNone
}
"notremovebecausedoesnotexist"in{
intercept[RuntimeException]{
Awaits.get(5,service.remove(-1))
}
}
}
}
Asyoucanseeintheprecedingcode,mostfixescenteraroundthenewsignaturesandthefactweareusingFuturesandneedtousetheAwaitsutilityandmocks.Wetesttheservicewithoutthedatabasecallviathiscode:
valservice:IProductService=newProductService(newProductMockedDao)
Wecanmoveontothenextservice,whichwillbethereview.
YourReviewServiceTestSpec.scalafileshouldlooklikethis:
classReviewServiceTestSpecextendsPlaySpec{
"ReviewService"must{
valservice:IReviewService=newReviewService(new
ReviewMockedDao)
"insertareviewproperly"in{
valreview=newmodels.Review
(Some(1),Some(1),"diegopacheco","TestingisCool")
service.insert(review)
}
"updateareviewt"in{
valreview=newmodels.Review
(Some(1),Some(1),"diegopacheco","TestingsosoCool")
service.update(1,review)
}
"notupdatebecausedoesnotexist"in{
intercept[RuntimeException]{
Awaits.get(5,service.update(333,null))
}
}
"findthereview1"in{
valreview=Awaits.get(5,service.findById(1))
review.get.idmustBeSome(1)
review.get.authormustBe"diegopacheco"
review.get.commentmustBe"TestingsosoCool"
review.get.productIdmustBeSome(1)
}
"findall"in{
valreviews=Awaits.get(5,service.findAll())
reviews.get.lengthmustBe1
reviews.get(0).idmustBeSome(1)
reviews.get(0).authormustBe"diegopacheco"
reviews.get(0).commentmustBe"TestingsosoCool"
reviews.get(0).productIdmustBeSome(1)
}
"remove1review"in{
valreview=Awaits.get(5,service.remove(1))
reviewmustBe1
valoldReview=Awaits.get(5,service.findById(1))
oldReviewmustBeNone
}
"notremovebecausedoesnotexist"in{
intercept[RuntimeException]{
Awaits.get(5,service.remove(-1))
}
}
}
}
Thatwasthereviewspecservicetestcode.Weapplythesamechangesaswedidforproduct.Nowweneedtomoveontothelastservicetest,whichwillbetheimage.
YourImageServiceTestSpec.scalafileshouldlooklikethis:
classImageServiceTestSpecextendsPlaySpec{
"ImageService"must{
valservice:IImageService=newImageService(new
ImageMockedDao)
"insertaimageproperly"in{
valimage=newmodels.Image
(Some(1),Some(1),"http://www.google.com.br/myimage")
service.insert(image)
}
"updateaimage"in{
valimage=newmodels.Image
(Some(2),Some(1),"http://www.google.com.br/myimage")
service.update(1,image)
}
"notupdatebecausedoesnotexist"in{
intercept[RuntimeException]{
Awaits.get(5,service.update(333,null))
}
}
"findtheimage"in{
valimage=Awaits.get(5,service.findById(1))
image.get.idmustBeSome(1)
image.get.productIdmustBeSome(1)
image.get.urlmustBe"http://www.google.com.br/myimage"
}
"findall"in{
valreviews=Awaits.get(5,service.findAll())
reviews.get.lengthmustBe1
reviews.get(0).idmustBeSome(1)
reviews.get(0).productIdmustBeSome(1)
reviews.get(0).urlmustBe"http://www.google.com.br/myimage"
}
"remove1image"in{
valimage=Awaits.get(5,service.remove(1))
imagemustBe1
valoldImage=Awaits.get(5,service.findById(1))
oldImagemustBeNone
}
"notremovebecausedoesnotexist"in{
intercept[RuntimeException]{
Awaits.get(5,service.remove(-1))
}
}
}
}
Wehavefixedalltheservicestests.Nowweneedtofixthecontrollertests.
ControllertestsNowlet'sfixthecontrollertests.Thefirstonewillbetheproductcontroller.
YourProductControllerTestSpec.scalafileshouldlooklikethis:
classProductControllerTestSpec
extends
PlaySpec
withOneServerPerSuitewithOneBrowserPerSuitewithHtmlUnitFactory{
"ProductController"should{
DBCleaner.cleanUp()
"insertanewproductshouldbeok"in{
goTo(s"http://localhost:${port}/product/add")
clickonid("name")
enter("BlueBall")
clickonid("details")
enter("BlueBallisaAwesomeandsimpleproduct")
clickonid("price")
enter("17.55")
submit()
}
"detailsfromtheproduct1shouldbeok"in{
goTo(s"http://localhost:${port}/product/details/1")
textField("name").valuemustBe"BlueBall"
textField("details").valuemustBe"BlueBallisaAwesome
andsimpleproduct"
textField("price").valuemustBe"17.55"
}
"updateproduct1shouldbeok"in{
goTo(s"http://localhost:${port}/product/details/1")
textField("name").value="BlueBall2"
textField("details").value="BlueBallisaAwesomeand
simpleproduct2"
textField("price").value="17.66"
submit()
goTo(s"http://localhost:${port}/product/details/1")
textField("name").valuemustBe"BlueBall2"
textField("details").valuemustBe"BlueBallisaAwesome
andsimpleproduct2"
textField("price").valuemustBe"17.66"
}
"deleteaproductshouldbeok"in{
goTo(s"http://localhost:${port}/product/add")
clickonid("name")
enter("BlueBall")
clickonid("details")
enter("BlueBallisaAwesomeandsimpleproduct")
clickonid("price")
enter("17.55")
submit()
goTo(s"http://localhost:${port}/product")
clickonid("btnDelete")
}
"Cleanupdbintheend"in{
DBCleaner.cleanUp()
}
}
}
TheControllerproducttestneedstocalltheDBCleanerfunctionatthebeginningofthetesttomakesurethatthedatabaseisinawell-knownstate;additionally,andafterrunningallthetests,weneedtocleanupthedatabasejusttobesafe.
Wewillnowapplythesamechangesforthereviewandimagecontrollertests.
YourReviewControllerTestSpecfileshouldlooklikethis:
classReviewControllerTestSpec
extendsPlaySpec
withOneServerPerSuitewithOneBrowserPerSuitewithHtmlUnitFactory{
DBCleaner.cleanUp()
"ReviewController"should{
"insertanewreviewshouldbeok"in{
goTo(s"http://localhost:${port}/product/add")
clickonid("name")
enter("BlueBall")
clickonid("details")
enter("BlueBallisaAwesomeandsimpleproduct")
clickonid("price")
enter("17.55")
submit()
goTo(s"http://localhost:${port}/review/add")
singleSel("productId").value="1"
clickonid("author")
enter("diegopacheco")
clickonid("comment")
enter("Testsareamazing!")
submit()
}
"detailsfromthereview1shouldbeok"in{
goTo(s"http://localhost:${port}/review/details/1")
textField("author").valuemustBe"diegopacheco"
textField("comment").valuemustBe"Testsareamazing!"
}
"updatereview1shouldbeok"in{
goTo(s"http://localhost:${port}/review/details/1")
textField("author").value="diegopacheco2"
textField("comment").value="Testsareamazing2!"
submit()
goTo(s"http://localhost:${port}/review/details/1")
textField("author").valuemustBe"diegopacheco2"
textField("comment").valuemustBe"Testsareamazing2!"
}
"deleteareviewshouldbeok"in{
goTo(s"http://localhost:${port}/review/add")
singleSel("productId").value="1"
clickonid("author")
enter("diegopacheco")
clickonid("comment")
enter("Testsareamazing!")
submit()
goTo(s"http://localhost:${port}/review")
clickonid("btnDelete")}
"Cleanupdbintheend"in{
DBCleaner.cleanUp()
}
}
}
Alright,wehavethetestsforthereviewcontrollerfixed.Nowwecanmovetothelastcontrollertestfortheimage.
YourImageControllerTestSpec.scalafileshouldlooklikethis:
classImageControllerTestSpec
extendsPlaySpec
withOneServerPerSuitewithOneBrowserPerSuitewithHtmlUnitFactory{
DBCleaner.cleanUp()
"ImageController"should{
"insertanewimageshouldbeok"in{
goTo(s"http://localhost:${port}/product/add")
clickonid("name")
enter("BlueBall")
clickonid("details")
enter("BlueBallisaAwesomeandsimpleproduct")
clickonid("price")
enter("17.55")
submit()
goTo(s"http://localhost:${port}/image/add")
singleSel("productId").value="1"
clickonid("url")
enter("https://thegoalisthering.files.wordpress.com/2012/01/
bluetennisball_display_image.jpg")
submit()
}
"detailsfromtheimage1shouldbeok"in{
goTo(s"http://localhost:${port}/image/details/1")
textField("url").valuemustBe
"https://thegoalisthering.files.wordpress.com/2012/01/
bluetennisball_display_image.jpg"
}
"updateimage1shouldbeok"in{
goTo(s"http://localhost:${port}/image/details/1")
textField("url").value=
"https://thegoalisthering.files.wordpress.com/2012/01/
bluetennisball_display_image2.jpg"
submit()
goTo(s"http://localhost:${port}/image/details/1")
textField("url").valuemustBe
"https://thegoalisthering.files.wordpress.com/2012/01/
bluetennisball_display_image2.jpg"
}
"deleteaimageshouldbeok"in{
goTo(s"http://localhost:${port}/image/add")
singleSel("productId").value="1"
clickonid("url")
enter("https://thegoalisthering.files.wordpress.com/2012/01/
bluetennisball_display_image.jpg")
submit()
goTo(s"http://localhost:${port}/image")
clickonid("btnDelete")
}
"Cleanupdbintheend"in{
DBCleaner.cleanUp()
}
}
}
Allright,allthecontrollertestsarefixednow.WecanrunalltheteststodoublecheckwhethereverythingisOK.
Runthefollowingcommand:
$activatortest
Yougetoutputasshowninthefollowingscreenshot:
Ifyouhaveproblemsrunningtheapplication(coveredinthenextsection),applytheevolution,andthenyoucanrunthetestsagain.Testsmighttakesometime,dependingonyourhardware.
RunningtheapplicationNowitistimetoruntheapplicationusing$activatorrun.Openyourwebbrowser,andgotohttp://localhost:9000/.Onceyoudothat,Playwilldetectthattheapplicationneedsevolutions,andwillapplythethreeevolutionswehave(1.sql,2.sql,and3.sql).However,youwillneedtoclickonthebuttontoapplytheevolution.
Afteryouclickontheredbutton,Applythisscriptnow!,Slickwillcreatethetables,andredirectyoutotheapplication.
SummaryWiththis,wereachtheendofthechapter.YoulearnedhowtoperformdatabasepersistenceusingSlick.YoualsolearnedhowtodoFRMmapping,andwerefactoredourapplicationandtestssotheyworkwithreactivepersistenceandthePlayframework.WethenexplainedhowtoaccessthedatabaseusingScalacode,andperformoperationsusingSQL.
Inthefollowingchapter,wewillseemoreaboutreports,andwewilluseourdatabasetogeneratereportsbasedonourPlayframeworkapplication.
Chapter7.CreatingReportsUpuntilnow,welearnedhowtobootstrapourapplicationusingActivator,developourwebapplicationusingtheScalaandPlayframework,andaaddreactivemicroservicescallusingRxScalafordataflowcomputations.WealsoperformedunittestandcontrollertestingusingtheBDDandPlayframework.Then,wepersisteddataintoMySQLusingSlick.Nowwewillmoveonwithourapplication.
Inthischapter,youwilllearnhowtowritereportswithJasperReports.JasperReportsisaverysolidreportingsolutionforJava,anditcanbeusedinScalaveryeasily.WewillcreatedatabasereportsusingJasper,andchangeourapplicationtohavesuchfunctionality.
Inthischapter,wewillcoverthefollowingtopics:
UnderstandingJasperReportsAddingdatabasereportstoourapplication
IntroducingJasperReportsJasperReports(http://community.jaspersoft.com/project/jasperreports-library)isaverypopularandsolidreportssolutionthatcangeneratereportsinseveralformats,suchas:
HTMLExcelWordOpenOfficeformatPDF
Inordertogetyourreports,youhaveavisualtoolcalledJaspersoftStudio,inwhichyoucandraganddropelementssuchaslabels,images,datafields,andmuchmore.Jasperwillstorethismetadata(thereportdefinition)inanXMLfile,alsoknownasJRXML.Ifyouwant,youcaneditandworkwiththisXMLwithoutanyeditor;however,itiswaybettertousetheJaspersoftStudiotooltogainproductivity.
Jaspercanworkwithseveraldatasources,suchasdatabases,XML,orevenobjectsinmemory.Forthisbook,wewillusethedatabasedatasourcetoaccessourMySQLdatabase.
JasperReportsworkflowJasperReportshasthesameexecutionstages,includingcompilingyourreportsandrenderinginaspecificformat,forexample,HTML.Thefirststageisthereportdesign.IfyouarenotusingtheJaspersoftStudiovisualtool,weassumethatyouhaveyourJRXMLfile.ThenextstepistocompileJRXMLintoaJasperfile.Thiscompilationphasedoesn'tneedtohappeneverytime;it'sneededonlyifyouchangetheJRXML.Otherwise,youcanusethesameJasperfile.TherearesomestrategiestocachetheJasperfile,sobasicallyyoucandoitonthebuildtimeoryoucancacheondemandintheapplication.Forourapplication,wewillbeusingthesecondapproach--cachingondemandintheapplication.
Thenextphaseistorenderorexport.YoucanexportthereporttothemanyformatsJaspersupports,suchasHTML,EXCEL,orPDF,forinstance.It'spossibletousethesamereportlayoutandexporttoasmanyformatsasyoulike.Forourapplication,wewillbeusingthePDFformat.
JaspersessionsAJRXMLhassectionsthatareevaluatedindifferentwaysandatdifferenttimes.Thefollowingdiagramshowsalltheavailablesessions:
Thedifferentsectionsareasfollows:
Title:ThisisprintedjustonetimePageHeader:ThisisprintedatthebeginningofallprintedpagesColumnHeader:ThisisprintedatthebeginningofeachdetailcolumnDetail:ThisiswhereeveryrecordreadfromthedatasourceisprintedColumnFooter:ThisisprintedattheendofeachdetailcolumnPageFooter:ThisisprintedattheendofallprintedpagesSummary:Thisisprintedattheendofthereport,andisoftenusedtoshowcalculations,totals,andsummarizationsingeneral
JasperisaveryflexiblereportsolutionthatalsoallowsustorungroovyscriptsinsideaJasperreporttododynamiccalculationsaswellasdynamiclayouts.Thisisusefulifyoudonotwanttoprintapagegivensomecondition,orbasedonwhatyouhaveinthedatabase,or
youdonotwanttoshowsomedata.
Next,wewillinstallJaspersoftStudioandstartcreatingreportsforourapplication.
InstallingJaspersoftStudio6Forthis,youwillneedtohaveJava8installed.Ifyoudon'thaveit,gobacktoChapter1,IntroductiontoFP,Reactive,andScala,andfollowthesetupinstructions.Jasperisreallygreatbecauseitworksonmultipleplatforms;however,itworksbetteronWindows.WeareusingLinux,sowewillneedtodealwithfonts.JasperReportsuseslotsofMicrosoft'scorefonts,suchasArialandTimesNewRoman.TherearesomeoptionstohavethesourcesonLinux.YoucanlookforamscorefontsinstalleronLinuxorjustcopythefontsfromWindows.
IfyouhaveadualbootLinux/Windowsinstallation,youcangotoyourWindowsdriveatthelocationWindowsDrive/Windows/Fonts.Youwillneedtocopyallfontfilesto/usr/share/fontsonLinuxandrun$sudofc-cache-fv.Thismighttakesometime--formyWindowsinstallation,itwasabout~300MBoffonts.YoucantestwhetheryouhaveWindowscorefontsonLinux.Openthewriterandcheckforthefonts.Youshouldseesomethingsimilartothis:
Whyisthissoimportant?BecauseJasperwon'tworkifyoudon'thavetherightfontinplace.Itwilljustthrowyourandomexceptionsthatwillnotmakesense,butitisverylikelytoberelatedtomissingfonts.
Oncewehavethefonts,wecangoaheadanddownloadJaspersoftStudio6.Forthisbook,wewillbeusingthe6.2.2version.Youcandownloaditfromhttp://community.jaspersoft.com/project/jasperreports-library/releases.IfyouareonLinux,it'shighlyrecommendedtousetheDEBpackage;otherwise,youwillneedtoinstallseveralotherdependencies.
OnceyoudownloadandinstallJaspersoftStudioandopentheprogram,youwillseeaUIsimilartothisonehere:
WehavesuccessfullyinstalledJaspersoftStudio.Now,wewillneedtoconfigureourMySQLdatasourceinordertostartcreatingreportsforourapplication.
ConfiguringMySQLDataAdapterinJaspersoftStudioOpenJaspersoftStudio6andclickonFile|New|DataAdapterWizard.Youwillseethefollowingscreen:
FilenameshouldbeMYSQL_DATAADAPTER.xml,andthenyoucanclickNext>.
Next,wewillneedtochoosethetypeofdatabaseadapter.Thereareseveraloptions,suchasCassandra,MongoDB,HBase,JSONfile,andsoon.
WeneedtopickDatabaseJDBCConnectionandclickNext>.
Now,wewillneedtoconfiguretheconnectiondetails.
Thefieldsshouldbecompletedasfollows:
Name:MySQLJDBCDriver:com.mysql.jdbc.DriverJDBCUrl:jdbc:mysql://localhost/RWS_DB?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
Username:rootPassword:Thisneedsbeblank,orputthepasswordifyouareusingone.
WewillalsoneedtoconfigurethedriverintotheJaspersoftStudioclasspath.Aswearerunningtheapplicationinthesamebox,wealreadyhavetheMySQLdriverdownloadwithSBTonthe~/.ivy2/cache/mysql/mysql-connector-java/jars/mysql-connector-java-6.0.3.jarfolder.Wewilljustneedtopointitoutonthethirdtab,calledDriverClasspath.
Nowwecantesttheconnectiontoseeifit'sallgood.
Great!NowwehaveourMySQLDatabaseAdapterconfigured,andwearereadytostartcreatingreportsforyourapplication.
CreatingaproductreportTocreateaproductreport,clickonFile|New|JasperReport.ThenselecttheInvoicetemplate.
NowyoucanclickonNext>andwewillsetupthenameofthereport.ThefilenamewillbeProducts.jrxml.ClickonNext>.Then,wewillneedtoselecttheDataSource:MySQL.
Now,youwillneedtoruntheSelectname,details,pricefromProduct;query.
AftersettingtheSQLquery,youcanclickonNext>.
Next,youwillneedtopickthefieldsthatwillbeusedinthereport.Selectallthefieldsfromtheleftlistandmovethemtotherightlist.ThenclickonNext>.Wedon'tneedgrouporderingforthisreport,sojustskipthegroupandclickonNext>again.
Congratulations!Wefinishedthesetup.Wecantakealook(haveareportpreview)ofthereportusingJaspersoftStudio.JustclickonthenewreportcalledProducts.jxml.Wewillremoveallthefieldsthatwedon'tneed,aswellasthelogo.Thenthereportwilllooklikethis:
WewillchangethetitletoProductsanddropallotherinformationbuttheNAME,DETAILS,andPRICEheaders,whichwillberetained.Wewillalsokeepthe$F{NAME},$F{DETAILS},and$F{PRICE}fields,whichwillcomefromtheMySQLdatabase.
Now,wecanseethereportpreview.WewillneedtoclickonthebottomtabnamedPreview.Thereareseveralpreviewoptions.WewillhavetopickMySQLasadatasourcefromthetopofthescreenandtheexportedformat;here,weareusingJavatoseetheUI.Youcanalsopickotherformats,suchasPDF,forinstance.
Next,wewillneedtocreatereportsforreviewsandimages.
CreatingareviewreportNow,wewillcreatethereviewreport.Wewilluseaverysimilarprocesstotheonethatweusedfortheproductreport.Let'sgetstartedoncreatingareviewreport:
1. ClickonFile|New|JasperReport.SelecttheInvoicetemplateandclickonNext>.2. ThefilenamewillbeReviews.jrxml.ThenclickonNext>.3. ChooseMySQLfromDataAdapterandclickonNext>.4. Query(Text)shouldcontainthefollowingcodesnippet:
Selectp.name,r.author,r.comment
fromProductp,Reviewr
wherep.id=r.product_id;
5. ThenclickNext>.6. Selectallthefields:name,author,andcommentthenclickNext.>7. Let'sskipthegroupbysectionandclickonNextandthenFinish.8. Wewillremovealltemplatelabelsandfieldsandjustkeepthedatabasefields,sowe
shouldhavesomethinglikethis:
That'sit!Wehavethereviewreport.Ifyoulike,youcanclickonthePreviewtabatthebottomofthescreenandselectMySQLandJavatoseethereport.Keepinmindthatyouwillneedtohavedata;otherwise,itwillbeempty.
CreatinganimagereportNowwewillcreatetheimagereport.Wewillfollowaverysimilarprocesstotheoneweusedfortheproductandreviewreport.AswehaveanimageURL,wewillalsodisplaytheimage,sowewillneedtouseadifferentcomponent.Let'sgetstartedoncreatinganimagereport:
1. ClickonFile|New|JasperReport.2. SelecttheInvoicetemplateandclickonNext.3. ThefilenamewillbeImages.jrxml.ThenclickonNext.4. ChooseMySQLfromDataAdapterandclickonNext>.5. Query(Text)shouldcontainthefollowingcodesnippet:
Selectp.name,i.url
fromImagei,Productp
wherep.id=i.product_id;
6. ThenclickNext>.7. Selectallthefields:name,url,andthenclickNext>.8. Let'sskipthegroupbysectionandclickonNext>andthenFinish.
Nowwewillneedtoremovealllabelsandfields,aswedidfortheotherreports,andjustkeepthelabelsandfieldsfromthedataadapter.
WeneedtoaddanimagecomponentcalledImage.Youcanfinditinthepaletteattheright-handsidecalledBasicElements.Justdraganddropitintothedetailband,asshowninthefollowingscreenshot:
Selectacustomexpressionandthentype$F{url}.
That'sit!Nowthatwehavetheimagereportwithimages,it'stimetochangethePlayframeworkapplicationinordertorenderthisreportinPDFformatoverthere.
IntegratingJasperReportswithPlayframeworkWewillneedtocreateanewfolderunderReactiveWebStore/appcalledreports.Then,wewillcopyallthreenew.jrxmlfilesfromtheJaspersoftStudiotothisfolderandsetupthebuilddependencies.
build.sbtFirstofall,wewillneedtoaddnewdependenciestothebuild.sbtfile.
Yourbuild.sbtfileshouldlooklikethisafteraddingtheJasperdependencies:
libraryDependencies++=Seq(
//....Otherdependencies....
"net.sf.jasperreports"%"jasperreports"%"6.2.2"withSources()
,"net.sf.jasperreports"%"jasperreports-functions"%"6.2.2",
"net.sf.jasperreports"%"jasperreports-chart-themes"%"6.2.2"
)
resolvers+="Jasper"at
"https://jaspersoft.artifactoryonline.com/jaspersoft/repo/"
resolvers+="JasperSoft"at
"https://jaspersoft.artifactoryonline.com/jaspersoft/jaspersoft-
repo/"
resolvers+="Jasper3rd"at
"https://jaspersoft.artifactoryonline.com/jaspersoft/
jaspersoft-3rd-party/"
resolvers+="mondrian-repo-cache"at
"https://jaspersoft.artifactoryonline.com/jaspersoft/
mondrian-repo-cache/"
resolvers+="spring-mil"at"http://repo.spring.io/libs-milestone"
resolvers+="spring-rel"at"http://repo.spring.io/libs-release"
resolvers+="oss"at
"https://oss.sonatype.org/content/groups/public/"
So,basically,weaddedallJasperReportsdependenciesandresolvers,whichareabunchofremoterepositorieswhereSBTcanlookforthejarfiles.Youcanrunthe$activatorcompilecommandontheconsoleinordertoreloadthenewdependencies.Afterrunningcompile,itisimportanttogenerateeclipsefilesagain,soyouwillneedtorun$activatoreclipse.
GenericreportbuilderNowisthetimetocodeinScala.WewillcreateagenericreportbuilderinScala.UnderReactiveWebStore/app/reports,wewillcreateanewScalaclasscalledReportBuilder.scala.
YourReportBuilder.scalafileshouldhavethefollowingcode:
packagereports
objectReportBuilder{
privatevarreportCache:scala.collection.Map[String,Boolean]=
newscala.collection.mutable.HashMap[String,Boolean].empty
defgenerateCompileFileName(jrxml:String):String=
"/tmp/report_"+jrxml+"_.jasper"
defcompile(jrxml:String){
if(reportCache.get(jrxml).getOrElse(true)){
JasperCompileManager.compileReportToFile(new
File(".").getCanonicalFile+"/app/reports/"+jrxml,
generateCompileFileName(jrxml))
reportCache+=(jrxml->false)
}
}
deftoPdf(jrxml:String):ByteArrayInputStream={
try{
valos:OutputStream=newByteArrayOutputStream()
valreportParams:java.util.Map[String,Object]=new
java.util.HashMap()
valcon:Connection=DriverManager.getConnection
("jdbc:mysql://localhost/RWS_DB?user=root&password
=&useUnicode=true&useJDBCCompliantTimezoneShift
=true&useLegacyDatetimeCode=false&serverTimezone=UTC")
compile(jrxml)
valjrprint:JasperPrint=JasperFillManager.fillReport
(generateCompileFileName(jrxml),reportParams,con)
valexporter:JRPdfExporter=newJRPdfExporter()
exporter.setExporterInput(newSimpleExporterInput(jrprint))
exporter.setExporterOutput
(newSimpleOutputStreamExporterOutput(os));
exporter.exportReport()
newByteArrayInputStream
((os.asInstanceOf[ByteArrayOutputStream]).toByteArray())
}catch{
casee:Exception=>thrownewRuntimeException(e)
}
}
}
Firstofall,wearesettingatemporarydirectorytostoretheJaspercompiledfilesinthegenerateCompileFileNamefunction.Asyoucansee,wearestoringthecompiledreportsat/tmp/.Ifyoudon'tuseLinux,youwillneedtochangethispath.
Next,wehavethecompilefunction,whichreceivesaJRXMLreportinparameter.ThereisareportcacheMapobjecttoperformanon-demandcachefortheJasperfiles.Thismaphasthe
JRXMLreport,isthekeyasaBooleanfile.Thissolutionallowsyoutocompilereportsondemand.
Finally,wehavethetoPdffunctionthatwillreceivethejrxmlfunctionandcompilethereportthatisneeded.ThisfunctionusesDriverManagertogettheSQLconnectioninordertosendtheconnectiontotheJasperengine.Finally,thereisthefill,processmanagedbyJasperFillManager,whichwillreceivetheJasperfileandreportsparameters(forus,anemptymap)andtheSQLconnection.
Afterfillingthereportwithdatafromdatabase,wecanexportthereportinPDFusingtheJRPdfExportercommand.Asthisisagenericfunction,wewillreturnaByteArrayInputStream,whichisanin-memorystreamstructure.
Now,thenextstepistochangeourcontrollersinordertobeabletogeneratereportsforproducts,reviews,andimages.
AddingthereporttotheproductcontrollerWewillneedtochangetheproductcontrollerinordertoexposethenewreportfunction.
YourProductController.scalafile,afteraddingthereportfunction,shouldlooksomethinglikethis:
@Singleton
classProductController@Inject()(valmessagesApi:MessagesApi,val
service:IProductService)extendsControllerwithI18nSupport{
//...restofthecontrollercode...
defreport()=Action{
importplay.api.libs.concurrent.
Execution.Implicits.defaultContext
Ok.chunked(Enumerator.fromStream(
ReportBuilder.toPdf("Products.jrxml")))
.withHeaders(CONTENT_TYPE->"application/octet-stream")
.withHeaders(CONTENT_DISPOSITION->"attachment;
filename=report-products.pdf"
)
}
}
Righthere,wehaveanewfunctioncalledreport.WewillneedtouseourReportBuildermethodpassingtheProducts.jrxmlasparameter.WeareusingtheOk.chunkedfunctioninordertobeabletostreamthereporttothebrowser.Wearealsosettingsomeresponseheaders,suchasthecontenttypeandthenameofthefile,whichwillbereportedtoproducts.pdf.
Now,wewillapplythesamecodetothereviewandimagecontrollers.
AddingthereporttothereviewcontrollerNowisthetimetocreatethereportfunctionforthereviewcontroller.Herewego.
YourReviewController.scalafile,afteraddingareportfunction,shouldlooksomethinglikethis:
@Singleton
classReviewController@Inject()
(valmessagesApi:MessagesApi,
valproductService:IProductService,
valservice:IReviewService)
extendsControllerwithI18nSupport{
//...restofthecontrollercode...
defreport()=Action{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
Ok.chunked(Enumerator.fromStream(
ReportBuilder.toPdf("Reviews.jrxml")))
.withHeaders(CONTENT_TYPE->"application/octet-stream")
.withHeaders(CONTENT_DISPOSITION->"attachment;
filename=report-reviews.pdf")
}
}
Wehavethesamelogichereaswehavefortheproductcontroller.Themaindifferenceisthejrxmlfileandthefilenameresponseheader.Now,wecanmovetothelastcontroller--theimagecontroller.
AddingthereporttotheimagecontrollerFinally,wewillapplythesamelogichereaswedidfortheproductandreviewcontroller,butnowitistimetochangetheimagecontroller.
YourImageController.scalafile,afteraddingreportfunction,shouldlooksomethinglikethis:
@Singleton
classImageController@Inject()
(valmessagesApi:MessagesApi,
valproductService:IProductService,
valservice:IImageService)
extendsControllerwithI18nSupport{
//...restofthecontrollercode...
defreport()=Action{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
Ok.chunked(Enumerator.fromStream(
ReportBuilder.toPdf("Images.jrxml")))
.withHeaders(CONTENT_TYPE->"application/octet-stream")
.withHeaders(CONTENT_DISPOSITION->"attachment;
filename=report-images.pdf")
}
}
Alright,wehavefinishedallthecontrollers.However,wewillneedtoconfigureroutes,otherwise,wewon'tbeabletocallthecontrollers--thisisthenextstep.
Routes-addingnewreportroutesNow,wewillneedtoaddthenewroutesforthereports.Forthat,wewilledittheconf/routesfile,asfollows:
GET/reportscontrollers.HomeController.reports
#
#Reports
#
GET/product/reportcontrollers.ProductController.report
GET/review/reportcontrollers.ReviewController.report
GET/image/reportcontrollers.ImageController.report
Wearedonewithroutesnow,andweneedtochangetheUIinordertoexposethenewreportfunctionality.Wewillcreateanewviewcontainingallreports,and,forthesakeofease,wewillalsoaddabuttonforeachresourceUI(product,review,andimage).
NewcentralizedreportsUIWewillneedtocreateanewviewatReactiveWebStore/views/reports_index.scala.html.
Yourreports_index.scala.htmlfileshouldlooksomethinglikethis:
@()(implicitflash:Flash)
@main("Reports"){
<ahref="/product/report"><imgheight="42"width="42"
src="@routes.Assets.at("images/product.png")">Products
Report</a><BR>
<ahref="/review/report"><imgheight="42"width="42"
src="@routes.Assets.at("images/review.png")">ReviewsReport
</a><BR>
<ahref="/image/report"><imgheight="42"width="42"
src="@routes.Assets.at("images/image.png")">Images
Report</a><BR>
}
Sohere,wewillbasicallylistallresources--product,review,andimagesandlinktherelativecontrollers,andwhentheuserclicksontherespectivelinkaPDFreportwillbedownloaded.Nowweneedtoediteachresource(product,image,andreview)viewinordertoaddalinkforthereportsthereaswell.
AddingthereportbuttonforeachviewLet'sedittheproductviewfirst.
Yourproduct_index.scala.htmlfileshouldlooksomethinglikethis:
@(products:Seq[Product])(implicitflash:Flash)
@main("Products"){
//...restoftheuicode...
<p>
<ahref="@routes.ProductController.blank"class="btnbtn-
success">
<iclass="icon-plusicon-white"></i>AddProduct</a>
<ahref="@routes.ProductController.report"class="btnbtn-
success">
<iclass="icon-plusicon-white"></i>ProductsReport</a>
</p>
}
Asyoucanseehere,weaddedanewbuttonpointingtothenewreportfunction.WewillneedtodothesameforthereviewandtheimageUI.
Yourreview_index.scala.htmlfileshouldlooksomethinglikethis:
@(reviews:Seq[Review])(implicitflash:Flash)
@main("Reviews"){
//...restoftheuicode...
<p>
<ahref="@routes.ReviewController.blank"class="btnbtn-
success"><iclass="icon-plusicon-white"></i>AddReview</a>
<ahref="@routes.ReviewController.report"class="btnbtn-
success"><iclass="icon-plusicon-white"></i>ReviewReport</a>
</p>
}
Nowwecanaddthefinalbuttontotheimageview.
Yourimage_index.scala.htmlfileshouldlooksomethinglikethis:
@(images:Seq[Image])(implicitflash:Flash)
@main("Images"){
//...restoftheuitemplate...
<p>
<ahref="@routes.ImageController.blank"class=
"btnbtn-success"><iclass="icon-plusicon-white"></i>Add
Image</a>
<ahref="@routes.ImageController.report"class=
"btnbtn-success"><iclass="icon-plusicon-white"></i>
ImagesReport</a>
</p>
}
Allset!Nowwecanrun$activatorrunandseethenewUIandreportbuttons.Goto
http://localhost:9000/:
Ifyougotohttp://localhost:9000/reports,orclickonReports,youwillseethefollowing:
That'sit!WehaveallthereportsworkingonthePlayframeworkapplication.
SummaryInthischapter,youlearnedhowtocreatecustomreportsusingJaspersoftStudioandJasperReports.Additionally,youalsochangedyourapplicationinordertointegratethePlayframeworkandJasperReports.
Inthenextchapter,youwilllearnhowtousetheAkkaframework.Wewillcontinuebuildingourapplicationandembracetheactormodelforanewkillerfeatureforyourapplication.
Chapter8.DevelopingaChatwithAkkaInthepreviouschapters,wepersisteddataintoMySQLusingSlickandwrotePDFreportsusingJasperreports.NowwewilladdmorefeaturesinourappusingAkka.
Inthischapter,youwilllearnhowtocreateActorsusingtheAkkaframework.WewilluseActorsincombinationwiththePlayframeworkandWebSocketsinordertohaveachatcapability.
Wewillcoverthefollowingtopicsinthischapter:
UnderstandingtheActormodelActorsystems,Actorrouting,anddispatchersMailboxes,Actorconfiguration,andpersistenceCreatingourChatApplicationTestingourActors
AddingthenewUIintroductiontoAkkaAkka(http://akka.io/)isaframeworktobuildconcurrent,distributed,andresilientmessage-drivenapplicationsinScala,Java,and.NET.BuildingapplicationswithAkkahasseveraladvantages,whichareasfollows:
Highperformance:Akkadeliversupto50millionmessagespersecondonacommodityhardwarehaving~2.5millionActorsperGBofRAM.Resilientbydesign:Akkasystemshaveself-healingpropertiesforlocalandremoteActors.Distributedandelastic:Akkahasallthemechanismstoscaleyourapplication,suchascluster,loadbalancing,partitioning,andsharding.AkkaletsyougroworshrinkyourActorsondemand.
TheAkkaframeworkprovidesgoodabstractionsforconcurrent,asynchronous,anddistributedprogramming,suchasActors,Streams,andFutures.Thereareplentyofgreatsuccesscasesinproduction,suchasBBC,Amazon,eBay,Cisco,TheGuardian,Blizzard,Gilt,HP,HSBC,Netflix,andsomanyothers.
Akkaisatrulyreactiveframeworkbecauseeverything,inthesenseofsendingandreceivingmessagestoActors,islockless,non-blockingIO,andasynchronous.
IntroductiontotheActormodelThekeyforconcurrencyprogrammingistoavoidasharedmutablestate.Asharedstateoftenrequireslocksandsynchronization,whichmakesyourcodelessconcurrentandmorecomplex.Actorssharenothing;theyhaveinternalstate,buttheydon'tsharetheirinternalstate.
Actorshavelocationtransparency;theycanruninalocalorremotesystemandacluster.It'salsopossibletomixlocalandremoteactors-thisisgreatforscalabilityandfitsperfectlyintoacloudenvironment.Actorscanrunanywhere,fromyourlocalbox,thecloud,bare-metaldatacenter,andLinuxcontainers.
WhatisanActor?Actorscanbealternativestothreads,callbacklisteners,singletonservices,EnterpriseJavaBeans(EJB),routers,loadbalancerorpool,andafinite-statemachine(FSM).TheActormodelconceptisnotnewatall;itwascreatedbyCarlHewittin1973.TheActormodelisheavilyusedinthetelecomindustryinrock-solidtechnologiessuchasErlang.ErlangandtheActormodelhadimmensesuccesswithcompaniessuchasEricssonandFacebook.
Actorshaveasimplewayofworking:
Unitofcodeorganization:ProcessingStorageCommunication
TheymanagetheinternalstatesTheyhaveamailboxTheycommunicatewithotheractorsusingmessagesTheycanchangethebehavioratruntime
MessageexchangeandmailboxesActorstalkwitheachotherviamessaging.Therearetwopatterns:oneiscalledaskandtheotheriscalledfireandforget.Bothmethodsareasynchronousandnon-blockingIO.WhenanActorsendsamessagetoanotherActor,itdoesnotsendthemessagedirectlytotheotherActor;itactuallysendsittotheActor'smailbox.
MessagesareenqueuedintheActormailboxinatime-orderedfashion.TherearedifferentmailboxesimplementationsinAkka.ThedefaultisFirstInFirstOut(FIFO)based.Thisisagooddefault;however,youmightneedadifferentalgorithm,whichisfineasyoucanchangethemailboxifyouneedto.Moredetailscanbefoundintheofficialdocumentation(http://doc.akka.io/docs/akka/2.4.9/scala/mailboxes.html#mailboxes-scala).ActorsliveinanActorsystem.YoucanhavemultipleActorsystemsinacluster:
AkkaencapsulatestheactorstateinmailboxanddecouplesitfromtheActorbehavior.TheActorbehavioristhecodeyouwillhaveinsideyourActor.YouwillneedtoseeActorsandAkkaasaprotocol.So,basically,youwillneedtodefinehowmanyActorsyouwillhaveandwhateachActorwilldointhesenseofcode,responsibility,andbehavior.TheActorsystem
hasActorsandsupervisors.SupervisorsareoneoftheAkkamechanismstodeliverfaulttoleranceandresiliency.SupervisorstakecareoftheActorinstances,andtheycanrestart,kill,orcreatemoreActorsasneeded.
TheActormodelisgreatforconcurrencyandscalability;however,likeeverysinglethingincomputerscience,therearetradeoffsandcons.Forinstance,Actorsrequireanewmindsetandadifferentwayofthinking.
Thereisnosilverbullet.Onceyouhaveyourprotocol,itmightbehardtoreuseyourActorsoutsideyourprotocol.Ingeneral,Actorscanbehardertocompose,ascomparedtoobject-orientedclassesorfunctionsinFunctionalProgramming,forinstance.
CodingactorswithAkkaLet'stakealookatthefollowingActorcodeusingtheAkkaframeworkandScala:
importakka.actor._
caseobjectHelloMessage
classHelloWorldActorextendsActor{
defreceive={
caseHelloMessage=>sender()!"HelloWorld"
casea:Any=>sender()!"Idon'tknow:"+a+"-Sorry!"
}
}
objectSimpleActorMainAppextendsApp{
valsystem=ActorSystem("SimpleActorSystem")
valactor=system.actorOf(Props[HelloWorldActor])
importscala.concurrent.duration._
importakka.util.Timeout
importakka.pattern.ask
importscala.concurrent.Await
implicitvaltimeout=Timeout(20seconds)
valfuture=actor?HelloMessage
valresult=Await.result(future,
timeout.duration).asInstanceOf[String]
println("Actorsays:"+result)
valfuture2=actor?"Cobol"
valresult2=Await.result(future2,
timeout.duration).asInstanceOf[String]
println("Actorsays:"+result2)
system.terminate()
}
IfyourunthisAkkacodeonsbtinyourconsole,youwillseeanoutputsimilartothis:
$sbtrun
Let'stakeacloserlookatthisAkkacodewejustwrote,inwhichwedefinedaScalaclasscalledHelloWorldActor.InorderforthisclassbeanActor,wewillneedtoextendActor.Actorsarereactivebydefault,whichmeansthattheyarewaitingtoreceivemessagestoreacttothemessages.Youwillneedtocodeyourbehaviorinaneventloop.InAkka,thisisdonebycodingthereceivefunctionwithapatternmatcherinScala.
Thepatternmatcherwilldefinewhattheactorcando.Youwillneedtocodeallthepossiblekindsofmessagesyouwantthatactortohandle.AsImentionedearlier,youwillneedtohaveaprotocol;soyourprotocolhasamessagecalledHelloMessage.It'sacommonpracticeinAkkatouseScalaobjectsasmessages.However,youcanpassprettymuchalltypesasmessages.It'sevenpossibletosendcaseclasseswithparameters.
Alright,wehaveourprotocol,whichisourActors,andthemessagestheycanexchange.NowwewillneedtocreateanActorsystemandstartourapplication.Asyoucansee,wewillusetheActorSystemobjecttocreateanActorsystem.Actorsystemsneedtohaveaname,whichcanbeanystringyoulike,aslongasitcontainsanyletter[a-z,A-Z,0-9]andnon-leading'-'or'_'.
Aftercreatingthesystem,youcancreateActors.ThesystemhasafunctioncalledactorOf,whichcanbeusedtocreateActors.YouwillneedtouseaspecialobjectcalledPropsandpasstheactorclass.Whydoweneeditthisway?It'sbecauseAkkamanagestheActorstate.YoushouldnottrytomanagetheActorinstancebyyourself.Thisisdangerousbecauseyoucanbreakreferentialtransparencyandyourcodemightnotwork.
Forthiscode,weareusingtheaskpattern.WewillusethistosendmessagestotheActor,andwewanttoknowwhattheActorwillreturn.Akkadoeseverythinginanasyncandnon-blockingway,asmentionedpreviously.However,sometimesyouwanttogettheanswernowandthen,unfortunately,youwillneedtoblock.
Inordertogettheanswernow,wewillneedtodefineatimeoutandusetheAwaitobject.WhenyousendamessagetoanActorusing?(theaskpattern),AkkawillreturnaFutureforyou.Then,youcanpasstheFuturewithatimeouttoAwait,andiftheanswercomesbackbeforethetimeout,youwillhavetheresponsefromtheActor.
Again,weareblockingherebecausewewanttogettheanswernow,andweareoutsidetheActorsystem.KeepinmindthatwhenanActortalkswithanotherActorinsidetheActorsystem,itshouldnotblockever.SobecarefulwiththeusageofAwait.
Anotherimportantthinginthiscodeisthatthesender()methodinsideoftheActorreceivesafunction.ThismeansthatyouwanttogetthereferenceoftheActorwhosendsthemessagetoyou.Asweareperformingsender()!method,wearesendingananswerbacktothecaller.Thesender()functionisanAkkaabstractiontodealwithresponsemessagestootherActorsorfunctioncallers.
Wealsohaveanothercase,withAny,whichmeansallothermessageswillbehandledbythat
casecode.
TheaskpatternisonewaytosendmessagestoActors.ThereisanotherpatterncalledFireAndForget"!".Fireandforgetwillsendamessageandwillnotblockandwaitfortheanswer.So,thereisnoanswer-inotherwords,Unit.
Let'slookatsomecodewiththeFireAndForgetmessageexchange:
importakka.actor._
objectMessage
classPrinterActorextendsActor{
defreceive={
casea:Any=>
println("Print:"+a)
}
}
objectFireAndForgetActorMainAppextendsApp{
valsystem=ActorSystem("SimpleActorSystem")
valactor=system.actorOf(Props[PrinterActor])
valvoidReturn=actor!Message
println("Actorsays:"+voidReturn)
system.terminate()
}
Ifyourunthiscodewith$sbtrun,youwillseeanoutputasfollows:
Here,wehaveaPrinterActormethod,whichacceptsprettymuchanythingandprintsontheconsole.Then,wewillcreateanActorsystemandjustsendamessagetoourActorwiththefireandforgetpattern,a.k.a"!",andasyoucansee,wewillreceiveUnit;finally,wewillawaittheshutdownoftheActorsystemusingtheterminateoption.
ActorroutingAkkaprovidesroutingfunctionality.ThisisusefulfromabusinesspointofviewbecauseyoucanroutetotherightActorinthesenseofbusinesslogicandbehavior.Forarchitecture,wecanusethisasloadbalancingandroutemessagestomoreActorstoachievefaulttoleranceandscalability.
Akkahasseveraloptionsforrouting,whichareasfollows:
RoundRobin:ThisisarandomlogictoeverydifferentActoronthepool.SmallestMailbox:ThissendsthemessagetotheActorwithfewermessages.ConsistentHashing:ThispartitionstheActorsperhashID.ScatterGather:Thissendsmessagetoallactors,andthefirsttoreplywins.TailChopping:Thissendstoarouterandomly,andifareplydoesn'tcomebackinasecond,itchoosesanewrouteandsendsagain,andsoon.
Let'sseethefollowingcodeinpractice:
importakka.actor._
importakka.routing.RoundRobinPool
classActorUpperCasePrinterextendsActor{
defreceive={
cases:Any=>
println("Msg:"+s.toString().toUpperCase()+"-"+
self.path)
}
}
objectRoutingActorAppextendsApp{
valsystem=ActorSystem("SimpleActorSystem")
valactor:ActorRef=system.actorOf(
RoundRobinPool(5).props(Props[ActorUpperCasePrinter]),name=
"actor")
try{
actor!"works1"
actor!"works2"
actor!"works3"
actor!"works4"
actor!"works5"
actor!"works6"
}catch{
casee:RuntimeException=>println(e.getMessage())
}
system.terminate()
}
Ifyourunthiscodeinsbtdoing$sbtrun,youwillgetanoutputasfollows:
So,herewehaveanActorUppercasePrinterfunctionthatprintswhateveritreceivesandcallsthetoStringfunction,andthentoUpperCase.Finally,italsoprintstheself.pathActor,whichwillbetheaddressoftheActor.Actorsarestructuredinahierarchicalstructure,similartoafilesystem.
TherearemultiplewaystouseAkka-Akkasupportscodeorconfiguration(application.conffile).Here,wearecreatingaround-robinpoolactorthathasfiveroutes.WearepassingthetargetActortotherouterthatwillbeourprinterActor.
Asyoucansee,whenwesendmessagesusingthefireandforgetpattern,everymessageisdeliveredtoadifferentActor.
PersistenceAkkaworksonmemory.However,itispossibletousepersistence.PersistenceisstillkindofexperimentalinAkka.However,itisstable.Forproduction,youcanuseadvancedpersistenceplugins,suchasApacheCassandra.Forthesakeofdevelopmentandeducation,wewilluseGoogleleveldbinourfilesystem.Akkahasmultiplepersistenceoptions,suchasviewsandpersistentActors.
Let'stakealookatapersistentactorusingtheGoogleleveldbandfilesystem:
importakka.actor._
importakka.persistence._
importscala.concurrent.duration._
classPersistenceActorextendsPersistentActor{
overridedefpersistenceId="sample-id-1"
varstate:String="myState"
varcount=0
defreceiveCommand:Receive={
casepayload:String=>
println(s"PersistenceActorreceived${payload}(nr=
${count})")
persist(payload+count){evt=>
count+=1
}
}
defreceiveRecover:Receive={
case_:String=>
println("recover...")
count+=1
}
}
objectPersistentViewsAppextendsApp{
valsystem=ActorSystem("SimpleActorSystem")
valpersistentActor=
system.actorOf(Props(classOf[PersistenceActor]))
importsystem.dispatcher
system.scheduler.schedule(Duration.Zero,2.seconds,
persistentActor,"scheduled")
}
Executingthe$sbtruncommandwillgiveyouthefollowingoutput:
Ifyourunthiscodewith$sbtrun,andstopandrunagain,youwillseethedataisbeingstoredandrecoveredeverytimeyoustopandstartagain.
Asyoucansee,yourActorneedstoextendPersistentActorinordertohavepersistencesupport.YouwillalsoneedtoprovideapersistenceID.
Here,youwillneedtoimplementtworeceivefunctions.Oneisforcommands(alsoknownasmessages),andtheotheroneisforrecovery.Thecommand'sreceiveloopwillbeactivatedwhenthisActorreceivesmessages,whiletherecoveronewillbeactivatedwhentheActorbootsupandwillreadthepersistentdatafromthedatabase.
So,thisActorherehasacountertocounteachmessageitreceives,andprintseverymessageitgetsontheconsole.That'sit;asyoucansee,itisprettysimple.Inordertousethisfunctionality,youwillalsoneedtoconfigureyourapplication.conf.
Yourapplication.conffileshouldlooksomethinglikethis:
akka{
system="SimpleActorSystem"
remote{
log-remote-lifecycle-events=off
netty.tcp{
hostname="127.0.0.1"
port=0
}
}
}
akka.cluster.metrics.enabled=off
akka.persistence.journal.plugin=
"akka.persistence.journal.leveldb"
akka.persistence.snapshot-store.plugin=
"akka.persistence.snapshot-store.local"
akka.persistence.journal.leveldb.dir="target/persistence/journal"
akka.persistence.snapshot-store.local.dir=
"target/persistence/snapshots"
#DONOTUSETHISINPRODUCTION!!!
#Seealsohttps://github.com/typesafehub/activator/issues/287
akka.persistence.journal.leveldb.native=false
So,herewearedefiningasimpleAkkasystem(localmode),andweareconfiguringthepersistenceforGoogleleveldb.Asyoucansee,wewillneedtoprovideapathforpersistence,andthispathmustexistontheOS.
Asweareusinganadditionalfunctionality,wewillalsoneedtochangebuild.sbtinordertoimportalljarsthatwewillneedinthesenseofAkka,persistence,andleveldb.
Yourbuild.sbtfileshouldlooksomethinglikethis:
//restofthebuild.sbtfile...
valakkaVersion="2.4.9"
libraryDependencies+="com.typesafe.akka"%%"akka-actor"%
akkaVersion
libraryDependencies+="com.typesafe.akka"%%"akka-kernel"%
akkaVersion
libraryDependencies+="com.typesafe.akka"%%"akka-remote"%
akkaVersion
libraryDependencies+="com.typesafe.akka"%%"akka-cluster"%
akkaVersion
libraryDependencies+="com.typesafe.akka"%%"akka-contrib"%
akkaVersion
libraryDependencies+="com.typesafe.akka"%%"akka-persistence"%
akkaVersion
libraryDependencies+="org.iq80.leveldb"%"leveldb"%"0.7"
libraryDependencies+="org.iq80.leveldb"%"leveldb-api"%"0.7"
libraryDependencies+="org.fusesource.leveldbjni"%"leveldbjni"%
"1.8"
libraryDependencies+="org.fusesource.leveldbjni"%"leveldbjni-
linux64"%"1.8"
libraryDependencies+="org.fusesource"%"sigar"%"1.6.4"
libraryDependencies+="org.scalatest"%"scalatest_2.11"%"2.2.6"
That'sit.That'sallweneedtopersisttheActor'sstate.
Note
Akkahaswaymorefunctionalities.Formore,checkoutthedefaultdocumentationathttp://doc.akka.io/docs/akka/2.4/scala.html?_ga=1.12480951.247092618.1472108365.
CreatingourchatapplicationNowthatweknowAkkabetter,wewillcontinuetodevelopourapplication.AkkahasagreatintegrationwiththePlayframework.WewilluseActorswiththeAkkaandPlayframeworkrightnow.Let'sbuildasimplechatfeatureforourapp.WewillchangethecodetoaddanewUIand,usingtheAkkatestkit,wewilltestoutactors.
ThePlayframeworkalreadyincludesAkkaontheclasspathforus,sowedon'tneedtoworryaboutit.However,wewillneedtoaddtheAkkatestkitdependencytothebuild.sbtfileinordertohavetheclassesinourclasspath.
Yourbuild.sbtshouldlooksomethinglikethis:
//restofthebuild.stb...
libraryDependencies++=Seq(
"com.typesafe.akka"%%"akka-testkit"%"2.4.4"%Test,
//restofthedeps...
)
//restofthebuild.stb...
Okay,nowyoucangototheconsoleandtype$activator,$reload,andthen$compile.Thiswillforcesbttodownloadthenewdependency.
NowwewillneedtocreateapackagecalledActors.ThispackageneedstobelocatedatReactiveWebStore/app/.WewillstartcreatinganActorHelperutilityobjectinordertohaveagenericfunctionfortheaskpatternthatwesawearlier.ItisanActorhelpergenericaskpatternutility.
YourActorHelper.scalafileshouldlooksomethinglikethis:
packageactors
objectActorHelper{
importplay.api.libs.concurrent.
Execution.Implicits.defaultContext
importscala.concurrent.duration._
importakka.pattern.ask
importakka.actor.ActorRef
importakka.util.Timeout
importscala.concurrent.Future
importscala.concurrent.Await
defget(msg:Any,actor:ActorRef):String={
implicitvaltimeout=Timeout(5seconds)
valresult=(actor?msg).mapTo[String].map{result=>
result.toString}
Await.result(result,5.seconds)
}
}
TheActorHelperhasjustonefunction:get.ThisfunctionwillgetananswerfromanyActorgiveninanymessage.However,asyoucansee,wehaveatimeoutoffiveseconds.Ifthe
resultdoesnotcomebackinthistime,anexceptionwillberaised.
Inthiscode,wearealsomappingtheActorresulttoaStringcallingthetoStringfunctionintheresultfuture.Thisisnotalotofcode;however,therearelotsofimports,anditmakesthecodecleanerandwecangetanswersfromActorswithlesscodeandfewerimports.
ThechatprotocolNowwewillneedtodefineourprotocol.Forthisfunctionality,wewillneedthreeActors.TheActorsthatwecreatewillbeasfollows:
ChatRoom:ThiswillhaveareferenceforallusersinthechatroomChatUser:Thiswillhaveoneinstanceperuser(activebrowser)ChatBotAdmin:ThissimpleBotAdminwillprovidestatsaboutthechatroom
ChatUserActorwillneedtojoinJoinChatRoomobjectinordertostartchatting.ChatUserActorwillalsoneedtosendmessagestoChatMessageclasstotheChatRoomActorthatwillbroadcastmessagestoallusers.TheChatBotAdminwillgetareportfromGetStatsobjectfromChatRoomActor.
Let'sstartcodingthisprotocol.First,wewillneedtodefinethemessagesthatwillbeexchangedbetweentheseActors,asshowninthefollowingpieceofcode:
packageactors
caseclassChatMessage(name:String,text:String)
caseclassStats(users:Set[String])
objectJoinChatRoom
objectTick
objectGetStats
Asyoucanseehere,wehaveaChatMessageclasswithanameandatext.Thiswillbethemessageeachuserwillsendonthechat.Then,wewillhaveastatsclass,whichhasasetofusers--thiswillbealltheusersloggedintothechatapplication.
Finally,wehavesomeactionmessages,suchasJoinChatRoom,Tick,andGetStats.So,JoinChatRoomwillbesentbyChatUserActortoChatRoomActorinordertojointhechat.TickwillbeascheduledmessagethatwillhappenfromtimetotimeinordertomakeChatBotAdminsendstatsaboutthechatroomtoallloggedusers.GetStatsisthemessagethatChatBotAdminActorwillsendtoChatRoomActorinordertogetinformationaboutwhoisintheroom.
Let'scodeourthreeactorsnow.
TheChatRoomActor.scalafileshouldlooksomethinglikethis:
packageactors
importakka.actor.Props
importakka.actor.Terminated
importakka.actor.ActorLogging
importakka.event.LoggingReceive
importakka.actor.Actor
importakka.actor.ActorRef
importplay.libs.Akka
importakka.actor.ActorSystem
classChatRoomActorextendsActorwithActorLogging{
varusers=Set[ActorRef]()
defreceive=LoggingReceive{
casemsg:ChatMessage=>
usersforeach{_!msg}
caseJoinChatRoom=>
users+=sender
contextwatchsender
caseGetStats=>
valstats:String="onlineusers["+users.size+"]-users["
+users.map(a=>a.hashCode().mkString("|")+"]"
sender!stats
caseTerminated(user)=>
users-=user
}
}
objectChatRoomActor{
varroom:ActorRef=null
defapply(system:ActorSystem)={
this.synchronized{
if(room==null)room=system.actorOf(Props[ChatRoomActor])
room
}
}
}
ChatRoomActorhasavarcalledusers,whichisasetofActorRef.ActorRefisagenericreferencetoanyactor.Wehavethereceivefunctionwiththreecases:ChatMessage,JoinChatRoom,andGetStats.
AJoinChatRoomwillbesentbytheChatUserActormethodinordertojointheroom.Asyoucansee,wearegettingtheActorRefmethodfromthesenderActorusingthesender()function,andweareaddingthisreferencetothesetofusers.Inthisway,thesetofActorRefrepresentstheonlinelogged-inusersinthechatroomrightnow.
TheothercaseiswiththeChatMessagemethod.Basically,wewillbroadcastthemessagetoallusersinthechat.Wedothisbecausewehavethereferenceforallactorsinusers.Then,wewillcalltheforeachfunctioninordertoiterateallusersonebyone,andthenwewillsendthemessageusingFireAndForget"!"toeachuserActorrepresentedbytheoperatorunderscore_.
TheGetStatscasecreatesastringwithallchatroomstats.Fornow,thestatsarejustthenumberofonlineusers,whichiscomputedbycallingthesizefunctionontheusersobject.WearealsoshowingallthehashcodesthatidentifyallActorsloggedin,justforfun.
That'sourChatRoomActorimplementation.Asyoucansee,itishardtotalkaboutoneActorwithoutdescribingtheother,astheprotocolwillalwaysbekindofcoupled.YoumightalsobewonderingwhywehaveacompanionobjectfortheChatRoomActormethod.
ThisobjectistoprovideaneasywaytocreateActorinstances.Wearecreatingasingleroomforourdesign;wedon'twanttohavemultiplechatrooms,sothat'swhywewillneedto
controlthecreationoftheroomActor.
Iftheroomisnull,wewillcreateanewroom;otherwise,wewillreturnthecachedinstanceoftheroomthatwealreadygotinthememory.WewillneedaninstanceoftheActorsysteminordertocreateactors,sothat'swhywearereceivingthesystemontheapplyfunction.TheapplyfunctionwillbecalledwhensomeonewritesacodelikeChatRoomActor(mySystem).
Now,let'smovetotheChatUserActorimplementation.
TheChatUserActor.scalafileshouldlooksomethinglikethis:
packageactors
importakka.actor.ActorRef
importakka.actor.Actor
importakka.actor.ActorLogging
importakka.event.LoggingReceive
importakka.actor.ActorSystem
importakka.actor.Props
classChatUserActor(room:ActorRef,out:ActorRef)extendsActorwith
ActorLogging{
overridedefpreStart()={
room!JoinChatRoom
}
defreceive=LoggingReceive{
caseChatMessage(name,text)ifsender==room=>
valresult:String=name+":"+text
out!result
case(text:String)=>
room!ChatMessage(text.split(":")(0),text.split(":")(1))
caseother=>
log.error("issue-notexpected:"+other)
}
}
objectChatUserActor{
defprops(system:ActorSystem)(out:ActorRef)=Props(new
ChatUserActor(ChatRoomActor(system),out))
}
ThisActorisalittlebiteasierthanthepreviousone.ChatUserActorreceives,asaparameter,theroomactorreferenceandalsoanoutactor.Theroomwillbeaninstanceoftheroomthattheuserwillusetocommunicatewithotherusers.TheActorRefmethodcalledoutisthePlayframeworkActorresponsibleforsendingtheanswerbacktothecontrollersandUI.
Weprettymuchjusthavetwocases:onewherewereceiveaChatMessageandtheotheristheChatUserActorsmethodinthechatroom.So,wewilljustneedtosendbacktotheUIusingtheoutActor.That'swhythereisafireandforgetmessagefortheoutActorwitharesult.UsinganewActormodelcanbedangerous,pleasereadmoreathttp://doc.akka.io/docs/akka/current/scala/actors.html.
ThereisanothercasethatjustreceivesastringthatwillbethemessagefromthatActoritself.RememberthateachActorrepresentsauserandabrowserfullofduplexconnectionsvia
WebSockets.Don'tworryaboutWebSocketsnow;wewillcoveritinmoredetaillaterinthischapter.
Forthiscasefunction,wearesendingtheChatMessagemethodtotheroom.Wewillsplitthemessagesintwoparts:theusernameandthetext,whichissplitby:.
Here,wealsohaveacompanionobjectforthesakeofgoodpractice.So,youcancallChatUserActor,passingtheActorsystemandacurriedparameterfortheoutactor.
Now,wewillmovetothelastActor:theBotAdminActor,whichshouldlooksomethinglikethis:
packageactors
importakka.actor.ActorRef
importakka.actor.Actor
importakka.actor.ActorLogging
importakka.event.LoggingReceive
importakka.actor.ActorSystem
importakka.actor.Props
importscala.concurrent.duration._
classChatBotAdminActor(system:ActorSystem)extendsActorwith
ActorLogging{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
valroom:ActorRef=ChatRoomActor(system)
valcancellable=system.scheduler.schedule(0seconds,
10seconds,self,Tick)
overridedefpreStart()={
room!JoinChatRoom
}
defreceive=LoggingReceive{
caseChatMessage(name,text)=>Unit
case(text:String)=>room!ChatMessage(text.split(":")(0),
text.split(":")(1))
caseTick=>
valresponse:String="AdminBot:"+ActorHelper.get
(GetStats,room)
sender()!response
caseother=>
log.error("issue-notexpected:"+other)
}
}
objectChatBotAdminActor{
varbot:ActorRef=null
defapply(system:ActorSystem)={
this.synchronized{
if(bot==null)bot=system.actorOf(Props
(newChatBotAdminActor(system)))
bot
}
}
}
Asyoucansee,thisActorreceivesthereferenceofthechatroomasaparameter.Usingthe
Actorsystem,itgetsthereferenceofthechatroomActor.ThisActorreceivesanActorSystemmessagebynow.
UsingtheActorsystemvariablecalledsystem,wewillalsoscheduleaTickforthisActorforeverytenseconds.Thistime,thewindowintervalwillbethetimeinwhichthebotwillnotifythechatroomaboutthecurrentstatus.
WewillalsooverridethepreStartfunction.AkkawillcallthisfunctionwhentheActoriscreatedontheactorsystem.Thisimplementationwillsendamessagetotheroom,whichisJoinChatRoom.
LikeallActors,thereisthereceivefunctionimplementation.FirstcasewithChatMessageisreturningUnit.Ifyouwanttomakethisbotrespondtopeople,removeUnitandwritetheproperScalacodeasyouwish.
Inthesecondcase,wewillhavetheStringmessagethatwillbesenttothechatroom.Finally,afterthiscase,wewillhavetheTickmethod,whichwillappeareverytenseconds.So,wewillusetheActorHelpertogetthestatsfromtheroom,andthenwewillsendastringmessagewiththeinformationabouttheroom.Thiswilltriggerthesecondcaseandbroadcastthemessagetothewholeroom.
Finally,wehaveacompanionobject.Wedon'twanttohavetwoinstancesofthebot,whichiswhywewillcontrolthisobjectcreationbydesign.We'redonewiththeactorsimplementations.Next,wewillneedtoworkanewcontrollerforthechatactors.
ThechatcontrollerWewillneedtocreateanewcontroller.ThiscontrollerwillbelocatedatReactiveWebStore/app/controllers.
ImplementingthechatcontrollerChatController.scalashouldlooksomethinglikethis:
packagecontrollers
importakka.actor.ActorSystem
importakka.stream.Materializer
importjavax.inject._
importplay.api._
importplay.api.mvc._
importplay.api.libs.streams._
importactors.ChatUserActor
importactors.ChatBotAdminActor
@Singleton
classChatController@Inject()(implicitvalsystem:ActorSystem,
materializer:Materializer)
extendsController{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
ChatBotAdminActor(system)
defindex_socket=Action{request=>
Ok(views.html.chat_index()(Flash(Map())))
}
defws=WebSocket.accept[String,String]{request=>
ActorFlow.actorRef(out=>ChatUserActor.props(system)(out))
}
}
TheChatControllermethodwilluseGoogleGuicetogetinjectedinstancesofActorSystemandanActormaterializerinstance.AmaterializerisneededbecauseitwillprovidetheinstanceoftheoutActorforeachuserinthesystem.
Asyoucansee,wewillcreateaninstanceoftheChatBotAdminmethodpassingthroughtheactorsystem,whichGoogleGuiceinjectedforus.Forthiscontroller,wewilljusthavetwofunctions:onefunctiontorenderthechatUI,andtheotheronetoservetheWebSocket.
ThePlayframeworkalreadyprovidesbuilt-inintegrationwithAkkaandWebSockets.So,wewilljustneedtousetheActorFlowmethodusingtheactorReffunctioninordertoobtainanoutActor.
Here,wewillcalltheChatUserActorcompanionobjectandcreateachatuserforthewebsocketpassingouttheActorsystemthecontrollerhas.Asyoucansee,thisreturnsWebSocket.accept,whichisafullduplexconnectionbetweenthewebbrowserandthebackend.
ConfiguringtheroutesNext,wewillneedtoexposeourcontrollerfunctionstotheUI.WewillneedtoaddmoreroutestotheReactiveWebStore/conf/routesfile:
routes
#
#AkkaandWebsockets
#
GET/chat/index_socketcontrollers.ChatController.index_socket
GET/chat/wscontrollers.ChatController.ws
Theroutesaredonenow.
WorkingontheUINow,itistimetocodeontheUIonboththeHTMLlayoutandtheWebSocketcodeinJavaScript.Wewillneedtocreateanewfile,locatedatReactiveWebStore/app/views.
Yourchat_index.scala.htmlfileshouldlooksomethinglikethis:
@()(implicitflash:Flash)
@main("Chat"){
<!DOCTYPEhtml>
<metacharset="utf-8"/>
<title>ChatRoom</title>
<scripttype="text/javascript">
varoutput;
varwebsocket=newWebSocket("ws://localhost:9000/chat/ws");
functioninit(){
output=document.getElementById("output");
websocket.onmessage=function(evt){
writeToScreen('<spanstyle="color:blue;">'+evt.data+
'</span>');
};
websocket.onerror=function(evt){
writeToScreen('<spanstyle="color:red;">ERROR:</span>'+
evt.data);
};
}
functiondoSend(message){
websocket.send(message);
}
functionwriteToScreen(message){
varpre=document.createElement("p");
pre.style.wordWrap="break-word";
pre.innerHTML=message;
$('#output').prepend(pre);
}
window.addEventListener("load",init,false);
</script>
<h3>Messages</h3>
<divid="output"style="width:800px;height:250px;overflow-y:
scroll;">
</div>
<divid="contentMessage">
<BR>
user:
<inputtype="text"name="txtUser"id="txtUser"/><BR><BR>
message:<inputtype="text"name="txtMessage"
id="txtMessage"/>
<BR>
<BR>
<ahref="#"class="btnbtn-success"
onclick="doSend(document.getElementById('txtUser').value+':'
+document.getElementById('txtMessage').value);">
<iclass="icon-plusicon-white"></i>SendMessage</a>
</div>
}
TheUIisverysimple.Thereisaninputtextforyoutoputyournameandthereisanotheroneforthetextmessageitself,andasendbutton.AsyoucanseeintheJavaScriptcode,thefirstthingthatwewilldoisopenaWebSocketconnectiontothews://localhost:9000/chat/wsURL.Then,wewillregistertheinitfunctiontorunoncethebrowserisready.
TheinitfunctioninJavaScriptwillcreatetwofunctionsforourWebSocket.OnefunctionwillrunwhenanyerroroccursandtheotherfunctionwillrunforeachmessageemittedbytheAkkabackend.
WewillhaveadoSendfunctioninJavaScriptinordertosendamessagetotheWebSocket.ThismessagewillbedeliveredtothecontrollerandthentotheAkkaactors.YoucanalsoseesomeJQueryandHTMLcodeinordertocreatenewelementsontheUI.Thisisdoneinordertodisplaythemessageinthechatroom.
OK,thereisonemorethingthatwewillneedtodo-addareferencetothechatUIonthemainpageofourapplication.
YourIndex.scala.htmlshouldlooksomethinglikethis:
@(message:String)(implicitflash:Flash)
@main("WelcometoReactiveWebStore"){
<divclass="row-fluid">
<BR>
<divclass="span12">
<divclass="row-fluid">
<divclass="span6">
<ahref="/product"><imgheight="42"width="42"
src="@routes.Assets.at("images/product.png")">Manage
Products</a><BR>
<ahref="/review"><imgheight="42"width="42"
src="@routes.Assets.at("images/review.png")">Manage
Reviews</a><BR>
<ahref="/image"><imgheight="42"width="42"
src="@routes.Assets.at("images/image.png")">Manage
Images</a><BR>
</div>
<divclass="span6">
<ahref="/reports"><imgheight="42"width="42"
src="@routes.Assets.at("images/reports.png")">Reports</a>
<BR>
<ahref="/chat/index_socket"><imgheight="42"width="42"
src="@routes.Assets.at("images/chat.png")">ChatRoom</a>
<BR>
</div>
</div>
</div>
</div>
}
WewillalsousetheopportunitytoimprovealittlebitoftheUIdesignusingTwitterBootstrapcolumndesign.Inthelastrow,youcanseeourlinktothechatUI.Now,wecanruntheapplicationandseeourchatworking.Run$activatorrun:
Asyoucansee,ournewchatUIlinkisthere.Now,let'shavefunwiththisnewfeature.Openfournewbrowsers(simulatefourdifferentusers),thengotothehttp://localhost:9000/chat/index_socketurlandlet'shavealittlechat.Youshouldseesomethingsimilartothis:
Almostdone.Ourchatfeatureworks;however,wewillneedtodomorethanjustafunctionalblackboxtestontheUI.Wewillneedunittests.Luckilyforus,wehavetheAkkatestkit,whichallowsustoeasilytestactors.
AddingAkkatestsWewillcreatethreemoretests:oneforeachactorthatwehave.TheyarelocatedatReactiveWebStore/test/.
ScalatestforAkkaActorChatUserActorSpec.scalashouldlooksomethinglikethis:
classOutActorextendsActor{
defreceive={
casea:Any=>Unit
}
}
classChatUserActorSpecextendsPlaySpec{
classActorsextendsTestKit(ActorSystem("test"))
"ChatUserActor"should{
"joinsthechatroomandsendamessage"innewActors{
valprobe1=newTestProbe(system)
valactorOutRef=TestActorRef[OutActor](Props[OutActor])
valactorRef=TestActorRef[ChatUserActor]
(ChatUserActor.props(system)(actorOutRef))
valuserActor=actorRef.underlyingActor
assert(userActor.context!=null)
valmsg="testUser:testmsg"
probe1.send(actorRef,msg)
actorRef.receive(msg)
receiveOne(2000millis)
}
}
}
TheAkkatestkitisverycoolasitallowsustotestactorswithaveryeasyDomainSpecificLanguage(DSL).It'spossibletochecktheActormailbox,theActorinternalstate,andsomuchmore.Thereisonetrickthatwewillneedtodobecauseweneedtoextendoneclass;inordertohavePlayworkingwiththeScalatest,wewillneedtousePlaySpec.However,wewillalsoneedtoextendoneclasstomaketheAkkatestkitwork,whichisTestKit.Wecan'textendbothatthesametime,butnoworries,thereisalwaysaworkaround.
Theworkaroundhereistocreateacaseclass,makethatcaseclassextendTestKit,andthenuseitinaspeccontext,thatis,inanewActor{}.
Here,wearecheckingifChatUserActorcanjointhechatroomproperly.ThisisdonebysimplycreatingtheActor,astheActorhasapreStartmethodthatwillauto-jointheroom.
WewillneedtocreateafakeimplementationoftheoutActorhere,whichiswhywehavetheOutActorimplementation.Wewillcreateaprobetotesttheactorsystem,andwewillalsouseaspecialfunctiontotesttheActors,calledTestActorRef.ThisabstractionprovidesawaytoaccesstheActor'sstateviaactorRef.underlyingActor,andthisisusefulbecauseyoucanchecktheActorinternalstatetovalidatethecode.TherestofthecodeisnormalAkkaandScalatestcode.Let'smovetothenexttest.
ChatroomActortestTheChatRoonActorSpec.scalafileshouldlooksomethinglikethis:
classChatRoomActorSpecextendsPlaySpec{
classActorsextendsTestKit(ActorSystem("test"))
"ChatRoomActor"should{
"acceptjoinsthechatrooms"innewActors{
valprobe1=newTestProbe(system)
valprobe2=newTestProbe(system)
valactorRef=TestActorRef[ChatRoomActor]
(Props[ChatRoomActor])
valroomActor=actorRef.underlyingActor
assert(roomActor.users.size==0)
probe1.send(actorRef,JoinChatRoom)
probe2.send(actorRef,JoinChatRoom)
awaitCond(roomActor.users.size==2,100millis)
assert(roomActor.users.contains(probe1.ref))
assert(roomActor.users.contains(probe2.ref))
}
"getstatsfromthechatroom"innewActors{
valprobe1=newTestProbe(system)
valactorRef=TestActorRef[ChatRoomActor]
(Props[ChatRoomActor])
valroomActor=actorRef.underlyingActor
assert(roomActor.users.size==0)
probe1.send(actorRef,JoinChatRoom)
awaitCond(roomActor.users.size==1,100millis)
assert(roomActor.users.contains(probe1.ref))
probe1.send(actorRef,GetStats)
receiveOne(2000millis)
}
"andbroadcastmessages"innewActors{
valprobe1=newTestProbe(system)
valprobe2=newTestProbe(system)
valactorRef=TestActorRef[ChatRoomActor]
(Props[ChatRoomActor])
valroomActor=actorRef.underlyingActor
probe1.send(actorRef,JoinChatRoom)
probe2.send(actorRef,JoinChatRoom)
awaitCond(roomActor.users.size==2,100millis)
valmsg=ChatMessage("sender","testmessage")
actorRef.receive(msg)
probe1.expectMsg(msg)
probe2.expectMsg(msg)
}
"andtrackusersrefandcounts"innewActors{
valprobe1=newTestProbe(system)
valprobe2=newTestProbe(system)
valactorRef=TestActorRef[ChatRoomActor]
(Props[ChatRoomActor])
valroomActor=actorRef.underlyingActor
probe1.send(actorRef,JoinChatRoom)
probe2.send(actorRef,JoinChatRoom)
awaitCond(roomActor.users.size==2,100millis)
probe2.ref!PoisonPill
awaitCond(roomActor.users.size==1,100millis)
}
}
}
So,herewehavethesameconceptsastheothertest.However,wehavemoreusageoftheAkkatestkitDSL.Forinstance,weareusingexpectMsgontheprobetocheckifanActorreceivedaspecificmessage.WearealsousingawaitCondtochecktheActor'sinternalstateinanassertion.
NowisthetimetotestthelastActormethod.
ChatBotAdminActortestChatBotAdminActorSpec.scalafileshouldlooksomethinglikethis:
classChatBotAdminActorSpecextendsTestKit(ActorSystem("test"))
withImplicitSender
withWordSpecLikewithMatcherswithBeforeAndAfterAll{
"ChatBotAdminActor"should{
"beabletocreateBotAdminintheChatRoomandTick"in{
valprobe1=newTestProbe(system)
valactorRef=TestActorRef[ChatBotAdminActor](Props(new
ChatBotAdminActor(system)))
valbotActor=actorRef.underlyingActor
assert(botActor.context!=null)
awaitCond(botActor.room!=null)
}
}
}
Forthistest,wewillcheckiftheactorcontextwasnotnull,andalsoiftheroomwascreatedandtheschedulerwasalsonotnull.Allgoodtogo.
Alright,that'sit!Thisisthelastactortest.Nowwearecompletelydone.Youcanrunthistestwith$activatortest,or,ifyouprefertheactivator,thenuse"test-onlyTESTCLASSNAME"-Dsbt.task.forcegc=falsetorunaspecifictestcase.
SummaryInthischapter,youlearnedhowtoworkwithAkkaactorsandcreatedawebchatusingAkka,thePlayframework,andWebSockets.AkkaisareallypowerfulsolutionthatcanbeusedwithorwithoutthePlayframework.Additionally,youlearnedabouttheActormodel,mailboxes,routing,persistence,Akkaconfiguration,messagepatterns,andhowtowritecodewithactorsinScalaandPlay.
Inthenextchapter,youwilllearnmoreaboutREST,JSON,andhowtomodelaRESTAPI,aswellashowtocreateaScalaclientforyourRESTservices.
Chapter9.DesignYourRESTAPIInthepreviouschapter,weaddedanewchatfeatureinourappusingAkka.Ourwebapplicationisclosetotheend.ThischapterwilladdtheRESTAPIinourPlayframeworkapplication.
WewillalsocreateaScalaclientusingthewslibraryfromthePlayframeworkinordertocallourRESTAPI.Laterinthischapter,wewilladdSwaggersupportandembedtheSwaggerUIinourapp.
Inthischapter,wewillcoverthefollowingtopics:
RESTandAPIdesignCreatingourAPIwithRESTandJSONCreatingaScalaclientAddingvalidationsAddingbackpressureAddingSwaggersupport
IntroductiontoRESTRepresentationalStateTransfer(REST)isanarchitecturalstyle.ItwasdefinedbyRoyFieldinginhisdoctoraldissertation.RESThappensovertheHTTP1.1protocolusingHTTPverbs,suchasGET,POST,DELETE,PUT,andUniformResourceIdentifier(URI),forinstance,/users/profile/1orsales/cart/add/2.
TheRESTarchitecturehasthefollowingproperties:
Simplicity:PrettymuchalllanguageshavelibrariestomanipulateHTTPURIs.Interoperability:RESTislanguage,platform,andOSagnostic.ScalableandReliable:AsRESTisbasedonHTTP,youcanusetheHTTPservertoscaleupyourapplicationinconjunctionwithHTTPloadbalancer,theHTTPcaches,andHTTPDNS.SeparationofConcerns(SOC):AsyouhaveaURI,that'syourcontract,notthecode,underlyingbackend,ordatabase.Thismeansthatyoucanchangethedatabaseorlanguagewithoutaffectingthecode.Client/Server:ThereisaserverthatprovidestheRESTinterfaceandtheclients,whichcalltheRESTendpoints.
WebservicesthatembracetheRESTprinciplesareoftencalledRESTful.
RESTAPIdesignWhenyouareworkingwithREST,therearesomeprinciplesthatyoushouldkeepinmind,andtheseprinciplesshouldprovideguidanceforyourdesignchoiceswhenyouaredoingAPIdesign.
HTTPverbsdesignThesearethefollowingverbsfoundinHTTP:
GET:ThisisoftenusedtoanswerqueriesPUT:ThisisoftenusedtoinsertdataPOST:ThisisoftenusedtoupdatedataDELETE:Thisisoftenusedtoremovedata
Whydowekeepsayingoften?Well,therearesomeexceptionsinregardsofsizelimitations.Forinstance,fortheGETverb,wecan'thavearequestbiggerthan8192bytesor8KB.Ifyouneedtosendabiggerpayload,wewillneedtousethePOSTverb.
UniformAPIRESTusesauniformAPI.Forexample,considerthefollowingpieceofcode:
GET/users/1=Listinformationaboutuserid1
PUT/users/1=Insertuser1
POST/users/1=Updateuser1
DELETE/users/1=Deleteuser1
GET/users/=ListsAllusers
Ifwechangetheresourcefromuserstosales,theAPIwouldalmostbethesame.RetrievingdataisdoneusingGETandupdateisdoneviaPOST,soit'sauniformAPI.
ResponsewithHTTPstatuscodesRESTrunstheerrorhandlerusingtheHTTP1.1statuscodes.Forinstance:
200->OK:ThisisoftenusedwiththeGETverb201->Created:ThisisoftenusedbythePUT/POSTverbs204->NoContent:ThisisoftenfortheDELETEverb400->InvalidRequest:ThisoftenmeansaninvalidrequestforthePOST/PUTverbs404->NotFound:ThisisoftenusedwiththeGETverb500->InternalServerError-UnexpectedServerError:Thisisoftenusedbyalltheverbs
RESTAPIpatternsTherearesomecommonspatternsforgoodandclearRESTAPIdesigns,asfollows:
Usenouns;donotuseverbs:Often,youcanusestandardURIs,suchas/cars/or/members/.Youshouldnotuse/getCars/orgetMembers/becauseyouareusingtheURIwithaverb,andtheverbalreadytellstheactions.GETmethodshouldnotchangestate:Ifyouwanttochangethestateoftheserver,youwillneedtouseverbssuchasPUT,POST,orDELETE.GETshouldnotchangethestateoftheserver,soitshouldalwaysbesafecallingGETasmanytimesasyouwant.Thisiscalledidempotent.Prefersub-resourcerelation:Let'ssaywehavearesourcecalled/users/,andauserhasprojects.It'salwaysagoodideatousesub-resources,suchas/users/1/projects/2,becausewehavearelationshipbetweenusersandprojects.UseHTTPheaders:HTTPheadersshouldbeusedforserialization,security,andallthekindsofmetadatayourapplicationneeds.TheHTTPHeadersareoftenusedforcontentnegotiation.Forinstance,youmightdothefollowing:
HTTPHEADERContent-Type=XML-GET/cars/1
HTTPHEADERContent-Type=JSON-GET/cars/1
TheURIisthesame;however,basedontheheadertype,itwillreturndatainXMLorJSONformat.Filter,SortingandPagination:Sometimes,yourdatamaybebig.It'salwaysagoodideatoprovidemechanismstosort,filter,andpaginateasfollows:
GET/projects/1/tasks?by=priority->Sorting
GET/projects/1/tasks?status=done->Filter
GET/projects/1/tasks?limit=30&offset=5->Pagination
APIversioningTherearetwowaystoperformAPIversioning.Firststrategyistoversionbytheendpointexplicitsuchas/v1/cars.Thesecondstrategyisbasedonmetadatasuchas/cars/,butthenyouwillpassanHTTPHEADERversionasv1.
Bothstrategieshaveprosandcons.Explicitversioningismoreclear,andyoucanalwayscreateanewversionanddon'tbreakyourconsumers.Headerstrategyismoreelegant;however,itcangettrickytomanage.
Someanti-patternstobeavoidedThereareseveraltrapsintheRESTAPIdesign,butthefollowingthingsneedtobeavoided:
GETverbforeverythingIgnoringHTTPheaderssuchasMIME-typesReturning200whenanerrorhappensReturning500foraninvalidparameteroramissingparameter
CreatingourAPIwithRESTandJSONAlright,nowisthetimetodesignaRESTAPIforyourPlayframeworkapplication.WewillcreateanAPItoexportalldatainthesystem.ThisAPIwillbeREADonly;however,youcanaddwriteoperationsifyoulike.
Lateroninthischapter,wewilladdsomebackpressuretolimittheAPIRESTrateforconsumersandcreateaScalaclientapplicationforourRESTAPI.So,firstofall,let'sgetstartedwiththePlayframework(server)first.
Wedon'tneedanyextralibraryinordertocreateaRESTAPIinourPlayframeworkapplication.Wewilljustneedanewcontrollerandnewroutes.Additionally,wewillleveragemostofthecodewemadeinthepreviouschapters.
RestApiContollerLet'screateanewcontrollerlocatedatReactiveWebStore/app/controllers.
RESTAPIFrontControllerimplementation
RestApiController.scalafileshouldlooksomethinglikethis:
packagecontrollers
@Singleton
classRestAPIController@Inject()
(valproductService:IProductService,
valreviewService:IReviewService,
valimageService:IImageService)extendsController{
importplay.api.libs.concurrent.Execution.
Implicits.defaultContext
deflistAllProducts=Action{
valfuture=productService.findAll()
valproducts=Awaits.get(5,future)
valjson=ProductsJson.toJson(products)
Ok(json)
}
deflistAllReviews=Action{
valfuture=reviewService.findAll()
valreviews=Awaits.get(5,future)
valjson=ReviewsJson.toJson(reviews)
Ok(json)
}
defprocessImages={
valfuture=imageService.findAll()
valimages=Awaits.get(5,future)
valjson=ImagesJson.toJson(images)
json
}
deflistAllImages=Action{
Ok(processImages)
}
}
Basically,wehavethreefunctionshere.Thesefunctionslistallproducts,images,andreviews.Asyoucanseeatthetopofthecontroller,weareinjectingthethreeservicesthatwehaveforproducts,images,andreviews.
Thecodeisprettymuchstraightforwardforallfunctions.First,wewillcalltheproperservice,andthenwewillwaitfortheresultwiththeawaitobject.Oncewehavethedata,wewillcallafunctiontoconvertthedatatoJSON.
Let'stakealookattheJSONhelpersobjectsthatweusedhere.
JSONmappingOurRESTcontrollerusedJSONhelperobjectstomapobjectstoJSON.First,wewillstartwiththeProductsJSONhelper.
ProductsJsonislocatedatReactiveWebStore/app/controllers/Product.scala:
objectProductsJson{
importplay.api.libs.json._
importplay.api.libs.json.Reads._
importplay.api.libs.functional.syntax._
implicitvalproductWrites:Writes[Product]=(
(JsPath\"id").write[Option[Long]]and
(JsPath\"name").write[String]and
(JsPath\"details").write[String]and
(JsPath\"price").write[BigDecimal]
)(unlift(Product.unapply))
implicitvalproductReads:Reads[Product]=(
(JsPath\"id").readNullable[Long]and
(JsPath\"name").read[String]and
(JsPath\"details").read[String]and
(JsPath\"price").read[BigDecimal]
)(Product.apply_)
deftoJson(products:Option[Seq[Product]])=Json.toJson(products)
}
Basically,therearethreeimportantconceptshere.First,wehaveproductsWrites,whichmapsfromJSONtomodel,andProductforwrites,whichisalsoknownasdeserialization.WehaveanothermappingforserializationcalledproductsReads,whichconvertsobjectstoJSON.
Asyoucansee,weneedtomapallfieldsexistinginourmodel,suchasID,name,details,andprice.Thismappingmustmatchpropertypesaswell.IDmappingusesreadNullablebecauseIDisoptional.
Finally,wehaveafunctiontoconvertfromJSONtoobject,calledtoJson,whichusesagenericPlayframeworklibrarycalledJSON.Let'smoveforthenexthelper--thereview.
ReviewsJsonislocatedatReactiveWebStore/app/controllers/Review.scalaandshouldlooksomethinglikethis:
objectReviewsJson{
importplay.api.libs.json._
importplay.api.libs.json.Reads._
importplay.api.libs.functional.syntax._
implicitvalreviewWrites:Writes[Review]=(
(JsPath\"id").write[Option[Long]]and
(JsPath\"productId").write[Option[Long]]and
(JsPath\"author").write[String]and
(JsPath\"comment").write[String]
)(unlift(Review.unapply))
implicitvalreviewReads:Reads[Review]=(
(JsPath\"id").readNullable[Long]and
(JsPath\"productId").readNullable[Long]and
(JsPath\"author").read[String]and
(JsPath\"comment").read[String]
)(Review.apply_)
deftoJson(reviews:Option[Seq[Review]])=Json.toJson(reviews)
}
Here,wehavethesameconceptsthatwesawearlierintheProductsJSONhelper.Wehaveamappingforreadsandwritesandafunctionwhichconvertsamodel.ReviewtoJSON.Let'smovetothelasthelper,theImageJson.
ImagesJsonislocatedatReactiveWebStore/app/controllers/Image.scala,whichshouldlooksomethinglikethis:
objectImagesJson{
importplay.api.libs.json._
importplay.api.libs.json.Reads._
importplay.api.libs.functional.syntax._
implicitvalimagesWrites:Writes[Image]=(
(JsPath\"id").write[Option[Long]]and
(JsPath\"productId").write[Option[Long]]and
(JsPath\"url").write[String]
)(unlift(Image.unapply))
implicitvalimagesReads:Reads[Image]=(
(JsPath\"id").readNullable[Long]and
(JsPath\"productId").readNullable[Long]and
(JsPath\"url").read[String]
)(Image.apply_)
deftoJson(images:Option[Seq[Image]])=Json.toJson(images)
}
Justaswiththeothertwomappers,wehavereads,writes,mappings,andthetoJsonfunction.Wearedonewithmappers,sonowthenextstepistocreatethenewroutes.
ConfiguringnewroutesWeneedtoaddthefollowingthreenewroutesforourRESTAPI,whichislocatedatReactiveWebStore/conf/routes:
#
#RESTAPI
#
GET/REST/api/product/all
controllers.RestAPIController.listAllProducts
GET/REST/api/review/all
controllers.RestAPIController.listAllReviews
GET/REST/api/image/allcontrollers.RestAPIController.listAllImages
Asyoucansee,wemappedallthelistoperationswejustcreated.
TestingtheAPIusingthebrowserNowwecanrun$activatorrunandtestournewRESTAPIusingourwebbrowser.
Gotohttp://localhost:9000/REST/api/product/all;youshouldseesomethingsimilartothefollowingscreenshot:
Let'slookatthereviewAPI.
Gotohttp://localhost:9000/REST/api/review/all;youshouldseeresultssimilartothefollowingscreenshot:
Finally,let'scheckouttheimageofRESTAPI.
Gotohttp://localhost:9000/REST/api/image/all;youshouldseeresultssimilartothefollowingscreenshot:
OK.NowwewillcontinuetoworkwithREST.Wejustfinishedtheserver;however,itisimportanttocreateaRESTclienttoconsumetheseRESTAPIs.
CreatingaScalaclientFirst,youwillneedtocreateanewproject.Gotoyourfilesystemandcreateafoldercalledrest-client.Then,createanotherfolderinsiderest-clientcalledproject.Insideproject,youwillneedtoaddthefollowingtwofiles:
build.properties:ThiscontainsanSBTconfiguration,suchasversionPlugins.sbt:ThiscontainsanSBTpluginsconfiguration
Let'sstartwithbuild.properties:
sbt.version=0.13.11
Asyoucanseehere,weareconfiguringthisprojecttouseSBTversion0.13.11.Now,wecanmovetothepluginsfile.
Configuringplugins.sbtYourplugins.sbtfileshouldlooksomethinglikethis:
addSbtPlugin("com.typesafe.sbteclipse"%"sbteclipse-plugin"%
"2.5.0")
addSbtPlugin("com.github.mpeltonen"%"sbt-idea"%"1.6.0")
Here,weareaddingEclipseandIntelliJsupport.Forthisbook,weareusingEclipse,butfeelfreetouseanythingyoulike.
Outsideoftheprojectfolder,underrest-client,wewillneedtoconfigurethebuild.sbtfile.
Configuringbuild.sbtYourbuild.sbtfileshouldlooksomethinglikethis:
name:="rest-client"
version:="1.0"
scalaVersion:="2.11.7"
scalaVersioninThisBuild:="2.11.7"
resolvers+=DefaultMavenRepository
resolvers+=JavaNet1Repository
resolvers+="OSSSonatype"at
"https://oss.sonatype.org/content/repositories/releases"
resolvers+="SonatypeOSSSnapshots"at
"https://oss.sonatype.org/content/repositories/snapshots"
resolvers+="SonatypeOSSSnapshots"at
"https://oss.sonatype.org/content/repositories/snapshots"
resolvers+="amateras-repo"at
"http://amateras.sourceforge.jp/mvn/"
libraryDependencies+="com.typesafe.play"%"play-ws_2.11"%
"2.5.6"
libraryDependencies+="org.scalatest"%"scalatest_2.11"%"2.2.6"
%Test
Sohere,weareusingScalaversion2.11.7,andwearedeclaringjusttwodependencies.Onedependencyisfortests,whichisscala-test,andtheotherdependencyisonthePlayframeworkwslibrary,whichwewillusetocallourRESTAPIs.
Let'salsocreatetwosourcefolders,asfollows:
src/main/scala:ThisistheScalasourcecodesrc/test/scala:ThisistheScalatestsourcecode
OK.Nowwecanrun$sbtcleancompileeclipseinordertodownloadthedependenciesfromthewebandcreatealltheEclipseprojectfilesthatweneed.
NowwecanimportthiscodeinEclipseandmoveon.
ScalaclientcodeFirstofall,wewillneedtocreateaFactorytoinstantiatetheWSPlayframeworklibrarytocallwebservices.Undertherest-client/src/main/scalalocation,let'screateapackagecalledclientandaddthefollowingcodeunderWSFactory.scala:
packageclient
objectWSFactory{
importakka.actor.ActorSystem
importakka.stream.ActorMaterializer
defws={
implicitvalsystem=ActorSystem()
implicitvalmaterializer=ActorMaterializer()
importcom.typesafe.config.ConfigFactory
importplay.api._
importplay.api.libs.ws._
importplay.api.libs.ws.ahc.{AhcWSClient,AhcWSClientConfig}
importplay.api.libs.ws.ahc.AhcConfigBuilder
importorg.asynchttpclient.AsyncHttpClientConfig
importjava.io.File
valconfiguration=Configuration.reference++
Configuration(ConfigFactory.parseString(
"""
|ws.followRedirects=true
""".stripMargin))
valparser=newWSConfigParser(configuration,
play.api.Environment.simple(
newFile("/tmp/"),null))
valconfig=newAhcWSClientConfig(wsClientConfig=
parser.parse())
valbuilder=newAhcConfigBuilder(config)
vallogging=new
AsyncHttpClientConfig.AdditionalChannelInitializer(){
overridedefinitChannel(channel:io.netty.channel.Channel):
Unit={
channel.pipeline.addFirst("log",new
io.netty.handler.logging.LoggingHandler("debug"))
}
}
valahcBuilder=builder.configure()
ahcBuilder.setHttpAdditionalChannelInitializer(logging)
valahcConfig=ahcBuilder.build()
newAhcWSClient(ahcConfig)
}
}
Theprecedingcodeisjusttechnical.ThesearethestepsneededtoinstantiatetheWSClientoutsidethePlayframework.IfthisclientwasawebapplicationusingthePlayframework,itwouldbewayeasieraswecanjustuseGoogleGuiceandinjectwhatweneed.
ThemainideayouneedtokeepinmindisthatyouneedtouseAkkaandActorSysteminordertousethisfeature.Asyoucansee,allthiscodeislockedinsideanobjectinasinglefunctioncalledws.
Wewillneedsomeutilityclasstoworkwithfutures.AsweusewslibrarytocallRESTAPIs,itreturnsFuture.So,let'screateanewpackagecalledutils:
YourAwaits.scalafileshouldlooklikesomethinglikethis:
packageutils
importscala.concurrent.Future
importscala.concurrent.duration._
importscala.concurrent.Await
objectAwaits{
defget[T](sec:Int,f:Future[T]):T={
Await.result[T](f,secseconds)
}
}
Theprecedingcodeisprettysimple.WeusedtheAwaitobjectandthenusedagenericTinordertoconverttheresulttoagenericparameterizedtype.Byusingthisparameter,wewillalsoreceivehowmanysecondsweshouldwaitbeforethetimeout.
CreatingourRESTclientproxiesWewillnowmakeRESTcalls;however,wewillcreateaScalaAPI.So,thedeveloperswhouseourrest-clientwon'tneedtodealwithRESTandjustexecutetheScalacode.Thisisgoodformanyreasons,someofwhichareasfollows:
SOC:WestillhaveseparationofconcernsbetweenthePlayframeworkandtheclientapplicationIsolation:IftheRESTAPIchanges,wewillneedtodealwithitontheproxylayerAbstraction:TherestoftheclientcodejustusesScalaanddoesnotknowanythingaboutRESTorHTTPcalls
Thesetechniquesareverycommonnowadayswithmicroservices.Thesetechniquescanalsobeknownasdriversorthickclients.Rightnow,wewillneedtocreatethreeproxies,oneforeachresource,thatwehaveontheRESTAPI.Let'screateanewpackagecalledproxy.
YourProductProxy.scalafileshouldlooksomethinglikethis:
packageproxy
importclient.WSFactory
importutils.Awaits
caseclassProduct
(varid:Option[Long],
varname:String,
vardetails:String,
varprice:BigDecimal){
overridedeftoString:String={
"Product{id:"+id.getOrElse(0)+",name:"+name+",
details:"+details+",price:
"+price+"}"
}
}
objectProductsJson{
importplay.api.libs.json._
importplay.api.libs.json.Reads._
importplay.api.libs.functional.syntax._
implicitvalproductWrites:Writes[Product]=(
(JsPath\"id").write[Option[Long]]and
(JsPath\"name").write[String]and
(JsPath\"details").write[String]and
(JsPath\"price").write[BigDecimal]
)(unlift(Product.unapply))
implicitvalproductReads:Reads[Product]=(
(JsPath\"id").readNullable[Long]and
(JsPath\"name").read[String]and
(JsPath\"details").read[String]and
(JsPath\"price").read[BigDecimal]
)(Product.apply_)
deftoJson(products:Option[Seq[Product]])=Json.toJson(products)
}
objectProductProxy{
importscala.concurrent.Future
importplay.api.libs.json._
importProductsJson._
valurl="http://localhost:9000/REST/api/product/all"
implicitvalcontext=
play.api.libs.concurrent.Execution.Implicits.defaultContext
deflistAll():Option[List[Product]]={
valws=WSFactory.ws
valfutureResult:Future[Option[List[Product]]]=
ws.url(url).withHeaders("Accept"->
"application/json").get().map(
response=>
Json.parse(response.body).validate[List[Product]].asOpt
)
valproducts=Awaits.get(10,futureResult)
ws.close
products
}
}
Wehavethreebigconceptsinthiscode.Firstofall,wehaveacaseclassthatrepresentstheproduct.TheprecedingcodeisverysimilartothecodewehaveonthePlayframeworkapplication.However,ifyoupayattention,youwillseeitismuchcleanerbecausewedon'thaveanymetadataaroundpersistence.
Youmightthink,thisisduplicatedcode!Itis,anditis100%okay.Duplicatecodeisdecoupled.RememberthatwehaveaRESTinterfaceandalsoaproxybetweentherestoftheclientcode,sowehaveatleasttwolayersofindirectionthatwecandealwithchanges.Ifthesetwocodebasessharethesameclass,wewouldhavecouplingandlessspacetoaccommodatechanges.
Thesecondbigconcepthereismapping.WewillreceiveJSON,andwewillwanttoconvertJSONtoourcaseclass,sowewillhavesimilarmappingthatwedidinthePlayframeworkapplication.
Finally,wehavetheproxyimplementation.WewillinstantiatethePlayframeworkWSlibraryusingourfactoryandcallthewsfunction.Then,wewillusetheurlfunctionpassingtheRESTAPIURIforproductsanddefineaheaderinordertoacceptJSON.WearealsodoingthisusingtheHTTPverb,GET.TheresponseismappedwithJson.parsepassingresponse.body.Additionally,wewillcallthevalidatefunctiontomakesurethisJSONmatchesourcaseclass.Thisvalidationisimportantbecausethenwecanbesurethattheformatdidnotchange,andthateverythingworksfine.WSwillreturnthisasaFuture,sowewilluseourAwaitshelpertogettheresult.
Let'smovetothenextproxy,thereview.
YourReviewProxy.scalafileshouldlooksomethinglikethis:
packageproxy
importclient.WSFactory
importutils.Awaits
caseclassReview
(varid:Option[Long],
varproductId:Option[Long],
varauthor:String,
varcomment:String)
{
overridedeftoString:String={
"Review{id:"+id+",productId:"+productId.getOrElse(0)
+",author:"+author+
",comment:"+comment+"}"
}
}
objectReviewsJson{
importplay.api.libs.json._
importplay.api.libs.json.Reads._
importplay.api.libs.functional.syntax._
implicitvalreviewWrites:Writes[Review]=(
(JsPath\"id").write[Option[Long]]and
(JsPath\"productId").write[Option[Long]]and
(JsPath\"author").write[String]and
(JsPath\"comment").write[String]
)(unlift(Review.unapply))
implicitvalreviewReads:Reads[Review]=(
(JsPath\"id").readNullable[Long]and
(JsPath\"productId").readNullable[Long]and
(JsPath\"author").read[String]and
(JsPath\"comment").read[String]
)(Review.apply_)
deftoJson(reviews:Option[Seq[Review]])=Json.toJson(reviews)
}
objectReviewProxy{
importscala.concurrent.Future
importplay.api.libs.json._
importReviewsJson._
valurl="http://localhost:9000/REST/api/review/all"
implicitvalcontext=
play.api.libs.concurrent.Execution.Implicits.defaultContext
deflistAll():Option[List[Review]]={
valws=WSFactory.ws
valfutureResult:Future[Option[List[Review]]]=
ws.url(url).withHeaders("Accept"->
"application/json").get().map(
response=>
Json.parse(response.body).validate[List[Review]].asOpt
)
valreviews=Awaits.get(10,futureResult)
ws.close
reviews
}
}
Here,wehavethesameprinciplesthatwehadontheproductproxy,butthistimeforreview.Asyoucansee,wewillcalladifferentURI.Now,let'smovetothelastproxy--theImageProxy.scalafile.
YourImageProxy.scalafileshouldlooksomethinglikethis:
packageproxy
importclient.WSFactory
importutils.Awaits
caseclassImage
(varid:Option[Long],
varproductId:Option[Long],
varurl:String)
{
overridedeftoString:String={
"Image{productId:"+productId.getOrElse(0)+",url:"+url
+"}"
}
}
objectImagesJson{
importplay.api.libs.json._
importplay.api.libs.json.Reads._
importplay.api.libs.functional.syntax._
implicitvalimagesWrites:Writes[Image]=(
(JsPath\"id").write[Option[Long]]and
(JsPath\"productId").write[Option[Long]]and
(JsPath\"url").write[String]
)(unlift(Image.unapply))
implicitvalimagesReads:Reads[Image]=(
(JsPath\"id").readNullable[Long]and
(JsPath\"productId").readNullable[Long]and
(JsPath\"url").read[String]
)(Image.apply_)
deftoJson(images:Option[Seq[Image]])=Json.toJson(images)
}
objectImageProxy{
importscala.concurrent.Future
importplay.api.libs.json._
importImagesJson._
valurl="http://localhost:9000/REST/api/image/all"
implicitvalcontext=
play.api.libs.concurrent.Execution.Implicits.defaultContext
deflistAll():Option[List[Image]]={
valws=WSFactory.ws
valfutureResult:Future[Option[List[Image]]]=
ws.url(url).withHeaders("Accept"->
"application/json").get().map(
response=>
Json.parse(response.body).validate[List[Image]].asOpt
)
valimages=Awaits.get(10,futureResult)
ws.close
images
}
}
That'sit.Wehavethesameconceptsasproductandreview.Wehavefinishedallourproxies.Now,itistimetotestourproxyimplementation.Thebestwaytodothisisviatests,solet'screateScalatestsforthesethreeimplementations.
CreatingScalaTesttestsfortheproxiesUnderthe/src/test/scalasourcefolder,wewillneedtocreateapackagecalledproxy.test.
YourProductProxtTestSpec.scalashouldlooksomethinglikethis:
packageproxy.test
importorg.scalatest._
importproxy.ProductProxy
classProductProxtTestSpecextendsFlatSpecwithMatchers{
"AProductRestproxy"should"returnallproducts"in{
valproducts=ProductProxy.listAll().get
productsshouldNot(be(null))
productsshouldNot(be(empty))
}
}
Thetestisquitesimple;wewilljusthavetocallthelistAlloperationinourproductproxyandthenaddsomeassertionstomakesuretheresultisnotnull.Wewillalsoshowalltheproductsintheconsole.
Now,wewillneedtocreatetestsforthereviewproxy,whichwillbesimilartotheproduct.
YourReviewProxyTestSpec.scalafileshouldlooksomethinglikethis:
packageproxy.test
importorg.scalatest._
importproxy.ReviewProxy
classReviewProxyTestSpecextendsFlatSpecwithMatchers{
"AReviewRESTProxy"should"returnallreviews"in{
valreviews=ReviewProxy.listAll().get
reviewsshouldNot(be(null))
reviewsshouldNot(be(empty))
for(r<-reviews){
println(r)
}
}
}
Here,weusedtheproxyideastotestthereview.WecalledtheproxyusingthelistAllfunctiontogetallthereviews.Later,wewillchecktoseeifthereviewisnotnull.Wewillprintallthereviews.Finally,it'stimetomovetothelastproxytest--theimageproxy.
YourImageProxyTestSpec.scalashouldlooksomethinglikethis:
packageproxy.test
importorg.scalatest._
importproxy.ImageProxy
importscala.concurrent.Future
importplay.api.libs.concurrent.Execution.Implicits.defaultContext
importjava.util.concurrent.CountDownLatch
classImageProxyTestSpecextendsFlatSpecwithMatchers{
"AImageRESTProxy"should"returnallimages"in{
valimages=ImageProxy.listAll().get
imagesshouldNot(be(null))
imagesshouldNot(be(empty))
for(i<-images){
println(i)
}
}
}
Samedealgoesfortheimageproxy.Wehaveallourtests;now,wecanrunthetests.YouwillneedtomakesureourReactiveWebStorePlayframeworkappisupandrunning.
Let'srunthistestwithsbt:
openyourconsoleandtypein$sbttest
Alright,itallworks!Ournextstepwillbetoaddbackpressure.
AddingbackpressureBackpressureisawell-knownconceptintheautomotiveindustry.Nowadays,thistermisusedinsoftwareengineeringaswell.Backpressureintheautomotiveworldreferstothepressureopposedtothedesiredflowofgassesinaconfinedplace,suchasapipe.Forsoftwareengineering,itisoftenrelatedtoslowingdownaproducer,whichcanbeanapplication,astreamprocessingengine,oreventheuseritself.
WhenweareexecutingREST,it'seasytoreachasituationwheretheclientcansaturatetheserver.Thiscanbeasecuritybreachtoo,whichisalsoknownastheDenialOfService(DOS)attack.
Therearetwoarchitecturalscenarios.Inthefirstscenario,yourRESTAPIisinternal,andyoujusthaveconsumersinyourcompany.Inthesecondscenario,youaremakingtheRESTAPIapublicAPIsothatitwillbeopentothewholeInternet.Forthisscenario,youreallyshouldhavebackpressure,alsoknownasthrottling.
It'spossibletoscaleourarchitectureinordertohandlemoreusers.Wewilldiscussthis,andthescalabilitytechniques,inChapter10,ScalingUp.
Currently,thereareseveralwaystoapplybackpressure.Forinstance,ifourcodeispureRxScala/RxJava,wecanapplybackpressureonobservables.Moredetailscanbefoundathttps://github.com/ReactiveX/RxJava/wiki/Backpressure.
AsweareexposingaRESTinterface,wewilladdbackpressureonthecontroller,sowewillneedtocreateanewclasswiththebackpressurecode.
Therearesomealgorithmsforbackpressure;wewillusetheleakybucketalgorithm.Thealgorithmitselfisverysimple--just30linesofScalacode.
Theleakybucketalgorithm
Theleakybucketmetaphorisprettysimple.Let'stakealookatitinthefollowingdiagram:
Themetaphorbehindthealgorithmisbasedaroundabucketwithholes.Thewaterflowsordripsintothebucketandleaksthroughtheholesofthebucket.Ifthereistoomuchwater,andthebucketisfullofwater,thewaterwillspilloutofthebucket--inotherwords,itwillbediscarded.
Thisalgorithmisusedinnetworkprograming,andalsobythetelecommunicationindustry.TheAPImanagersolutionsarealsousecasesforthisalgorithm.
ThisconceptallowsRATElimitconstraints.Wecanexpressthebackpressureratelimitsinrequestspertime.Time,inthiscase,isoftenmeasuredinsecondsorminutes,sowehaveRequestsPerSecond(RPS)orRequestsPerMinute(RPM).
Youcanimplementthisalgorithmwithaqueue.However,inourimplementation,wewillnotuseaqueue;wewillusetimeinordertocontroltheflow.Ourimplementationwillalsobelockfree,ornon-blocking,aswewon'tusethreadsorexternalresources.
NowisthetimetocodealeakybucketinScala.Firstofall,wewillcreatethiscodefortheReactiveWebStoreapplication.WewillneedtocreateanewpackagelocatedatReactiveWebStore/app.Thenewpackagenamewillbebackpressure.
Scalaleakybucketimplementation
YourLeakyBucket.scalafileshouldhavethefollowingfiles:
packagebackpresurre
importscala.concurrent.duration._
importjava.util.Date
classLeakyBucket(varrate:Int,varperDuration:FiniteDuration){
varnumDropsInBucket:Int=0
vartimeOfLastDropLeak:Date=null
varmsDropLeaks=perDuration.toMillis
defdropToBucket():Boolean={
synchronized{
varnow=newDate()
if(timeOfLastDropLeak!=null){
vardeltaT=now.getTime()-timeOfLastDropLeak.getTime()
varnumberToLeak:Long=deltaT/msDropLeaks
if(numberToLeak>0){
if(numDropsInBucket<=numberToLeak){
numDropsInBucket-=numberToLeak.toInt
}else{
numDropsInBucket=0
}
timeOfLastDropLeak=now
}
}else{
timeOfLastDropLeak=now
}
if(numDropsInBucket<rate){
numDropsInBucket=numDropsInBucket+1
returntrue;
}
returnfalse;
}
}
}
Asyoucanseehere,wecreatedaScalaclassthatreceivestwoparameters:rateandperDuration.Rateisaninteger,whichshowshowmanyrequestsweareabletohandlebeforeapplyingbackpressure.PerDurationisaScalaFiniteDuration,whichcanbeanymeasureoftime,suchasmilliseconds,seconds,minutes,orhours.
Thisalgorithmkeepstrackofthetimeforthelastdropinthebucket.Asyoucansee,thecodeissynchronized,butitisfinebecausewewon'tcalleitherexternalresourcesorthreads.
First,wewillgetthecurrenttimewithnewaDate().Thefirsttimewerunthealgorithm,wewillfailontheelsestatement,andwewillgetthecurrenttimeaslastleak.
Thesecondtimeitruns,itwillenteronthefirstIfstatement.Then,wewillcalculatethedelta(diff)betweenthelastleakandnow.ThisdeltawillbedividedbythetimeinmillisecondsthatyoupassedonperDuration.Ifthedeltaisgreaterthan0,thenweleak;otherwisewedrop.Then,wewillcapturethetimeagainforthelastleak.
Finally,wewillcheckthedroprate.Iftherateissmaller,wewillincrementandreturntrue,whichmeanstherequestcanproceed;otherwise,wewillreturnfalse,andtherequestshouldnotproceed.
NowthatwehavethisalgorithmcodedinScala,wecancallforoneofourcontrollers.WewilladdthisbackpressureontheimageRESTAPI.
YourRestApiController.scalashouldlooksomethinglikethis:
packagecontrollers
classRestAPIController@Inject()
(valproductService:IProductService,
valreviewService:IReviewService,
valimageService:IImageService)extendsController{
import
play.api.libs.concurrent.Execution.Implicits.defaultContext
//RESToftheController...
importscala.concurrent.duration._
varbucket=newLeakyBucket(5,60seconds)
defprocessImages={
valfuture=imageService.findAll()
valimages=Awaits.get(5,future)
valjson=ImagesJson.toJson(images)
json
}
defprocessFailure={
Json.toJson("TooManyRequests-TryAgainlater...")
}
deflistAllImages=Action{
bucket.dropToBucket()match{
casetrue=>Ok(processImages)
casefalse=>
InternalServerError(processFailure.toString())
}
}
}
Here,wewillcreatealeakybucketwithfiverequestsperminute.Wehavetwofunctions:OnetoprocessimagesthatwillcalltheserviceandconverttheobjectstoJSON,andtheothertoprocessfailures.TheprocessFailuremethodwilljustsendamessagesayingthattherearetoomanyrequests,andwecan'tacceptrequestsrightnow.
So,forthelistAllImagesfunction,wewilljustcallthebuckettryingtodropandusetheScalapatternmatcherinordertoprocesstheproperresponse.Iftheresponseistrue,wewillreturnJSONwitha200HTTPcode.Otherwise,wewillreturna500internalerroranddenythatrequest.Here,weimplementedaglobalRATElimiter;however,mostofthetime,peopleperformthisoperationperuser.Now,let'sopenthewebbrowserandtrymorethanfiverequestswithinaminute.Youshouldseesomethinglikethefollowingscreenshotathttp://localhost:9000/REST/api/images/all:
Alright,itworks!Ifyouwaitaminuteandmakerequestsagain,youwillseethattheflowgetsbacktonormal.Thenextstepistoaddanewclienttest,becauseweknowthatifwecallanimagetoomuchinourRESTAPI,wewillbethrottled.
Wewillneedtoaddonemoretestintherest-clientScalaproject.Forthat,wewillneedtochangetheImageProxyTestSpec.
Testingbackpressure
YourImageProxyTestSpec.scalashouldlooksomethinglikethis:
packageproxy.test
classImageProxyTestSpecextendsFlatSpecwithMatchers{
//RESTofthetests...
"AImageRESTProxy"should"sufferbackpressure"in{
vallatch=newCountDownLatch(10)
varerrorCount:Int=0
for(i<-1to10){
Future{
try{
valimages=ImageProxy.listAll().get
imagesshouldNot(be(null))
for(i<-images){
println(i)
}
}catch{
caset:Throwable=>errorCount+=1
}
latch.countDown()
}
}
while(latch.getCount>=1)
latch.await()
errorCountshouldbe>=5
}
}
So,forthistest,wewillcallImageProxytentimes.Weknowthatnotallrequestswillbeservedaswehavebackpressureontheserver.Here,wecancalltheproxywithatry...catchblockandhaveanerrorcounter.Eachtimeitfails,wecanincrementit.So,here,weareexpectedtofailatleastfivetimes.
WearecreatingthecodewithFeaturesbecausewewanttherequeststohappenatthesametime.WewillneedtouseCountDownLatchfunction,whichisaJavautilityclassthatletsuswaitforallFuturestofinishbeforemovingon.ThisisdonebythecountDownfunction.Everytimeweexecutecountdown,wedecrementtheinternalcounter.Asyoucansee,wecreatedtheCountDownLatchfunctionwithten.
Finally,wehaveawhileblocktowaitforuntilthecounterhaspendingFutures.Nowwewait.Onceit'salldone,wecanchecktheerrorcount;itshouldbeatleastfive.That'sit.Wehavetestedourbackpressuremechanismanditallworks!
Now,itistimetomovetothenextfeaturethatwewillimplementinourapplication:Swagger--wewilladdSwaggersupporttoourRESTAPI.
AddingSwaggersupportSwagger(http://swagger.io/)isasimpleJSONandUIrepresentationtoolforRESTAPIs.Itcangeneratecodeinseverallanguages.Italsocreatesaverynicedocumentation,whichisalsoarunnableSwaggercodethatallowsyoutocallRESTwebservicesfromthedocumentationitgenerates.WewillneedtomakesomechangesinourPlayframeworkapplicationinordertogetSwaggerupandrunningwiththePlayframework.
Firstofall,wewillneedtoaddtheSwaggerdependencytobuild.sbt:
//Restofbuildfile...
libraryDependencies++=Seq(
//Restofotherdeps...
"io.swagger"%%"swagger-play2"%"1.5.2-SNAPSHOT"
)
Asyoucansee,weareusingaSnapshotversion.WhyuseSnapshot?Rightnow,itisnotsupportedonastableversion.Inordertoresolvethisdependency,wewillneedtouseGitandcloneanotherproject.Youcangetmoredetailsathttps://github.com/CreditCardsCom/swagger-play.Basically,youwillneedtowriteacommandasfollows:
$gitclonehttps://github.com/CreditCardsCom/swagger-play.git
$cdswagger-play/
$sbtpublishLocal
Now,wewillneedtoenableSwaggeronReactiveWebStore/conf/application.conf.
Yourapplication.conffileshouldlooksomethinglikethis:
play.modules{
enabled+="play.modules.swagger.SwaggerModule"
}
Next,wecanchangeourcontrollerinordertoaddSwaggersupport.SwaggerhasannotationinordertomaptheRESToperations.
YourRestAPIController.scalafileshouldlooksomethinglikethis:
packagecontrollers
@Singleton
@Api(value="/REST/api",description="RESToperationson
Products,ImagesandReviews.")
classRestAPIController@Inject()
(valproductService:IProductService,
valreviewService:IReviewService,
valimageService:IImageService)extendsController{
import
play.api.libs.concurrent.Execution.Implicits.defaultContext
@ApiOperation(
nickname="listAllProducts",
value="FindAllProducts",
notes="ReturnsallProducts",
response=classOf[models.Product],
httpMethod="GET"
)
@ApiResponses(Array(
newApiResponse(code=500,message="InternalServer
Error"),
newApiResponse(code=200,message="JSONresponsewith
data")
)
)
deflistAllProducts=Action{
valfuture=productService.findAll()
valproducts=Awaits.get(5,future)
valjson=ProductsJson.toJson(products)
Ok(json)
}
@ApiOperation(
nickname="listAllReviews",
value="FindAllReviews",
notes="ReturnsallReviews",
response=classOf[models.Review],
httpMethod="GET"
)
@ApiResponses(Array(
newApiResponse(code=500,message="InternalServerError"),
newApiResponse(code=200,message="JSONresponsewith
data")
)
)
deflistAllReviews=Action{
valfuture=reviewService.findAll()
valreviews=Awaits.get(5,future)
valjson=ReviewsJson.toJson(reviews)
Ok(json)
}
importscala.concurrent.duration._
varbucket=newLeakyBucket(5,60seconds)
defprocessImages={
valfuture=imageService.findAll()
valimages=Awaits.get(5,future)
valjson=ImagesJson.toJson(images)
json
}
defprocessFailure={
Json.toJson("TooManyRequests-TryAgainlater...")
}
@ApiOperation(
nickname="listAllImages",
value="FindAllImages",
notes="ReturnsallImages-Thereisthrottlingof5
reqs/sec",
response=classOf[models.Image],
httpMethod="GET"
)
@ApiResponses(Array(
newApiResponse(code=500,message="InternalServerError"),
newApiResponse(code=200,message="JSONresponsewith
data")
)
)
deflistAllImages=Action{
bucket.dropToBucket()match{
casetrue=>Ok(processImages)
casefalse=>InternalServerError(processFailure.toString())
}
}
}
Herewehaveseveralannotations.Firstofall,[email protected],wewilldefinetherootpathoftheRESTAPI.Then,foreachRESTAPIoperation,wehavethe@[email protected]@ApiOperationdefinestheRESTAPIitselfwhereyoucandefinetheparametersandtheHTTPverb,andalsoputsomenotes(documentation).It'salsopossibletodescribetheresult;inourcase,itwillbeaJSONrepresentationofthemodels.
That'sit!WehavethecontrollermappedtoSwagger.ThenextstepistoaddarouteforSwagger.Thisneedstobedonebyaddinganewlineasshowninthefollowingpieceofcodewhich,islocatedatReactiveWebStore/conf/routes:
//RESToftheotherroutes..
GET/swagger.jsoncontrollers.ApiHelpController.getResources
SwaggerUISwaggerwillgenerateaJSONresponseforourRESTAPI.It'spossibletousetheSwaggerUI,whichisveryniceandgiveslotsoffacilitiestothedeveloper.TherearetwowayswecanworkwiththeSwaggerUI:WecanuseitasastandaloneorwecanembedtheSwaggerUIintoourPlayframeworkapplication.
ThestrategywewillpickhereisembeddingtheSwaggerUIinourapplication.IfyouhavemultipleRESTAPIswithmultiplePlayapplicationsormicroservices,itisagoodideatohavethestandaloneinstallationoftheswaggerUI.
Intheprevioussteps,weenabledSwaggerinourapplication.Openyourbrowserandtypehttp://localhost:9000/swagger.json.Youcanfollowtheinstructionsathttp://swagger.io/swagger-ui/.Insummary,youwillgetthefollowingoutput:
BuildandinstallSwaggerStandalone
Nowwewilldownload,build,andinstallSwaggerStandalone.Let'sgetstartedbywritingthefollowinglinesofcode:
$sudoapt-getupdate$sudoapt-getinstallnodej$sudoapt-getinstallnpm
$gitclonehttps://github.com/swagger-api/swagger-ui.git$cdswagger-ui/$
sudo-Enpminstall-g$sudo-Enpmrunbuild$npmrunserve$GOTO:
http://localhost:8080/
OnceyoustarttheSwaggerUI,youcangotothebrowser,whereyouwillseethefollowing
output:
Now,let'sembedtheSwaggerUIintoourPlayframeworkapplication.
Wewillneedtocopythecontentfrom/swagger-ui/dist/intoourPlayframeworkapplicationunderReactiveWebStore/public.Then,wewillcreateafoldercalledswaggerui.
WewillneedtoeditonefileinordertoputourswaggerJSONURI.OpenReactiveWebStore/public/swaggerui/index.htmlandchangethe40tothefollowingcodeline:
url="http://localhost:9000/swagger.json";
That'sit.NowwewillneedtocreatealinkfromourapplicationtoembedtheswaggerUI.So,let'schangeReactiveWebStore/app/views/index.scala.html.
Yourindex.scala.htmlfileshouldlooksomethinglikethis:
@(message:String)(implicitflash:Flash)
@main("WelcometoReactiveWebStore"){
<divclass="row-fluid">
<BR>
<divclass="span12">
<divclass="row-fluid">
<divclass="span6">
<ahref="/product"><imgheight="42"width="42"
src="@routes.Assets.at("images/product.png")">Manage
Products</a><BR>
<ahref="/review"><imgheight="42"width="42"
src="@routes.Assets.at("images/review.png")">Manage
Reviews</a><BR>
<ahref="/image"><imgheight="42"width="42"
src="@routes.Assets.at("images/image.png")">Manage
Images</a><BR>
</div>
<divclass="span6">
<ahref="/reports"><imgheight="42"width="42"
src="@routes.Assets.at("images/reports.png")">Reports</a>
<BR>
<ahref="/chat/index_socket"><imgheight="42"width="42"
src="@routes.Assets.at("images/chat.png")">ChatRoom</a>
<BR>
<ahref="/assets/swaggerui/index.html"><imgheight="42"
width="42"
src="@routes.Assets.at("images/swagger.png")">SwaggerREST
API</a><BR>
</div>
</div>
</div>
</div>}
NowwecanrunourPlayapplicationwith$activatorrun.
Openthebrowserandgotohttp://localhost:9000/.Youwillseethefollowingscreenshot:
NowwecanopentheSwaggerUIbyclickingontheSwaggerRESTAPIlinkorbyjustgoingtohttp://localhost:9000/assets/swaggerui/index.html.Itshouldlooksomethinglikethis:
Asyoucansee,theSwaggerUIisverynice.Youcanclickoneachoperationandseemoredetailsonhowtheywork,whichHTTPverbtheyuse,andwhattheURIis.ThereisaTryitout!button.Let'sclickontheTryitout!buttonforproducts,whichwouldlooksomethinglikethis:
Asyoucansee,wehaveourJSONresultandalsosomeCURLsamplesaswell.
SummaryInthischapter,youlearnedhowtodesignaRESTAPIandchangedyourPlayframeworkapplicationinordertohaveSwaggersupport.YoucreatedaScalaclientlibraryusingproxytechniques,aswellasScalatestsfortheAPIs.Additionally,youwereintroducedtobackpressureusingtheleakybucketalgorithm.
Inthenextchapter,whichwillbethefinalchapter,youwilllearnaboutsoftwarearchitectureandscalability/resiliencytechniques,suchasdiscoverability,loadbalancers,caches,AkkaCluster,andtheAmazonCloud.
Chapter10.ScalingupInthepreviouschapters,webuiltaScalaandPlayframeworkapplication.WeusedthemosteffectiveframeworksandtoolsaroundtheScalaecosystem,suchasPlayframeworkandAkka;andweusedtheReactiveandFunctionalProgrammingtechniquesusingFuturesandRxScala.Additionally,wecreatedreportswithJasperandChatwithWebSockets.Thisisthefinalchapter,andwewilllearnhowtodeployandscaleourapplication.
Inthischapter,wewillcoverthefollowingtopics:
StandalonedeployArchitectureprinciplesReactivedriversanddiscoverabilityMid-Tierload-balancer,timeouts,Backpressure,andcachingScalingupmicroserviceswithanAkkaclusterScalinguptheinfrastructurewithDockerandAWScloud
StandalonedeployThroughoutthisbook,weusedtheActivatorandSBTbuildanddevelopmenttools.However,whenwetalkaboutproduction,wecan'truntheapplicationwithSBTorActivator;weneedtodoastandalonedeploy.
WhataboutstandardJavaServletcontainers,suchasTomcat?Tomcatisgreat;however,Playisgreater.Youwon'tgetbetterperformancebydeployingyourapponTomcat.ThestandaloneplayusesNetty,whichhassuperiornetworkstack.
TherearesomesmallchangesthatwewillneedtomakeinordertodeployourapplicationforJasperreports.Don'tworry;thesechangesareverysimpleandstraightforward.
ReportsfolderWeneedtomovethereportstemplate(JRXMLfiles)fromthesourcefoldertothepublicfolder.Whydoweneedtodothis?Becausewhenwegeneratethestandalonedeploy,theywon'tbeincludedintheapplicationjars.WhatisinsidethepublicfolderwillbepackedanddeployedintoaproperJARfile.That'swhyweneedtomakethischange.
CreateafoldercalledreportsatReactiveWebStore/public/.ThenmovealltheJRXMLfilesthere.
ChangingreportbuilderAsourtemplateswillbeinsideaJARfile,weneedtochangetheloadinglogicinordertogetthetemplatesproperly.UnderReactiveWebStore/app/report/,weneedtochangeReportBuilder.scala,whichshouldlooksomethinglikethisafterediting:
packagereports
objectReportBuilder{
privatevarreportCache:scala.collection.Map[String,Boolean]=
newscala.collection.mutable.HashMap[String,Boolean].empty
defgenerateCompileFileName(jrxml:String):String=
"/tmp/report_"+jrxml+"_.jasper"
defcompile(jrxml:String){
if(reportCache.get(jrxml).getOrElse(true)){
valdesign:JasperDesign=JRXmlLoader.load(
Play.resourceAsStream("/public/reports/"+jrxml)
(Play.current).get)
JasperCompileManager.compileReportToFile(design,
generateCompileFileName(jrxml))
reportCache+=(jrxml->false)
}
}
deftoPdf(jrxml:String):ByteArrayInputStream={
try{
valos:OutputStream=newByteArrayOutputStream()
valreportParams:java.util.Map[String,Object]=
newjava.util.HashMap()
valcon:Connection=
DriverManager.getConnection("jdbc:mysql://localhost/RWS_DB?
user=root&password=&useUnicode=
true&useJDBCCompliantTimezoneShift=
true&useLegacyDatetimeCode=false&serverTimezone=UTC")
compile(jrxml)
valjrprint:JasperPrint=
JasperFillManager.fillReport(generateCompileFileName(jrxml),
reportParams,con)
valexporter:JRPdfExporter=newJRPdfExporter()
exporter.setExporterInput(newSimpleExporterInput(jrprint));
exporter.setExporterOutput(
newSimpleOutputStreamExporterOutput(os));
exporter.exportReport()
newByteArrayInputStream
((os.asInstanceOf[ByteArrayOutputStream]).toByteArray())
}catch{
casee:Exception=>thrownewRuntimeException(e)
null
}
}
}
Themainchangesthatwemadewerearoundthecompilefunction.Now,weareusingtheJasperJRXmlLoaderinordertoloadtheJaspertemplatefromanInputStreammethod.PassingtheInputStreammethodprovidedbythePlay.resourceAsStreamfunction.Asyou
cansee,wearepassingthenewpath,/public/reports/,inordertogetthetemplates.Therestofthecodeisprettymuchthesameastheoneweexecutedearlier.
Nowwearereadytodeploy.Inordertodoso,wewillneedtorunacommandonactivator,whichisasfollows:
$activatoruniversal:packageZipTarball
Youwillseethefollowingresult:
Assoonasthetaskisfinished,ourappwillbepackedintotheReactiveWebStore/target/universal/directory,andyouwillseeareactivewebstore-1.0-SNAPSHOT.tgzfile.
Thenyouneedtoextractthefileandyoushallhavethefollowingdirectory:
reactivewebstore-1.0-SNAPSHOTbin:Scriptstoruntheappconf:Allconfigfiles:routes,logging,messagesbib:AllJARsincludingthird-partydependenciesshare:Alldocumentationabouttheapp
DefiningthesecretBeforewerunourstandaloneapplication,wewillneedtoapplyonemorechange.Weneedtochangethedefaultsecret.Locatethereactivewebstore-1.0-SNAPSHOT/conf/application.conffile.
Changethesecrettofollowingintheapplication.conffile:
play.crypto.secret="changeme"
Youwillneedtoprovideadifferentvalue.Itcanbeanything,aslongasyoudon'tcallitchangeme.Ifyoudon'tchangethis,yourapplicationwillnotbootup.Youcangetmoreinformationathttp://www.playframework.com/documentation/latest/ApplicationSecret.
Ifyoujustwanttotestthedeployfornow,let'scallitplayworks.
Now,weareallsettostarttheapplication.
RunningthestandalonedeployInordertoruntheapp,wewillusethescriptgeneratedbytheuniversaltask.Gotothereactivewebstore-1.0-SNAPSHOTdirectoryandthenrun$bin/reactivewebstore,whichwouldlooksomethinglikethis:
Now,youcanopenyourbrowserandgotohttp://localhost:9000/.
That'sit;wehaveourappupandrunning.Feelfreetotestallthefeatureswebuilt--theyallwork!
Itshouldlooksomethinglikethis:
ArchitectureprinciplesScalabilityisabouthandlingmoreusers,traffic,anddata;andinordertodoit,wewillneedtoapplysomeprinciplesandtechniques.Ourapplicationisalreadyusingthemostmoderntechniquesandtechnologies,suchasfunctionalandReactiveXprogramming,RxScala,Akkaframework,Play,andmuchmore.However,inordertoscale,wewillneedtohaveaninfrastructureinplaceandotherkindsofsystemthatwillallowustohandlemoreusers.
Agoodapplicationarchitectureshouldbecreatedaroundthefollowingprinciples:
SeparationofConcerns(SOC)(moredetailsathttps://en.wikipedia.org/wiki/Separation_of_concerns)ServiceOrientation(SOA/microservices)PerformanceScalability/Resiliency
Let'sseetheseprinciplesindetail.
Serviceorientation(SOA/microservices)Serviceorientationisabouthavingahigherlevelofabstraction,whichisalsocalledservicesormicroservices.SOAisnotaboutaspecifictechnology,butaboutprinciplessuchassharedservices,flexibility,andintrinsicoperability.IfyouwanttolearnmoreaboutSOA,checkouttheSOAManifestoathttp://www.soa-manifesto.org/.MicroservicesisaparticularflavorofSOAwherethemaindifferenceisthefocusonthegranularity,autonomy,andisolation.Ifyouwanttolearnmoreaboutmicroservices,youcancheckouthttps://www.linkedin.com/pulse/soa-microservices-isolation-evolution-diego-pachecoaswellashttp://microservices.io/.
PerformanceTherightalgorithmmightmakeyourapplicationworksmoothlyandthewrongalgorithmmightmakeyourusershaveapoorexperience.Performanceisachievedbydesign--firstofall,choosetherightsetofcollectionsandtherightsetofalgorithmsandframeworks.However,performanceneedstobemeasuredandtunedeventually.Thepracticeyoushoulddoinyourapplicationwithregardofperformanceisstresstesting.ThebeststresstestingtoolsintheScalaecosystemisGatling.Gatling(http://gatling.io/#/)allowsyoutocodeinScalausingaverysimpleyetpowerfulDSL.GatlingfocusesonHTTPandlatencypercentilesanddistributions,whichistherightthingtodonowadays.Latencyisnotonlyusedforthesakeofperformanceandscalability,butitisalsoheavilyrelatedtouserexperienceaseverythingisonline.
Scalability/ResiliencyScalabilityisoneofthemainreasonswhywedosoftwarearchitecture,becauseanarchitecturethatdoesnotscaledoesnothavebusinessvalue.WewillcontinuetalkingaboutScalabilityprinciplesduringthischapter.Resiliencyisabouthowmuchthesystemcanresistandkeepoperatingunderthemostadversesituations,suchashardwarefailureorinfrastructurefailure.Resiliencyisanoldterm.Currently,thereisanewandmoremodernandaccurateprinciplecalledantifragility.ThisprinciplewaswelldevelopedandusedinpracticebyNetflix.Antifragilityisaboutsystemsandarchitecturethatcanadaptandfailovertoothersystemsandotheroperationalmodestokeepworkingnomatterwhat.Ifyouwanttoknowmoreaboutantifragility,youcanvisithttp://diego-pacheco.blogspot.com.br/2015/09/devops-is-about-anti-fragility-not-only.htmlandhttp://diego-pacheco.blogspot.com.br/2015/11/anti-fragility-requires-chaos.html.
ScalabilityprinciplesHavingarchitecturearoundtheseprinciplesmakesitpossibletoscaleyourapplicationup.However,wewillstillneedtorelyonotherprinciplesandtechniquestoscaleit.
Thereareseveralprinciplesandtechniquesforscalability,whichareasfollows:
Verticalandhorizontalscaling(upandout)CachingProxyLoadbalancerThrottlingDatabaseclusterCloudcomputing/containersAutoScalingReactivedrivers
Verticalandhorizontalscaling(upandout)Youcanaddmoreresources,havebetterhardware,oryoucanaddmoreboxes.Thesearethetwobasicwaystoscale.Youcanalwaysimproveandtuneyourapptousefewerresourcesandgetmorefromasinglebox.Recently,therewereseveralimprovementsinthisareaaroundreactiveprogrammingthatusesfewerresourcesanddeliversmorethroughput.However,therearealwayslimitstowhichasingleboxcanprovideinsenseofscalingup,whichiswhywealwaysneedtobeabletoscaleout.
CachingDatabasesaregreat.However,thereisalatencycosttocallatraditionaldatabase.Agreatwaytofixthisishavingamemorycache,whichyoucanuseasasubsetofyourdataandgetthebenefitoffastretrieval.Playframeworkhascachesupport.Ifyouwanttolearnmore,checkouthttps://www.playframework.com/documentation/2.5.x/ScalaCache.
Thereareotheroptionsinsenseofcaching.Therearelotsofcompaniesthatusethememoryasadefinitivedatastorenowadays.Forthis,youcanconsidertoolssuchasRedis(http://redis.io/)andMemcached(https://memcached.org/).However,ifyouwanttoscaleRedisandMemcached,youwillneedsomethinglikeNetflix/Dynomite(https://github.com/Netflix/dynomite).DynomiteprovidesaclusterbasedonAWSDynamopaperforRedis,whichhasthefollowingbenefits:
HighthroughputandlowlatencyMulti-regionsupport(AWScloud)TokenawareConsistenthashingReplicationShardingHighavailability
Note
Ifyouwanttolearnmoreaboutdynomite,checkouthttps://github.com/Netflix/dynomite/wiki.
LoadbalancerAloadbalancerisakeytooltoscaleservers.So,let'ssay,youhave10boxeswithourPlayframeworkapplicationor10Dockercontainers.Wewillneedsomethinginfrontofourapplicationtodistributethetraffic.Thereareseveralserversthatcandothis,suchasNGINX(https://nginx.org/)andApacheHTTPServer(https://httpd.apache.org/).Ifyouwanttoscaleyourapplication,thisistheeasiestsolutionforit.Configurationandmoredetailscanbefoundathttps://www.playframework.com/documentation/2.5.x/HTTPServer#Setting-up-a-front-end-HTTP-server.
Loadbalancersareoftenproxyserversaswell.YoucanusethemtohaveHTTPSsupport.Ifyouwant,youcanhaveHTTPSonPlayframeworkaswell(https://www.playframework.com/documentation/2.5.x/ConfiguringHttps).KeepinmindthatyouwillneedtochangeswaggerembeddedinstallationasallthecodethatwehavepointstotheHTTPinterface.IfyouaredoingdeploysintheAWScloud,youwillneedtochangesomeoftheconfigurationtoforwardtheproxies,whichyoucanfindathttps://www.playframework.com/documentation/2.5.x/HTTPServer#Setting-up-a-front-end-HTTP-server.
ThrottlingThisisalsoknownasBackpressure.WecoveredthrottlinginChapter9,DesignYourRESTAPI.Youcangetmoredetailsthere.However,themainideaistolimittherequestforeachuser.Thisisalsoawaytomakesurethatasingleuserdoesnotstealallcomputationalresources.Thisisalsoimportantfromthesecuritypointofview,especiallyfortheservicesthatareInternet-facingoralsoknownasedge.AnothergreatwaytoprotectandhavethiscapabilityisusingNetflix/Zuul(https://github.com/Netflix/zuul).
DatabaseclusterSometimes,theproblemisnotontheapplicationside,butinthedatabase.Whenwetalkaboutscalability,weneedbeabletoscaleeverything.WeneedtohavethesameconceptsfordatabasesthatwehavefortheMid-Tier.Fordatabases,itisimportanttoworkwiththefollowing:
ClusteringIndexMaterializedviewsDatapartition
Forourapplication,weusedtheMySQLdatabase.Herearesomeresourcesthatcanhelpyouscalethedatabaseandapplythepreviousconcepts:
http://dev.mysql.com/doc/refman/5.7/en/faqs-mysql-cluster.htmlhttp://www.fromdual.com/mysql-materialized-viewshttp://dev.mysql.com/doc/refman/5.7/en/optimization-indexes.htmlhttp://dev.mysql.com/doc/refman/5.7/en/partitioning.htmlhttps://dev.mysql.com/doc/refman/5.7/en/partitioning-overview.html
Cloudcomputing/containersScalingupapplicationintraditionaldatacentersisalwayshardbecauseweneedtohavethehardwareinplace.Thisgetsdonebythepracticeofcapacityplanning.Capacityplanningisgreattomakesurewedon'tspendmoneybeyondourbudget.However,itisveryhardtogetitdoneright.Softwareishardtopredict,andthat'sagreatadvantageofthecloud.Cloudisjustanotherlevelofabstraction.Hardwareandnetworksbecomelogical,andtheyareencapsulatedbehindAPIs.Thismakesiteasiertoscaleourapplicationaswecanrelyoncloudelasticityandscaleondemandwhenweneedto.However,thearchitectureneedstobereadyforthismomentandusethetoolsandtechniquesdescribedinthischapter.Currently,thereareseveralpublicclouds;thebestoptionsareasfollows:
AWS--AmazonCloud(https://aws.amazon.com/)GoogleCloud(https://cloud.google.com/)MicrosoftAzureCloud(https://azure.microsoft.com/en-us/)
Today,Cloudisnottheonlybigelephantintheroom.WealsohavetheLinuxcontainers,suchasDocker(https://www.docker.com/)andLXC(https://linuxcontainers.org/).Containersprovideanotherlevelofabstraction,andtheycanrunonthecloudoronpremises.Thismakesyourapplicationmoreportableandalsomorecosteffective.Containersalsoscale.Themainadvantagearoundcontainersisspeedandflexibility.It'swayfastertobootupacontainerincomparisonwithavirtualizedimageinanypubliccloud.Theyarealsoportableandcanruneverywhere.
AutoScalingCurrently,thisisoneofthegreatestresourcesofcloudcomputing.Basically,youcandefineabaseimage,whichisastateofanoperationalsystemsuchasLinux,andthecloudwillcreateanddestroyinstancesforyouondemand.Theseinstancescanbecreatedbytheincreaseincomputationalresources,suchasmemory,CPU,network,orevenbasedoncustomrules.Thisisthekeyconcerninordertohaveelasticity.IfyouwanttolearnmoreaboutAutoScaling,checkouthttps://aws.amazon.com/autoscaling/.
AnoteaboutautomationInordertouseallthesetechniquesandtechnologiesatscale,weneedtohavefullautomation(https://en.wikipedia.org/wiki/List_of_build_automation_software)becauseitisimpossibletohandleallthiswithmanualwork.Whenweareusingthecloudorcontainers,thereisnootherwayaround;everythingneedstobeautomated.Thereareseveraltoolsthathelpusachievethisgoal,suchasAnsible(https://www.ansible.com/).
Don'tforgetabouttelemetryWhenyouhaveallinfrastructuresinplace,youwillalsoneedtohavemonitoring,alerting,andproperdashboards.Thereareplentyofgreattoolsforcontainersandpublicclouds,suchasSensu(https://sensuapp.org/)andPrometheus(https://prometheus.io/).
ReactiveDriversanddiscoverabilityReactiveDrivers:WetalkedalotanddidalotofreactivecodeusingPlayframeworkandRxScala.However,tohavefullbenefitsofReactiveXprogramming,youneedtomakesureeverythingisanon-blockingIOandreactive.Inotherwords,weneedtohaveallofourdriversreactive.SlickisgreatbecauseitgivesusreactivitywiththeMySQLdatabase.Wewillneedtoapplythesameprincipleseverywherewehaveadriverorconnectionpoint.Therearelotsoflibrariesbecomingreactivethesedays.Forinstance,ifyouwanttocacheusingRedis,youcanuseLettuce(https://github.com/mp911de/lettuce),whichisreactive.
Whenweworkwithmicroservices,wetendtohavehundredsofmicroserviceinstances.Thesemicroserviceswillrunoncontainersand/orcloudcomputingunits.Youcan'tpointtospecificIPsbecausethecodewillnotbemanagedandwillnotsurviveinacloud/containerenvironment.Cloud/containerinfrastructureisephemeral,andyoudon'tknowwhenaninstancewillbeterminated.That'swhyyouneedtobeabletoswitchtoanotheravailabilityzoneorregionatanymoment.
Therearetoolsthatcanhelpusapplythesechangesinourcode.ThesetoolsareNetflix/Eureka(https://github.com/Netflix/eureka)andConsul(https://www.consul.io/),orevenApacheZookeeper(https://zookeeper.apache.org/).Eurekahasoneadvantage--itiseasiertouseandhastoolsaroundtheJVMecosystem,whichwasbattletestedbyNetflix.
EurekaisacentralregistrywheremicroservicesregistertheirIPandmetadata.EurekahasaRESTAPI.MicroservicescanusetheEurekaAPItoqueryandsearchexistingapplications.Eurekacanruninamulti-vpc/multi-regionenvironment.ThereareotherJVMcomponents,suchasribbon(https://github.com/Netflix/ribbon)andkaryon(https://github.com/Netflix/karyon),whichcanautomaticallyregisterandretrieveeurekainformationandmetadata.
BasedontheEurekainformation,youcanperformmicroserviceloadbalancingandfailovertootheravailabilityzonesandregionsautomatically.WhyuseEurekaifIcanuseDNS?DNSforMid-TierloadbalancingisnottherightchoiceasDNSisnotflexibleandthetimeoutisquitebig.Ifyouwantknowmoreaboutdiscoverability,checkouthttp://microservices.io/patterns/service-registry.html.
Eurekaoverview-EurekaarchitectureoverviewontheAWScloud
Asyoucanseeintheprecedingdiagram,youwilldeploytheEurekaserveratleastinthreeAvailabilityZones(AZs)inordertohaveavailability.Then,Eurekadatawillbereplicatedtoeachserver.OurapplicationsormicroserviceswillregisterinEureka,andotherapplications/microservicescanretrievethismetadata,suchasIPaddress,tothemtotheRESTcalls.Ifyouwantlearnmore,youcancheckouthttps://github.com/Netflix/eureka/wiki/Eureka-at-a-glance.
Mid-Tierloadbalancer,timeouts,Backpressure,andcachingEureka,Zookeeper,orConsulareonlyonepartoftheequation.WestillneedsomesoftwareontheclientsidethatcanusetheEurekainformationinordertodoMid-Tierloadbalancing,failover,andcaching.TheNetflixstackhasacomponentforthat,whichiscalledribbon(https://github.com/Netflix/ribbon).Withribbon,youcanautomaticallyresolvethemicroserviceIPsfromEureka,doretries,andfailovertootherAZsandregions.Ribbonhasacacheconcept;however,itisonpreloadedcache.
Ribbonideasaresimple.Thegreatthingaboutribbonisthateverythingisreactive,andyoucanuseRxJavaandRxScalainordertoworkwiththestack.Ifyoudon'twanttouseribbon,youcanstillcreateasimpleintegrationlayerwithScalaandperformthesameconcerns,suchasloadbalancing,failover,andcaching.
WhataboutBackpressure?BackpressurecanbedonewithRxJavaandRxscala,andyouwillbeabletodoitontheclientsideaswell.YoucanlearnmoreaboutBackpressureinRxathttps://github.com/ReactiveX/RxJava/wiki/Backpressure.
So,ifIhaveclient-sideloadbalancing,failover,caching,andBackpressure,amIgoodtogo?Yes,youare;however,wecanalwaysdobetter.Workingwithmicroservicesisnoteasyaseverythingisaremotecall,andremotecallscanfail,hang,ortimeout.Theseconsarehardanddangerousifnotmanagedwell.Thereisanothersolutionthatcanhelpusalotwiththisconcept;itiscalledHystrix(https://github.com/Netflix/Hystrix).
HystrixisalibraryfortheJVMdesignedforlatencyandfaulttoleranceprotection.Ataglance,Hystrixisawrapperaroundanyremotecodethatcantaketimeorgowrong.
Hystrixhasthreadisolationandprovidesadedicatedthreadpoolforeachresource.Thisisgreatbecauseitpreventsyoufromrunningoutofresources.Ithasanexecutionpatterncalledcircuitbreaker.Circuitbreakerwillpreventrequestsfromtearingdownthewholesystem.Additionally,ithasadashboardwherewecanvisualizethecircuits,so,atruntime,wecanseewhat'sgoingon.Thiscapabilityisgreatnotonlyforsenseoftelemetry,butalsobecauseitiseasytotroubleshootandvisualizewheretheproblemis.
Itcanbefurtherexplainedwiththehelpofthefollowingflowchart:
ThecodeyouwanttoprotectwillbearoundaHystrixcommand.Thiscommandcanbemanipulatedinsyncorasyncprogrammingmodels.ThefirstthingthatHystrixwillcheckisifthecircuitisclosed,whichisgood,andhowitshouldbe.Then,itchecksiftherearethreadsavailableforthatcommand,andifthereareavailablethreads,thenthecommandwillbeexecuted.Ifthisfails,ittriestogetafallbackcode,whichisasecondoptionthatyoucanprovideincaseoffailure.Thisfallbackshouldbestatic;however,youcanbeloadingdatainthebackgroundandthenreturnonthefallback.AnotheroptionisfallbacktootherAZorRegion.
FollowingisasnapshotofhowaHystrixdashboardcircuitbreakerviewwouldwork:
Intheprecedingimage,wecanseetheHystrixdashboardsample,wherewecanvisualizecriticalinformation,suchassuccessanderrorrateandifthecircuitisopenorclosed.IfyouwantlearnmoreabouttheHystrixdashboard,checkouthttps://github.com/Netflix/Hystrix/wiki/Dashboard.
ScalingupmicroserviceswithanAkkaclusterOurapplicationalsousesAkka.InordertoscaleAkka,wewillneedtouseanAkkacluster.TheAkkaclusterallowsustoclusterizeseveralActorsystemsinseveralmachines.IthasspecialActorroutersthatareclusteraware,andwecanusetheseActorstorouterequeststothewholecluster;moredetailscanbefoundathttp://doc.akka.io/docs/akka/2.4.9/java/cluster-usage.html#Cluster_Aware_Routers.
TheAkkaclusterprovidesmembershipprotocolandlifecycle.Basically,wecanbenotifiedbytheclusterwhenanewmemberjoinsorwhenamemberleavesthecluster.Giventhiscapability,itispossibleforustocodeascalablesolutionaroundthesesemantics.Asweknowwhenamemberjoins,wecandeploymorenodes,andwecanalsodropnodesondemand.
AsimplesamplewouldbetocreateanActorcalledfrontend,andwhenweseethisActor,wecoulddeploythreebackendActorsacrossthecluster.IfthefrontendActorleaves,wecouldundeploytheotherActors.AllthislogiccanbearchivedusingthemembershipprotocolandclusterseventsthatAkkageneratesforus.AfrontendActorisanotaUIorwebapplication,itisjustanActorthatreceiveswork.So,let'ssaywewanttogenerateanalyticsaroundourproductscatalog.WecouldhaveafrontendActorwhoreceivesthatrequestanddelegatestheworktobackendActors,whichwillbedeployedacrosstheclusteranddelivertheanalyticalwork.
ThefollowingimageistheprocessviewofanAkkaclustermembershipprotocol:
Asyoucanseeintheprecedingimage,thereisasetofstates.Firstofall,thenodeisjoining
thecluster;thenthenodecanbeup.Oncethenodeisup,itcanleavethecluster.Thereareintermediatestates,suchasleavingandexisting.
TheAkkaclusterprovidesmanyoptionstoscaleourActorsystem.AnotherinterestingoptionistousethepatternofdistributedPub/Sub.IfyouarefamiliarwithJMSTopics,itisalmostthesameidea.Forthosewhoarenotfamiliar,youcancheckouthttp://doc.akka.io/docs/akka/2.4.9/java/distributed-pub-sub.html.
Note
IfyouwantlearnmoreabouttheAkkacluster,youcancheckouthttp://doc.akka.io/docs/akka/2.4.9/common/cluster.html.
ScalinguptheinfrastructurewithDockerandAWScloudScalingupwiththeAWScloudiseasy,asatanymoment,withasimpleclickontheAWSconsole,youcanchangethehardwareandusemorememory,CPU,orbetternetwork.Scale-outisnothard;however,weneedtohavegoodautomationinplace.ThekeyprincipletoscaleistohavetheAutoScalinggroupsinplacewithgoodpolicies.Youcanlearnmoreaboutitathttp://docs.aws.amazon.com/autoscaling/latest/userguide/policy_creating.html.
Thereareotherinterestingservicesandcomponentsthatcanhelpyouscaleyourapplication.However,youwillneedtokeepinmindthatthiscanleadtocoupling.TheITindustryismovingtowardthecontainerdirectionbecauseitisfaster,andit'seasytodeployinotherpublicclouds.
WecanscaleoutwithDockeraswell,becausethereareclustermanagersthatcanhelpusscaleourcontainers.Currently,thereareseveralsolutions.Inthesenseofcapabilitiesandmaturity,thefollowingarethebettersolutions:
DockerSwarm(https://docs.docker.com/swarm/overview/)Kubernetes(http://kubernetes.io/)ApacheMesos(http://mesos.apache.org/)
DockerSwarm:ThisisaclusterforDocker.DockerSwarmisveryflexibleandintegrateswellwithotherDockerecosystemtools,suchasDockermachine,Dockercompose,andConsul.Itcanhandlehundredsofnodes,andyoucanlearnmoreaboutthemathttps://blog.docker.com/2015/11/scale-testing-docker-swarm-30000-containers/.
Kubernetes:ThiswascreatedbyGoogle,anditisafullsolutionfordevelopmentautomation,operation,andscalingDockercontainers.TheKubernetesclusterhastworoles,amasternodethatcoordinatesthecluster,schedulesapplications,andkeepsapplicationsonadesiredstate;andtherearenodes,thatareworkersthatrunapplications.Itcanhandlehundredsofcontainersandscaleverywell.Tolearnmoreaboutit,checkouthttp://blog.kubernetes.io/2016/03/1000-nodes-and-beyond-updates-to-Kubernetes-performance-and-scalability-in-12.html.
ApacheMesos:ThiswascreatedbyTwitter.Itisveryinteresting,asyoucanrunabaremetalonapremisesdatacenteroronapubliccloud.MesosallowsyoutouseDockercontainersaswell.Ifyouwanttolearnmoreaboutmesos,checkoutthefollowingpaper:
http://mesos.berkeley.edu/mesos_tech_report.pdf
SummaryInthischapter,youlearnedhowtodeployyourPlayframeworkapplicationasastandalonedistribution.Additionally,youlearnedseveralarchitecturalprinciples,techniques,andtools,tohelpyouscaleoutyourapplicationtothousandsofusers.
Withthis,wealsoreachtheendofthisbook.Ihopeyouenjoyedthisjourney.WebuiltaniceapplicationusingScala,PlayFramework,Slick,REST,Akka,Jasper,andRxScala.Thankyouforyourtime.IwishyouthebestinyourcodingcareerwiththeScalalanguage.