Spring 使用经验谈

Posted sp42a

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring 使用经验谈相关的知识,希望对你有一定的参考价值。

首先说说大潮流的 Spring Boot。因为项目强制要求的缘故,我不得不将 Spring 升级到 Spring Boot,但我个人觉得,这不但没有必要(不是做微服务),而且有以下几个问题:

  • Boot 采用新的的打包机制,默认生成一个体积非常大的 jar 包。这样导致更新很麻烦,每次上次那么大的 jar 耗时间久(lib 下面的依赖 jar)。以前我更新项目有个技巧,也不是用 war 包(第一次的时候可以是),而是同步本地的到远程的即可,因为本地事实的就是最终编译结果,通过 SFTP 同步两边即可。或者不同步,你知道更新哪个 class/jsp 上传文件即可,名副其实的“增量更新”。现在可好了——上传完整的 jar 包就是“全量更新” 。
  • 同样也正是这个原因,更加网页的静态文件非常不方便。——难道我改 html 里面的一个字符就要重新打包 jar 并上传?太不科学了吧!?
  • 由于 Boot 整合了 Tomcat 服务器,那些日志我都不知道哪里看,有肯定是有——那就要重新学习、改变习惯呗。以前的话,用 Tomcat 自带的脚本: catalina.sh run 就可以观察日志,非常方便。

当然,鼎鼎大名的 Boot 不是没有对应的解决方法,百度一下就有了。但鄙人就觉得不太优雅去处理咯。首先 lib/*.jar 可外置游离于 WEB-INF/lib 下面,依靠 Java 层面去指定某个目录,一个项目没啥事,多个项目呢?——这个就花费了人们去管理这个目录的“心智”;其次,前台页面的,也可以游离出去,不是放在 webapps 下面,所以管理维护成本又上升了——我何必那样子呢——老的方法不是很好么。最后更新的问题,Boot 项目允许你打包为 war 包而不是 jar 包,你解压一下不就可以上传了么。

还有个不痛不痒的问题,Boot 不能设置全局的 Welcome 默认页,例如(index.htm/index.jsp 等)。尽管可以单独设置某个 path 下面的,但全局不行。看来 Boot 不打算为传统页面模式提供支持。

说了那么多不好的话,但此时此刻,得说说好话。不过的确也是实事求是地说,Boot 使用起来很方便,简化了很多,但是无非仍是 Spring + Spring MVC 的高层封装,对于搞微服务的 Spring Cloud,那真是刚需。但话又说回来,你不是搞微服务,就是普通的 Web 开发,是不是 Boot 真的差异不大。

话归正题,让我们重新学习 Spring & MVC 吧!

Spring 最小引用

比起我初学 Spring 那时候,Spring 5.x 对依赖简化了不少,我那时候还需要 Common-lang 和 Common Log 之类的,现在不用了。下面是使用 Spring MVC 的最小引用

<dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-webmvc</artifactId>
     <version>5.3.6</version>
</dependency>

webmvc 自动依赖 web、context 等的包。

各种配置

为传统的模式的 Spring MVC 和 Boot 的都提供配置支持。

启动 Spring

<!-- 启用 Spring -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- JSP 页面编码 -->
<jsp-config>
    <jsp-property-group>
        <url-pattern>*.jsp</url-pattern>
        <page-encoding>UTF-8</page-encoding>
        <trim-directive-whitespaces>true</trim-directive-whitespaces>
    </jsp-property-group>
</jsp-config>
<!-- // -->

/WEB-INF 下配置 applicationContext.xml,指定扫描的包:

<!-- 扫描的包 -->
<context:component-scan base-package="net.bingosoft" />

<!--方便 JSP 调用注入的组件-->
<bean class="com.ajaxjs.spring.DiContextUtil" />

Spring MVC 配置

在指定的包之中新建一个 Spring MVC 配置类,继承 BaseWebInitializer

import com.ajaxjs.spring.BaseWebInitializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@Configuration
@EnableWebMvc
public class WebInitializer extends BaseWebInitializer {
}

于是要为 MVC 设一个配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
 	http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
 	http://www.springframework.org/schema/context/spring-context-4.0.xsd">

	<mvc:default-servlet-handler />
</beans>

对 Request、Response 的扩展

web.xml 加入一个过滤器。

<!-- 对 Request、Response 的扩展 -->
<filter>
    <filter-name>InitMvcRequest</filter-name>
    <filter-class>com.ajaxjs.web.InitMvcRequest</filter-class>
</filter>
<filter-mapping>
    <filter-name>InitMvcRequest</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Boot 下使用:继承 InitMvcRequest 并声明为组件即可。

@Component
@Order(1)
InitMvcRequestFilter extends InitMvcRequest {
}

如果不自动扫描包,那么加入下面 bean:

<!--对 Request、Response 的扩展-->
<bean class="com.ajaxjs.web.InitMvcRequest" />

Spring 工具类

就地取材,Spring 本身包含不少优秀的工具类,既然 Spring 为底层必定依赖的库,那么请大胆使用其工具栏吧~我知道的是,有些团队约束(或禁止)成员去写自己的工具类,例如 StringUtil.isEmpty() 之类的。

// 最常用的,判断是否空字符串
StringUtils.hasText();
// 将第一个字符改大写
StringUtils.capitalize(Str);

ObjectUtils.isEmpty([]);
CollectionUtils.isEmpty(List/Map);

Base64Utils.encodeToString();
Base64Utils.decodeFromString();

StreamUtils.copyToByteArray();

UriUtils.encode(v.toString(), "utf-8");

/**
 * 将字符进行 URL 编码,默认 UTF-8 编码
 * 
 * jdk自带的URL编码工具类 URLEncoder 在对字符串进行URI编码的时候,会把空格编码为 + 号。
 * 空格的URI编码其实是:%20 搜素引擎上不少人都遇到这个问题,哀声一片。
 * 解决办法大都是对编码后的字符串,进行 + 号替换为 %20。总感觉这种方式不优雅
 * 
 * @param str 正常的 Java 字符串
 * 
 * @return 已 URL 编码的字符串
 */
public static String urlEncode(String str) {
	try {
		return URLEncoder.encode(str, StandardCharsets.UTF_8.toString());
	} catch (UnsupportedEncodingException e) {
		LOGGER.warning(e);
		return null;
	}
}

/**
 * 将 URL 编码的字符还原,默认 UTF-8 编码
 * 
 * @param str 已 URL 编码的字符串
 * @return 正常的 Java 字符串
 */
public static String urlDecode(String str) {
	try {
		return URLDecoder.decode(str, StandardCharsets.UTF_8.toString());
	} catch (UnsupportedEncodingException e) {
		LOGGER.warning(e);
		return null;
	}
}

以前写的

/**
 * 将一个字符串分隔为字符串数组,分隔符 可以是,、/、-、\\(注意转义)、|、; 作为分隔符。 常在读取配置的时候使用。
 * 
 * @param str 输入的字符串
 * @return 分隔后的数组
 */
public static String[] split(String str) {
	return str.split(",|/|-|\\\\\\\\|\\\\||;");
}

@Test
public void testSplit() {
	assertEquals(2, split("a,b").length);
	assertEquals(3, split("a/b/c").length);
	assertEquals(4, split("a\\\\b\\\\c\\\\d").length);
	assertEquals(5, split("a|b|c|d|e").length);
	assertEquals(5, split("a;b;c;d;e").length);
}

/**
 * 重复字符串 repeat 次并以 div 分隔
 * 
 * @param str    要重复的字符串
 * @param div    字符串之间的分隔符
 * @param repeat 重复次数
 * @return 结果
 */
public static String repeatStr(String str, String div, int repeat) {
	StringBuilder s = new StringBuilder();
	int i = 0;

	while (i++ < repeat) {
		s.append(str);
		if (i != repeat)
			s.append(div);
	}

	return s.toString();
}

@Test
public void testRepeatStr() {
	assertEquals(repeatStr("Hi", ",", 3), "Hi,Hi,Hi");
}

/**
 * 输入 a,看 a 里面是否包含另一个字符串 b,忽略大小写。
 * 
 * @param a 输入字符串 a
 * @param b 另一个字符串 b
 * @return true 表示包含
 */
public static boolean containsIgnoreCase(String a, String b) {
	return a.toLowerCase().contains(b.toLowerCase());
}

@Test
public void testContainsIgnoreCase() {
	assertTrue(containsIgnoreCase("abc", "A"));
}

/**
 * 随机字符串
 */
private static final String STR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

/**
 * 生成指定长度的随机字符,可能包含数字
 * 
 * @param length 户要求产生字符串的长度
 * @return 随机字符
 */
public static String getRandomString(int length) {
	Random random = new Random();
	StringBuffer sb = new StringBuffer();

	for (int i = 0; i < length; i++) {
		int number = random.nextInt(62);
		sb.append(STR.charAt(number));
	}

	return sb.toString();
}

/**
 * 是否空字符串
 * 
 * @param str 要判断的字符串
 * @return true 表示为为空字符串,否则不为空
 */
public static boolean isEmptyString(String str) {
	return str == null || str.isEmpty() || str.trim().isEmpty();
}
/**
 * 将第一个字母大写
 * 
 * @param str 字符串
 * @return 字符串
 */
public static String firstLetterUpper(String str) {
	// return str.substring(0, 1).toUpperCase() + str.substring(1); // 另外一种写法
	return Character.toString(str.charAt(0)).toUpperCase() + str.substring(1);
}
/**
 * 判断数组是否为空
 * 
 * @param arr 输入的数组
 * @return true 表示为素组不是为空,是有内容的,false 表示为数组为空数组,length = 0
 */
public static boolean isNull(Object[] arr) {
	return arr == null || arr.length == 0;
}

assertTrue(isNull(new Object[] {}));
assertFalse(isNull(new Object[] { null }));

/**
 * 判断 collection 是否为空
 * 
 * @param collection Map输入的集合
 * @return true 表示为集合不是为空,是有内容的,false 表示为空集合
 */
public static boolean isNull(Collection<?> collection) {
	return collection == null || collection.isEmpty();
}

/**
 * 判断 map 是否有意义
 * 
 * @param map 输入的
 * @return true 表示为 map 不是为空,是有内容的,false 表示为空 map
 */
public static boolean isNull(Map<?, ?> map) {
	return map == null || map.isEmpty();
}

@Test
public void testCollection() {
	assertTrue(isNull(new HashMap<String, String>() {
		private static final long serialVersionUID = 1L;
	}));
	assertTrue(isNull(new ArrayList<String>() {
		private static final long serialVersionUID = 1L;
	}));

	List<Object> list = null;
	assertTrue(isNull(list));
}


// ------------BASE 64
/**
 * BASE64 编码
 * 
 * @param bytes 输入的字节数组
 * @return 已编码的字符串
 */
public static String base64Encode(byte[] bytes) {
	return Base64.getEncoder().encodeToString(bytes);
}

/**
 * BASE64 编码
 * 
 * @param str 待编码的字符串
 * @return 已编码的字符串
 */
public static String base64Encode(String str) {
	return base64Encode(str.getBytes(StandardCharsets.UTF_8));
}

/**
 * BASE64 解码,但以 Byte 形式返回
 * 
 * @param str 待解码的字符串
 * @return 已解码的 Byte
 */
public static byte[] base64DecodeAsByte(String str) {
	try {
		return Base64.getDecoder().decode(str);
	} catch (IllegalArgumentException e) {
		LOGGER.warning("BASE64 解码失败", e);
		return null;
	}
}

/**
 * BASE64 解码 这里需要强制捕获异常。
 * 中文乱码:http://s.yanghao.org/program/viewdetail.php?i=54806
 * 
 * @param str 待解码的字符串
 * @return 已解码的字符串
 */
public static String base64Decode(String str) {
	byte[] b = base64DecodeAsByte(str);
	return b == null ? null : byte2String(b);
}

  Stream

/**
 * 使用内存操作流,读取二进制,也就是将流转换为内存的数据。 InputStream 转换到 byte[]. 从输入流中获取数据, 转换到 byte[]
 * 也就是 in 转到内存。虽然大家可能都在内存里面了但还不能直接使用,要转换
 * 
 * @param in 输入流
 * @return 返回本实例供链式调用
 */
public static byte[] inputStream2Byte(InputStream in) {
	// 使用内存操作流,读取二进制
	try (ByteArrayOutputStream out = new ByteArrayOutputStream();) {
		write(in, out, true);

		return out.toByteArray();
	} catch (IOException e) {
		LOGGER.warning(e);
		return null;
	}
}

以上是关于Spring 使用经验谈的主要内容,如果未能解决你的问题,请参考以下文章

spring练习,在Eclipse搭建的Spring开发环境中,使用set注入方式,实现对象的依赖关系,通过ClassPathXmlApplicationContext实体类获取Bean对象(代码片段

Spring boot:thymeleaf 没有正确渲染片段

Spring Rest 文档。片段生成时 UTF-8 中间字节无效 [重复]

QT 实用代码片段

初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段

初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段