MoinMoin Logo
  • Comments
  • Immutable Page
  • Menu
    • Navigation
    • RecentChanges
    • FindPage
    • Local Site Map
    • Help
    • HelpContents
    • HelpOnMoinWikiSyntax
    • Display
    • Attachments
    • Info
    • Raw Text
    • Print View
    • Edit
    • Load
    • Save
  • Login

Navigation

  • Start
  • Sitemap

Upload page content

You can upload content for the page named below. If you change the page name, you can also upload content for another page. If the page name is empty, we derive the page name from the file name.

File to load page content from
Page name
Comment

Revision 18 as of 2016-09-28 12:19:23
  • Java
  • Spring
  • SpringBoot

SpringBoot

Example

pom.xml

   1 <?xml version="1.0" encoding="UTF-8"?>
   2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   3     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   4     <modelVersion>4.0.0</modelVersion>
   5     <groupId>hello</groupId>
   6     <artifactId>test-spring-boot</artifactId>
   7     <version>0.1.0</version>
   8     <parent>
   9         <groupId>org.springframework.boot</groupId>
  10         <artifactId>spring-boot-starter-parent</artifactId>
  11         <version>1.4.0.RELEASE</version>
  12     </parent>
  13     <dependencies>
  14         <dependency>
  15             <groupId>org.springframework.boot</groupId>
  16             <artifactId>spring-boot-starter-thymeleaf</artifactId>
  17         </dependency>
  18         <dependency>
  19             <groupId>org.mariadb.jdbc</groupId>
  20             <artifactId>mariadb-java-client</artifactId>
  21             <version>1.4.4</version>
  22         </dependency>
  23         <dependency>
  24             <groupId>commons-dbcp</groupId>
  25             <artifactId>commons-dbcp</artifactId>
  26             <version>1.4</version>
  27         </dependency>
  28 
  29          <dependency>
  30             <groupId>org.springframework</groupId>
  31             <artifactId>spring-jdbc</artifactId>
  32             <version>4.3.2.RELEASE</version>
  33         </dependency>
  34 
  35     </dependencies>
  36     <properties>
  37         <start-class>hello.Application</start-class>
  38     </properties>
  39     <build>
  40         <plugins>
  41             <plugin>
  42                 <groupId>org.springframework.boot</groupId>
  43                 <artifactId>spring-boot-maven-plugin</artifactId>
  44             </plugin>
  45         </plugins>
  46     </build>
  47     <repositories>
  48         <repository>
  49             <id>spring-milestone</id>
  50             <url>http://repo.spring.io/libs-release</url>
  51         </repository>
  52     </repositories>
  53     <pluginRepositories>
  54         <pluginRepository>
  55             <id>spring-milestone</id>
  56             <url>http://repo.spring.io/libs-release</url>
  57         </pluginRepository>
  58     </pluginRepositories>
  59 </project>

src/main/java/hello/GreetingController.java

   1 package hello;
   2 
   3 import org.springframework.stereotype.Controller;
   4 import org.springframework.ui.Model;
   5 import org.springframework.web.bind.annotation.RequestMapping;
   6 import org.springframework.web.bind.annotation.RequestParam;
   7 
   8 import org.slf4j.Logger;
   9 import org.slf4j.LoggerFactory;
  10 import org.springframework.web.bind.annotation.ResponseBody;
  11 import org.springframework.beans.factory.annotation.Autowired;
  12 
  13 import java.util.List;
  14 import java.util.ArrayList;
  15 
  16 @Controller
  17 public class GreetingController {
  18     private final Logger logger = LoggerFactory.getLogger(GreetingController.class);
  19     @Autowired
  20     DummyDAO dummyDAO;
  21 
  22     public GreetingController(){
  23         logger.debug("Greeting controller created.");
  24     }
  25 
  26     @RequestMapping("/greeting")
  27     // http://localhost:8080/greeting?name=nnnn 
  28     public String greeting(@RequestParam(value="name", required=false, defaultValue="World") String name, Model model) {
  29         logger.info("Greeting endpoint called.");
  30         model.addAttribute("name", name);
  31         return "greeting";
  32     }
  33 
  34     @RequestMapping(value="/dummy",produces="application/json")
  35     @ResponseBody
  36     // http://localhost:8080/dummy
  37     public List<Dummy> dummy(){
  38       List<Dummy> list= new java.util.ArrayList<Dummy>();
  39       Dummy dummy = new Dummy();
  40       dummy.setFieldA("AAA");
  41       dummy.setFieldB("CCC");
  42       list.add(dummy);
  43 
  44       Dummy dummy2 = new Dummy();
  45       dummy2.setFieldA("AAA2");
  46       dummy2.setFieldB("CCC2");
  47       list.add(dummy2);
  48 
  49       return list;
  50     }
  51 
  52     @RequestMapping(value="/dummyname",produces="application/json")
  53     @ResponseBody
  54     // http://localhost:8080/dummyname
  55     public String getDummyName(){
  56       return dummyDAO.getNameFromDummy();
  57     }
  58 
  59 
  60 }

src/main/java/hello/Application.java

   1 package hello;
   2 
   3 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
   4 import org.springframework.boot.SpringApplication;
   5 import org.springframework.context.annotation.ComponentScan;
   6 import org.springframework.context.annotation.Bean;
   7 import org.springframework.beans.factory.annotation.Autowired;
   8 import org.springframework.beans.factory.annotation.Qualifier;
   9 
  10 import org.apache.commons.dbcp.BasicDataSource;
  11 import org.springframework.jdbc.core.JdbcTemplate;
  12 
  13 import org.slf4j.Logger;
  14 import org.slf4j.LoggerFactory;
  15 
  16 @ComponentScan
  17 @EnableAutoConfiguration
  18 public class Application {
  19     private static Logger logger;
  20 
  21     public static void main(String[] args) {
  22         logger = LoggerFactory.getLogger(Application.class);
  23         logger.info("Starting application");
  24         SpringApplication.run(Application.class, args);
  25     }
  26 
  27     @Bean(name="basicDataSource",destroyMethod="close")
  28     public BasicDataSource createBasicDataSource(){
  29       logger.info("Creating basicDataSource");
  30       BasicDataSource bds = new BasicDataSource();
  31       bds.setDriverClassName("org.mariadb.jdbc.Driver");
  32       bds.setUrl("jdbc:mariadb://localhost:3306/springmvchtml");
  33       bds.setUsername("usertest");
  34       bds.setPassword("usertest");
  35       bds.setInitialSize(3);
  36       return bds;
  37     }
  38 /*
  39     @Bean(name="jdbcTemplate")
  40     public JdbcTemplate jdbcTemplate(){
  41       logger.info("Creating jdbcTemplate");
  42       return new JdbcTemplate( basicDataSource() );
  43     }
  44 */
  45     @Bean(name="jdbcTemplate")
  46     public JdbcTemplate createJdbcTemplate(@Autowired @Qualifier("basicDataSource") BasicDataSource bds  ){
  47       logger.info("Creating jdbcTemplate");
  48       return new JdbcTemplate( bds );
  49     }
  50 
  51 }

src/main/java/hello/ThreadTimer.java

   1 package hello;
   2 
   3 import org.springframework.stereotype.Component;
   4 import javax.annotation.PostConstruct;
   5 import org.springframework.beans.factory.annotation.Autowired;
   6 import java.text.MessageFormat;
   7 import org.slf4j.Logger;
   8 import org.slf4j.LoggerFactory;
   9 import java.util.ArrayList;
  10 
  11 @Component
  12 public class ThreadTimer extends Thread {
  13   private int delaySeconds;
  14   private Logger logger;
  15   private boolean running;
  16   private Object monitor=new Object();
  17   private ArrayList<Object> subscribers;
  18 
  19   //@Autowired
  20   //WaitThread waitThread;
  21 
  22   public ThreadTimer() {
  23     this.logger = LoggerFactory.getLogger(ThreadTimer.class);
  24     logger.info("Created instance of " + this.getClass().getSimpleName());
  25     this.running = true;
  26     this.delaySeconds = 5 * 1000;
  27     this.setName(this.getClass().getSimpleName() + "_" + this.getName());
  28     this.subscribers = new ArrayList<Object>();
  29   }
  30 
  31   public void addSubscriber(Object subscriber){
  32     this.subscribers.add(subscriber);
  33   }
  34 
  35   @PostConstruct
  36   public void init() {
  37     logger.info("Starting the thread");
  38     this.start();
  39   }
  40 
  41   @Override
  42   public void run() {
  43     while (running) {
  44       try {
  45         Thread.sleep(this.delaySeconds);
  46         logger.info("Delay " + this.getClass().getSimpleName());
  47 
  48         for(Object o: this.subscribers){
  49           synchronized(o){
  50             o.notify();
  51           }
  52         }
  53       }
  54       catch (InterruptedException e) {
  55           logger.info("ThreadTimer interrupted exception:" + e.getMessage() );
  56       }
  57       catch (Exception e) {
  58           e.printStackTrace();
  59           logger.info("ThreadTimer exception:" + e.getMessage() );
  60           stopRunning();
  61       }
  62     }
  63     logger.info("Exited " + this.getClass().getSimpleName());
  64   }
  65 
  66   public void startRunning() {
  67     this.running = true;
  68   }
  69 
  70   public void stopRunning() {
  71     this.running = false;
  72   }
  73 
  74   public void destroy(){
  75     logger.info("Called destroy");
  76     this.stopRunning();
  77     this.interrupt();
  78   }
  79 }

src/main/java/hello/WaitThread.java

   1 package hello;
   2 
   3 import org.springframework.stereotype.Component;
   4 import javax.annotation.PostConstruct;
   5 import java.text.MessageFormat;
   6 import org.slf4j.Logger;
   7 import org.slf4j.LoggerFactory;
   8 import org.springframework.beans.factory.annotation.Autowired;
   9 
  10 @Component
  11 public class WaitThread extends Thread {
  12   private Logger logger;
  13   private boolean running;
  14   private Object monitor;
  15 
  16   @Autowired
  17   ThreadTimer timerThread;
  18 
  19   public WaitThread() {
  20     this.logger = LoggerFactory.getLogger(WaitThread.class);
  21     logger.info("Created instance of " + this.getClass().getSimpleName());
  22     this.running = true;
  23     this.setName(this.getClass().getSimpleName() + "_" + this.getName());
  24     this.monitor=new Object();
  25   }
  26 
  27   @PostConstruct
  28   public void init() {
  29     this.timerThread.addSubscriber(this);
  30     logger.info("Starting the thread");
  31     this.start();
  32   }
  33 
  34   public void run() {
  35     while (running) {
  36       try {
  37         synchronized(this){
  38           this.wait();
  39           logger.info("Notification received.");
  40         }
  41       }
  42       catch (InterruptedException e) {
  43           logger.info("ThreadTimer interrupted exception:" + e.getMessage() );
  44       }
  45       catch (Exception e) {
  46           logger.info("WaitThread exception:" + e.getMessage() );
  47           stopRunning();
  48       }
  49     }
  50     logger.info("Exited " + this.getClass().getSimpleName());
  51   }
  52 
  53   public void startRunning() {
  54     this.running = true;
  55   }
  56 
  57   public void stopRunning() {
  58     this.running = false;
  59   }
  60 
  61   public void destroy(){
  62     logger.info("Called destroy");
  63     this.stopRunning();
  64     this.interrupt();
  65   }
  66 }

src/main/java/hello/Dummy.java

   1 package hello;
   2 
   3 public class Dummy{
   4   private String fieldA;
   5   private String fieldB;
   6 
   7   public Dummy(){
   8   }
   9 
  10   public String getFieldA(){
  11     return fieldA;
  12   }
  13 
  14   public String getFieldB(){
  15     return fieldB;
  16   }
  17 
  18   public void setFieldA(String arg){
  19     fieldA = arg;
  20   }
  21 
  22   public void setFieldB(String arg){
  23     fieldB = arg;
  24   }
  25 }

src/main/java/hello/DummyDAO.java

   1 package hello;
   2 
   3 import org.slf4j.Logger;
   4 import org.slf4j.LoggerFactory;
   5 
   6 import org.springframework.beans.factory.annotation.Autowired;
   7 import org.springframework.beans.factory.annotation.Qualifier;
   8 import org.springframework.jdbc.core.JdbcTemplate;
   9 import org.springframework.stereotype.Component;
  10 
  11 @Component
  12 public class DummyDAO {
  13   @Autowired
  14   @Qualifier("jdbcTemplate")
  15   private JdbcTemplate jdbcTemplate;
  16   private Logger logger;
  17 
  18   public DummyDAO() {
  19     logger = LoggerFactory.getLogger(DummyDAO.class);
  20     logger.info("Created " + this.getClass().getSimpleName());
  21   }
  22 
  23   public String getNameFromDummy() {
  24     return this.jdbcTemplate.queryForObject("select name from dummy limit 1", String.class);
  25   }
  26 }

src/main/resources/templates/greeting.html

   1 <!DOCTYPE HTML>
   2 <html xmlns:th="http://www.thymeleaf.org">
   3 <head>
   4     <title>Getting Started: Serving Web Content</title>
   5     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
   6 </head>
   7 <body>
   8     <p th:text="'Hello, ' + ${name} + '!'" />
   9 </body>
  10 </html>

src/main/resources/application.properties

   1 logging.file=/tmp/testout.log

src/main/resources/logback-spring.xml

   1 <?xml version="1.0" encoding="UTF-8"?>
   2 <configuration>
   3 <!--
   4     <include resource="org/springframework/boot/logging/logback/defaults.xml" />
   5     <property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}spring.log}"/>
   6     <include resource="org/springframework/boot/logging/logback/file-appender.xml" />
   7 -->
   8     <!-- override spring logback default behaviour -->
   9     <appender name="FILE" class="ch.qos.logback.core.FileAppender">
  10         <file>${filelog}</file>
  11         <encoder>
  12                 <pattern>%date{ISO8601} [%thread] %-5level %logger{35} - %msg%n</pattern>
  13         </encoder>
  14     </appender>
  15 
  16     <appender name="GREETFILE" class="ch.qos.logback.core.FileAppender">
  17         <file>/tmp/greet.log</file>
  18         <encoder>
  19                 <pattern>%date{ISO8601} [%thread] %-5level %logger{35} - %msg%n</pattern>
  20         </encoder>
  21     </appender>
  22 
  23     <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
  24         <layout class="ch.qos.logback.classic.PatternLayout">
  25                 <Pattern>%yellow(%date{ISO8601}) %green([%thread]) %highlight(%-5level) %cyan(%logger{35}) - %white(%msg%n) </Pattern>
  26         </layout>
  27     </appender>
  28 
  29     <root level="INFO">
  30         <appender-ref ref="FILE" />
  31         <appender-ref ref="CONSOLE"/>
  32     </root>
  33 
  34     <logger name="hello.GreetingController" level="debug" additivity="false">
  35         <appender-ref ref="GREETFILE"/>
  36         <appender-ref ref="CONSOLE" />
  37     </logger>
  38 
  39 </configuration>

src/main/resources/test.sql

   1 --JDBC URL: jdbc:mariadb://localhost:3306/springmvchtml
   2 --mysql -u root -p
   3 create database springmvchtml;
   4 create user 'usertest'@'%' identified by '????????';
   5 create user 'usertest'@'localhost' identified by '????????';
   6 grant all on springmvchtml.* to 'usertest'@'%';
   7 grant all on springmvchtml.* to 'usertest'@'localhost';
   8 show grants for 'usertest'@'%';
   9 show grants for 'usertest'@'localhost';
  10 create table springmvchtml.dummy (name varchar(255) ) ;
  11 insert into springmvchtml.dummy (name) values('aaaa');
  12 insert into springmvchtml.dummy (name) values('bbbb');
  13 commit;
  14 -- mysql -u usertest -p
  15 

JAR files list

  • jar tf target/test-spring-boot-0.1.0.jar | grep "\.jar"

BOOT-INF/lib/jackson-core-2.8.1.jar
BOOT-INF/lib/spring-web-4.3.2.RELEASE.jar
BOOT-INF/lib/unbescape-1.1.0.RELEASE.jar
BOOT-INF/lib/javassist-3.20.0-GA.jar
BOOT-INF/lib/mariadb-java-client-1.4.4.jar
BOOT-INF/lib/jul-to-slf4j-1.7.21.jar
BOOT-INF/lib/groovy-2.4.7.jar
BOOT-INF/lib/hibernate-validator-5.2.4.Final.jar
BOOT-INF/lib/classmate-1.3.1.jar
BOOT-INF/lib/tomcat-embed-el-8.5.4.jar
BOOT-INF/lib/tomcat-embed-core-8.5.4.jar
BOOT-INF/lib/commons-pool-1.6.jar
BOOT-INF/lib/thymeleaf-spring4-2.1.5.RELEASE.jar
BOOT-INF/lib/spring-context-4.3.2.RELEASE.jar
BOOT-INF/lib/thymeleaf-2.1.5.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-tomcat-1.4.0.RELEASE.jar
BOOT-INF/lib/thymeleaf-layout-dialect-1.4.0.jar
BOOT-INF/lib/spring-boot-autoconfigure-1.4.0.RELEASE.jar
BOOT-INF/lib/snakeyaml-1.17.jar
BOOT-INF/lib/spring-boot-starter-web-1.4.0.RELEASE.jar
BOOT-INF/lib/spring-boot-starter-logging-1.4.0.RELEASE.jar
BOOT-INF/lib/spring-webmvc-4.3.2.RELEASE.jar
BOOT-INF/lib/spring-beans-4.3.2.RELEASE.jar
BOOT-INF/lib/slf4j-api-1.7.21.jar
BOOT-INF/lib/spring-jdbc-4.3.2.RELEASE.jar
BOOT-INF/lib/jackson-annotations-2.8.1.jar
BOOT-INF/lib/commons-dbcp-1.4.jar
BOOT-INF/lib/ognl-3.0.8.jar
BOOT-INF/lib/spring-boot-starter-1.4.0.RELEASE.jar
BOOT-INF/lib/tomcat-embed-websocket-8.5.4.jar
BOOT-INF/lib/spring-boot-starter-thymeleaf-1.4.0.RELEASE.jar
BOOT-INF/lib/spring-tx-4.3.2.RELEASE.jar
BOOT-INF/lib/logback-classic-1.1.7.jar
BOOT-INF/lib/spring-boot-1.4.0.RELEASE.jar
BOOT-INF/lib/spring-expression-4.3.2.RELEASE.jar
BOOT-INF/lib/logback-core-1.1.7.jar
BOOT-INF/lib/validation-api-1.1.0.Final.jar
BOOT-INF/lib/spring-core-4.3.2.RELEASE.jar
BOOT-INF/lib/jackson-databind-2.8.1.jar
BOOT-INF/lib/jcl-over-slf4j-1.7.21.jar
BOOT-INF/lib/log4j-over-slf4j-1.7.21.jar
BOOT-INF/lib/spring-aop-4.3.2.RELEASE.jar
BOOT-INF/lib/jboss-logging-3.3.0.Final.jar

Add HTTPS/SSL support

Keystore creation

#create keystore with key springboot
keytool -genkey -alias springboot -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore springboot.p12 -validity 3650

Enter keystore password: 12345678
Re-enter new password: 12345678
What is your first and last name?
  [Unknown]:  Spring Boot
What is the name of your organizational unit?
  [Unknown]:  Boot
What is the name of your organization?
  [Unknown]:  Boot
What is the name of your City or Locality?
  [Unknown]:  Lisbon
What is the name of your State or Province?
  [Unknown]:  Estremadura
What is the two-letter country code for this unit?
  [Unknown]:  PT
Is CN=Spring Boot, OU=Boot, O=Boot, L=Lisbon, ST=Estremadura, C=PT correct?
  [no]:  yes

#list keys 
keytool -list -v -keystore springboot.p12 -storetype pkcs12  

Code to add in Application class/configuration

// import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
// import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
// import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
// import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
// import java.io.FileNotFoundException;
// import org.apache.catalina.connector.Connector;
// import org.springframework.beans.factory.annotation.Value;
// import javax.inject.Inject;
// import org.springframework.util.ResourceUtils;

public class TCC implements TomcatConnectorCustomizer{
    private String keystoreFile;
    private String keystorePassword;
    private String keystoreType;
    private String keystoreAlias;

    public TCC(String keystoreFile, String keystorePassword, String keystoreType, String keystoreAlias){
        this.keystoreFile=keystoreFile;
        this.keystorePassword=keystorePassword;
        this.keystoreType=keystoreType;
        this.keystoreAlias=keystoreAlias;
    }

    public void customize(Connector connector){
        connector.setSecure(true);
        connector.setScheme("https");
        connector.setAttribute("keystoreFile", keystoreFile);
        connector.setAttribute("keystorePass", keystorePassword);
        connector.setAttribute("keystoreType", keystoreType);
        connector.setAttribute("keyAlias", keystoreAlias);
        connector.setAttribute("clientAuth", "false");
        connector.setAttribute("sslProtocol", "TLS");
        connector.setAttribute("SSLEnabled", true);
    }
}

public class ESCC implements EmbeddedServletContainerCustomizer{
    private String keystoreFile;
    private String keystorePassword;
    private String keystoreType;
    private String keystoreAlias;

    public ESCC(String keystoreFile, String keystorePassword, String keystoreType, String keystoreAlias){
        this.keystoreFile=keystoreFile;
        this.keystorePassword=keystorePassword;
        this.keystoreType=keystoreType;
        this.keystoreAlias=keystoreAlias;
    }

    public void customize(ConfigurableEmbeddedServletContainer container){
        TomcatEmbeddedServletContainerFactory containerFactory = (TomcatEmbeddedServletContainerFactory) container;

        TomcatConnectorCustomizer tcc = new TCC(keystoreFile,keystorePassword,keystoreType,keystoreAlias);
        containerFactory.addConnectorCustomizers(tcc);
    }
}

@Bean
@Inject
public EmbeddedServletContainerCustomizer containerCustomizer(
    @Value("${keystore.file}") String keystoreFile,
    @Value("${keystore.password}") String keystorePassword,
    @Value("${keystore.type}") String keystoreType,
    @Value("${keystore.alias}") String keystoreAlias) throws FileNotFoundException
{
    final String absoluteKeystoreFile = ResourceUtils.getFile(keystoreFile).getAbsolutePath();
    EmbeddedServletContainerCustomizer escc = new ESCC(absoluteKeystoreFile,keystorePassword,keystoreType,keystoreAlias);
    return escc;
}

pom.xml

<!-- Add dependency to support @Inject annotation -->
<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>
  • Add server.port=8443 to application.properties or use system property -Dserver.port=8443
  • Run java -Dfilelog=/tmp/out.log -Dkeystore.file=springboot.p12 -Dkeystore.password=12345678 -Dkeystore.type=PKCS12 -Dkeystore.alias=springboot -jar target/test-spring-boot-0.1.0.jar
  • Access https://localhost:8443/greeting?name=x

Environment variables

src/main/resources/application.properties

logging.file=/tmp/testout.log
server.port=8443
foo.bar=bohica

   1 import org.springframework.core.env.Environment;
   2 import org.springframework.beans.factory.annotation.Value;
   3 //....................
   4     @Value("${foo.bar}")
   5     private String fooBar;
   6 
   7     @RequestMapping(value="/env",produces="application/json")
   8     @ResponseBody
   9     public List<String> getEnv(){
  10       List<String> ret = new ArrayList<String>();
  11       ret.add( env.getProperty("foo.bar") );
  12       ret.add(fooBar);
  13       return ret;
  14     }
  15 //....................
  16 

Override value in application.properties

  • java -jar target/test-spring-boot-0.1.0.jar --foo.bar=snafu
  • MoinMoin Powered
  • Python Powered
  • GPL licensed
  • Valid HTML 4.01