记一次Spring小项目的搭建

介绍

开发环境:

  • Win10
  • Java 8
  • maven 3.6.3
  • VS-Code 1.56.2

项目结构:
Spring 5.3.6,Hibernate 5.4.30.Final,tomcat 8.5

项目最终结构:
src
为了尽量能看到全貌,没有子文件夹的文件夹都折叠了,pom.xml 是在src的同级目录下。
完整的结构如下:

├───.classpath
├───pom.xml
├───.project
├───.settings
├───.vscode
├───src
│   ├───main
│   │   ├───filters
│   │   │   ├───dev
│   │   │   └───prod
│   │   ├───java
│   │   │   └───com
│   │   │       └───seagate
│   │   │           └───flawscananalytic
│   │   │               ├───bean        
│   │   │               ├───controller  
│   │   │               ├───dao
│   │   │               │   └───impl    
│   │   │               ├───entity      
│   │   │               ├───model       
│   │   │               ├───repository  
│   │   │               ├───service     
│   │   │               │   └───impl    
│   │   │               └───util        
│   │   ├───resources
│   │   │   ├───db
│   │   │   │   ├───mysql
│   │   │   │   └───pgsql
│   │   │   └───META-INF
│   │   └───webapp
│   │       ├───data
│   │       │   └───images
│   │       ├───logs
│   │       ├───views
│   │       │   ├───common
│   │       │   ├───issue
│   │       │   └───releaseNote
│   │       └───WEB-INF
│   └───test
│       ├───java
│       │   └───com
│       │       └───seagate
│       │           └───flawscananalytic
│       │               └───test
│       └───resources
│           └───META-INF

静态资源文件太多,从树里面删掉了,位于src/main/webapp/resources。target也删掉了。

gitee:ssh_demo

过程

VS-Code Java环境
VS-Code Java环境的搭建,网上有很多教程,应该都能搭建起来。主要是安装Java Extension Package插件,然后配置好Java和maven环境。

2.1 创建Maven项目

在vscode中使用命令 maven create maven project,选择maven-archetype-webapp,然后填好group ID和 artifact ID。创建完项目后,用VS-Code 打开项目。然后手动添加缺失的几个目录,结果如下:

.
|-- pom.xml
|-- src
|   |-- main
|   |   |-- java
|   |   |   `-- com        
|   |   |       `-- example
|   |   |           |-- bean      
|   |   |           |-- controller
|   |   |           |-- dao       
|   |   |           |   `-- impl  
|   |   |           |-- entity    
|   |   |           |-- model      
|   |   |           |-- respository
|   |   |           |-- service    
|   |   |           |   `-- impl   
|   |   |           `-- util       
|   |   |-- resources      
|   |   `-- webapp
|   |       |-- WEB-INF
|   |       |   `-- web.xml
|   |       `-- index.jsp
|   `-- test
|       |-- java
|       |   `-- com
|       |       `-- example
|       `-- resources
`-- target
    |-- classes
    |   `-- com
    |       `-- example
    |           |-- bean
    |           |-- controller
    |           |-- dao
    |           |   `-- impl
    |           |-- entity
    |           |-- model
    |           |-- respository
    |           |-- service
    |           |   `-- impl
    |           `-- util
    |-- generated-sources
    |   `-- annotations
    |-- generated-test-sources
    |   `-- test-annotations
    `-- test-classes
        `-- com
            `-- example

2.2 环境配置

有三个环境,jetty的开发环境,tomcat的开发环境和发布环境。开发环境是用MySQL8,发布环境是用Postgresql。
在src/main 下新建文件夹src/main/filters/dev 和 src/main/filters/prod 这对应pom文件中的Profiles。分别在dev 和 prod 文件夹中添加config.properties,配置各自的环境。我的prod/config.properties配置内容是:

# postgresql
jdbc.driverClassName=org.postgresql.Driver
jdbc.url=jdbc:postgresql://ip:port/releasenote
jdbc.username=xxx
jdbc.password=xxx
#hibernate
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

在dev/config.properties添加相应的内容。

2.3 pom.xml 和 数据库文件

pom文件过长,就直接放链接啦。
创建文件夹src/main/resources/db,其中放sql文件。
创建数据库releasenote,三张表TB_MD_RELEASE_NOTE,TB_USER,TB_ISSUE。

-- mysql 8
create database if not exists releasenote;
use releasenote;
drop table if exists TB_MD_RELEASE_NOTE;
CREATE TABLE TB_MD_RELEASE_NOTE(
  OBJ_ID bigint PRIMARY key AUTO_INCREMENT,
  VERSION VARCHAR(20),
  CONTENT text,
  PUBLISH_DATE TIMESTAMP);
insert into TB_MD_RELEASE_NOTE (`version`, `content`, publish_date)
values ('0.0.0', '# 0.0.0', '2021-4-23 13:12:34'),('0.0.1', '# 0.0.1', '2021-4-23 14:12:34');

use releasenote;
create table TB_USER(
    OBJ_ID bigint PRIMARY key AUTO_INCREMENT,
    username varchar(20));
create table TB_ISSUE(
    OBJ_ID BIGINT PRIMARY key AUTO_INCREMENT,
    title varchar(40),
    content text,
    create_time timestamp,
    user_ID bigint);
insert INTO TB_USER(`username`)
VALUES ("init"),
    ("backup");
insert into TB_ISSUE(`title`, `content`, `create_time`, `user_ID`)
VALUES ("can not plot scopy for m15", "for m15, when plot SN xxxxx, get error.", "2021-5-12 14:05:34", 1), ("IPS seems very useful, how about ....", "the method of calculate ... is ...", "2021-5-17 14:22:43", 1 );

2.4 spring配置

新建文件src/main/resources/config.properties,在其中放置前面filters文件中的配置,如下:

# common variables
dataPath = /releasenote
siteDisplay=CN
websiteVesion=1.0.0
# jdbc
jdbc.driverClassName=${jdbc.driverClassName}
jdbc.url = ${jdbc.url}
# authentication
jdbc.username=${jdbc.username}
jdbc.password=${jdbc.password}
# hibernate
hibernate.connection.autocommit=false
hibernate.format_sql=true
hibernate.show_sql=false
hibernate.hbm2ddl.auto=update
hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
hibernate.dialect=${hibernate.dialect}

新建文件夹src/main/resources/META-INF,在META-INF中新建文件applicationContext.xml。配置spring以及hibernate。
applicationContext.xml:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation="
      http://www.springframework.org/schema/aop 
     http://www.springframework.org/schema/aop/spring-aop.xsd 
     http://www.springframework.org/schema/beans 
     http://www.springframework.org/schema/beans/spring-beans.xsd 
     http://www.springframework.org/schema/jdbc 
     http://www.springframework.org/schema/jdbc/spring-jdbc.xsd 
     http://www.springframework.org/schema/tx 
     http://www.springframework.org/schema/tx/spring-tx.xsd 
     http://www.springframework.org/schema/jee 
     http://www.springframework.org/schema/jee/spring-jee.xsd 
     http://www.springframework.org/schema/context 
     http://www.springframework.org/schema/context/spring-context.xsd 
     http://www.springframework.org/schema/data/jpa 
     http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
">
    <!--********************************************配置Spring***************************************-->
    <!-- 自动扫描 -->
    <context:component-scan base-package="com.example">
        <!-- 扫描时跳过 @Controller 注解的JAVA类(控制器) -->
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    <!--********************************************配置hibernate********************************************-->
    <!--扫描配置文件(这里指向的是之前配置的那个config.properties)-->
    <context:property-placeholder location="classpath:/config.properties" />
    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}" />
        <!--数据库连接驱动-->
        <property name="url" value="${jdbc.url}" />
        <!--数据库地址-->
        <property name="username" value="${jdbc.username}" />
        <!--用户名-->
        <property name="password" value="${jdbc.password}" />
        <!--密码-->
        <!-- druid-->
        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="1"/>
        <property name="minIdle" value="1"/>
        <property name="maxActive" value="3"/>
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="60000"/>
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>
    </bean>
    <!-- 配置entityManagerFactory -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan" value="com.example"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            </bean>
        </property>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>                <!--hibernate根据实体自动生成数据库表 validate/update/create-->
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>                <!--指定数据库方言-->
                <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>                <!--在控制台显示执行的数据库操作语句-->
                <prop key="hibernate.format_sql">${hibernate.format_sql}</prop>                <!--在控制台显示执行的数据哭操作语句(格式)-->
            </props>
        </property>
    </bean>
    <!-- 事物管理器配置  -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <!-- Spring Data Jpa配置 -->
    <jpa:repositories base-package="com.example.dao" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/>
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

2.5 Spring-mvc 配置

spring-mvc配置:1,启动注解,使用默认的servlet处理,并扫描controller类; 2,配置视图解析,增加视图的前缀和后缀; 3,对静态资源的映射; 4,转换器配置,将返回的对象转为json格式数据; 5,开启文件上传配置;
src/main/resources/META-INF/spring-mvc.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 启动注解驱动的spring MVC功能,注册请求url和注解POJO类方法的映射-->
    <mvc:annotation-driven />
    <!-- use @Value("${}") in Bean-->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations" value="classpath:config.properties" />
    </bean>
    <bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />
    <!-- 只扫描Controller注解的类 -->
    <context:component-scan base-package="com.seagate.flawscananalytic.controller">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
    </context:component-scan>
    <mvc:default-servlet-handler />
    <!-- 视图解析器-->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
        <property name="prefix" value="views/" />
        <!-- 前缀 -->
        <property name="suffix" value=".jsp" />
        <!-- 后缀 -->
    </bean>
    <!--这里是对静态资源的映射-->
    <!-- <mvc:resources mapping="/js/**" location="/resources/js/" />
    <mvc:resources mapping="/css/**" location="/resources/css/" />
    <mvc:resources mapping="/img/**" location="/resources/img/" /> -->
    <mvc:resources mapping="/assets/**" location="/resources/" />
    <!-- to show uploaded images, maybe not suitable-->
    <mvc:resources mapping="/images/**" location="/data/images/"></mvc:resources>
    <!-- 通过实体类返回json格式数据的关键配置 -->
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                    <property name="supportedMediaTypes">
                        <list>
                            <value>application/json;charset=UTF-8</value>
                        </list>
                    </property>
                </bean>
            </list>
        </property>
    </bean>
    <!--upload by multipart/form-data -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- one of the properties available; the maximum file size in bytes -->
        <property name="maxUploadSize" value="104857600" />
    </bean>
</beans>

2.6 web.xml

1,加载spring配置;2,spring监听器;3,spring-mvc配置;4,字符集
web.xml:

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">

  <welcome-file-list>
    <welcome-file>/index.jsp</welcome-file>
  </welcome-file-list>
  <!--加载Spring的配置文件到上下文中去-->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      classpath:META-INF/applicationContext.xml
    </param-value>
  </context-param>
  <!-- Spring监听器 -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <!--  字符集过滤  -->
  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
      <param-name>forceEncoding</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  <!-- spring MVC config start-->
  <servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:META-INF/spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>spring</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

2.7 mvc

以releasenote表为例,src/main/java/com/example/entity/ReleaseNote.java:

package com.example.entity;

import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import org.hibernate.annotations.Type;

@Entity
@Table(name = "TB_MD_RELEASE_NOTE")
@SequenceGenerator(name = "TB_MD_RELEASE_NOTE_OBJ_ID_SEQ", sequenceName = "TB_MD_RELEASE_NOTE_OBJ_ID_SEQ", allocationSize = 1, initialValue = 24)
public class ReleaseNote {

    @Id
    @Column(name = "OBJ_ID", unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "TB_MD_RELEASE_NOTE_OBJ_ID_SEQ")
    // @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long objId;

    @Column(name = "\"VERSION\"")
    private String version;

    @Column(name = "\"CONTENT\"")
    @Type(type = "text")
    private String content;

    @Column(name = "PUBLISH_DATE")
    private Timestamp publishDate;
    /*getter,setter,toString省略了*/
}

src/main/java/com/example/util/TimeUtil.java:

package com.example.util;

import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;

public class TimeUtil {

    static DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String timestampToString(Timestamp time) {
        return df.format(time);
    }

    /**
     * convert string time to timestamp
     * 
     * @param time 5/11/2021 12:21:02 PM or 2021-5-11 12:21:02
     * @return Timestamp
     */
    public static Timestamp stringToTimestamp(String time) {
        Date date = null;
        int[] datePart = null, timePart = null;
        String[] parts = time.split(" ");
        int year, month, day;
        if (time.contains("/")) {
            if (parts.length == 3) {
                String APMarker = parts[2];
                datePart = Arrays.stream(parts[0].split("/")).mapToInt(Integer::parseInt).toArray();
                timePart = Arrays.stream(parts[1].split(":")).mapToInt(Integer::parseInt).toArray();
                if (APMarker.equals("PM")) {
                    timePart[0] = timePart[0] + 12;
                    if (timePart[0] >= 24) {
                        datePart[1] += 1;
                    }
                }
                year = datePart[2];
                month = datePart[0];
                day = datePart[1];
            } else {
                System.out.println("can not convert string:" + time + " to Timestamp, wrong former");
                return null;
            }
        } else if (time.contains("-")) {
            if (parts.length == 2) {
                datePart = Arrays.stream(parts[0].split("-")).mapToInt(Integer::parseInt).toArray();
                timePart = Arrays.stream(parts[1].split(":")).mapToInt(Integer::parseInt).toArray();
                year = datePart[0];
                month = datePart[1];
                day = datePart[2];
            } else {
                System.out.println("can not convert string:" + time + " to Timestamp, wrong former");
                return null;
            }
        } else {
            System.out.println("can not convert string:" + time + " to Timestamp, wrong former");
            return null;
        }
        Calendar c = Calendar.getInstance();
        c.set(year, month - 1, day, timePart[0], timePart[1], timePart[2]);
        date = c.getTime();
        return new Timestamp(date.getTime());
    }

    /**
     * formate time string from 5/11/2021 12:21:02 PM to 2021-5-12 00:21:02
     * 
     * @param date
     * @return
     */
    public static String formatString(String date) {
        Timestamp time = stringToTimestamp(date);
        return timestampToString(time);
    }

    public static String getCurrentTime() {
        String time = df.format(new Date());
        return time;
    }
}

src/main/java/com/example/bean/ReleaseNoteBean.java:

package com.example.bean;

import com.example.entity.ReleaseNote;
import com.example.util.TimeUtil;

public class ReleaseNoteBean implements Comparable<ReleaseNoteBean> {
    private Long objId;
    private String version;
    private String content;
    private String publishdate;
    public static String strFormat = "yyyy-MM-dd HH:mm:ss";

    public void fromDto(ReleaseNote releasenote) {
        this.objId = releasenote.getObjId();
        this.version = releasenote.getVersion();
        this.content = releasenote.getContent();
        this.publishdate = TimeUtil.timestampToString(releasenote.getPublishDate());
    }

    public ReleaseNote toDto() {
        ReleaseNote releaseNote = new ReleaseNote(); // ? is this one not controlled by bean factory
        try {
            releaseNote.setObjId(this.objId);
        } catch (NullPointerException e) {
            System.out.println("NullPointerException: ReleaseNoteBean.objId");
        }
        releaseNote.setVersion(this.version);
        releaseNote.setContent(this.content);
        releaseNote.setPublishDate(TimeUtil.stringToTimestamp(this.publishdate));
        return releaseNote;
    }
    
    @Override
    public int compareTo(ReleaseNoteBean bean) {
        int compareResult = 0;
        String[] versionArr = this.version.split("\\.");
        String[] otherVersionArr = bean.getVersion().split("\\.");
        if (versionArr.length > otherVersionArr.length) {
            String[] newOtherVersionArr = new String[versionArr.length];
            for (int i = 0; i < otherVersionArr.length; i++) {
                newOtherVersionArr[i] = otherVersionArr[i];
            }
            for (int i = otherVersionArr.length; i < versionArr.length; i++) {
                newOtherVersionArr[i] = "0";
            }
            otherVersionArr = newOtherVersionArr;
        }
        if (versionArr.length < otherVersionArr.length) {
            String[] newVersionArr = new String[otherVersionArr.length];
            for (int i = 0; i < versionArr.length; i++) {
                newVersionArr[i] = versionArr[i];
            }
            for (int i = versionArr.length; i < otherVersionArr.length; i++) {
                newVersionArr[i] = "0";
            }
            versionArr = newVersionArr;
        }
        for (int i = 0; i < versionArr.length; i++) {
            if (Integer.valueOf(versionArr[i]) < Integer.valueOf(otherVersionArr[i])) {
                compareResult = -1;
                break;
            }
            if (Integer.valueOf(versionArr[i]) > Integer.valueOf(otherVersionArr[i])) {
                compareResult = 1;
                break;
            }
        }
        return compareResult;
    }
    /* getter,settring,toString*/
}

src/main/java/com/example/dao/ReleaseNoteDao.java:

package com.example.dao;

import java.util.List;

import com.example.entity.ReleaseNote;

public interface ReleaseNoteDao {

    public void saveReleaseNote(ReleaseNote releaseNote);

    public ReleaseNote getReleaseNote(Long objId);

    public List<ReleaseNote> getAllReleaseNotes();

    public void deleteReleaseNote(Long objId);
}

src/main/java/com/example/dao/impl/ReleaseNoteDaoImpl.java:

package com.example.dao.impl;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;

import com.example.dao.ReleaseNoteDao;
import com.example.entity.ReleaseNote;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository("ReleaseNoteDao")
public class ReleaseNoteDaoImpl implements ReleaseNoteDao {

    @PersistenceContext // (unitName = "entityManagerFactory")
    EntityManager em;

    @Transactional
    public void saveReleaseNote(ReleaseNote releaseNote) {
        if (releaseNote.getObjId() == null)
            em.persist(releaseNote);
        else
            em.merge(releaseNote);
    }

    @Transactional
    public ReleaseNote getReleaseNote(Long objId) {
        ReleaseNote releaseNote = null;
        releaseNote = em.find(ReleaseNote.class, objId);
        return releaseNote;
    }

    @Transactional
    public List<ReleaseNote> getAllReleaseNotes() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<ReleaseNote> q = cb.createQuery(ReleaseNote.class);
        Root<ReleaseNote> root = q.from(ReleaseNote.class);
        q.distinct(true);
        q.select(root);
        q.orderBy(cb.asc(root.get("objId")));
        return em.createQuery(q).getResultList();
    }

    @Transactional
    public void deleteReleaseNote(Long objId) {
        ReleaseNote releaseNote = getReleaseNote(objId);
        if (releaseNote != null) {
            em.remove(releaseNote);
        } else {
            System.out.println("try to delete null;objiId:" + String.valueOf(objId));
        }
    }
}

2.8 用Junit4测试

先写一个基础测试类,src/main/test/java/com/example/BaseTestCaseJunit44.java:

package com.example;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:META-INF/applicationContext.xml", "classpath:META-INF/spring-mvc.xml" })
@Transactional(transactionManager = "transactionManager")
@Rollback(value = true)
public class BaseTestCaseJunit44 {
    // test function can not accept args
    @Rule
    public TestName name = new TestName();
    @Before
    public void before() {
        System.out.println("********************" + " START ***************************");
        System.out.println("Test: " + name.getMethodName());
    }
    @After
    public void after() {
        System.out.println("********************" + " END ***************************");
    }
    @Test
    public void testBase() {
    }
}

然后写一个dao的测试,src/main/test/java/com/example/DaoTest.java:

package com.example;

import com.example.bean.ReleaseNoteBean;
import com.example.dao.ReleaseNoteDao;

import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

public class DaoTest extends BaseTestCaseJunit44 {

    @Autowired
    ReleaseNoteDao releaseNoteDao;

    @Test
    public void testInit() {
    }

    @Test
    public void testReleaseNoteGetOne() {
        Long objId = releaseNoteDao.getAllReleaseNotes().get(0).getObjId();
        releaseNoteDao.getReleaseNote(objId);
    }

    @Test
    public void testReleaseNoteSave() {
        ReleaseNoteBean releaseNote = new ReleaseNoteBean();
        releaseNote.setVersion("t.t.t");
        releaseNote.setContent("# Release t.t.t");
        releaseNote.setPublishdate("2021-5-13 13:13:13");
        releaseNoteDao.saveReleaseNote(releaseNote.toDto());
    }

    @Test
    public void testReleaseNoteDelete() {
        Long objId = releaseNoteDao.getAllReleaseNotes().get(0).getObjId();
        releaseNoteDao.deleteReleaseNote(objId);
    }
}

在控制台中,切到当前项目所在目录,执行mvn test:

xxx@xxx MINGW64 /c/CodeProjects/Java/isolate/demos/ssh_demo (dev)     
$ mvn test                                                                           test (default-test) on project demo
[INFO] Scanning for projects...
[INFO] 
[INFO] --------------------------< com.example:demo >--------------------------      eports for the individual test resu
[INFO] Building demo Maven Webapp 1.0-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------      and [date].dumpstream.
[INFO] 
[INFO] --- maven-resources-plugin:3.0.2:resources (default-resources) @ demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO] Copying 3 resources 
[INFO] Copying 11 resources                                                           following articles:
[INFO]                                                                               n
[INFO] --- maven-compiler-plugin:2.4:compile (default-compile) @ demo ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-resources-plugin:3.0.2:testResources (default-testResources) @ demo 
---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:2.4:testCompile (default-testCompile) @ demo ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.22.1:test (default-test) @ demo ---
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.DaoTest
******************** START ***************************
Test: testInit
******************** END ***************************
******************** START ***************************
Test: testReleaseNoteSave
******************** END ***************************
******************** START ***************************
Test: testReleaseNoteDelete
******************** END ***************************
******************** START ***************************
Test: testReleaseNoteGetOne
******************** START ***************************
Test: testBase
******************** END ***************************
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.872 s - in c[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------      
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------      
[INFO] Total time:  7.721 s
[INFO] Finished at: 2021-05-25T08:56:20+08:00
[INFO] ------------------------------------------------------------------------      

配置完成。

2.9 controller以及视图

src/main/java/com/example/service/ReleaseNoteService.java:

package com.example.service;

import java.util.List;

import com.example.bean.ReleaseNoteBean;

public interface ReleaseNoteService {

    public Long saveReleaseNote(ReleaseNoteBean releaseNote);

    public ReleaseNoteBean getReleaseNote(Long objId);

    public List<ReleaseNoteBean> getAll();

    public void deleteReleaseNote(Long objId);
}

src/main/java/com/example/service/impl/ReleaseNoteServiceImpl.java:

package com.example.service.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.example.bean.ReleaseNoteBean;
import com.example.dao.ReleaseNoteDao;
import com.example.entity.ReleaseNote;
import com.example.service.ReleaseNoteService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service("releaseNoteServiceImpl")
@Transactional
public class ReleaseNoteServiceImpl implements ReleaseNoteService {
    @Autowired
    @Qualifier("ReleaseNoteDao")
    ReleaseNoteDao releaseNoteDao;
    @Transactional
    public Long saveReleaseNote(ReleaseNoteBean releaseNoteBean) {
        ReleaseNote releaseNote = releaseNoteBean.toDto();
        releaseNoteDao.saveReleaseNote(releaseNote);
        return releaseNote.getObjId();
    }
    @Transactional
    public ReleaseNoteBean getReleaseNote(Long objId) {
        ReleaseNote releaseNote = releaseNoteDao.getReleaseNote(objId);
        ReleaseNoteBean releaseNoteBean = new ReleaseNoteBean();
        releaseNoteBean.fromDto(releaseNote);
        return releaseNoteBean;
    }
    @Transactional
    public List<ReleaseNoteBean> getAll() {
        List<ReleaseNoteBean> releaseNoteBeans = new ArrayList<ReleaseNoteBean>();
        List<ReleaseNote> releaseNotes = releaseNoteDao.getAllReleaseNotes();
        for (ReleaseNote releaseNote : releaseNotes) {
            ReleaseNoteBean releaseNoteBean = new ReleaseNoteBean();
            releaseNoteBean.fromDto(releaseNote);
            releaseNoteBeans.add(releaseNoteBean);
        }
        Collections.sort(releaseNoteBeans);
        return releaseNoteBeans;
    }
    @Transactional
    public void deleteReleaseNote(Long objId) {
        releaseNoteDao.deleteReleaseNote(objId);
    }
}

相关依赖文件:ActionResult.java, Host.java, imageController.java
src/main/java/com/example/controller/ReleaseNoteController.java:

package com.example.controller;

import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import com.alibaba.fastjson.JSON;
import com.example.bean.ActionResult;
import com.example.bean.ReleaseNoteBean;
import com.example.service.ReleaseNoteService;
import com.example.util.Host;
import com.example.util.TimeUtil;

@Controller
@RequestMapping("/")
public class ReleaseNoteController {
    @Value("${siteDisplay}")
    private String siteDisplay;
    @Value("${websiteVesion}")
    private String websiteVesion;

    @Autowired // (name = "releaseNoteServiceImpl")
    private ReleaseNoteService releaseNoteService;

    @RequestMapping(value = "ReleaseNoteAdmin.do", method = RequestMethod.GET)
    public String releaseNoteAdmin(Model model, HttpServletRequest request) throws UnknownHostException {
        InetAddress addr = InetAddress.getByName(request.getRemoteAddr());
        String id = Host.getId(addr);
        model.addAttribute("uid", id);
        List<ReleaseNoteBean> releaseNotes = releaseNoteService.getAll();
        model.addAttribute("siteDisplay", siteDisplay);
        model.addAttribute("websiteVesion", websiteVesion);
        model.addAttribute("releaseNotes", releaseNotes);
        return "releaseNote/mdReleaseNoteAdmin";
    }

    @RequestMapping(value = "ReleaseNoteMain.do", method = RequestMethod.GET)
    public String releaseNoteMain(Model model, HttpServletRequest request) throws Exception, UnknownHostException {
        InetAddress addr = InetAddress.getByName(request.getRemoteAddr());
        String id = Host.getId(addr);
        model.addAttribute("uid", id);
        List<ReleaseNoteBean> releaseNotes = releaseNoteService.getAll();
        model.addAttribute("siteDisplay", siteDisplay);
        model.addAttribute("websiteVesion", websiteVesion);
        model.addAttribute("releaseNotes", releaseNotes);
        return "releaseNote/mdReleaseNote";
    }

    @RequestMapping(value = "getReleaseNotesList.do", method = RequestMethod.POST)
    @ResponseBody
    public ActionResult releaseNotesList(Model model) throws Exception {
        ActionResult result = new ActionResult(true);
        List<ReleaseNoteBean> releaseNotes = releaseNoteService.getAll();
        result.setRtnVal(releaseNotes);
        result.setSuccess(true);
        return result;
    }

    @RequestMapping(value = "editor.do", method = RequestMethod.GET)
    public String editor(Model model, @RequestParam("objId") Long objId) {
        model.addAttribute("releasenote_id", objId);
        return "releaseNote/mdReleaseNoteEditor";
    }

    @RequestMapping(value = "getReleaseNote.do", method = RequestMethod.POST)
    public @ResponseBody ActionResult getReleaseNote(Model model, @RequestParam("objId") Long objId) throws Exception {
        ActionResult result = new ActionResult(true);
        ReleaseNoteBean releasenote = releaseNoteService.getReleaseNote(objId);
        result.setRtnVal(releasenote);
        result.setSuccess(true);
        return result;
    }

    @ResponseBody
    @RequestMapping(value = "releaseNote/image/upload.do", method = RequestMethod.POST)
    public String uploadImage(HttpServletRequest req, @RequestParam(value = "guid") String guid,
            @RequestParam(value = "editormd-image-file", required = true) MultipartFile file,
            HttpServletRequest request) throws Exception {
        String trueFileName = file.getOriginalFilename();
        String suffix = trueFileName.substring(trueFileName.lastIndexOf("."));
        String fileName = System.currentTimeMillis() + "_" + guid + suffix;
        // String path = "C:/flawScanAnalytic/releaseNote/images";

        ServletContext servletContext = req.getServletContext();
        String path = servletContext.getRealPath("/");
        System.out.println(path);

        // ..src/main/app/data/images/xx.png(/jpg/..)
        File targetFile = new File(path, "data/images/" + fileName);
        if (!targetFile.exists()) {
            targetFile.mkdirs();
        }

        try {
            file.transferTo(targetFile);
            // ImageUtil.reSize(targetFile, targetFile, 320, 320, true);
        } catch (Exception e) {
            e.printStackTrace();
        }

        Map<String, Object> json = new HashMap<String, Object>();
        json.put("url", "images/" + fileName);
        json.put("success", 1);
        json.put("message", "upload success!");
        String result = JSON.toJSONString(json);
        return result;
    }

    @RequestMapping(value = "releaeNote/updateReleaseNote.do", method = RequestMethod.POST)
    public @ResponseBody ActionResult saveReleaseNote(@RequestParam("objId") Long objId,
            @RequestParam("content") String content) throws Exception {
        ActionResult rst = new ActionResult(false);
        ReleaseNoteBean releaseNoteBean = releaseNoteService.getReleaseNote(objId);
        if (releaseNoteBean == null) {
            throw new Exception(" objid:" + objId + " not exists");
        }
        releaseNoteBean.setContent(content);
        releaseNoteService.saveReleaseNote(releaseNoteBean);
        rst.setSuccess(true);
        return rst;
    }

    @RequestMapping(value = "releaeNote/newReleaseNote.do", method = RequestMethod.POST)
    public @ResponseBody ActionResult newReleaseNote(@RequestParam("version") String version,
            @RequestParam("content") String content, @RequestParam("publishDate") String publishDT) throws Exception {
        ActionResult rst = new ActionResult(false);
        ReleaseNoteBean releaseNoteBean = new ReleaseNoteBean();
        releaseNoteBean.setVersion(version);
        releaseNoteBean.setContent(content);
        // publishDT "4/24/2021 2:38:10 PM"
        publishDT = TimeUtil.formatString(publishDT);
        releaseNoteBean.setPublishdate(publishDT);
        releaseNoteService.saveReleaseNote(releaseNoteBean);
        rst.setSuccess(true);
        return rst;
    }

    @RequestMapping(value = "releaeNote/deleteReleaseNote.do", method = RequestMethod.POST)
    public @ResponseBody ActionResult deleteReleaseNote(@RequestParam("objId") Long objId) throws Exception {
        ActionResult rst = new ActionResult(false);
        ReleaseNoteBean releaseNoteBean = releaseNoteService.getReleaseNote(objId);
        if (releaseNoteBean == null) {
            throw new Exception(" objid:" + objId + " not exists");
        }
        releaseNoteService.deleteReleaseNote(objId);
        rst.setSuccess(true);
        return rst;
    }
}

2.10 编写视图并测试

src/main/webapp/views/releaseNote 中创建视图mdReleaseNote.jsp,mdReleaseNoteAdmin.jsp,mdReleaseNoteEditor.jsp,mdReleaseNoteList.jsp.
src/main/webapp/views/common中创建通用的一些视图backToTop.jsp,contextMenu.jsp,mdEditor.jsp,navbar.jsp.
src/main/webapp/resources中添加静态资源。
src/main/webapp/resources/js/myJS中添加自己写的脚本mdReleaseNoteContext.js.
.vscod中创建task.json:

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Jetty debug",
            "type": "shell",
            "command": "mvn jetty:run",
            "group": "build",
            "isBackground": false,
            "problemMatcher": [],
            "options": {
                "env": {
                    "maven_opts": "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000"
                }
            }
        },
        {
            "label": "dev tomcat debug",
            "type": "shell",
            "command": "mvn jetty:run -Pdev-tomcat",
            "group": "build",
            "isBackground": false,
            "problemMatcher": [],
            "options": {
                "env": {
                    "maven_opts": "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000"
                }
            }
        },
        {
            "label": "prod debug",
            "type": "shell",
            "command": "mvn jetty:run -Pprod",
            "group": "build",
            "isBackground": false,
            "problemMatcher": [],
            "options": {
                "env": {
                    "maven_opts": "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000"
                }
            }
        },
    ]
}

launch.json:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "Debug (Attach) - Remote",
            "request": "attach",
            "hostName": "localhost",
            "port": 8000
        }
    ]
}

在vscode中使用命令tasks:run build task, 然后到debug,选择Debug(Attach) - Remote,开始调试,或者直接F5开始调试。
如图:
view
计划通。

2.11 slf4j和log4j2

添加log,slf4j+log4j2. 所需要的依赖已经在上方pomxml里面了
在src/main/resources/META-INF 和 src/main/resources下都添加文件log4j2.xml
log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="WARN" monitorInterval="30">
    <!--先定义所有的appender-->
    <appenders>
        <!--这个输出控制台的配置-->
        <console name="Console" target="SYSTEM_OUT">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符-->
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
        </console>

        <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,这个也挺有用的,适合临时测试用-->
        <File name="log" fileName="/releasenote/logs/test.log" append="false">
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
        </File>

        <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingFile name="RollingFileInfo" fileName="/releasenote/logs/info.log" filePattern="/releasenote/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>

        <RollingFile name="RollingFileWarn" fileName="/releasenote/logs/debug.log" filePattern="/releasenote/logs/$${date:yyyy-MM}/debug-%d{yyyy-MM-dd}-%i.log">
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 -->
            <DefaultRolloverStrategy max="20"/>
        </RollingFile>

        <RollingFile name="RollingFileError" fileName="/releasenote/logs/error.log" filePattern="/releasenote/logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log">
            <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>
    </appenders>

    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
        <logger name="com.seagate.flawscananalytic" level="INFO"></logger>

        <logger name="org.springframework" level="INFO"></logger>
        <logger name="org.mybatis" level="INFO"></logger>

        <logger name="org.hibernate" level="INFO"></logger>

        <!-- <logger name="org.hibernate.type" level="TRACE"></logger> -->
        <!-- <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"></logger>
        <logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="DEBUG"></logger> -->
        <!-- <logger name="org.hibernate.SQL" level="TRACE"></logger> -->
        <!-- <logger name="org.hibernate.engine.QueryParameters" level="DEBUG"></logger>
        <logger name="org.hibernate.engine.query.HQLQueryPlan" level="DEBUG"></logger> -->
        <root level="all">
            <appender-ref ref="Console"/>
            <appender-ref ref="RollingFileInfo"/>
            <appender-ref ref="RollingFileWarn"/>
            <appender-ref ref="RollingFileError"/>
        </root>
    </loggers>
</configuration>

测试一下。在src/test/java/com/example下新建utilTest.java.
utilTest.java:

package com.example;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class utilTest extends BaseTestCaseJunit44 {
    // int test, will use src/main/resources/log4j2.xml
    static final Logger logger = LoggerFactory.getLogger(utilTest.class);

    @Test
    public void testLog() {
        logger.trace("trace message");
        logger.debug("debug message");
        logger.info("info message");
        logger.warn("warn message");
        logger.error("error message");
        // logger.fatal("fatal message");
    }

    @Test
    public void testUtil() {

    }
}

运行单元测试。
console的输出:
console log
文件的输出/releasenote/logs:

xxx@xxx MINGW64 /c/releasenote/logs
$ tree .
.
|-- 2021-05
|   |-- debug-2021-05-20-1.log
|   |-- error-2021-05-20-1.log
|   `-- info-2021-05-20-1.log
|-- debug.log
|-- error.log
|-- info.log
`-- test.log

1 directory, 7 files

通。

碰到的问题

3.1 junit 错误

3.1.1 No tests found matching

第一次是因为使用的junit的版本不对。spring5 要求junit的版本要大于等于4.12.
报错的具体内容如下:

initializationError
Failed
0.01s
Message:
N/A
Stack trace:
java.lang.Exception: No tests found matching [{ExactMatcher:fDisplayName=testBase], {ExactMatcher:fDisplayName=testBase(com.example.BaseTestCaseJunit44)], {LeadingIdentifierMatcher:fClassName=com.example.BaseTestCaseJunit44,fLeadingIdentifier=testBase]] from org.junit.internal.requests.ClassRequest@3c72f59f
	at org.junit.internal.requests.FilterRequest.getRunner(FilterRequest.java:35)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createFilteredTest(JUnit4TestLoader.java:83)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createTest(JUnit4TestLoader.java:74)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.loadTests(JUnit4TestLoader.java:49)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:513)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)

3.1.2 Class not found

后面还有一次是因为执行了mvn clean 后立马进行单元测试。

Class not found com.example.BaseTestCaseJunit44
java.lang.ClassNotFoundException: com.example.BaseTestCaseJunit44
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
URLClassLoader.java:382
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
ClassLoader.java:424
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
Launcher.java:349
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
ClassLoader.java:357
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.loadClass(RemoteTestRunner.java:766)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.loadClasses(RemoteTestRunner.java:490)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:513)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)

使用mvn test.

3.2 配置xml文件时碰到的问题

3.2.1 dao层没法获取session

起初hibernatenate使用的时sessionFactory的方式执行操作。配置spring的时候,扫描组件,直接扫描了所有的,然后就报了这个错。(复现不出来了😂)
通过扫描的时候排除Controller注解就行了。

    <!--********************************************配置Spring***************************************-->
    <!-- 自动扫描 -->
    <context:component-scan base-package="com.example">
        <!-- 扫描时跳过 @Controller 注解的JAVA类(控制器) -->
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

参考:Spring MVC注解方式service和controller的扫描顺序

3.2.2 连接Mysql8时url解析失败问题

通常情况下,连接MySQL8时的连接需要加上时区等几个参数。这个url我配置在config.properties文件中。
jdbc.url=jdbc:mysql://localhost:3306/releasenote?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
这在config.properties文件没问题,但是当进行测试,加载xml配置文件的时候就出了问题,因为xml中的&需要写成&amp;不然会解析失败。但是在config.properties中出现;会分行。
最后解决办法就是只保留一个参数,写成jdbc.url=jdbc:mysql://localhost:3306/releasenote?serverTimezone=UTC
也能运行。 但是应该会在某些情况下碰到问题。凑合着先用吧。

3.2.3 注入transactionManager失败

起初使用sessionFactory,后来改成entityManger,property也改成entityManagerFactory了,但是测试的时候,报错没法注入transactionManager,由于entityManager没getter,setter方法。
错误地地方是,transactionManager的类错了。

  • 当使用entityManagerFactory的方式时,transactionManager的类为org.springframework.orm.jpa.JpaTransactionManager,属性为:entityManagerFactory
  • 当使用sessionFactory的方式,transactionManager的类为:org.springframework.orm.hibernate5.HibernateTransactionManager,属性为sessionFactory

3.3 JPA 问题

3.3.1 一对多映射,错误的删除了所有的记录

使用中entity:User 和 entity:Issue存在一对多映射,在创建entity的时候进行了双向绑定,但是错误的使用级联 All,导致删除一个Issue记录会同时把从属与同一个User下所有的Issue以及User本身都删掉了。
改正后的:

#Entity User:
...
    @OneToMany(cascade = CascadeType.ALL, targetEntity = Issue.class, fetch = FetchType.LAZY, mappedBy = "user")
    List<Issue> issues;
...

#Entity Issue:
...
    @ManyToOne(targetEntity = User.class, cascade = { CascadeType.MERGE, CascadeType.REFRESH }, optional = false)
    @JoinColumn(name = "userID") // referencedColumnName default is the pk
    User user;
...

cascade表示级联的选项;fetch表示更新的时机;mapperBy表示关系由哪个维护,此例中表示由Entity Issue中的user维护;targetEntity表示关系映射到哪个entity;optional,表示是否可以为空。

3.4 其他一些问题

3.4.1 如何获取客户端主机名

通过javax.servlet.ServletRequest.getRemoteAddr() 获取客户端IP,然后通过Java.net.InetAddress.getByName(IP)生成Java.net.InetAddress对象,最后通过Java.net.InetAddress.getHostName()获得主机名。

import java.net.InetAddress;
import java.net.UnknownHostException;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@RequestMapping(value="sompage",method=RequestMethod.GET)
public @ResponseBody String control(HttpServletRequest request) throws UnknownHostException{
	InetAddress addr = InetAddress.getByName(request.getRemoteAddr());
	String hostName = addr.getHostName();
	return hostName;
	}

3.4.2 图片上传

上传文件需要依赖的两个类:

        <!-- multipartResolver dependency-->
        <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>${fileupload.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>${commonsio.version}</version>
        </dependency>

首先要在spring-mvc配置文件中配置好上传文件的bean:

    <!--upload by multipart/form-data -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- one of the properties available; the maximum file size in bytes -->
        <property name="maxUploadSize" value="104857600" />
    </bean>

然后在controller定义好文件上传的映射:

@RequestMapping(value = "image/upload.do", method = RequestMethod.POST)
    @ResponseBody
    public String uploadMap(@RequestParam(value = "editormd-image-file", required = true) MultipartFile file,
            HttpServletRequest request) {
        Map<String, Object> json = new HashMap<String, Object>();
        int success = 0;
        String msg = "";

        if (file.isEmpty()) {
            log.info("empty file");
            msg = "empty file";
            json.put("success", success);
            json.put("message", msg);
            String result = JSON.toJSONString(json);
            return result;
        } else {
            log.info("===================image upload=====================");
            log.info("size: " + file.getSize());
            log.info("type: " + file.getContentType());
            log.info("name: " + file.getName());
            log.info("originalName: " + file.getOriginalFilename());
            log.info("========================================");
        }
        String trueFileName = file.getOriginalFilename();
        String curTime = TimeUtil.getCurrentTime().replace(" ", "_");
        curTime = curTime.replace(":", "-");
        String suffix = trueFileName.substring(trueFileName.lastIndexOf("."));
        String fileName = trueFileName.split("\\.")[0].replace(" ", "_") + "_" + curTime + suffix;

        String imagePath = dataPath + "/images";
        File imageFolder = new File(imagePath);
        if (!imageFolder.exists()) {
            imageFolder.mkdirs();
        }
        File targetFile = new File(imagePath, fileName);
        log.info("file: " + imagePath + '/' + fileName);

        // 保存
        try {
            FileUtils.copyInputStreamToFile(file.getInputStream(), targetFile);

        } catch (Exception e) {
            log.error(e.getLocalizedMessage());
            log.error("save image faile");
            msg = "save image faile";
            json.put("success", success);
            json.put("message", msg);
            String result = JSON.toJSONString(json);
            return result;
        }

        success = 1;
        msg = "upload  success!";

        json.put("url", "image/" + fileName);
        json.put("success", success);
        json.put("message", msg);
        String result = JSON.toJSONString(json);
        return result;

    }

文件上传后会保存在一个固定的路径下,起初通过HttpServletRequest.getServletContext().getRealPath()获取相对项目的路径,但是担心更新发布后,这些文件会不小心删掉,就改用固定的路径。

文件显示的映射:

    @RequestMapping(value = "image/**", method = RequestMethod.GET)
    public void getMapPic1(HttpServletRequest request, HttpServletResponse httpResponse) {
        String url = request.getRequestURI();
        String filename = url.substring(url.lastIndexOf('/'));
        String suffix = filename.substring(filename.lastIndexOf(".")).toLowerCase();

        String imagePath = dataPath + "/images";
        String filePath = imagePath + '/' + filename;
        log.info("filePath:" + filePath);
        File file = new File(filePath);
        FileInputStream fis = null;
        try {
            String type = "jpeg";
            if (contentType.containsKey(suffix))
                type = contentType.get(suffix);
            httpResponse.setContentType("image/" + type);
            OutputStream out = httpResponse.getOutputStream();
            fis = new FileInputStream(file);
            byte[] b = new byte[fis.available()];

            fis.read(b);
            out.write(b);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

通过流的方式获取图片。在之前相对项目路径的保存文件的情况下,可以在spring-mvc的配置中添加相应的静态资源映射就好了。

3.5 未解决的问题

3.5.1 Repository不能注入

想在项目中使用Repository的方式写dao,但是进行测试时,报错没有办法注入XXXRepository,找不到实现类。
看网上说是没扫描的,但是我确实添加了对Repository的扫描,componentScan 扫描了Repository,也添加了对repository的配置<jpa:repositories base-package="com.seagate.flawscananalytic.dao" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"/>.

破案:
正确的写法:
<jpa:repositories base-package="com.seagate.flawscananalytic.dao" entity-manager-factory-ref="entityManagerFactory" transaction-manager-ref="transactionManager"></jpa:repositories >.

3.5.2 controller返回视图路径的问题

假设当前前端的url为/project/module1/page,
然后对page的controller类似如下:

...
RequestMapping("/)
class xxxController(){
	...
	@RequestMappint("module1/page")
	public String page(...){
		...
		return 'module1/xxx'
	}
	...
}

在视图解析的配置中,我配置的前缀为’views/’,后缀为’.jsp’。在这种情况下,controller返回的映射路径为\project\module1\views\module1\xxx.jsp,似乎映射的路径时跟当前路径有关。这好像是因为我配置的前缀是相对路径的原因。不知道前缀能不能写成绝对路径如/views/

还有其他的一些问题,bootstrap的,后端的想不起来。

参考

搭建这个项目中参考了很多大佬们的文章和项目,但是有些找不到了(对不住了-😂),此处一并感谢他们。

  1. SpringMVC图片上传与显示
  2. 用 slf4j.Logger 打印日志
  3. Spring-Data-JPA 定义实体类关系:一对多(增删改查)
  4. SpringData JPA多表操作(增,删)
  5. Spring Data JPA中的mappedBy
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值