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

от автора

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

В итоге вечерами дома в свободное от работы время я покрыл за 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 Да-да, я не люблю писать javacode, шлите пулл реквесты с ним, сейчас по коду либы очень понятно, что каждый метод ее делает…

ссылка на оригинал статьи http://habrahabr.ru/post/259005/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *