Projekt: git gradle spring-boot

http://git.e-strix.com/sample_fx_app.git/

W każdym (nawet freelancer’skim) projekcie przychodzi moment na to aby zacząć pisać kod i tu już dostajemy wiatr w żagiel i stopy wody pod kilem. W zasadzie ogranicza nas tylko lenistwo z jednej strony i kreatywność z drugiej.

Dla potrzeb prezentacji wybiorę sobie coś na czasie, a będzie to:

  • spring-boot
  • gradle
  • git
  • javafx

A zatem bez zbędnych patosów, teorii i literatury zacznijmy od stworzenia kilku plików.

Tworzenie projektu

Zacznijmy od utworzenia repozytorium. Jeśli nie widziałeś mojego wpisu na ten temat to zobacz pierw: php7-git-gitlist.

A teraz do rzeczy. Struktura projektu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
└───javafx-git-sample
    │     .gitignore
    │     build.gradle
    │     latest.properties
    │     settings.gradle
    │
    ├───gradle
    │        release.gradle
    │
    └───src
        └───main
            ├───java
            │    └───pl
            │        └───estrix
            │            └───javafx
            │                    FilterProp.java
            │                    JavaApplication.java
            │                    MainController.java
            │
            └───resources
                │     logo.png
                │     main.fxml
                │
                └───META-INF
                        spring.factories

Plik .gitignore

1
2
3
4
.gradle/
.idea/
build/
*.iml

Plik build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
buildscript {
    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.1.RELEASE")
    }
}

apply from: 'gradle/release.gradle'
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'


bootJar {
    mainClassName = 'pl.estrix.javafx.JavaApplication'
    manifest {
        attributes 'Start-Class': 'pl.estrix.javafx.JavaApplication'
    }
}

springBoot {
    mainClassName = 'pl.estrix.javafx.JavaApplication'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter")
    testCompile("junit:junit")
}


def buildTime() {
    new Date().format('yyyy.MM.dd HH:mm')
}

def generatedResources = "$buildDir/resources/main/"
sourceSets {
    main {
        output.dir(generatedResources, builtBy: 'generateMyResources')
    }
}

task generateMyResources {
    doLast {
        def generatedTmp = new File(generatedResources)
        if( !generatedTmp.exists() ) {
            generatedTmp.mkdirs()
        }
        def generated = new File(generatedResources, "filter.properties")

        generated.text = "estrix.application.biuld-time=${buildTime()}\n" +
                "estrix.application.name=${project.name}\n" +
                "estrix.application.version=${project.version}\n"
    }
}

Plik latest.properties

1
2
3
4
#Tue Apr 10 16:59:22 CEST 2018
bugfix=0
major=1
minor=0

Plik settings.gradle

1
rootProject.name = 'JavaFxGitSample'

Plik gradle/release.gradle

Notatka: Pomysł na zarządzanie wersjami pochodzi z https://www.youtube.com/watch?v=Y6SVoXFsw7I

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class Version {
    int major
    int minor
    int bugfix
    boolean snapshot = true

    static Version load(File versionFile){
        def props = new Properties()
        versionFile.withInputStream {
            stream -> props.load(stream)
        }
        def major = Integer.parseInt(props['major'])
        def minor = Integer.parseInt(props['minor'])
        def bugfix = Integer.parseInt(props['bugfix'])
        return new Version(major:major, minor:minor, bugfix:bugfix)
    }

    String toString(){
        "${major}.${minor}.${bugfix}${snapshot ? '-SNAPSHOT':''}"
    }

    void store(File versionFile){
        def props = new Properties()
        props['bugfix'] =  bugfix.toString()
        props['major']  =  major.toString()
        props['minor']  =  minor.toString()
        props.store(versionFile.newWriter(), null)
    }

    Version next(String releaseType = "bugfix"){
        if(releaseType == "major"){
            return new Version(major:major+1, minor:0, bugfix:0)
        }else if(releaseType == "minor"){
            return new Version(major:major, minor:minor+1, bugfix:0)
        }else{
            return new Version(major:major, minor:minor, bugfix:bugfix+1)
        }
    }
}

def latest = Version.load(rootProject.file("latest.properties"))
allprojects{
    version = latest.next()
}
gradle.taskGraph.whenReady { graph ->
    if(graph.hasTask(":release")){
        allprojects{
            version = latest.next(release.type)
            version.snapshot = false
        }
    }
}

task persistVersionInfo(type:Exec){
    commandLine "git" , "commit","-i", "latest.properties", "-m", "' update version to ${-> version }'"
    doFirst{
      version.store(file("latest.properties"))
   }
}

task tagWorkspace(type:Exec){  
   commandLine 'git', 'tag', "${-> version }"
   commandLine 'git', 'push', 'origin',  'HEAD:master'
}

task release(type:Release){
    type = System.getProperty("type")
    dependsOn  "build", "persistVersionInfo", "tagWorkspace"
}

import org.gradle.api.internal.tasks.options.Option

class Release extends DefaultTask{
    @Option(option="type", description="the type of a release.")
    String type = "bugfix"

    @TaskAction void release(){
        println "doing a $type release of ${project.version}"
    }
}

Plik src/main/java/pl/estrix/javafx/FilterProp.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package pl.estrix.javafx;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties("filter.properties")
public class FilterProp {

    @Value("${estrix.application.name}")
    private String name;
    @Value("${estrix.application.version}")
    private String version;
    @Value("${estrix.application.biuld-time}")
    private String biuldTime;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public String getBiuldTime() {
        return biuldTime;
    }

    public void setBiuldTime(String biuldTime) {
        this.biuldTime = biuldTime;
    }
}

Plik src/main/java/pl/estrix/javafx/JavaApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package pl.estrix.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@SpringBootApplication
@ComponentScan("pl.estrix.javafx")
@PropertySource("classpath:filter.properties")
@Configuration
public class JavaApplication extends Application {

    private ConfigurableApplicationContext context;
    private Parent rootNode;

    public static void main(final String[] args) {
        Application.launch(args);
    }

    @Override
    public void init() throws Exception {
        SpringApplicationBuilder builder = new SpringApplicationBuilder(JavaApplication.class);
        context = builder.run(getParameters().getRaw().toArray(new String[0]));

        FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"));
        loader.setControllerFactory(context::getBean);
        rootNode = loader.load();

    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setScene(new Scene(rootNode, 480, 320));
        primaryStage.centerOnScreen();
        primaryStage.show();
    }

    @Override
    public void stop() throws Exception {
        context.close();
    }

}

Plik src/main/java/pl/estrix/javafx/MainController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package pl.estrix.javafx;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MainController {

    @Autowired
    private FilterProp filterProp;

    @FXML
    private Label propName;
    @FXML
    private Label propVersion;
    @FXML
    private Label propBuildTime;

    @FXML
    public void initialize() {
        propName.setText(filterProp.getName());
        propVersion.setText(filterProp.getVersion());
        propBuildTime.setText(filterProp.getBiuldTime());
    }
}

Plik src/main/resources/main.fxml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.Cursor?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="320.0" prefWidth="480.0" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="pl.estrix.javafx.MainController">
   <children>
      <HBox alignment="CENTER" style="-fx-background-color: #ffffff;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
         <children>
            <AnchorPane prefHeight="320.0" prefWidth="255.0" HBox.hgrow="ALWAYS">
               <children>
                  <ImageView fitHeight="320.0" fitWidth="255.0" pickOnBounds="true" preserveRatio="true" AnchorPane.leftAnchor="0.0">
                     <image>
                        <Image url="@logo.png" />
                     </image>
                  </ImageView>
               </children>
            </AnchorPane>
            <VBox prefHeight="320.0" prefWidth="217.0" style="-fx-background-color: #ffffff;" HBox.hgrow="ALWAYS">
               <children>
                  <Label alignment="CENTER" contentDisplay="CENTER" maxWidth="1.7976931348623157E308" text="e-Strix.com" textAlignment="CENTER">
                     <VBox.margin>
                        <Insets bottom="10.0" top="10.0" />
                     </VBox.margin>
                  </Label>
                  <GridPane>
                    <columnConstraints>
                      <ColumnConstraints hgrow="SOMETIMES" maxWidth="106.0" minWidth="10.0" prefWidth="58.0" />
                      <ColumnConstraints hgrow="SOMETIMES" maxWidth="163.0" minWidth="10.0" prefWidth="163.0" />
                    </columnConstraints>
                    <rowConstraints>
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                      <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
                    </rowConstraints>
                     <children>
                        <Label text="Name" />
                        <Label text="Version" GridPane.rowIndex="1" />
                        <Label text="Buil Time" GridPane.rowIndex="2" />
                        <Label fx:id="propName" text="Label" GridPane.columnIndex="1" />
                        <Label fx:id="propVersion" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="1" />
                        <Label fx:id="propBuildTime" text="Label" GridPane.columnIndex="1" GridPane.rowIndex="2" />
                     </children>
                  </GridPane>
               </children>
            </VBox>
         </children>
         <cursor>
            <Cursor fx:constant="SW_RESIZE" />
         </cursor>
      </HBox>
   </children>
</AnchorPane>

Plik src/main/resources/META-INF/spring.factories

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
pl.estrix.javafx.JavaApplication

Plik src/main/resources/logo.png

1
wget http://e-strix.com/wp-content/uploads/2018/04/logo.png -P src/main/resources/

Budowa lokalna

Uruchamianie

1
gradle clean generateMyResources bootRun

Budowanie jar

1
gradle clean generateMyResources bootJar

Uruchamianie

1
java -jar build\libs\JavaFxGitSample-1.0.1-SNAPSHOT.jar

No dobra, teraz wiemy jak zbudować aplikację. Ale docelowo ma to robić Ci, zatem zabieramy się za konfigurację job’ów.

Konfiguracja Jenkins

Budowa zdalna

Jenkins new project

W polu: Repository URL

git@git.e-strix.com:repositories/javafx-git-sample.git

W polu: Repository browser

gitlist

W polu: Repository browser -> URL

git.e-strix.com

W polu: Budowanie -> Invoke Gradle -> Version

Gradle_4_6

W polu: Budowanie -> Tasks

clean generateMyResources bootJar

 

Podnoszenie wersji

Kroki takie jak w przypadku „Budowa zdalna” z tą różnicą że w:

W polu: Budowanie -> Tasks

clean release --type minor

Gdzie wartość –type przyjmuje wartości:

  • major
  • minor
  • bugfix – domyślny

Publikacja na Landing Page

W polu: Budowanie -> Execute Shell -> Command

mkdir -p release_lib

rm -f ./release_lib/*

cp ${WORKSPACE}/../javafx-git-sample-release/build/libs/*.jar ./release_lib/

rm -f /home/appuser/public_html/download/*.jar

cp ./release_lib/*.jar /home/appuser/public_html/download/

 

Zakończenie

Jak widzisz storzenie projektu jest dość proste. Mimo że musiałem kilka godzin spędzić na odnalezieniu odpowiedniej dla mojego projektu ścieżki rozwoju. Przetestowałem kilka rozwiązań, pluginów i stwierdziłem że jednak najlepsze rozwiązanie to będzie ręczne napisanie skryptu (mimo że zapożyczyłem rozwiązanie od mądrych ludzi z internetu).

Landing Page dla naszego projektu

Wstęp

Dziś zajmiemy się promocją naszego przyszłego projektu. Do tego najlepiej wykorzystać coś prostego, no dobra ja jestem z tych co nie lubią się męczyć ze stroną internetową. W sumie to narzędzie nie powinno sprawiać problemów, nie ma się co szarpać jeśli nie mamy ludzi od promocji.

Zatem weźmy się do roboty, strona produktu…

I jeszcze raz od początku, zacznijmy od serwera Apache.

Apache + PHP

Już to przerabialiśmy mnóstwo razy, ale i tak zrobimy sobie jeszcze raz, ale trochę inaczej. Zatem do dzieła. Aktualizujemy repozytoria:

apt-get install python-software-properties software-properties-common
LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php
apt-get update

i dalej serwer:

apt-get install apache2 php7.2 php7.2-fpm libapache2-mod-php7.2

I konfigurujemy. Wchodzimy do katalogu stron:

cd /etc/apache2/sites-available/

I tworzymy naszą konfigurację:

vim app.e-strix.com.conf

dalej, uzupełniamy zawartość pliku:

<VirtualHost *:80>
 ServerAdmin admin@e-strix.pl
 ServerName app.e-strix.com
 DocumentRoot /var/www/app.e-strix.com/public_html

 <Directory "/var/www/app.e-strix.com/public_html/">
   AllowOverride All
   Order allow,deny
   Allow from all
  </Directory>


 ErrorLog /var/www/app.e-strix.com/error.log
 CustomLog /var/www/app.e-strix.com/access.log combined
</VirtualHost>

Dodajemy katalog:

mkdir -p /var/www/app.e-strix.com

Teraz dodamy użytkownika i dodamy do grupy www-data:

adduser appuser
usermod -a -G appuser www-data

Tworzymy katalog publc_html w katalohu użytkowanika:

mkdir -p /home/appuser/public_html/

Teraz zrobimy coś nowego, czyli link symboliczny:

ln -s /home/appuser/public_html /var/www/app.e-strix.com

Po co nam to? O tym później…

A teraz niespodzianka, coś nowego. Postawimy sobie serwer FTP’a.

FTP

Po co on nam jest? A no po to by WordPress sam sobie dociągał pluginy czy też aktualizacje. Na początku wspominałem o chęci pozbycia się problemów a to rozwiązanie daruje nam wiele prac administracyjnych.

Teraz instalujemy na VPS’ie demona:

apt-get install vsftpd

Teraz zróbmy kopię zapasową oryginalnego pliku z konfiguracją:

mv /etc/vsftpd.conf /etc/vsftpd.conf.orig

I teraz potrzebujemy własnej konfiguracji, czyli tworzymy nasz plik:

vim /etc/vsftpd.conf

Z zawartością:

listen=YES
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=022
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
chroot_local_user=YES
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
allow_writeable_chroot=YES
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=50000
seccomp_sandbox=NO
user_sub_token=$USER
local_root=/home/$USER/public_html
userlist_enable=YES
userlist_file=/etc/vsftpd.userlist
userlist_deny=NO

Notatka: Zwróć uwagę że kopiowanie całości ustawień doda specyficzny znak końca linii, i wtedy serwer może się nie uruchomić.

Teraz zaczyna się już wszystko układać w całość.

Dodajmy teraz użytkownika do listy vsftpd.userlist.

echo "appuser" | tee -a /etc/vsftpd.userlist

I restartujemy naszego daemona:

service vsftpd restart

 I tu druga ciekawostka, czyli baza danych.

MySQL

No dobra, zajmijmy się dodaniem bazki. Tutaj też coś mało problemowego. Trzymam się firmy Oracle z większością produktów, dlatego małe projekty stawiam na bazach MySQL. Możesz się sprzeczać o to że są lepsze, że np. Postgresql jest o niebo lepsze, że bla bla bla. Uwierz mi, że źle utworzone zapytanie zarżnie każdą bazę, lub też przechowywanie zbyt dużej ilości danych archiwalnych przymuli każdy filesystem, a nam chodzi tu o prostotę i dostępność.

Zaczynamy oczywiście od instalacji:

apt-get install mysql-server php7.2-mysql

W trakcie tego procecsu, musimy mieć ustalone hasło do root. I w zasadzie tyle. Po zakończonym procesie instalacji możemy utworzyć bazę dla naszego CMS’a i użytkownika z którego będzie się ten CMS łączył do bazki.

Notatka: Nie łączymy się do bazy za pośrednictwem użytkownika root! W przyszłości pokażę dlaczego.

A zatem wchodzimy do MySQL’a z użytkownika root:

mysql -u root -p

I po wpisaniu hasła przystępujemy do tworzenia naszej bazy:

CREATE DATABASE app_e_strix_com;

Mamy bazę, więc dodamy użytkownika:

CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'password';

Nazwa użytkownika bazy jest zbieżna z nazwą użytkownika systemy, ale nie jest zależna. Mamy dowolność w dodawaniu nazw oraz haseł 😉

Teraz nadamy prawa do bazy. Nie chcemy aby żaden problemik pozamiatał nam tabele więc nie damy pełnych praw:

GRANT CREATE,DELETE,INSERT,SELECT,UPDATE ON app_e_strix_com.* TO appuser@localhost;

Notatka: Delete jest dyskusyjne, możemy w prawdzie ustawić trigger, aby nam przenosił dane w inne miejsce ale w tej chwili wykracza to poza potrzeby prezentacji.

Teraz już mamy strukturę naszych katalogów, więc spokojnie możemy dodać naszą konfigurację Apache, a zatem:

a2ensite app.e-strix.com

I przeładowanie:

service apache2 reload

Notatka: Dlaczego teraz a nie wcześniej? Odpowiedź jest prosta, po instalacji MySQL trzeba przeładować apache, aby nowo zainstalowane moduły były widoczne. W prawdzie można przeładować kilka razy, nie zaszkodzi, ale stwierdziłem że wykonam to właśnie w taki sposób.

A teraz nasz CMS, czyli instalacja WordPress’a.

WordPress

W prawdzie można użyć hostingu z już zainstalowanym CMSem, ale takie rozwiązanie jest mało elastyczne. Mam na myśli instalacje innych rzeczy niż tylko jednego narzędzia.

Przejdźmy do katalogu użytkownika:

cd /home/appuser/

I pobieramy WordPress:

wget https://wordpress.org/latest.tar.gz

A teraz rozpakujemy paczkę:

tar zxvf latest.tar.gz

I zawartość przenosimy do katalogu public_html:

mv ./wordpress/* ./public_html/

I czyścimy niepotrzebne pliki:

 rm -r latest.tar.gz wordpress/

I nadajemy prawa do katalogu:

chown -R appuser:www-data public_html/
chmod -R 774 public_html/

Z poziomy administracji już wszystko gra, teraz użyjemy przeglądarki do finalizacji procesu instalacji:

A teraz instalacja numer 2, czyli już coś ładnie wygląda.

1. Wpisujemy dane naszej bazki

Teraz wypełnijmy formularz danymi, które ustaliliśmy przy tworzeniu bazy MySQL. I przechodzimy dalej.

 

2. Ustalamy konto administratora

Warto je sobie zapisać, żeby nie szukać w przyszłości. Mam na myśli sytuację, gdy będzie potrzeba coś zmienić, lub dodać osobę zajmującą się stroną. Gdy nasza aplikacja nabierze rozgłosu, może wystąpić sytuacja że nie będzie możliwości ogarnięcia wszystkiego w pojedynkę.

Klikamy przycisk „Let’s go!”.

Wypełniamy formularz i przechodzimy dalej:

Zakończenie

No i teraz mamy wszystko. Pozostaje nam wybrać sobie ładny layout i theme. Podłączyć statystyki googlowskie, lub coś innego np Piwik’a, lub zupełnie coś innego… Potem integracja z AdSense, może strona na początku na siebie zarobi, i integracja z portalem społecznościowym, ja wybiorę facebook. Ale o tym w przyszłości. Na chwilę obecną mamy domyślny motyw i to w rezultacie jest dobrym fundamentem do pracy.