Сборка docker image для spring web service java приложения

docker – служба для создания образов и запуска их в контейнерах.

1. Проверяем есть ли на машине docker служба

docker --version
Docker version 19.03.6, build 369ce74a3c

Если ее нет то устанавливаем, как описано здесь https://docs.docker.com/engine/install/ubuntu/
Проверяем статус и если нужно запускаем службу docker:

sudo service docker status
sudo systemctl start docker

2. Собираем jar вашего приложения

Либо пример spring web service приложения можно скачать отсюда https://github.com/maria-shpatserman/webservice
Допустим у вас простой web service , который можно запустить командой

java -jar webservice-0.0.1-SNAPSHOT.jar

А его работоспособность проверить командой

~/IdeaProjects/webservice$ http GET "http://localhost:8080/greeting?name=Mike"
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/json
Date: Wed, 05 Jan 2022 20:20:29 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked{
"content": "Hello, Mike!",
"id": 2
}

3. Запаковываем приложение в docker image.

Для этого в корне проекта создаем Dockerfile. Прописываем в него следующие строчки

FROM openjdk:11
MAINTAINER netunix
WORKDIR /app
COPY ./target/webservice-0.0.1-SNAPSHOT.jar .
EXPOSE 8080
CMD ["java","-jar","/app/webservice-0.0.1-SNAPSHOT.jar"]

Для удобства работы с docker службой в Intellij Idea можно поставить дополнительный плагин Settings→Plugins→Docker

docker plugin

Основные Docker-инструкции
FROM – на основании какого родительского образа будет создан ваш образ. Здесь можно поискать образы уже иещющие jdk для запуска java приложений https://hub.docker.com/_/openjdk
COPY – копирует в контейнер файлы и папки
EXPOSE – указывает какой порт необходимо открыть
CMD – указывает команду, которая бцудет выполнена когда контейнер будет запущен
Собираем образ командой, в текущей папке у Вас должен лежать Dockerfile:

docker build -t webeservice_img

Если не указывать параметр t ( tag) то создастся образ имеющий только IMAGE_ID вида cc31ee19650d. И запускать его в контерйнере можно будет только по ID, поэтому лучше всегда давать имя для образа.

4. Запускаем контейнер с собранным образом

Проверяем, что образ появился в списке доступных для запуска образов:

docker images

Запускаем образ в новом контейнере:

docker run -dp 8080:8080 webeservice_img

Параметр p — пробросить порт из контейнера на хост
Параметр d -запустить контейнер в background-e
Проверяем, что наше приложение все также работает:

http GET "http://localhost:8080/greeting?name=Mike"

Проверяем текущий статус контейнеров docker:

docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cdb377dee50e webeservice_img "java -jar /app/webs…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->8080/tcp interesting_zhukovsky

Видим, что имя ему присвоилось автоматичечски сгенерированное.
Останавливаем контейнер по его ID:

docker stop cdb377dee50e

Запускаем контейнер и пробрасываем портиз 8080 на 80 и указываем свое собственное имя:

docker run -dp 80:8080 --name webservice webeservice_img

Теперь проверяем что все работает и на порту 80:

http GET "http://localhost/greeting?name=Mike"

Также проверяем имя полученного контейнера:

docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                           PORTS               NAMES
e2d4672b1719        3e3378e233e8        "java -jar /app/webs…"   7 minutes ago       Up 7 minutes                  0.0.0.0:80->8080/tcp   webservice
cdb377dee50e        3e3378e233e8        "java -jar /app/webs…"   About an hour ago   Exited (143) About an hour ago                       interesting_zhukovsky

Логи stdout вашего приложения можно посмотреть командой:

docker logs webservice

5. Сохрняем созданный образ в Docker Hub

У вас должна быть своя страничка на https://hub.docker.com/
Имя образа, который вы хотите сохранить в Docker Hub должно начинаться с имени вашего репозитория, поэтому делаем переименование нашего образа:

docker tag webeservice_img shpatsermanm/webeservice_img

Теперь делаем команду сохранения образа в репозиторий:

docker push shpatsermanm/webeservice_img

Если вам понадобится этот образ где-то на другой машине ( или на текущей вы его удалите командой docker rmi), то образ всегда можно будет скачать командой:

docker pull shpatsermanm/webeservice_img:latest

Выравнивание и масштабирование картинок в ячейках TableLayout

1. Настройка статического интерфейса

К примеру, нам нужно в интерфейсе сделать блок с картинками внизу экрана так, чтобы он занимал 25% высоты экрана. Для того, чтобы делать относительные размеры объектов интерфейса нужно применять LinearLayout в качестве родительского интерфейса, которому задавать параметр android:weightSum="100" (т.е. 100%). Тогда в соответствии с задачей поделим элементы на 75% и 25% соответственно, используя в дочерних объектах параметр android:layout_weight ( т.е. «вес» эелемента). Чем больше «вес» элемента , тем больше места он должен занимать. Параметр, который регулируется этим «весом» задается значением 0. Так как в нашем случае мы будем менять высоту,то задаем дочерним элементам android:layout_height="0dp".
Накидаем следующий родительский интерфейс :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"   
    tools:context="ru.netunix.scaleimages.MainActivity"
    android:weightSum="100"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:text="Hello World!"
        android:layout_weight="15"/>
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="60"
        android:src="@drawable/q05image"/>
    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="25"
        android:id="@+id/tableQuestion">     
    </TableLayout>
</LinearLayout>

При этом желаемая таблица TableLayout с картинками будет занимать именно 25% всей высоты экрана. Зададим идентификатор таблицы в разметке android:id="@+id/tableQuestion".

2. Выравнивание картинок в статическом интерфейсе

Для того чтобы выровнять высоту строк опять воспользуемся LinearLayout и укажем ему android:weightSum="100", а также зададим ему идентификатор в разметке android:id="@+id/subLinear". Таким образом, если в статике у нас будет две строки, которые должны быть одной высоты, то обеим TableRow нужно будет указывать одинаковый «вес» по android:layout_weight="50", и чтобы они отмасштабировались задавать нулевой параметр android:layout_height="0dp".
К, примеру, у нас в таблице должно быть 2 строки и в верхней строке должно быть 3 картинки, а в нижней 2. Опишем интерфейс следующим образом:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="ru.netunix.scaleimages.MainActivity"
    android:weightSum="100"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:text="Hello World!"
        android:layout_weight="15"/>
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="60"
        android:src="@drawable/q05image"/>
    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="25"
        android:id="@+id/tableQuestion">
        <LinearLayout
            android:orientation="vertical"
            android:weightSum="100"
            android:id="@+id/subLinear"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TableRow
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="50">
                <ImageView
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1.0"
                    android:src="@drawable/q02v1"/>
                <ImageView
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1.0"
                    android:src="@drawable/q02v2"/>
                <ImageView
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1.0"
                    android:src="@drawable/q02v3"/>
            </TableRow>
            <TableRow
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="50">
                <ImageView
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1.0"
                    android:src="@drawable/q02v4"/>
                <ImageView
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1.0"
                    android:src="@drawable/q02v5"/>        
            </TableRow>
        </LinearLayout>
    </TableLayout>
</LinearLayout>

Получаем следующего вида рабочий Android интерфейс:

3. Выравнивание динамически добавленных строк и картинок в таблице

К примеру, Вы заранее не знаете какие картинки Вы будите загружать, сколько их, их размер и т.д. Будем наполнять таблицу TableLayout динамически. Удалим из интерфейса все дочерние элементы блока LinearLayout с идентификатором "@+id/subLinear". Получим следующую разметку :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="ru.netunix.scaleimages.MainActivity"
    android:weightSum="100"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:text="Hello World!"
        android:layout_weight="15"/>
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="60"
        android:src="@drawable/q03image"/>
    <TableLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="25"
        android:id="@+id/tableQuestion">
        <LinearLayout
            android:orientation="vertical"
            android:weightSum="100"
            android:id="@+id/subLinear"
            android:layout_width="match_parent"
            android:layout_height="match_parent">        
        </LinearLayout>
    </TableLayout>
</LinearLayout>

Чтобы динамически наполнить таблицу и получить интрефейс, как на картинке вверху, добавим следующий код в метод onCreate() нашей Activity:

  @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        //Разметка отвечающая за равную высоту строк таблицы
        LinearLayout mySubLinear = (LinearLayout)findViewById(R.id.subLinear);
        //Создаем первую строку
        TableRow row = new TableRow(this);
        //Выставляем ей параметры в 50% ( так как строк будет 2)
        TableRow.LayoutParams rowParams = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT,0,50.0f);
        row.setLayoutParams(rowParams);
        //Параметры для ячеек таблицы(картинок).Все должны быть одинаковой ширины.
        TableRow.LayoutParams params = new TableRow.LayoutParams(0, TableRow.LayoutParams.MATCH_PARENT,1.0f);
        //Подготовка изображения
        ImageView imageView1 = new ImageView(this);
        Picasso.with(this).load("file:///android_asset/qimages_ru/q02v3.jpg")
                .into(imageView1);
        //Устанавливаем параметры для изображения
        imageView1.setLayoutParams(params);
        //Добавляем изображение в первый столбец
        row.addView(imageView1);
        ImageView imageView2 = new ImageView(this);
        Picasso.with(this).load("file:///android_asset/qimages_ru/q02v2.jpg")
                .into(imageView2);
        imageView2.setLayoutParams(params);
        //Добавляем изображение во второй столбец
        row.addView(imageView2);
        
        ImageView imageView3 = new ImageView(this);
        Picasso.with(this).load("file:///android_asset/qimages_ru/q02v1.jpg")
                .into(imageView3);
        imageView3.setLayoutParams(params);
        //Добавляем изображение в третий столбец
        row.addView(imageView3);
        //Добавляем первую строку в таблицу
        mySubLinear.addView(row);
        
        //Аналогично создаем вторую строку и выставляем ей параметры
        TableRow row2 = new TableRow(this);
        row2.setLayoutParams(rowParams);
        //Наполняем вторую строку
        ImageView imageView4 = new ImageView(this);
        Picasso.with(this).load("file:///android_asset/qimages_ru/q02v1.jpg")
                .into(imageView4);
        imageView4.setLayoutParams(params);
        row2.addView(imageView4);
        ImageView imageView5 = new ImageView(this);
        Picasso.with(this).load("file:///android_asset/qimages_ru/q02v2.jpg")
                .into(imageView5);
        imageView5.setLayoutParams(params);
        //Добавляем вторую строку в таблицу
        row2.addView(imageView5);
        //Добавляем вторую строку в таблицу
        mySubLinear.addView(row2);

Один из главных параметров в коде — это rowParams, в котором в случае заранее неизвестного числа строк RowNumber коэффициент «веса» каждой строки надо будет получать делением суммарного коэффициента всех строк в subLinear( у нас это 100) на число строк:

TableRow.LayoutParams rowParams = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT,0,(100.0f/RowNumber));

В параметрах rowParams не забываем выставить height в 0,чтобы строки получили одинаковую высоту.
Для элементов ячеек (они у нас ImageView) также создаем свои параметры params.Так как коэффициент "веса" у всех картинок будет одинаковым и равен 1, то все столбцы будут одной ширины. Обязательно выставляем параметр width для ячеек в 0, чтобы картинки корректно отмасштабировались.
Остальное видно из комментариев в коде. Организовать добавление строк и (или) ячеек в цикле for не сложно. Для добавления библиотки Picasso в скрипте .gradle добавляем строчку compile 'com.squareup.picasso:picasso:2.5.2'.

Подписываем Android apk приложения

1. Создаем хранилище ключей

Вызываем команду для создания хранилища ключей и указываем алиас ключа:

keytool -genkey -keystore mykeys.keystore -alias keyjsonclient -validity 10000 -keyalg RSA -keysize 2048

В одном хранилище могут храниться более 1 ключа. Файл mykeys.keystore создается в текущей папке. При первом обращении к хранилищу нужно будет указать его пароль и подтвердить его и также задать и подтвердить пароль для ключа. При дальнейшем добавлении ключей в хранилище пароль к самому хранилищу указывается 1 раз, а подтверждаются только данные ключа.

Пример создания ключа в хранилище:

Enter keystore password:  
What is your first and last name?
  [Unknown]:  Maria Shpatserman
What is the name of your organizational unit?
  [Unknown]:  
What is the name of your organization?
  [Unknown]:  NetUnix
What is the name of your City or Locality?
  [Unknown]:  
What is the name of your State or Province?
  [Unknown]:  
What is the two-letter country code for this unit?
  [Unknown]:  RU
Is CN=Maria Shpatserman, OU=Unknown, O=NetUnix, L=Unknown, ST=Unknown, C=RU correct?
  [no]:  yes

Enter key password for 
	(RETURN if same as keystore password):  
Re-enter new password: 

Просматриваем созданные в хранилище ключи командой:

keytool -list -keystore mykeys.keystore -v

2. Подписываем свой apk файл

В IDE в выходной папке сборки (out либо target) находим свой не выровненный файл-архив нашего приложения jsonclient.unaligned.apk.
Не забываем удалить подпись созданную IDE (удаляем META-INF).
Вызываем команду для подписи файла:

jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore /home/masha/keystore/mykeys.keystore jsonclient.unaligned.apk keyjsonclient
Enter Passphrase for keystore: 
Enter key password for keyjsonclient: 
jar signed.

Указываем пароль к хранилищу и к ключу. И получаем подписанный архив.
Если возникло сообщение об ошибке вида

jarsigner: unable to sign jar: java.util.zip.ZipException: invalid entry compressed size (expected 417 but got 414 bytes)

Значит подпись уже в архиве есть. Удаляем подпись открыв архив и удалив папку META-INF. Вызываем команду jarsigner по новой.
Если возникло предупреждение:

Warning: 
No -tsa or -tsacert is provided and this jar is not timestamped. Without a timestamp, users may not be able to validate this jar after the signer certificate's expiration date (2044-04-16) or after any future revocation date.

Значит Вы используете версию JDK >6 ( к примеру, Java 7 ). И при вызове команды jarsigner нужно использовать дополнительный параметр:

jarsigner -tsa http://timestamp.digicert.com ....

3. Выравниваем apk файл

Из папки androidsdk вызываем команду zipalign:

/home/masha/androidsdk/android-sdk-linux/build-tools/21.1.2/zipalign -v -f 4 jsonclient.unaligned.apk jsonclient.apk

Если для подписи файла использовать утилиту apksigner, то выравнивание файла с помощью утилиты zipalign нужно выполнять перед командой apksigner.
Поздравляю теперь Вы можете выкладывать свой подписанный и выровненный файл в Google play Market.

Создание Web Service на языке Python

Для запуска веб-приложения, а в нашем случае лишь WebService воспользуемся библиотекой Werkzeug.
Вызовем метод

 run_simple('localhost', 4000, application)

В качестве аргументов метод принимает адрес сервера, где будет распологаться веб-сервис, порт на , котором , будет «слушать» сервер и имя метода реализующего саму логику работы с запросами от клиента и возвратом ему ответов.
application — это и есть имя метода , реализующего логику Request-Responce.
Данный метод нужно помечать тегом @Request.application , в качестве аргумента ему передается request , а возвращает он клиенту Response .
Простой пример веб-приложения можно увидеть ниже:

from werkzeug.wrappers import Request, Response

@Request.application
def application(request):
    return Response('Hello World!')

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 4000, application)

Реализуем веб-сервис, который выполняет 3 метода : "get_products","get_alcohol_products","get_eatable_products". Для этого импортируем следующие пакеты:

from jsonrpc import JSONRPCResponseManager, dispatcher

Внутри метода application привяжем в словаре dispatcher реализацию желаемых методов следующим образом:

@Request.application
    def application(self,request):        
        dispatcher["get_alcohol_products"] = self.get_alcohol_products
        dispatcher["get_eatable_products"] = self.get_eatable_products
        dispatcher["get_products"] = self.get_products
        response = JSONRPCResponseManager.handle(request.data, dispatcher)          
        return Response(response.json, mimetype='application/json')

Таким образом при получении JSON запроса от клиента, содержащего название желаемого метода, будет вызываться соответствующий метод класса.
К примеру, HTTP Post запрос от клиента с JSON :

{"jsonrpc": "2.0", "params": [], "method": "get_products", "id": 0}

Вызовет метод self.get_products() в нашем классе.

Проверить работоспособность веб-сервиса можно с помощью плагина Postman Launcher к броузеру Chrome. Либо воспользоваться тестом из самого проекта.
Полная версия веб-сервиса представлена ниже и ее также можно скачать здесь.

'''
Created on Aug 25, 2016
@author: Maria Shpatserman
'''
from werkzeug.wrappers import Request, Response
from werkzeug.serving import run_simple

from jsonrpc import JSONRPCResponseManager, dispatcher
import xml.etree.ElementTree as ET
import logging
import ast
import json


class ServerJson(object):
    '''
    classdocs
    '''
    logging.basicConfig(filename='../logs/myapp.log', level=logging.INFO)
    

    def __init__(self):
        '''
        Constructor
        '''
        self.readJsonFile()
        
        logging.info(self.json_object)
        
        
    def readJsonFile(self):
        with open('../data/products.json', 'r') as f:
            self.json_object = json.load(f) 
    
              
    
    @Request.application
    def application(self,request):
        logging.info(request.data)
        dispatcher["get_alcohol_products"] = self.get_alcohol_products
        dispatcher["get_eatable_products"] = self.get_eatable_products
        dispatcher["get_products"] = self.get_products
        response = JSONRPCResponseManager.handle(request.data, dispatcher)
        logging.info(response)
        logging.info(response.json)
    
        return Response(response.json, mimetype='application/json')
    def main(self):
        run_simple('localhost', 4000, self.application)
    
    def get_products(self):
        logging.info(self.json_object)
        return self.json_object
    
    def get_alcohol_products(self):        
        return self.json_object["products"]["alcohol"]
    
    
    def get_eatable_products(self):
        return self.json_object['products']['eatable']
    
if __name__ == '__main__':
    ServerJson().main()