Entwickeln für die Cloud

Ein Beispielprojekt auf der Google Cloud Platform

Magdeburger Developer Days 2017 - Hendrik Pilz

Disclaimer

Ich werde nicht von Google bezahlt oder unterstützt.

Just Code 4 Fun & Profit ;-)

Cloud?

  • SaaS - Software as a Service
  • IaaS - Infrastructure as a Service
  • PaaS - Platform as a Service

Google Cloud Platform

Google App Engine

  • PaaS-Angebot
  • Sprachen: Node.js, Ruby, C#, Go, Python, PHP, Java oder eigener Docker-Container
  • Freies Kontingent:
    1 GB Code + Static Files
    28 instance-hours / Tag

Standard vs. Flexible Environment

Standard Flexible
stable beta
Java 7 Java 8
Google Container Docker Container
Java Servlet 2.5 individuell

Google Cloud Storage

  • Blob-Storage
  • Nicht zu verwechseln mit "Blobstore"
  • Freies Kontingent: 5 GB

Google Cloud Datastore

Mehr zu Kontingenten und Preisen

Die Beispiel-Anwendung

  • GAECMS - ein Blog / CMS für die Google App Engine
  • Frontend mit JSP, HTML, CSS, JS
  • Content & Media Library mit Cloud Storage als Speicherort
  • Cloud Datastore als Datenbank

DEMO

Projekt erstellen

Projekt erstellen

Projektname festlegen
Projektname festlegen

Projekt erstellen

Projekt Dashboard
Projekt Dashboard

App Engine Anwendung

Anwendung erstellen
Anwendung erstellen

App Engine Anwendung

Sprache wählen
Sprache wählen

App Engine Anwendung

Region wählen
Region wählen

App Engine Einstellungen

Einstellungen
Einstellungen

Cloud Storage Browser

Bucket-Inhalt anzeigen
Bucket-Inhalt anzeigen

Projekt-Struktur

/src/main/java -> Java Code
/src/main/webapp -> JSPs
/src/main/webapp/WEB-INF/appengine-web.xml
/src/main/webapp/WEB-INF/web.xml
/src/test/java -> Unit Tests
/pom.xml -> Maven-Konfiguration
                    

<?xml version="1.0" encoding="utf-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
    <application>gaecms-demo</application>
    <version>1</version>
    <threadsafe>true</threadsafe>
    <sessions-enabled>true</sessions-enabled>
    <system-properties>
        <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
    </system-properties>
</appengine-web-app>
                    

WEB-INF/appengine-web.xml
cloud.google.com/appengine/docs/standard/java/config/appref


.level = INFO
                    

WEB-INF/logging.properties

Dependencies

groupId:artifactId:version

  • com.googlecode.objectify:objectify:5.1.15
  • com.google.appengine:appengine-api-1.0-sdk:1.9.46*
  • com.google.cloud:google-cloud-storage:1.0.1
  • ...

Test Dependencies

groupId:artifactId:version

  • com.google.appengine:appengine-testing:1.9.46*
  • com.google.appengine:appengine-api-labs:1.9.46*
  • com.google.appengine:appengine-api-stubs:1.9.46*
  • com.google.appengine:appengine-tools-sdk:1.9.46*
  • ...

<build>
    [...]
    <plugin>
        <groupId>com.google.appengine</groupId>
        <artifactId>appengine-maven-plugin</artifactId>
        <version>1.9.46</version>
        <configuration>
            <jvmFlags>
                <jvmFlag>-Xdebug</jvmFlag>
                <jvmFlag>-Xrunjdwp:transport=dt_socket,address=1044,server=y,suspend=n</jvmFlag>
            </jvmFlags>
            <disableUpdateCheck>true</disableUpdateCheck>
        </configuration>            
    </plugin>                        
    [...]
</build>
                    

Maven Build

Wichtige Maven Goals

  • appengine:devserver_start
  • appengine:devserver_stop
  • appengine:update

Benutzer-Authentifizierung

  • basierend auf Google-Account
  • Zustände:
    • nicht authentifiziert
    • authentifiziert
      • Administrator
      • kein Administrator
  • Weitere Rollen & Berechtigungen: TODO

<web-app [...]>
    [...]
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>admin</web-resource-name>
            <url-pattern>/admin/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>    
    [...]
</web-app>                 
                    

Geschützter Bereich in web.xml


<% if (request.isUserInRole("admin")) {
        UserService userService = UserServiceFactory.getUserService();
%>
<li><a href="/admin/Write">Write Blog</a></li>
<li><a href="/admin/MediaLibrary">Media Library</a></li>
<li><a href="<%= userService.createLogoutURL("/")%>">Logout</a></li>
<% }%>
                    

Geschützter Bereich in JSP-Datei

Zugriff auf den Google Cloud Datastore


public class ObjectifyHelper implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ObjectifyService.register(BlogEntry.class);
        ObjectifyService.register(Comment.class);
        ObjectifyService.register(MediaFile.class);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // do nothing
    }

}                            
                    

ObjectifyHelper registriert Entity-Klassen


<web-app [...]>
    [...]
    <listener>
        <listener-class>de.hepisec.gaecms.ObjectifyHelper</listener-class>
    </listener>
    <filter>
        <filter-name>ObjectifyFilter</filter-name>
        <filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ObjectifyFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>                 
    [...]
</web-app>
                    

Objectify in web.xml aktivieren


@Entity
public class BlogEntry {
    @Id
    private String id;
    @Index
    private Date dateCreated = new Date();
    @Index
    private Date datePublished = null;
    @NotNullOrEmpty
    private String title;
    @NotNullOrEmpty
    private String text;
    @Load
    private List<Ref<Comment>> comments;
    
    /** Getter, Setter etc. **/
}
                    

Objectify Entity
github.com/objectify/objectify/wiki/Entities


public List<Ref<Comment>> getCommentsRef() {
    return comments;
}

public List<Comment> getComments() {
    List<Comment> commentObjects = new ArrayList<>();

    for (Ref<Comment> ref : comments) {
        commentObjects.add(ref.get());
    }

    return commentObjects;
}

public void setComments(List<Ref<Comment>> comments) {
    this.comments = comments;
}                        
                    

Getter / Setter für List<Ref<Comment>> comments;

Queries mit Objectify

Code sagt mehr als 1000 Worte

Google Cloud Storage

  • Blobs werden in Buckets gespeichert
  • Default- / Staging-Bucket = 5 GB frei
  • Weitere Buckets: $$$
  • Staging-Bucket wird wöchentlich geleert
  • SDK greift auf echte Buckets zu

$ ./google-cloud-sdk/install.sh 
$ gcloud config set project gaecms-demo
$ gcloud auth application-default login                        
                    

Installation und Authentifizierung des Google Cloud SDK


private String getContent() {
    Storage storage = StorageOptions.getDefaultInstance().getService();

    try {
        BlobId blobId = BlobId.of(getBucket(), id);

        if (null == storage.get(blobId)) {
            return "";
        }

        return new String(storage.readAllBytes(blobId), "UTF-8");
    } catch (StorageException | UnsupportedEncodingException ex) {
        Logger.getLogger(EditableContent.class.getName()).log(Level.WARNING, ex.getMessage(), ex);
        return "";
    }
}                        
                    

Cloud Storage lesen


Storage storage = StorageOptions.getDefaultInstance().getService();

BlobId blobId = BlobId.of(getBucket(), id);
byte[] content = pContent.getBytes("UTF-8");
BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType("text/plain").build();

try (WriteChannel writer = storage.writer(blobInfo)) {
    writer.write(ByteBuffer.wrap(content, 0, content.length));
}                        
                    

Cloud Storage schreiben


<cms:EditableContent id="impressum" mode="inline" />
<cms:EditableContent id="datenschutz" mode="markdown" />
<cms:EditableContent id="html-text" mode="html" />
<cms:EditableContent id="plain-text" mode="plain" />
                    

Debugging & Tests

  • Debugging mit dem Development Server
  • Das SDK enthält einen embedded Datastore

DEMO


public class BlogEntryControllerTest {    
    private final LocalServiceTestHelper helper = new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
    protected Closeable session;
        
    @BeforeClass
    public static void setUpClass() {
        ObjectifyService.setFactory(new ObjectifyFactory());
    }
        
    @Before
    public void setUp() {
        session = ObjectifyService.begin();
        ObjectifyService.register(BlogEntry.class);
        helper.setUp();
    }
    
    @After
    public void tearDown() {
        session.close();
        helper.tearDown();
    }                        
    /** Tests **/
}
                    

Fork on GitHub

Vielen Dank!

@HendrikPilz

www.hepisec.de

Diese Präsentation ist hier verfügbar
www.hepisec.de/devdays-2017/