Inhaltsverzeichnis
1. Einführung
二、代码实践
2.1、首先我们添加 freemarker 依赖包
2.2、然后创建一个代码模版
2.3、最后生成目标代码
2.1. Zuerst fügen wir das Freemarker-Abhängigkeitspaket hinzu
2.2 und erstellen dann eine Codevorlage
2.3. Generieren Sie schließlich den Zielcode
Heim Java javaLernprogramm Wie SpringBoot Freemarker integriert, um den Codegenerator zu implementieren

Wie SpringBoot Freemarker integriert, um den Codegenerator zu implementieren

May 10, 2023 pm 08:37 PM
springboot freemarker

    1. Einführung

    Im eigentlichen Softwareprojektentwicklungsprozess kann ich Ihnen sehr verantwortungsvoll sagen, ob Sie tatsächlich haben Wenn Sie mehr als 5 Jahre lang Code geschrieben haben, können Sie sagen, dass Sie Ihre Fähigkeit, einfache funktionale Anforderungen wie Hinzufügen, Löschen, Ändern und Überprüfen zu entwickeln, völlig verloren haben. Zumindest bin ich dieser Typ.

    Es ist jedoch unbestreitbar, dass die überwiegende Mehrheit der Softwarefunktionen, bis hin zur grundlegendsten Einheit, im Wesentlichen das Hinzufügen, Löschen, Ändern und Abfragen einer einzelnen Tabelle sind!

    Es ist nur so, dass Dinge, die in der Vergangenheit möglicherweise mit einer einzelnen Tabelle erledigt wurden, jetzt möglicherweise mehrere Tabellen oder mehrere Bibliotheken erfordern. Die Codeebenen ähneln dem Stapeln von Blöcken komplex es ist.

    Ich erinnere mich, als ich in den frühen Tagen Projekte durchführte, musste ich jedes Mal, wenn ich dem Projekt eine einzelne Tabelle hinzufügte, einen Satz auf Codeebene entsprechend der Idee neu schreiben Der Code des <code>MVC-Frameworks benötigt mindestens 20 Minuten, um alle grundlegenden Ergänzungen, Löschungen, Änderungen und Suchvorgänge zu schreiben Höchstens 10 Minuten. MVC框架的思想,重新编写一套CURD的代码,写完所有的基础的增删改查,至少需要20分钟,手快的情况下,最快也要10分钟。

    假如某个新开发的功能,要新增10张表,按照这个时间计算,至少要100分钟,仔细想想,其实你会发现大部分的时间都浪费在这些简单而又重复的编程圈子中去了。

    那有没有一个办法,将这些简单的CURD代码,全部都标准化、公共化呢?这样我们的可以省下很多时间来投入业务场景的开发。

    答案是肯定的,有!

    我记得早期我最先接触的是MybatisGenertor工具包,通过这个工具包,我们可以省去大部分的mybaitsxml文件的curd编写工作。

    还有我们所熟悉的JPA,里面有一套公共的持久层动态代理类,它可以自动根据名称生成SQL语句,能为开发省下不少的事情。

    但是我这个人比较懒,我想搞一个工具,从controllerserviceentity dao层,全部的crud代码,包括单元测试类,通过工具自动生成好。

    像这样的工具,现在网上也有不少,例如我们所熟悉的Mybatis-plus插件,它就可以做到这一点,也是非常好用。

    但是有的公司就不喜欢它,原因也很简单,里面的很多公共方法封装的过于深入,而且很多crudsql全部都是动态生成,你根本看不到。

    总之啊就是一句,不在自己掌控之内的,很多程序员总是带着各种疑虑~~

    当然,还有一个明显的疑虑,就是对微服务的开发,不能全面支持,比如你项目采用的是SpringBoot +Dubbo组合来开发,这个时候生成的controller,完全没啥用处,而且还很鸡肋。

    因此在这种情况下,你得基于当前的项目软件开发规则,自己开发一套代码生成器,以满足快速开发的需要。

    下面我就简单的介绍一下,如何自行开发一套代码生成器,过程如下!

    二、代码实践

    其实开发一套代码生成器,真没大家想象中的那么复杂,其中用的最重要一项技术,就是利用模板来生成代码,例如我们经常使用的模板引擎freemarker,它就可以帮助我们实现这一点。

    2.1、首先我们添加 freemarker 依赖包

    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.23</version>
    </dependency>
    Nach dem Login kopieren

    2.2、然后创建一个代码模版

    下面我们以动态创建实体类为例,编写一个实体类的模板entity.java.ftl,其中${}里面定义的是动态变量。

    package ${package};
    
    import java.io.Serializable;
    
    /**
     * <p>
     * ${tableComment}
     * </p>
     *
     * @author ${author}
     * @since ${date}
     */
    public class ${entityClass} implements Serializable {
    
        private static final long serialVersionUID = 1L;
        
        <#--属性遍历-->
        <#list columns as pro>
    
        /**
         * ${pro.comment}
         */
        private ${pro.propertyType} ${pro.propertyName};
        </#list>
    
        <#--属性get||set方法-->
        <#list columns as pro>
        public ${pro.propertyType} get${pro.propertyName?cap_first}() {
            return this.${pro.propertyName};
        }
    
        public ${entityClass} set${pro.propertyName?cap_first}(${pro.propertyType} ${pro.propertyName}) {
            this.${pro.propertyName} = ${pro.propertyName};
            return this;
        }
        </#list>
    }
    Nach dem Login kopieren

    2.3、最后生成目标代码

    最后我们基于freemarker编写一个测试类!

    public class CodeGeneratorDemo {
    
        public static void main(String[] args) throws IOException, TemplateException {
            Map<String, Object> objectMap = new HashMap<>();
            //定义包路径
            objectMap.put("package", "com.example.test");
            //定义实体类
            objectMap.put("entityClass", "Student");
    
            //定义实体类属性
            List<Map<String, Object>> columns = new ArrayList<>();
            //姓名字段
            Map<String, Object> column1 = new HashMap<>();
            column1.put("propertyType", "String");
            column1.put("propertyName", "name");
            column1.put("comment", "姓名");
            columns.add(column1);
            //年龄字段
            Map<String, Object> column2 = new HashMap<>();
            column2.put("propertyType", "Integer");
            column2.put("propertyName", "age");
            column2.put("comment", "年龄");
            columns.add(column2);
    
            //定义类的属性
            objectMap.put("columns", columns);
            //定义作者
            objectMap.put("author", "张三");
            //定义创建时间
            objectMap.put("date", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
            //定义类描述
            objectMap.put("tableComment", "学生信息");
    
            //生产目标代码
            Configuration configuration = new Configuration(Configuration.VERSION_2_3_23);
            configuration.setDefaultEncoding(Charset.forName("UTF-8").name());
            configuration.setClassForTemplateLoading(CodeGeneratorDemo.class, "/");
            Template template = configuration.getTemplate("/templates/entity.java.ftl");
            FileOutputStream fileOutputStream = new FileOutputStream(new File("../src/main/java/com/example/generator/Student.java"));
            template.process(objectMap, new OutputStreamWriter(fileOutputStream, Charset.forName("UTF-8").name()));
            fileOutputStream.close();
            System.out.println("文件创建成功");
    
        }
    }
    Nach dem Login kopieren

    运行程序,输出的文件结果如下!

    package com.example.test;
    
    import java.io.Serializable;
    
    /**
     * <p>
     * 学生信息
     * </p>
     *
     * @author 张三
     * @since 2021-08-22
     */
    public class Student implements Serializable {
    
        private static final long serialVersionUID = 1L;
        
    
        /**
         * 姓名
         */
        private String name;
    
        /**
         * 年龄
         */
        private Integer age;
    
        public String getName() {
            return this.name;
        }
    
        public Student setName(String name) {
            this.name = name;
            return this;
        }
        public Integer getAge() {
            return this.age;
        }
    
        public Student setAge(Integer age) {
            this.age = age;
            return this;
        }
    }
    Nach dem Login kopieren

    与预期的效果一致,成功生成!

    例如小编我就是采用这种方式,首先把要通过工具生成的代码,全部通过模板方式定义好。

    Wie SpringBoot Freemarker integriert, um den Codegenerator zu implementieren

    然后通过连接数据库的方式,把需要自动生成的表结构查询出来,封装成数据渲染参数,最后传入到freemarker中去,非常简单、快速的生成与自己预期想要的代码,所有单表的crud全部一步到位!

    下面这个就是小编,基于当前项目定制开发的一款代码生成器,项目采用SpringBoot + Dubbo框架开发,没有Controller

    Wenn eine neu entwickelte Funktion das Hinzufügen von 10 neuen Tabellen erfordert, dauert es basierend auf dieser Zeit mindestens 100 Minuten. Wenn Sie sorgfältig darüber nachdenken, werden Sie feststellen, dass die meiste Zeit verschwendet wird Diese einfachen Aufgaben begab ich mich in den repetitiven Programmierkreis.

    Wie SpringBoot Freemarker integriert, um den Codegenerator zu implementierenGibt es eine Möglichkeit, all diese einfachen CURD-Codes zu standardisieren und zu veröffentlichen? Auf diese Weise können wir viel Zeit sparen und in die Entwicklung von Geschäftsszenarien investieren.

    #🎜🎜#Die Antwort ist ja, ja! #🎜🎜##🎜🎜# Ich erinnere mich, dass das erste, mit dem ich in den frühen Tagen in Kontakt kam, das MybatisGenertor-Toolkit war. Mit diesem Toolkit können wir die meisten mybaits <code>curd Vorbereitung von Code>xml-Dateien. #🎜🎜##🎜🎜#Es gibt auch JPA, mit dem wir vertraut sind und das über eine Reihe dynamischer Proxy-Klassen der öffentlichen Persistenzschicht verfügt, die automatisch SQL-Anweisungen generieren können Basierend auf dem Namen kann es viel Arbeit für die Entwicklung sparen. #🎜🎜##🎜🎜#Aber ich bin ein fauler Mensch. Ich möchte ein Tool aus controller, service, entity , dao-Ebene werden alle crud-Codes, einschließlich Unit-Test-Klassen, automatisch von Tools generiert. #🎜🎜##🎜🎜#Es gibt mittlerweile viele solcher Tools im Internet, wie zum Beispiel das bekannte Plug-in Mybatis-plus, das dies kann und zudem sehr einfach zu bedienen ist. #🎜🎜##🎜🎜#Aber einige Unternehmen mögen es nicht. Der Grund ist auch sehr einfach. Viele öffentliche Methoden sind zu tief gekapselt und viele cruds Es wird alles dynamisch generiert und Sie können es überhaupt nicht sehen. #🎜🎜##🎜🎜#Kurz gesagt, es liegt nicht in ihrer Kontrolle. ~~#🎜🎜##🎜🎜# Natürlich gibt es noch einen weiteren offensichtlichen Zweifel: Die Entwicklung von Microservices ist nicht möglich Wenn Ihr Projekt beispielsweise mit einer Kombination aus SpringBoot + Dubbo entwickelt wird, ist der zu diesem Zeitpunkt generierte Controller völlig nutzlos und auch sehr geschmacklos . #🎜🎜##🎜🎜#In diesem Fall müssen Sie also Ihren eigenen Codegenerator basierend auf den aktuellen Regeln für die Softwareentwicklung des Projekts entwickeln, um den Anforderungen einer schnellen Entwicklung gerecht zu werden. #🎜🎜##🎜🎜# Jetzt werde ich kurz vorstellen, wie Sie selbst einen Codegenerator entwickeln. Der Prozess ist wie folgt! #🎜🎜##🎜🎜# 2. Code-Praxis #🎜🎜##🎜🎜#Tatsächlich ist die Entwicklung eines Codegenerators nicht so kompliziert, wie alle denken. Die wichtigste verwendete Technologie istdie Verwendung von Vorlagen zum Generieren von Code, wie zum Beispiel die von uns häufig verwendete Template-Engine freemarker, können uns dabei helfen, dies zu erreichen. #🎜🎜#

    2.1. Zuerst fügen wir das Freemarker-Abhängigkeitspaket hinzu

    rrreee

    2.2 und erstellen dann eine Codevorlage

    #🎜🎜# Nehmen wir die dynamische Erstellung von Entitätsklassen als Geben Sie ein Beispiel ein und schreiben Sie eine Vorlage für die Entitätsklasse entity.java.ftl, in der ${} dynamische Variablen definiert. #🎜🎜#rrreee

    2.3. Generieren Sie schließlich den Zielcode

    #🎜🎜#Schließlich schreiben wir eine Testklasse basierend auf freemarker! #🎜🎜#rrreee#🎜🎜#Führen Sie das Programm aus und die Ergebnisse der Ausgabedatei sind wie folgt! #🎜🎜#rrreee#🎜🎜#Der Effekt stimmt mit dem Erwarteten überein und wurde erfolgreich erzeugt! #🎜🎜##🎜🎜#Ich verwende zum Beispiel diese Methode. Zunächst werden alle vom Tool zu generierenden Codes über Vorlagen definiert. #🎜🎜##🎜🎜#Wie integriert SpringBoot Freemarker, um den Codegenerator zu implementieren#🎜🎜##🎜🎜# Fragen Sie dann durch Herstellen einer Verbindung zur Datenbank die Tabellenstruktur ab, die automatisch generiert werden muss, kapseln Sie sie in Datenrenderingparameter und übergeben Sie sie schließlich an freemarker ist sehr einfach. Generieren Sie schnell den gewünschten Code, und alle Einzeltabellen-crud können in einem Schritt fertiggestellt werden! #🎜🎜##🎜🎜#Das Folgende ist ein vom Herausgeber angepasster und entwickelter Codegenerator basierend auf dem aktuellen Projekt. Das Projekt wurde mit dem SpringBoot + Dubbo-Framework entwickelt und verfügt über keinen Controller Layer, alle Codes in den Screenshots werden mit dem Codegenerator generiert und können direkt durch Unit-Tests ausgeführt werden. Die Entwicklung ist sehr schnell! #🎜🎜##🎜🎜##🎜🎜##🎜🎜#

    Das obige ist der detaillierte Inhalt vonWie SpringBoot Freemarker integriert, um den Codegenerator zu implementieren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

    Erklärung dieser Website
    Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn

    Heiße KI -Werkzeuge

    Undresser.AI Undress

    Undresser.AI Undress

    KI-gestützte App zum Erstellen realistischer Aktfotos

    AI Clothes Remover

    AI Clothes Remover

    Online-KI-Tool zum Entfernen von Kleidung aus Fotos.

    Undress AI Tool

    Undress AI Tool

    Ausziehbilder kostenlos

    Clothoff.io

    Clothoff.io

    KI-Kleiderentferner

    AI Hentai Generator

    AI Hentai Generator

    Erstellen Sie kostenlos Ai Hentai.

    Heißer Artikel

    R.E.P.O. Energiekristalle erklärten und was sie tun (gelber Kristall)
    3 Wochen vor By 尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O. Beste grafische Einstellungen
    3 Wochen vor By 尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O. So reparieren Sie Audio, wenn Sie niemanden hören können
    3 Wochen vor By 尊渡假赌尊渡假赌尊渡假赌
    WWE 2K25: Wie man alles in Myrise freischaltet
    4 Wochen vor By 尊渡假赌尊渡假赌尊渡假赌

    Heiße Werkzeuge

    Notepad++7.3.1

    Notepad++7.3.1

    Einfach zu bedienender und kostenloser Code-Editor

    SublimeText3 chinesische Version

    SublimeText3 chinesische Version

    Chinesische Version, sehr einfach zu bedienen

    Senden Sie Studio 13.0.1

    Senden Sie Studio 13.0.1

    Leistungsstarke integrierte PHP-Entwicklungsumgebung

    Dreamweaver CS6

    Dreamweaver CS6

    Visuelle Webentwicklungstools

    SublimeText3 Mac-Version

    SublimeText3 Mac-Version

    Codebearbeitungssoftware auf Gottesniveau (SublimeText3)

    Wie Springboot Jasypt integriert, um die Verschlüsselung von Konfigurationsdateien zu implementieren Wie Springboot Jasypt integriert, um die Verschlüsselung von Konfigurationsdateien zu implementieren Jun 01, 2023 am 08:55 AM

    Einführung in Jasypt Jasypt ist eine Java-Bibliothek, die es einem Entwickler ermöglicht, seinem Projekt mit minimalem Aufwand grundlegende Verschlüsselungsfunktionen hinzuzufügen und kein tiefes Verständnis der Funktionsweise der Verschlüsselung erfordert. standardbasierte Verschlüsselungstechnologie. Passwörter, Text, Zahlen, Binärdateien verschlüsseln ... Geeignet für die Integration in Spring-basierte Anwendungen, offene API, zur Verwendung mit jedem JCE-Anbieter ... Fügen Sie die folgende Abhängigkeit hinzu: com.github.ulisesbocchiojasypt-spring-boot-starter2 Die Vorteile von Jasypt schützen unsere Systemsicherheit. Selbst wenn der Code durchgesickert ist, kann die Datenquelle garantiert werden.

    Wie SpringBoot Redisson integriert, um eine Verzögerungswarteschlange zu implementieren Wie SpringBoot Redisson integriert, um eine Verzögerungswarteschlange zu implementieren May 30, 2023 pm 02:40 PM

    Nutzungsszenario 1. Die Bestellung wurde erfolgreich aufgegeben, die Zahlung erfolgte jedoch nicht innerhalb von 30 Minuten. Die Zahlung ist abgelaufen und die Bestellung wurde automatisch storniert. 2. Die Bestellung wurde unterzeichnet und es wurde 7 Tage lang keine Bewertung durchgeführt. Wenn die Bestellung abläuft und nicht ausgewertet wird, wird die Bestellung standardmäßig positiv bewertet. Wenn der Händler die Bestellung innerhalb von 5 Minuten nicht erhält, wird die Bestellung abgebrochen Es wird eine SMS-Erinnerung gesendet ... Für Szenarien mit langen Verzögerungen und geringer Echtzeitleistung können wir die Aufgabenplanung verwenden, um eine regelmäßige Abfrageverarbeitung durchzuführen. Zum Beispiel: xxl-job Heute werden wir auswählen

    So implementieren Sie verteilte Sperren mit Redis in SpringBoot So implementieren Sie verteilte Sperren mit Redis in SpringBoot Jun 03, 2023 am 08:16 AM

    1. Redis implementiert das Prinzip der verteilten Sperren und warum verteilte Sperren erforderlich sind. Bevor über verteilte Sperren gesprochen wird, muss erläutert werden, warum verteilte Sperren erforderlich sind. Das Gegenteil von verteilten Sperren sind eigenständige Sperren. Wenn wir Multithread-Programme schreiben, vermeiden wir Datenprobleme, die durch den gleichzeitigen Betrieb einer gemeinsam genutzten Variablen verursacht werden. Normalerweise verwenden wir eine Sperre, um die Richtigkeit der gemeinsam genutzten Variablen sicherzustellen Die gemeinsam genutzten Variablen liegen im gleichen Prozess. Wenn es mehrere Prozesse gibt, die gleichzeitig eine gemeinsam genutzte Ressource betreiben müssen, wie können sie sich dann gegenseitig ausschließen? Heutige Geschäftsanwendungen sind in der Regel Microservice-Architekturen, was auch bedeutet, dass eine Anwendung mehrere Prozesse bereitstellen muss. Wenn mehrere Prozesse dieselbe Datensatzzeile in MySQL ändern müssen, ist eine Verteilung erforderlich, um fehlerhafte Daten zu vermeiden wird zu diesem Zeitpunkt eingeführt. Der Stil ist gesperrt. Punkte erreichen wollen

    So lösen Sie das Problem, dass Springboot nach dem Einlesen in ein JAR-Paket nicht auf die Datei zugreifen kann So lösen Sie das Problem, dass Springboot nach dem Einlesen in ein JAR-Paket nicht auf die Datei zugreifen kann Jun 03, 2023 pm 04:38 PM

    Springboot liest die Datei, kann aber nach dem Packen in ein JAR-Paket nicht auf die neueste Entwicklung zugreifen. Es gibt eine Situation, in der Springboot die Datei nach dem Packen in ein JAR-Paket nicht lesen kann ist ungültig und kann nur über den Stream gelesen werden. Die Datei befindet sich unter resources publicvoidtest(){Listnames=newArrayList();InputStreamReaderread=null;try{ClassPathResourceresource=newClassPathResource("name.txt");Input

    Vergleich und Differenzanalyse zwischen SpringBoot und SpringMVC Vergleich und Differenzanalyse zwischen SpringBoot und SpringMVC Dec 29, 2023 am 11:02 AM

    SpringBoot und SpringMVC sind beide häufig verwendete Frameworks in der Java-Entwicklung, es gibt jedoch einige offensichtliche Unterschiede zwischen ihnen. In diesem Artikel werden die Funktionen und Verwendungsmöglichkeiten dieser beiden Frameworks untersucht und ihre Unterschiede verglichen. Lassen Sie uns zunächst etwas über SpringBoot lernen. SpringBoot wurde vom Pivotal-Team entwickelt, um die Erstellung und Bereitstellung von Anwendungen auf Basis des Spring-Frameworks zu vereinfachen. Es bietet eine schnelle und einfache Möglichkeit, eigenständige, ausführbare Dateien zu erstellen

    So implementieren Sie Springboot+Mybatis-plus, ohne SQL-Anweisungen zum Hinzufügen mehrerer Tabellen zu verwenden So implementieren Sie Springboot+Mybatis-plus, ohne SQL-Anweisungen zum Hinzufügen mehrerer Tabellen zu verwenden Jun 02, 2023 am 11:07 AM

    Wenn Springboot + Mybatis-plus keine SQL-Anweisungen zum Hinzufügen mehrerer Tabellen verwendet, werden die Probleme, auf die ich gestoßen bin, durch die Simulation des Denkens in der Testumgebung zerlegt: Erstellen Sie ein BrandDTO-Objekt mit Parametern, um die Übergabe von Parametern an den Hintergrund zu simulieren dass es äußerst schwierig ist, Multi-Table-Operationen in Mybatis-plus durchzuführen. Wenn Sie keine Tools wie Mybatis-plus-join verwenden, können Sie nur die entsprechende Mapper.xml-Datei konfigurieren und die stinkende und lange ResultMap konfigurieren Schreiben Sie die entsprechende SQL-Anweisung. Obwohl diese Methode umständlich erscheint, ist sie äußerst flexibel und ermöglicht es uns

    Wie SpringBoot Redis anpasst, um die Cache-Serialisierung zu implementieren Wie SpringBoot Redis anpasst, um die Cache-Serialisierung zu implementieren Jun 03, 2023 am 11:32 AM

    1. Passen Sie den RedisTemplate1.1-Standard-Serialisierungsmechanismus an. Die API-basierte Redis-Cache-Implementierung verwendet die RedisTemplate-Vorlage für Daten-Caching-Vorgänge. Öffnen Sie hier die RedisTemplate-Klasse und zeigen Sie die Quellcodeinformationen der Klasse publicclassRedisTemplateextendsRedisAccessorimplementsRedisOperations an. Schlüssel deklarieren, verschiedene Serialisierungsmethoden des Werts, der Anfangswert ist leer @NullableprivateRedisSe

    So erhalten Sie den Wert in application.yml in Springboot So erhalten Sie den Wert in application.yml in Springboot Jun 03, 2023 pm 06:43 PM

    In Projekten werden häufig einige Konfigurationsinformationen benötigt. Diese Informationen können in der Testumgebung und in der Produktionsumgebung unterschiedliche Konfigurationen haben und müssen möglicherweise später basierend auf den tatsächlichen Geschäftsbedingungen geändert werden. Wir können diese Konfigurationen nicht fest im Code codieren. Am besten schreiben Sie sie in die Konfigurationsdatei. Sie können diese Informationen beispielsweise in die Datei application.yml schreiben. Wie erhält oder verwendet man diese Adresse im Code? Es gibt 2 Methoden. Methode 1: Wir können den Wert, der dem Schlüssel in der Konfigurationsdatei (application.yml) entspricht, über den mit @Value versehenen Wert erhalten. Diese Methode eignet sich für Situationen, in denen es relativ wenige Mikrodienste gibt: Tatsächlich Projekte, wenn das Geschäft kompliziert ist, Logik

    See all articles