GrabDuck

Изобретаем велосипед на Java — пишем свой Framework (DI, ORM, MVC and etc)

:

Все началось с того, что я решил написать свою Java Common библиотеку. Очень часто для типичных задач на Stack Overflow находятся решения в 3-5-10 строк кода. Копипастить себе в проекты надоело. Решил вынести это в отдельную библиотеку, которую можно сделать Open Source и использовать в текущих и будущих своих проектах. Безусловно есть такие хорошие либы как Google Guava и Apache Commons, которые я тоже использую в работе, но они достаточно «правильные» и гибкие, что выливается не в одну строчку кода.

В итоге вечерами дома в свободное от работы время я покрыл за 1 неделю следующие направления: Sleep/Delay/Pause, Timer for Benchmark, Random range generator, File operations, Tasks/Threads, Reflection, JSON, URL, Logging, Strings. Смотрел свои проекты на предмет копи-паста стандартных решений и писал решения в библиотеку. В очередной раз применил TDD подход для разработки библиотеки. Сначала пишешь тест на не существующие классы и методы, а потом реализуешь код, что бы тесты стали зелеными. Решает две проблемы: во-первых, ты пытаешься удобно использовать свои классы и их методы до их реализации, во-вторых, у тебя остаются тесты, которые в будущем могут свалиться, и ты поймешь, что у тебя сломалось при очередном рефакторинге или багфиксе.

Дальше, больше. Я начал анализировать, как я и многие другие, типично использую Spring/JBoss и понял, что legacy и широта возможности все усложняет. Можно реализовать упрощенный типичный Dependency Injection. Сказано, сделано. Добавил в свою библиотеку DI Framework. Мои знакомые смотрели мою реализацию и говорили что разобраться, как устроен Spring просто нереально, там полная жесть наследований и обверток, а у тебя все видно прям на первом уровне реализации и все понятно. Им было интересно, как работать с аннотациями и т. д.

Реализовав DI Framework я задумался над тем, что еще чуть-чуть и будет полноценный Enterprise Server. Осталось добавить ORM и Web-сервер с MVC, REST и security. Все в лучших традициях, так сказать. И меня затянуло. Еще неделька вечерами после работы, ссоры с женой, и получился Simplified Enterprise Server. Я не придерживался стандартов JavaEE, так как писал, как бы мне казалось, было удобно и понятно использовать. Сам я на работе использую Spring Boot, Spring Data, JPA 2.0, Spring MVC, Spring Rest, Spring Security. До этого делал проект на JBoss, видел другую сторону JEE, так сказать. Но вся это универсальность и гибкость конечно в тему. Но когда тебе нужно быстро накидать прототип в стиле JEE или тебе нужно научится кодить серьездные проекты на Java, окунаться в мир Spring, Hibernate и т.д. долго и кропотливо. Единственная альтернатива это Spring Boot, но реально там много происходит скрыто от тебя и если ты не знаешь как работает Spring под капотом, это только тебя тормозит, так как любой шаг в лево или в право это полный нырок в детали…

В итоге код фреймворка на гитхабе github.com/evgenyigumnov/common

Пример веб-сервиса использующего этот фреймворк на гитхабе github.com/evgenyigumnov/example и в онлайне его тоже можно посмотреть java.igumnov.com:8181 Пользователь: demo Пароль: demo

Структура примера:

./:
pom.xml
./javascript:
user.js
./pages:
index.html
layout.html
login.html
./sql:
1.sql
./src/main/java/com/igumnov/common/example:
App.java
ExampleUser.java

pom.xml

    <dependencies>
        <!-- подключаем наш фреймворк -->
        <dependency>
            <groupId>com.igumnov</groupId>
            <artifactId>common</artifactId>
            <version>3.15</version>
        </dependency>
        <!-- подключаем БД -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.187</version>
        </dependency>
        <!-- подключаем Bootstrap, AnglularJS и тд из webjars проекта -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>angular-ui-bootstrap</artifactId>
            <version>0.12.0</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>angularjs</artifactId>
            <version>1.3.8</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>3.3.1</version>
        </dependency>
    </dependencies>

App.java

public class App {

    public static void main(String[] args) throws Exception {

        ORM.connectionPool("org.h2.Driver", "jdbc:h2:mem:test", "SA", "", 1, 3); // Создаем пул коннекций к БД (максимум 3 коннекта)
        ORM.applyDDL("sql"); // Накатываем на базу объявления таблиц или оно это пропускает если уже далало
        WebServer.init("localhost", 8181); // Задаем начальные параметры веб-сервера
        WebServer.security("/login", "/login", "/logout"); // Говорим что у нас включена безопасность которая должна работать по URL-ам
        WebServer.addRestrictRule("/*", new String[]{"user_role"}); // Ограничиваем доступ только для пользователям с ролью user_role
        WebServer.addAllowRule("/static/*"); // Даем доступ для всех к статическому контенту
        WebServer.addClassPathHandler("/static", "META-INF/resources/webjars"); // Указываем откуда брать этот статический контент из classpath от webjars
        WebServer.addAllowRule("/js/*"); // Даем доступ для всех к нашим Java Script-ам
        WebServer.addStaticContentHandler("/js", "javascript"); // Указываем в какой папке на винте лежат наши Java Script


        WebServer.addTemplates("pages",0); // Указываем в какой папке на винте лежат шаблоны страниц
        // Добавляем контроллер по урл "/", который добавляет в модель текущее время и говорит, что нужно отобразить index.html
        WebServer.addController("/", (request, model) -> {
            model.put("time", new Date());
            return "index";
        });
        // Добавляем контроллер по урл "/login", который говорит, что нужно отобразить login.html
        WebServer.addController("/login", (request, model) -> {
            return "login";
        });

       // Добавляем REST-контроллер по урл "/rest/user" и указываем что могут методом POST/PUT прислать JSON-объект типа ExampleUser.class
        WebServer.addRestController("/rest/user", ExampleUser.class, (request, postObj) -> {
            switch (request.getMethod()) {
                case (WebServer.METHOD_GET):  // Прилетел GET запрос
                    ArrayList<Object> users;
                    try {
                        users = ORM.findAll(ExampleUser.class); // Извлекаем список пользователей
                    } catch (Exception e) {
                        throw new WebServerException(e.getMessage()); // Словили ошибку, которая будет сериализована в JSON
                    }
                    return users; // Возвращаем массив пользователей который сам сериализуется в JSON
                case (WebServer.METHOD_POST): // Прилетел POST запрос
                    ExampleUser ret = null;
                    try {
                        ret = (ExampleUser) ORM.insert(postObj); // Вставляем его в БД
                    } catch (Exception e) {
                        throw new WebServerException(e.getMessage());
                    }
                    return ret; // В случае успеха просто  возвращаем добавленного пользователя в виде JSON
                case (WebServer.METHOD_DELETE): // Прилетел DELETE запрос
                    ExampleUser user;
                    try {
                        user = (ExampleUser) ORM.findOne(ExampleUser.class, request.getParameter("userName")); // Ищем юзера в БД
                        if(user.getUserName().equals("demo")) { // Если юзер demo не даем удалять
                            throw new WebServerException("You cant delete user demo");
                        } else {
                            ORM.delete(user); // Иначе шлем в БД delete-запрос
                        }
                    } catch (Exception e) {
                        throw new WebServerException(e.getMessage());  // Словили ошибку, которая будет сериализована в JSON
                    }
                    return user; // Возвращаем JSON юзера, которого удалили в случае успеха данной операции
                default:
                    throw new WebServerException("Unsupported method"); // Ругаемся если прилетел запрос иного типа, например PUT или иной
            }
        });


        ArrayList<Object> users = ORM.findAll(ExampleUser.class); // Берем из БД всех пользователей

        if (users.size() == 0) { // В таблице с пользователями пусто
            ExampleUser user = new ExampleUser();
            user.setUserName("demo");
            user.setUserPassword("demo");
            ORM.insert(user); // Добавляем demo/demo пользователя в БД
            WebServer.addUser("demo", "demo", new String[]{"user_role"}); // Сообщаем веб-серверу что есть пользователь demo/demo с ролью user_role
        }

        users.stream().forEach((user) -> {  // Перебираем список пользователей полученный из БД
            ExampleUser u = (ExampleUser) user;
            WebServer.addUser(u.getUserName(), u.getUserPassword(), new String[]{"user_role"}); // Сообщаем веб-серверу о новом пользователе с ролью user_role
        });

        WebServer.start(); // Если до этого места кода дошло управление и ничего не вывалилось по Exception, то стартуем веб-сервер :)

    }

}

ExampleUser.java

// Данный класс используется для JSON сериализации и десериализации и также для меппинга в БД
public class ExampleUser {

    @Id(autoIncremental = false) // Необходимо ORM знать какое поле является Primary Key и генерится ли при insert значение этого поля
    private String userName;
    private String userPassword;
...
}

1.sql

# Создаем таблицу в БД где будем хранить через ORM объекты типа ExampleUser.class
CREATE TABLE ExampleUser (userName VARCHAR(255) PRIMARY KEY, userPassword VARCHAR(255))

login.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<!-- Указываем что нужно использовать декоратор layout из layout.html -->
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout">
<body>
<!-- Объявляем наш контент блок который будет подставлен в layout.html -->
<div layout:fragment="content">
    <form name="form" action="/j_security_check" method="POST">
        <div class="modal-header">
            <h3 class="modal-title">Login</h3>
        </div>
        <div class="modal-body">
            <div class="form-group">
                <input type="text" name="j_username" class="form-control" value="" placeholder="Login"/>
            </div>
            <div class="form-group">
                <input type="password" name="j_password" class="form-control" placeholder="Password"/>
            </div>
            <div class="form-group">
                <button type="submit" id="login" class="btn btn-primary">OK</button>
            </div>
        </div>
    </form>

</div>
</body>
</html>

index.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<!-- Указываем что нужно использовать декоратор layout из layout.html -->
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout">
<body>
<!-- Объявляем наш контент блок который будет подставлен в layout.html -->
<div layout:fragment="content">
    <!-- Подключаем наш контроллер на AngularJS-->
    <script src="/js/user.js"></script>
    <h1 th:text="${time}"></h1> // Выводим текущее время переданное в модель
    <!-- Обозначаем область действия нашего контроллера UserCtrl -->
    <div ng-controller="UserCtrl">
        <table class="table">
            <thead>
            <tr>
                <th>Name</th>
                <th>Password</th>
                <th></th>
            </tr>
            </thead>
            <tbody>
           <!-- В цикле заполняем таблицу пользователями -->
            <tr ng-repeat="user in users">
                <td>{{user.userName}}</td>
                <td>{{user.userPassword}}</td>
               <!-- По клику на крестик вызываем функцию на контроллере для удаления пользователя -->
                <td><a href="#"><span class="glyphicon glyphicon-remove" tooltip="Delete" ng-click="deleteUser(user)"/></a></td>
            </tr>
            </tbody>
        </table>
        <div ng-model="user">
        <!-- Форма добавления пользователя -->
            <div class="form-group">
                <input type="text" class="form-control" ng-model="user.userName" placeholder="Login"/>
            </div>
            <div class="form-group">
                <input type="password" class="form-control" ng-model="user.userPassword" placeholder="Password"/>
            </div>
            <div class="form-group">
                <!-- По клику на кнопке вызываем функцию в контроллере добавляющую пользователя -->
                <button class="btn btn-primary" ng-click="addUser(user)">Add</button>
            </div>
        </div>
    </div>
</div>
</body>
</html>

layout.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<!-- Область действия нашего приложения на AngularJS -->
<html ng-app="com.igumnov.common.example">
<head>
    <title>Title</title>
    <link rel="stylesheet" href="/static/bootstrap/3.3.1/css/bootstrap.min.css" />
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script src="/static/angularjs/1.3.8/angular.min.js"></script>
<script src="/static/angularjs/1.3.8/angular-resource.min.js"></script>
<script src="/static/angular-ui-bootstrap/0.12.0/ui-bootstrap-tpls.min.js"></script>
<div class="container">
<!-- Сюда будет вставляться контентный блок -->
    <div layout:fragment="content"></div>
</div>
</body>
</html>

user.js

angular.module('com.igumnov.common.example', ['ui.bootstrap', 'ngResource'])
    .factory('User', ['$resource', function ($resource) { // Объявляем REST-ресурс User
        return $resource('/rest/user', {}, {
            list: { // Список юзеров
                method: 'GET',
                cache: false,
                isArray: true // Результат вызова массив
            },
            add: { // Добавляем юзера
                method: 'POST',
                cache: false,
                isArray: false // Результат вызова один объект
            },
            delete: { // Удаляем юзера
                method: 'DELETE',
                cache: false,
                isArray: false // Результат вызова один объект
            }
        });
    }])
    .controller('UserCtrl', function ($scope, User) { // Обьявляем наш контроллер UserCtrl
        $scope.users = User.list({}); // Заполняем список пользователя при инициализации контроллера
        $scope.addUser = function (user) { // Функция добавления пользователя
            User.add({},user,function (data) { // Дергаем REST-интерфейс
                $scope.users = User.list({});   // В случае успеха, перезаполняем список пользователей
            }, function (err) {
                alert(err.data.message); // В случае ошибки, выводим ошибку
            });
        }
        $scope.deleteUser = function (user) { // Функция удаления пользователя
            User.delete({"userName" : user.userName},user,function (data) { // Дергаем REST-интерфейс
                $scope.users = User.list({}); // В случае успеха, перезаполняем список пользователей
            }, function (err) {
                alert(err.data.message); // В случае ошибки, выводим ошибку
            });
        }

    });

В заключении, буду рад любой критике и предложению по улучшению кода библиотеки. Для себя я получил профит в разминании мозга при написание библиотеки и использовании замыканий/лямбд. Иногда скучно писать коммерческие продукты, хочется создать свой космический корабль. Не стесняйтесь форкать мою либу и самим ее модифицировать под свои нужды. Она достаточно проста и легка для внесения в нее модификаций. Буду признателен, если вы будете присылать пулл-реквесты, чтобы ваши доработки вносились в библиотеку. Я лично настроен достаточно быстро их проверять и принимать. Я просто фанатик-программер, меня это втыкает. Люблю кодить!

PS Да-да, я не люблю писать javadoc, шлите пулл-реквесты с ним, сейчас по коду либы очень понятно, что каждый метод ее делает…