Blog

spring踩坑-java.lang.NoClassDefFoundError: org/jboss/logging/BasicLogger

在spring 开发中,一开始 我遇到的 是 hibernate 类冲突,查了一下说,删掉就行了。

删掉后,果然就好了,第二次,我想将错误复现时,发现找不到了,并且出现新错误:

Caused by: java.lang.NoClassDefFoundError: org/jboss/logging/BasicLogger
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at org.apache.catalina.loader.WebappClassLoaderBase.findClassInternal(WebappClassLoaderBase.java:2541)
	at org.apache.catalina.loader.WebappClassLoaderBase.findClass(WebappClassLoaderBase.java:858)
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1301)
	at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1166)
	at org.hibernate.validator.internal.util.logging.LoggerFactory.make(LoggerFactory.java:29)
	at org.hibernate.validator.internal.util.Version.<clinit>(Version.java:27)
	at org.hibernate.validator.internal.engine.ConfigurationImpl.<clinit>(ConfigurationImpl.java:65)
	at org.hibernate.validator.HibernateValidator.createGenericConfiguration(HibernateValidator.java:41)
	at javax.validation.Validation$GenericBootstrapImpl.configure(Validation.java:276)
	at org.springframework.validation.beanvalidation.LocalValidatorFactoryBean.afterPropertiesSet(LocalValidatorFactoryBean.java:250)
	at org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean.afterPropertiesSet(OptionalValidatorFactoryBean.java:40)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624)
	... 28 more

发现是缺少了  jboss-logging的jar包,添加了就行。

于是我添加了 jboss-logging.jar  下载链接
因为org/jboss/logging/BasicLogger is inside jboss-logging.jar

springmvc基础-springmvc与spring的整合与关系

<!--  
	需要进行 Spring 整合 SpringMVC 吗 ?
	还是否需要再加入 Spring 的 IOC 容器 ?
	是否需要再 web.xml 文件中配置启动 Spring IOC 容器的 ContextLoaderListener ?
		
	1. 需要: 通常情况下, 类似于数据源, 事务, 整合其他框架都是放在 Spring 的配置文件中(而不是放在 SpringMVC 的配置文件中).
	实际上放入 Spring 配置文件对应的 IOC 容器中的还有 Service 和 Dao. 
	2. 不需要: 都放在 SpringMVC 的配置文件中. 也可以分多个 Spring 的配置文件, 然后使用 import 节点导入其他的配置文件
-->
	
<!--  
	问题: 若 Spring 的 IOC 容器和 SpringMVC 的 IOC 容器扫描的包有重合的部分, 就会导致有的 bean 会被创建 2 次.
	解决:
	1. 使 Spring 的 IOC 容器扫描的包和 SpringMVC 的 IOC 容器扫描的包没有重合的部分. 
	2. 使用 exclude-filter 和 include-filter 子节点来规定只能扫描的注解
-->
	
<!--  
	SpringMVC 的 IOC 容器中的 bean 可以来引用 Spring IOC 容器中的 bean. 
	返回来呢 ? 反之则不行. Spring IOC 容器中的 bean 却不能来引用 SpringMVC IOC 容器中的 bean!
-->

一、springmvc与spring的整合举例:

1、配置 web.xml ,配置了两个容器。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  
  	<!-- 配置启动 Spring IOC 容器的 Listener -->
	<!-- needed for ContextLoaderListener -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:springioc.xml</param-value>
	</context-param>

	<!-- Bootstraps the root web application context before servlet initialization -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
  
  
  <servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

</web-app>

2、配置父容器 springioc.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"
	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-4.0.xsd">

    <context:component-scan base-package="com.test.springmvc">
		<context:exclude-filter type="annotation" 
			expression="org.springframework.stereotype.Controller"/>
		<context:exclude-filter type="annotation" 
			expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
	</context:component-scan>

	<!-- 配置数据源, 整合其他框架, 事务等. -->

</beans>

3、配置springmvc.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-4.0.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

	<!-- 配置自动扫描的包 -->
	<context:component-scan base-package="com.test.springmvc" use-default-filters="false">
		<context:include-filter type="annotation" 
			expression="org.springframework.stereotype.Controller"/>
		<context:include-filter type="annotation" 
			expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
	</context:component-scan>
	
	<!-- 配置视图解析器 -->
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="prefix" value="/WEB-INF/views/"></property>
		<property name="suffix" value=".jsp"></property>
	</bean>


</beans>

4、编辑服务器:

package com.test.springmvc.handlers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

//	//因为父容器 配置文件 规定了 不扫描handler 所以,helloWorld无法注入
//	@Autowired
//	private HelloWorld helloWorld;
	
	public UserService() {
		System.out.println("UserService Constructor...");
	}
	
}

5、编辑控制器:

package com.test.springmvc.handlers;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller // spring里面 又名handler 处理器
public class HelloWorld {

	/**
	 * 1、使用RequestMapping 注解来映射 请求的url
	 * 2、返回值会通过视图解析器解析为实际的物理视图,
	 * 对于org.springframework.web.servlet.view.InternalResourceViewResolver
	 * 会做如下解析,prefix + returnVal + suffix 得到实际的物理视图。然后做转发操作。
	 * 即 WEB-INF/views/success.jsp
 	 */

	@Autowired
	private UserService userService;
	
	public HelloWorld() {
		System.out.println("HelloWorld Constructor...");
	}
	
	@RequestMapping("/helloworld")
	public String hello( HttpServletRequest request,HttpServletResponse response){
		System.out.println("hello request" + request.toString());
		System.out.println("hello response" + response.toString());
		
		System.out.println("success");
		System.out.println(userService);
		
		return "success";
		//return "/WEB-INF/views/success.jsp";
     }
	
	
}

二、springmvc与spring容器关系

spring容器是父容器,springmvc是子容器。

子容器(相当于局部变量)可以访问父容器的对象(相当于全局变量)
但是父容器不能访问子容器中的对象。

springmvc基础-运行流程图

备注:

1、
<mvc:default-servlet-handler/>可以解决 请求静态资源问题。<mvc:annotation-driven></mvc:annotation-driven>可以保证非静态资源也能请求,当配置了<mvc:default-servlet-handler/>

2、在DispatcherServlet.doDispatch(request, response)中有一个 mappedHandler= getHandler(processedRequest);

mappedHandler 就是 HandlerExecutionChain (里面包含了控制器方法handler和拦截器Interceptors)

HandlerExecutionChain 由 handlerMapping 根据 处理器 和请求映射而来。

当没有配置1中的<mvc:default-servlet-handler/>和<mvc:annotation-driven>时,handlerMapping 包括:【
BeanNameUrlHandlerMapping
DefaultAnnotationHandlerMapping

当有配置1中时:handlerMapping 包括:【
RequestMappingHandlerMapping
SimpleUrlHandlerMapping
BeanNameUrlHandlerMapping

3、HandlerAdapter 里面有 MessageConverter

 

springmvc中级-异常处理

一、先构建一个普通程序

1、测试页面:

<a href="testExceptionHandlerExceptionResolver?i=10">测试异常</a>

2、编辑控制器:

@RequestMapping("/testExceptionHandlerExceptionResolver")
public String testExceptionHandlerExceptionResolver(@RequestParam("i") int i){
	System.out.println("result: " + (10 / i));
	return "success";
}

3、结果成功页面,就显示成功两字,很简单就不写了

二、编辑异常捕获

1、编辑捕获异常的代码

//这个是 之前已经写好的 控制器代码
@RequestMapping("/testExceptionHandlerExceptionResolver")
public String testExceptionHandlerExceptionResolver(@RequestParam("i") int i){
	System.out.println("result: " + (10 / i));
	return "success";
}

/**
 * 1. 在 @ExceptionHandler 方法的入参中可以加入 Exception 类型的参数, 该参数即对应发生的异常对象
 * 2. @ExceptionHandler 方法的入参中不能传入 Map. 若希望把异常信息传导页面上, 需要使用 ModelAndView 作为返回值
 * 3. @ExceptionHandler 方法标记的异常有优先级的问题. 
 * 4. @ControllerAdvice: 如果在当前 Handler 中找不到 @ExceptionHandler 方法来出来当前方法出现的异常, 
 * 则将去 @ControllerAdvice 标记的类中查找 @ExceptionHandler 标记的方法来处理异常. 
 */
@ExceptionHandler({ArithmeticException.class})
public ModelAndView handleArithmeticException(Exception ex){
	System.out.println("@ExceptionHandler+ArithmeticException.class+出异常了: " + ex);
	ModelAndView mv = new ModelAndView("error");
	mv.addObject("exception", ex);
	return mv;
}
	
@ExceptionHandler({RuntimeException.class})
public ModelAndView handleArithmeticException2(Exception ex){
	System.out.println("@ExceptionHandler+RuntimeException.class+ [出异常了]: " + ex);
	ModelAndView mv = new ModelAndView("error");
	mv.addObject("exception", ex);
	return mv;
}

注意:上面两个异常捕获器,精度不一样,实际使用时,抛出的异常按精度来捕获。【还有,异常捕获器中不能在方法中添加map 形参,这和普通的控制器不同,所以保存信息,必须先新建ModelAndView,然后保存并返回】

上面两个异常捕获器,作用域在这个控制器的类文件中,如果想要作用于所有的控制器类文件的话,需要新建一个异常捕获器类。

//因为是注解,所有这个全局异常捕获器,需要放在 注解扫描器 能够扫描到的目录
//在转发配置中,我们可以看到 <!-- 配置自动扫描的包 -->
//<context:component-scan base-package="com.test.springmvc"></context:component-scan>


@ControllerAdvice
public class SpringMVCTestExceptionHandler {

	@ExceptionHandler({RuntimeException.class})
	public ModelAndView handleArithmeticException(Exception ex){
		System.out.println("全局+@ControllerAdvice+@ExceptionHandler+ArithmeticException.class+----> 出异常了: " + ex);
		ModelAndView mv = new ModelAndView("error");
		mv.addObject("exception", ex);
		return mv;
	}
	
}

2、返回结果失败页面,就显示失败两字,很简单就不写了。

核心注意:上面提到的属于同一层的异常捕获,出现异常时,只能先内部捕获后全局捕获,且按照精确度优先原则。这一层的异常捕获,只要有一个捕获了,该层的其他捕获器就不捕获了。

三、产生异常测试

除数为0,抛出异常。

<a href="testExceptionHandlerExceptionResolver?i=0">测试异常</a>

四、@ResponseStatus注解

直接在异常页,显示编辑过的响应头信息。
1、可以注解一个类或一个方法,当注解为一个类时,该异常作用于所有控制器。

@ResponseStatus(value=HttpStatus.FORBIDDEN, reason="用户名和密码不匹配!")
public class UserNameNotMatchPasswordException extends RuntimeException{

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	public  UserNameNotMatchPasswordException() {
		System.out.println("全局+@ResponseStatus+RuntimeException+ArithmeticException.class+----> 出异常了: ");
	}
	
}

2、也可以直接注解在控制器方法上。此时,控制器无论是否抛出异常,都会按照设置的异常信息,展示异常网页。(也就是无论是否有异常,控制器自己都会捕获异常,并展示@ResponseStatus设置好的异常信息)

@ResponseStatus(reason="测试",value=HttpStatus.NOT_FOUND)
@RequestMapping("/testResponseStatusExceptionResolver")
public String testResponseStatusExceptionResolver(@RequestParam("i") int i){
	if(i == 13){
		System.out.println("throw +testResponseStatusExceptionResolver...");
		throw new UserNameNotMatchPasswordException();
	}
	System.out.println("testResponseStatusExceptionResolver...");
		
	return "success";
}

注意:@ResponseStatus注解

1、如果控制器没有抛出异常,也不会跳转到success页面,而是用tomcat默认的异常页面,例如:  HTTP Status 404 – 测试

2、如果出现异常,且有@ExceptionHandler, @ControllerAdvice  异常捕获类,那么异常会被二次捕获了。【先ResponseStatus捕获,然后ExceptionHandler又捕获,这两种捕获器属于两个层次】

[FirstIntercept] preHandle
throw +testResponseStatusExceptionResolver...
全局+@ResponseStatus+RuntimeException+ArithmeticException.class+----> 出异常了: 
@ExceptionHandler+RuntimeException.class+ [出异常了]: com.test.springmvc.test.UserNameNotMatchPasswordException
[FirstIntercept] afterCompletion

3、如果出现异常,且没有@ExceptionHandler, @ControllerAdvice  异常捕获类,那么就由ResponseStatus 捕获一次就结束了。

五、DefaultHandlerExceptionResolver

这个就是springmvc默认的异常处理,比如我们控制器编写错误,出异常了,如果我们写异常处理模块,那么默认情况下就是这个类处理的。比如控制器只支持post请求,然后用get请求就报错,请求方法不支持。

目前测试到的异常情况看,默认异常处理器,捕获到异常后,不会将异常传递给其他异常捕获器了。

直接在网页中显示错误:

HTTP Status 405 - Request method 'GET' not supported

type Status report

message Request method 'GET' not supported

description The specified HTTP method is not allowed for the requested resource.

Apache Tomcat/8.0.36

控制台有错误信息:

org.springframework.web.servlet.PageNotFound handleHttpRequestMethodNotSupported
WARNING: Request method 'GET' not supported

六、SimpleMappingExceptionResolveer

在控制器中:

@RequestMapping("/testSimpleMappingExceptionResolver")
public String testSimpleMappingExceptionResolver(@RequestParam("i") int i){
	String [] vals = new String[10];
	System.out.println(vals[i]);
	return "success";
}

测试网页:

http://localhost:8080/springmvc-2/testSimpleMappingExceptionResolver?i=10

编辑转发配置器:

<!-- 配置使用 SimpleMappingExceptionResolver 来映射异常 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
	<!-- 配置请求域 异常属性名 ex,默认值为 exception -->
	<property name="exceptionAttribute" value="ex"></property>
	<!-- 异常类型 映射 跳转页  -->
	<property name="exceptionMappings">
		<props>
			<prop key="java.lang.ArrayIndexOutOfBoundsException">error</prop>
		</props>
	</property>
</bean>

七、异常处理器的优先级

同一个异常:

DefaultHandlerExceptionResolver (优先级第一,捕获到异常后,就将异常截断,在网页中展示异常信息,不会再往下传递)

@ResponseStatus(优先级第二,捕获到异常后,会将异常往后传递,返回结果以最后一个异常返回网页信息为准)

在控制器中,添加这个@ResponseStatus注解,就算没有抛出异常,也会展示指定状态的异常的网页。

@ResponseStatus(reason="测试",value=HttpStatus.NOT_FOUND)
@RequestMapping("/testResponseStatusExceptionResolver")
public String testResponseStatusExceptionResolver(@RequestParam("i") int i){
	if(i == 13){
		System.out.println("throw +testResponseStatusExceptionResolver...");
		throw new UserNameNotMatchPasswordException();
	}
	System.out.println("testResponseStatusExceptionResolver...");
		
	return "success";
}

如果上面的控制器没有@ResponseStatus(reason="测试",value=HttpStatus.NOT_FOUND) ,然后异常时抛出了UserNameNotMatchPasswordException类

对UserNameNotMatchPasswordException查看编码信息:

@ResponseStatus(value=HttpStatus.FORBIDDEN, reason="用户名和密码不匹配!")
public class UserNameNotMatchPasswordException extends RuntimeException{

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	public  UserNameNotMatchPasswordException() {
		System.out.println("全局+@ResponseStatus+RuntimeException+ArithmeticException.class+----> 出异常了: ");
	}
	
}

看上面的异常类,如果项目中没有低优先级的异常捕获器,那么网页就返回错误信息就结束了。如果有其他低优先级的异常了,那么异常还会被继续处理。

@ExceptionHandler和@ControllerAdvice(优先级第三,异常被捕获后,不会继续往下传递了)

@ExceptionHandler适用于和控制器位于同一个文件中,@ControllerAdvice单独一个文件,作用域针对所有的控制器。

SimpleMappingExceptionResolver(异常映射器,优先级最低,针对异常的类型,指定错误和异常的返回页)

<!-- 配置使用 SimpleMappingExceptionResolver 来映射异常 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
	<!-- 配置请求域 异常属性名 ex,默认值为 exception -->
	<property name="exceptionAttribute" value="ex"></property>
	<!-- 异常类型 映射 跳转页  -->
	<property name="exceptionMappings">
		<props>
			<prop key="java.lang.ArrayIndexOutOfBoundsException">error</prop>
		</props>
	</property>
</bean>

补充:

针对指定的异常信息返回页,可以在返回页的网页中,直接读取异常信息。
默认jstl 表达式为:  ${requestScope.exception},在SimpleMappingExceptionResolver 中信息配置时,将异常的属性名改成了ex,所以错误信息展示页 该调用 ${requestScope.ex}来获取异常信息。

springmvc中级-自定义拦截器

Spring MVC也可以使用拦截器对请求进行拦截处理,用户 可以自定义拦截器来实现特定的功能,自定义的拦截器必 须实现HandlerInterceptor接口。

preHandle():这个方法在业务处理器处理请求之前被调用,在该 方法中对用户请求 request 进行处理。如果程序员决定该拦截器对 请求进行拦截处理后还要调用其他的拦截器,或者是业务处理器去 进行处理,则返回true;如果程序员决定不需要再调用其他的组件 去处理请求,则返回false。

postHandle():这个方法在业务处理器处理完请求后,但是DispatcherServlet 向客户端返回响应前被调用,在该方法中对 用户请求request进行处理。

afterCompletion():这个方法在 DispatcherServlet 完全处理完请 求后被调用,可以在该方法中进行一些资源清理的操作。


自定义拦截器大白话:

1、首先新建一个拦截器类

就是 implements HandlerInterceptor 实现接口类就行。

public class FirstIntercept implements HandlerInterceptor{

	/**
	 * 该方法在目标方法之前被调用.
	 * 若返回值为 true, 则继续调用后续的拦截器和目标方法. 
	 * 若返回值为 false, 则不会再调用后续的拦截器和目标方法. 
	 * 
	 * 可以考虑做权限. 日志, 事务等. 
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// TODO Auto-generated method stub
		System.out.println("[FirstIntercept] preHandle");
		return true;
	}


	/**
	 * 调用目标方法之后, 但渲染视图之前. 
	 * 可以对请求域中的属性或视图做出修改. 
	 */
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		// TODO Auto-generated method stub
		System.out.println("[FirstIntercept] postHandle");

		
	}


	/**
	 * 渲染视图之后被调用. 释放资源
	 */
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		// TODO Auto-generated method stub
		System.out.println("[FirstIntercept] afterCompletion");

	}

}

2、编辑转发配置文件

<mvc:interceptors>
       
        <!-- 配置自定义拦截器 -->
	<bean class="com.test.springmvc.test.FirstIntercept"></bean>
	
	<!-- 配置 带参数的 自定义拦截器,SencondIntercept和FirstIntercept 代码一样的只是改了名字,这里就不写了 -->
	<mvc:interceptor>
            <!-- 拦截器只作用在 /emps 请求域中,一定要记得“/”,否则没有效果 -->
	    <mvc:mapping path="/emps"/>
	    <bean class="com.test.springmvc.test.SecondIntercept"></bean>
	</mvc:interceptor>
	
</mvc:interceptors>

3、拦截器执行顺序:

(1)如果只配置了FirstIntercept,且里面的 preHandle方法返回true,且没有  配置 带参数的 自定义拦截器(即SencondIntercept
那么url请求,控制台输出结果:

[FirstIntercept] preHandle
[FirstIntercept] postHandle
[FirstIntercept] afterCompletion

(2)如果上面的FirstIntercept里面的preHandle方法返回false。且没有  配置 带参数的 自定义拦截器(即SencondIntercept
那么url请求,控制台输出结果:

[FirstIntercept] preHandle

(3)如果配置了FirstInterceptSecondIntercept,且两者里面的preHandle方法都返回true,且请求域是emps
控制台输出结果:

[FirstIntercept] preHandle
[SecondIntercept] preHandle
[SecondIntercept] postHandle
[FirstIntercept] postHandle
[SecondIntercept] afterCompletion
[FirstIntercept] afterCompletion

(4)如果配置了FirstInterceptSecondIntercept请求域是emps
FirstIntercept的preHandle方法返回true,SecondIntercept的preHandle方法返回false,控制台输出结果:

[FirstIntercept] preHandle
[SecondIntercept] preHandle
[FirstIntercept] afterCompletion

备注:下图中的虚线不是执行过程实线才是真正的执行过程

 

特别提醒:当初在改项目代码时,因为代码是热更新的,但是测试结果来看,热更新后,Intercept 的测试输出 有bug ,因为多输出了 一遍 下面的:
[FirstIntercept] preHandle
[FirstIntercept] postHandle
[FirstIntercept] afterCompletion

所以,测试代码时最好还是重启一下服务器,这样就不会出现意想不到的bug。

4、拦截器顺序与异常

当自定义拦截器的preHandle方法返回true,遇到springmvc出现异常时:

1、DefaultHandlerExceptionResolver  系统默认自带异常处理器。
里面实现了很多异常 ,如:handleHttpRequestMethodNotSupported

我测试控制器规定了请求方法只能是post,当我get请求时,出异常了。

@RequestMapping(value="/testDefaultHandlerExceptionResolver",method=RequestMethod.POST)
public String testDefaultHandlerExceptionResolver(){
	System.out.println("testDefaultHandlerExceptionResolver...");
	return "success";
}

控制台打印的是:(看出来,自定义拦截器根本没有调用到)

org.springframework.web.servlet.PageNotFound handleHttpRequestMethodNotSupported
WARNING: Request method 'GET' not supported

网页显示:

HTTP Status 405 - Request method 'GET' not supported

type Status report

message Request method 'GET' not supported

description The specified HTTP method is not allowed for the requested resource.

Apache Tomcat/8.0.36

2、其他 遇到下面的异常捕获器:

@ResponseStatus (优先级第一)

@ExceptionHandler,@ControllerAdvice(优先级第二)

SimpleMappingExceptionResolver (优先级第三)

控制台结果显示:(自定义拦截器调用了方法一和方法三)

[FirstIntercept] preHandle
[FirstIntercept] afterCompletion

 

springmvc中级-文件上传与下载

Spring MVC 为文件上传提供了直接的支持,这种支持是通 过即插即用的 MultipartResolver 实现的。Spring 用 Jakarta Commons FileUpload 技术实现了一个MultipartResolver 实现类:CommonsMultipartResovler

Spring MVC 上下文中默认没有装配 MultipartResovler,因 此默认情况下不能处理文件的上传工作,如果想使用 Spring 的文件上传功能,需现在上下文中配置 MultipartResolver

defaultEncoding: 必须和用户 JSP 的 pageEncoding 属性 一致,以便正确解析表单的内容。

为了让 CommonsMultipartResovler 正确工作,必须先 将 Jakarta Commons FileUpload 及 Jakarta Commons io 的类包添加到类路径下。

文件上传大白话:

1、添加文件上传的jar包

commons-fileupload-1.2.1.jar
commons-io-2.0.jar

2、编辑转发配置文件

<!-- 文件上传 配置  multipartresolver -->
<bean id="multipartResolver" 
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="utf-8"></property>
<property name="maxUploadSize" value="102400"></property>
</bean>

3、网页测试页:

<form action="testFileUpload" method="post" enctype="multipart/form-data">
File:<input type="file" name="name-file"/>
Desc:<input type="text" name="name-desc"/>
<input type="submit" name="name-submit" value="submit" />
</form>

4、编辑控制器controler

@RequestMapping("/testFileUpload")
public String testFileUpload(@RequestParam("name-desc") String desc, 
		@RequestParam("name-file") MultipartFile file) throws IOException{
	System.out.println("name-desc: " + desc);
	System.out.println("OriginalFilename: " + file.getOriginalFilename());
	System.out.println("InputStream: " + file.getInputStream());
	return "success";
}

 

文件下载:

已经在HttpMessageConverter详解中的,ResponseEntity<byte[]>已经提到过了。

@RequestMapping("/testResponseEntity")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException{
	byte [] body = null;
	ServletContext servletContext = session.getServletContext();
	InputStream in = servletContext.getResourceAsStream("/files/abc.txt");
	body = new byte[in.available()];
	in.read(body);
		
	HttpHeaders headers = new HttpHeaders();
	headers.add("Content-Disposition", "attachment;filename=abc.txt");
		
	HttpStatus statusCode = HttpStatus.OK;
		
	ResponseEntity<byte[]> response = new ResponseEntity<byte[]>(body, headers, statusCode);
	return response;
}

 

 

 

springmvc中级-国际化详解

一、国际化与locale

springmvc 中,根据local 属性,实现  信息本地化。

1、当接受到请求时,SpringMVC 会在上下文中查找一个本地化解析器(LocalResolver),找到后使用它获取请求 所对应的本地化类型信息。AcceptHeaderLocaleResolver:根据 HTTP 请求头的 Accept-Language 参数确定本地化类型,如果没有显式定义 本地化解析器, SpringMVC 使用该解析器。
CookieLocaleResolver:根据指定的 Cookie 值确定本地化类型。
SessionLocaleResolver:根据 Session 中特定的属性确定本 地化类型

2、SpringMVC 还允许装配一个动态更改本地化类型的拦截 器,这样通过指定一个请求参数就可以控制单个请求的本 地化类型。
LocaleChangeInterceptor:从请求参数中获取本次请求对应的本地化类型。
比如在转发配置器中,可以编辑如下:

<!-- 动态修改国际化信息 -->
<!-- 配置sessionLocalResolver -->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>
<!-- 配置localeChangeIntercept -->
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean>
</mvc:interceptors>

总体原理路线:

二、获取locale

1、local来源

服务器读取浏览器设置,然后利用默认类   AcceptHeaderLocaleResolver  读取默认放在 request中的local。【locale应该是浏览器中的Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7 】

2、国际化前期准备

首先编辑国际化文件,然后转发配置表中进行配置。

i18n.properties

i18n.username=Username
i18n.password=Password

i18n_zh_CN.properties

i18n.username=\u7528\u6237\u540D
i18n.password=\u5BC6\u7801

i18n_en_US.properties

i18n.username=Username
i18n.password=Password

在转发配置器中:编辑国际化 bean

<!-- 配置国际化资源文件 -->
<bean id="messageSource"
	class="org.springframework.context.support.ResourceBundleMessageSource">
	<property name="basename" value="i18n"></property>	
</bean>

3、在jsp页面获取:

index.jsp ,用来跳转到tetsi18n.jsp

<a href="testi18n">测试国际化</a>

tetsi18n.jsp 文件

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
.....

<body>
<br>
<!--  fmt标签必须经过转发后才能正确显示结果  -->
<br>
<fmt:message key="i18n.username"></fmt:message>
<fmt:message key="i18n.password"></fmt:message>
<a href="testi18n">testi18n</a>
...
</body>

4、在control控制器中获取:

// 在控制器类中 自动注入 转发配置文件中 已经配置好的国际化 messageSource bean
@Autowired
private ResourceBundleMessageSource messageSource;

//国际化控制器,
@RequestMapping("/testi18n")
public String testI18n(Locale locale){
        //从messageSource 中 选取国际化信息
	String val = messageSource.getMessage("i18n.username", null, locale);
	System.out.println(val); 
	return "testi18n";
}

三、修改locale

默认情况下:locale是存放在request里面的,无法修改locale。
需要将locale存放到session中,通过操作修改session里面的locale。

1、编辑转发配置文件:

<!-- 配置国际化资源文件,前面编辑过的,这里不用改 -->
<bean id="messageSource"
	class="org.springframework.context.support.ResourceBundleMessageSource">
	<property name="basename" value="i18n"></property>	
</bean>
	
<!-- 动态修改国际化信息 -->
<!-- 配置sessionLocalResolver -->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>
<!-- 配置localeChangeIntercept -->
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean>
</mvc:interceptors>

2、编辑testi18n.jsp【用超链接修改了locale值】

<!--  fmt标签必须经过转发后才能正确显示结果  -->
<fmt:message key="i18n.username"></fmt:message>
<fmt:message key="i18n.password"></fmt:message>
<a href="testi18n">testi18n</a>
<a href="testi18n?locale=zh_CN">测试国际化中文</a>
<a href="testi18n?locale=en_US">测试国际化英文</a>

 

 

字符编码-教程(8)-字符串长度与字符编码

字符串长度与字符编码


在双字节中文编码(DBCS)中,“一个汉字算两个英文字符!一个汉字算两个英文字符……”【其实这句话的本意是:在双字节中文编码中,兼容了ASCII,导致英文编码只占一个字节,中文编码占二个字节,所以一个汉字算两个英文】

比如下面的C代码:
如果用gbk编码保存文件,”我爱你祖国”编码的二进制流占10个字节。程序输出字符串长度为10
如果用utf-8编码保存文件,”我爱你祖国”编码的二进制流占15个字节。程序输出字符串长度为15

#include <stdio.h>
#include <string.h>

int main(){
	
char str[20]="我爱你祖国";

printf("%d\n",strlen(str)); 

getchar(); 
}

/************

在C语言中:strlen 在处理汉字串时,与处理一般的英文串是一样的,
就是计算从串首地址开始检查到'\0'字符的位置,然后计算两个地址的差,返回差值,
也就是字符串中有多个字符(字节)'\0'字符不是汉字的组成部分!
对于一个汉字占几个字节,不同的字符集是不同的,
如果环境变量设置为utf8,则一个汉字占三个字节
如果设置成gbk类,则一个汉字占两个字节

************/

一个字符和两个字节的故事: 这时候,从旧社会里走过来的程序员开始发现一个奇怪的现象:他们的strlen函数靠不住了,一个汉字不再是相当于两个字符了,而是一个!是的,从 UNICODE 开始,无论是半角的英文字母,还是全角的汉字,它们都是统一的“一个字符”!同时,也都是统一的“两个字节”,请注意“字符”和”字节”两个术语的不同,“字节”是一个8位的物理存贮单元,而“字符”则是一个文化相关的符号。在UNICODE 中,一个字符就是两个字节。一个汉字算两个英文字符的时代已经快过去了。【备注:前面这段话不靠谱,Unicode是字符集,Unicode不是两个字节,具体要看编码方式,测试字符串长度函数:判断长度依据是计算字符数 还是计算字节数】

字符编码-教程(7)-国际化-Unicode与ANSI

一、编码国际化产生原因:

全世界很多个国家都在为自己的文字编码,并且互不想通,不同的语言字符编码值相同却代表不同的符号(例如:韩文编码EUC-KR中“한국어”的编码值正好是汉字编码GBK中的“茄惫绢”)。因此,同一份文档,拷贝至不同语言的机器,就可能成了乱码,这给各国和各地区交换信息带来了很大的困难,同时也给国际化(本地化)编程造成了很大的麻烦。

二、UNICODE和UCS方案

于是人们就想:我们能不能定义一个超大的字符集,它可以容纳全世界所有的文字字符,再对它们统一进行编码,让每一个字符都对应一个不同的编码值,从而就不会再有乱码了。

如果说“各个国家都在为自己文字独立编码”是百家争鸣,那么“建立世界统一的字符编码”则是一统江湖,谁都想来做这个武林盟主。早前就有两个机构试图来做这个事:
(1) 国际标准化组织(ISO),他们于1984年创建ISO/IEC JTC1/SC2/WG2工作组,试图制定一份“通用字符集”(Universal Character Set,简称UCS),并最终制定了ISO 10646标准。
(2) 统一码联盟,他们由Xerox、Apple等软件制造商于1988年组成,并且开发了Unicode标准(The Unicode Standard,这个前缀Uni很牛逼哦—Unique, Universal, and Uniform)。

1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。1991年,不包含CJK统一汉字集的Unicode 1.0发布。随后,CJK统一汉字集的制定于1993年完成,发布了ISO 10646-1:1993,即Unicode 1.1。

从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码;ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。两个项目仍都独立存在,并独立地公布各自的标准。不过由于Unicode这一名字比较好记,因而它使用更为广泛。

备注:

ISO 10646 定义了 UCS-4 和UCS-2 两种编码形式。
其中UCS-4其编码固定占用4个字节,UCS-2其编码固定占用2个字节

Unicode编码点分为17个平面(plane),每个平面包含216(即65536)个码位(code point)。17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从0016到1016,共计17个平面。且第一个平面称为“基本多语言平面”(Basic Multilingual Plane,简称BMP)【ISO 10646 不会超出这些平面赋值,0016代表二进制0000 0000,1016代表二进制0001 0000,所以Unicode 17个平面只占用了三个字节不到,也就是占了21位 编码位

UCS 是所有其他字符集标准的一个超集. 它保证与其他字符集是双向兼容的. 就是说, 如果你将任何文本字符串翻译到 UCS格式, 然后再翻译回原编码, 你不会丢失任何信息。UCS 包含了用于表达所有已知语言的字符。对于还没有加入的语言,由于正在研究怎样在计算机中最好地编码它们, 因而最终它们都将被加入。

1、编码方式产生原因

需要注意的是,Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。比如UTF-8是Unicode的实现方式之一。

比如,汉字的 Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

它们造成的结果是:1)出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。2)Unicode 在很长一段时间内无法推广,直到互联网的出现。

2、编码方式分类

 UTF-32与UCS-4

在Unicode与ISO 10646合并之前,ISO 10646标准为“通用字符集”(UCS)定义了一种31位的编码形式(即UCS-4),其编码固定占用4个字节(实际上只用了31位,最高位必须为0),编码空间为0x00000000~0x7FFFFFFF(可以编码20多亿个字符)。下面让我们做一些简单的数学游戏:

UCS-2有2^16=65536个码位,UCS-4有2^31=2147483648个码位。

UCS-4有20多亿个编码空间,但实际使用范围并不超过0x10FFFF,并且为了兼容Unicode标准,ISO也承诺将不会为超出0x10FFFF的UCS-4编码赋值。由此UTF-32编码被提出来了,它的编码值与UCS-4相同,只不过其编码空间被限定在了0~0x10FFFF之间。因此也可以说:UTF-32是UCS-4的一个子集备注: UTF-32编码长度是固定的,UTF-32中的每个32位值代表一个Unicode码位,并且与该码位的数值完全一致。

UTF-16与UCS-2

除了UCS-4,ISO 10646标准为“通用字符集”(UCS)定义了一种16位的编码形式(即UCS-2),其编码固定占用2个字节,它包含65536个编码空间(可以为全世界最常用的63K字符编码,为了兼容Unicode,0xD800-0xDFFF之间的码位未使用)。例:“汉”的UCS-2编码为6C49。

UCS-2对于ascii里的那些“半角”字符,UNICODE 包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于“半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。

但俩个字节并不足以正真地“一统江湖”(a fixed-width 2-byte encoding could not encode enough characters to be truly universal),于是UTF-16诞生了,与UCS-2一样,它使用两个字节为全世界最常用的63K字符编码,不同的是,它使用4个字节对不常用的字符进行编码。UTF-16属于变长编码。

前面提到过:Unicode编码点分为17个平面(plane),每个平面包含216(即65536)个码位(code point),而第一个平面称为“基本多语言平面”(Basic Multilingual Plane,简称BMP),其余平面称为“辅助平面”(Supplementary Planes)。其中“基本多语言平面”(0~0xFFFF)中0xD800~0xDFFF之间的码位作为保留,未使用。UCS-2只能编码“基本多语言平面”中的字符,此时UTF-16与UCS-2的编码一样(都直接使用Unicode的码位作为编码值),例:“汉”在Unicode中的码位为6C49,而在UTF-16编码也为6C49。另外,UTF-16还可以利用保留下来的0xD800-0xDFFF区段的码位来对“辅助平面”的字符的码位进行编码,因此UTF-16可以为Unicode中所有的字符编码。

UTF-16中如何对“辅助平面”进行编码呢?

Unicode的码位区间为0~0x10FFFF,除“基本多语言平面”外,还剩0xFFFFF个码位(并且其值都大于或等于0x10000)。对于“辅助平面”内的字符来说,如果用它们在Unicode中码位值减去0x10000,则可以得到一个0~0xFFFFF的区间(该区间中的任意值都可以用一个20-bits的数字表示)。该数字的前10位(bits)加上0xD800,就得到UTF-16四字节编码中的前两个字节;该数字的后10位(bits)加上0xDC00,就得到UTF-16四字节编码中的后两个字节。例如:

(这个字念啥?^_^)
上面这个汉字的Unicode码位值为2AEAB,减去0x10000得到1AEAB(二进制值为0001 1010 1110 1010 1011),前10位加上D800得到D86B,后10位加上DC00得到DEAB。于是该字的UTF-16编码值为D86BDEAB(该值为大端表示,小端为6BD8ABDE)。


UTF-16是Unicode字符集的一种转换方式,即把Unicode的码位转换为16比特长的码元串行,以用于数据存储或传递。UTF-16编码规则如下:

2.2.1 从U+D800到U+DFFF的码位(代理区)

因为Unicode字符集的编码值范围为0-0x10FFFF,而大于等于0x10000的辅助平面区的编码值无法用2个字节来表示,所以Unicode标准规定:基本多语言平面内,U+D800..U+DFFF的值不对应于任何字符,为代理区。因此,UTF-16利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。

但是在使用UCS-2的时代,U+D800..U+DFFF内的值被占用,用于某些字符的映射。但只要不构成代理对,许多UTF-16编码解码还是能把这些不符合Unicode标准的字符映射正确的辨识、转换成合规的码元. 按照Unicode标准,这种码元串行本来应算作编码错误.

2.2.2 从U+0000至U+D7FF以及从U+E000至U+FFFF的码位

第一个Unicode平面(BMP),码位从U+0000至U+FFFF(除去代理区),包含了最常用的字符。UTF-16与UCS-2编码在这个范围内的码位为单个16比特长的码元,数值等价于对应的码位。BMP中的这些码位是仅有的码位可以在UCS-2被表示。

2.2.3 从U+10000到U+10FFFF的码位

辅助平面(Supplementary Planes)中的码位,大于等于0x10000,在UTF-16中被编码为一对16比特长的码元(即32bit,4Bytes),称作 code units called a 代理对(surrogate pair),具体方法是:

Ø 码位减去0x10000, 得到的值的范围为20比特长的0..0xFFFFF(因为Unicode的最大码位是0x10ffff,减去0x10000后,得到的最大值是0xfffff,所以肯定可以用20个二进制位表示),写成二进制形式:yyyy yyyy yyxx xxxx xxxx。

Ø 高位的10比特的值(值的范围为0..0x3FF)被加上0xD800得到第一个码元或称作高位代理(high surrogate), 值的范围是0xD800..0xDBFF。由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead surrogates)。

Ø 低位的10比特的值(值的范围也是0..0x3FF)被加上0xDC00得到第二个码元或称作低位代理(low surrogate), 现在值的范围是0xDC00..0xDFFF。 由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail surrogates)。

Ø 最终的UTF-16(4字节)的编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。

按照上述规则,Unicode编码0x10000-0x10FFFF的UTF-16编码有两个WORD,第一个WORD的高6位是110110,第二个WORD的高6位是110111。可见,第一个WORD的取值范围(二进制)是11011000 00000000到11011011 11111111,即0xD800-0xDBFF。第二个WORD的取值范围(二进制)是11011100 00000000到11011111 11111111,即0xDC00-0xDFFF。上面所说的从U+D800到U+DFFF的码位(代理区),就是为了将一个WORD(2字节)的UTF-16编码与两个WORD的UTF-16编码区分开来。

由于高位代理、低位代理、BMP中的有效字符的码位,三者互不重叠,搜索是简单的: 一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。这意味着UTF-16是自同步(self-synchronizing):可以通过仅检查一个码元就可以判定给定字符的下一个字符的起始码元。 UTF-8也有类似优点,但许多早期的编码模式就不是这样,必须从头开始分析文本才能确定不同字符的码元的边界。

由于最常有的字符都在基本多文种平面中,许多软件的处理代理对的部分往往得不到充分的测试。这导致了一些长期的bug与潜在安全漏洞,甚至在广为流行得到良好评价的应用软件。


UTF-8

从前述内容可以看出:无论是UTF-16/32还是UCS-2/4,一个字符都需要多个字节来编码,这对那些英语国家来说多浪费带宽啊!(尤其在网速本来就不快的那个年代。。。)由此,UTF-8产生了。在UTF-8编码中,ASCII码中的字符还是ASCII码的值,只需要一个字节表示,其余的字符需要2字节、3字节或4字节来表示。

UTF-8的编码规则:

(1) 对于ASCII码中的符号,使用单字节编码,其编码值与ASCII值相同(详见:U0000.pdf)。其中ASCII值的范围为0~0x7F,所有编码的二进制值中第一位为0(这个正好可以用来区分单字节编码和多字节编码)。

(2) 其它字符用多个字节来编码(假设用N个字节),多字节编码需满足:第一个字节的前N位都为1,第N+1位为0,后面N-1 个字节的前两位都为10,这N个字节中其余位全部用来存储Unicode中的码位值。

字节数 Unicode UTF-8编码
1 000000-00007F 0xxxxxxx
2 000080-0007FF 110xxxxx 10xxxxxx
3 000800-00FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 010000-10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8 有以下编码规则:

  1. 如果一个字节,最高位(第 8 位)为 0,表示这是一个 ASCII 字符(00 – 7F)。可见,所有 ASCII 编码已经是 UTF-8 了。
  2. 如果一个字节,以 11 开头,连续的 1 的个数暗示这个字符的字节数,例如:110xxxxx 代表它是双字节 UTF-8 字符的首字节。
  3. 如果一个字节,以 10 开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节

3、编码方式总结

(1) 简单地说:Unicode属于字符集,不属于编码,UTF-8、UTF-16等是针对Unicode字符集的编码。
(2) UTF-8、UTF-16、UTF-32、UCS-2、UCS-4对比:

对比 UTF-8 UTF-16 UTF-32 UCS-2 UCS-4
编码空间 0-10FFFF 0-10FFFF 0-10FFFF 0-FFFF 0-7FFFFFFF
最少编码字节数 1 2 4 2 4
最多编码字节数 4 4 4 2 4
是否依赖字节序

4、编码的储存和传输


  • windows换装: 从前多种字符集存在时,那些做多语言软件的公司遇上过很大麻烦,他们为了在不同的国家销售同一套软件,就不得不在区域化软件时也加持那个双字节字符集咒语,不仅要处处小心不要搞错,还要把软件中的文字在不同的字符集中转来转去。UNICODE 对于他们来说是一个很好的一揽子解决方案,于是从 Windows NT 开始,MS 趁机把它们的操作系统改了一遍,把所有的核心代码都改成了用 UNICODE 方式工作的版本,从这时开始,WINDOWS 系统终于无需要加装各种本土语言系统,就可以显示全世界上所有文化的字符了。
  • UNICODE和DBCS的转换: 但是,UNICODE 在制订时没有考虑与任何一种现有的编码方案保持兼容,这使得 GBK 与UNICODE 在汉字的内码编排上完全是不一样的,没有一种简单的算术方法可以把文本内容从UNICODE编码和另一种编码进行转换,这种转换必须通过查表来进行。
  • BMP
    • UCS-4根据最高位为0的最高字节分成2^7=128个group。每个group再根据次高字节分为256个plane。每个plane根据第3个字节分为256行 (rows),每行包含256个cells。当然同一行的cells只是最后一个字节不同,其余都相同。
    • group 0的plane 0被称作Basic Multilingual Plane, 即BMP。或者说UCS-4中,高两个字节为0的码位被称作BMP。

  • UTF: UNICODE 来到时,一起到来的还有计算机网络的兴起,UNICODE 如何在网络上传输也是一个必须考虑的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF8就是每次8个位传输数据,而UTF16就是每次16个位,只不过为了传输时的可靠性,从UNICODE到UTF时并不是直接的对应,而是要过一些算法和规则来转换。
  • UCS和UTF的区别: UCS规定了怎么用多个字节表示各种文字。怎样传输这些编码,是由UTF(UCS Transformation Format)规范规定的,常见的UTF规范包括UTF-8、UTF-7、UTF-16。
  • Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。比如,汉字“严”的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
  • 这里就有两个严重的问题
    • 第一个问题是,如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?
    • 第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
  • 它们造成的结果是:
    • 出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。
    • unicode在很长一段时间内无法推广,直到互联网的出现。
  • UTF-8
    • 互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。
    • UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
    • 下面是Unicode和UTF-8转换的规则
       1 Unicode 
       2    
       3 UTF-8 
       4    
       5 0000 - 007F 
       6    
       7 0xxxxxxx 
       8    
       9 0080 - 07FF 
      10    
      11 110xxxxx 10xxxxxx 
      12    
      13 0800 - FFFF 
      14    
      15 1110xxxx 10xxxxxx 10xxxxxx

例如”汉”字的Unicode编码是6C49。6C49在0800-FFFF之间,所以要用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是:0110 1100 0100 1001,将这个比特流按三字节模板的分段方法分为0110 110001 001001,依次代替模板中的x,得到:1110-0110 10-110001 10-001001,即E6 B1 89,这就是其UTF8的编码。

三、ANSI编码方案


  • 字节: 很久很久以前,有一群人,他们决定用8个可以开合的晶体管来组合成不同的状态,以表示世界上的万物。他们看到8个开关状态是好的,于是他们把这称为”字节”。
  • 每个字节可表示256个不同的状态: 再后来,他们又做了一些可以处理这些字节的机器,机器开动了,可以用字节来组合出很多状态,状态开始变来变去。他们看到这样是好的,于是它们就这机器称为”计算机”。开始时,计算机只在美国用。八位的字节一共可以组合出256(2的8次方)种不同的状态。
  • 控制码: 在ASCII码中,第0~31号及第127号(共33个)是控制字符或通讯专用字符,如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(振铃)等;通讯专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等。
  • ASCII: 他们又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了。大家看到这样,都感觉很好,于是大家都把这个方案叫做“Ascii”编码(American Standard Code for Information Interchange,美国信息互换标准代码)。当时世界上所有的计算机都用同样的ASCII方案来保存英文文字。
  • 扩展字符集: 后来,就像建造巴比伦塔一样,世界各地的都开始使用计算机,但是很多国家用的不是英文,他们的字母里有许多是ASCII里没有的,为了可以在计算机保存他们的文字,他们决定采用127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128到255这一页的字符集被称“扩展字符集”。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。从此之后,贪婪的人类再没有新的状态可以用了,美帝国主义可能没有想到还有第三世界国家的人们也希望可以用到计算机吧!
  • 疑问:ANSI编码是什么
      • ANSI,American National Standard Institite,美国国家标准协会。
      • ANSI编码是一种字符代码,为使计算机支持更多语言,通常使用 0x00~0x7f 范围的1 个字节来表示 1 个英文字符。超出此范围的使用0x80~0xFFFF来编码,即扩展的ASCII编码。
      • ANSI编码 为使计算机支持更多语言,通常使用 0x80~0xFFFF 范围的 2 个字节来表示 1 个字符。比如:汉字 ‘中’ 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。
      • 不同的国家和地区制定了不同的标准,由此产生了 GB2312、GBK、GB18030、Big5、Shift_JIS 等各自的编码标准。这些使用多个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文Windows操作系统中,ANSI 编码代表 GBK 编码;在繁体中文Windows操作系统中,ANSI编码代表Big5;在日文Windows操作系统中,ANSI 编码代表 Shift_JIS 编码。
      • 不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。ANSI编码表示英文字符时用一个字节,表示中文用两个或四个字节。

很长, 终于讲完了我想要说后些名字和细节,但是还有一些名词在上边没有提到,这里再单独解释一下。

大白话:关于代码页(code page)问题。处理多语言的编码方案有两种。

1、将所有语言的每一个符号都统一编码到一个字符集里面。如Unicode方案。

2、将文件编码抽象成一种,根据系统语言环境,再用具体的编码保存。如ANSI方案。但“ANSI编码”确实只存在于Windows系统。ANSI其实是多种编码的集合,具体的编码与解码方案则是根据代码页(code page)来实现的。而code page 则与本地计算机的 语言有关。

那么Windows系统是如何区分ANSI背后的真实编码的呢?

微软用一个叫“Windows code pages”(在命令行下执行chcp命令可以查看当前code page的值)的值来判断系统默认编码,比如:简体中文的code page值为936(它表示GBK编码,win95之前表示GB2312,详见:Microsoft Windows’ Code Page 936),繁体中文的code page值为950(表示Big-5编码)。

我们能否通过修改Windows code pages的值来改变“ANSI编码”呢?

命令提示符下,我们可以通过chcp命令来修改当前终端的active code page,例如:
(1) 执行:chcp 437,code page改为437,当前终端的默认编码就为ASCII编码了(汉字就成乱码了);
(2) 执行:chcp 936,code page改为936,当前终端的默认编码就为GBK编码了(汉字又能正常显示了)。
上面的操作只在当前终端起作用,并不会影响系统默认的“ANSI编码”。(更改命令行默认codepage参看:设置cmd的codepage的方法)。

Windows下code page是根据当前系统区域(locale)来设置的,要想修改系统默认的“ANSI编码”,我们可以通过修改系统区域来实现(“控制面板” =>“时钟、语言和区域”=>“区域和语言”=>“管理”=>“更改系统区域设置…”):

图中的系统locale为简体中文,意味着当前“ANSI编码”实际是GBK编码。当你把它改成Korean(Korea)时,“ANSI编码”实际是EUC-KR编码,“한국어”就能正常显示了;当你把它改成English(US)时,“ANSI编码”实际是ASCII编码,“汉字”和“한국어”都成乱码了。(改了之后需要重启系统的。。。)

说明:locale是国际化与本地化中重要的概念,本文不深入讲解该内容。


  • 内码: 字符必须编码后才能被计算机处理。 计算机使用的缺省编码方式就是计算机的内码。上文提到的ASCII, GB2312, big5等都可以叫做内码
  • Code page
    • UNICODE跟Code Page应该说是显示全世界语言的两个解决方案
    • 前一个方案是将全世界的语言的每一个编码都映射成一个编号
    • 后一种解决方案则是根据不同的采取重复的编号,根据不同的Code Page来决定一个编号是什么字符。同一个编号在不同的Code Page下代表不同的字符
    • 如果你安装的是英文版的XP,Code Page选择的是简体中文,那么你可以正常显示Unicode字符和Code page为中文的编号,无法显示Code Page为繁体和日语的编号。如果要显示后者,则必须切换成相应的Code page,或者将字符编码换成Unicode。
  • 字符集和代码页
    对于ANSI编码方式,存在不同的字符集(Charset)。同样的字节序列,在不同的字符集下表示的字符不一样。要正确解析一个ANSI字符串,还要选择正确的字符集,否则就可能导致所谓的乱码现象。不同语言版本的操作系统,都有一个默认的字符集。在不指定字符集的情况下,系统会使用此字符集来解析 ANSI 字符串。也就是说,如果我们在简体中文版的Windows下打开了一个由日文操作系统保存的 ANSI 文本文件(仅包含 ANSI 字符串的文本文件),我们看到的将是乱码。但是,如果我们使用Visual Studio之类的带编码选择的文本编辑器打开此文件,并且选择正确的字符集,我们将可以看到它的原貌。注意:简体中文字符集中的繁体字和繁体中文字符集中的繁体字,编码不一定相同(事实证明,似乎是完全不同)。
    每个字符集都有一个唯一的编号,称为代码页(Code Page)。简体中文(GB2312)的代码页为936,而系统默认字符集的代码页为0,它表示根据系统的语言设置来选择一个合适的字符集。

字符编码-教程(6)-简体汉字的区位码、国标码、内码、外码及字型码

GB2312、GBK、GB18030等GB类汉字编码方案的具体实现方式是怎样的?区位码是什么?国标码是什么?内码、外码、字形码又是什么意思?它们是如何转换的,又为什么要这样转换?

下面以GB2312为例来加以说明(由于GBK、GB18030是以GB2312为基础扩展而来,因此编码实现方式与GB2312一样)。

一、区位码

1、整个GB2312字符集分成94个区,每区有94个位,每个区位上只有一个字符,即每区含有94个汉字或符号,用所在的区和位来对字符进行编码(实际上就是字符编号、码点编号),因此称为区位码(或许叫“区位号”更为恰当)。

换言之,GB2312将包括汉字在内的所有字符编入一个94 * 94的二维表,行就是“区”、列就是“位”,每个字符由区、位唯一定位,其对应的区、位编号合并就是区位码。比如“万”字在45区82位,所以“万”字的区位码是:45 82(注意,GB类汉字编码为双字节编码,因此,45相当于高位字节,82相当于低位字节)。

2、GB2312字符集中:

1)01~09区(682个):特殊符号、数字、英文字符、制表符等,包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母等在内的682个全角字符;

2)10~15区:空区,留待扩展;

3)16~55区(3755个):常用汉字(也称一级汉字),按拼音排序;

4)56~87区(3008个):非常用汉字(也称二级汉字),按部首/笔画排序;

5)88~94区:空区,留待扩展。

二、国标码(交换码)

1、为了避开ASCII字符中的不可显示字符0000 0000 ~ 0001 1111(十六进制为0 ~ 1F,十进制为0 ~ 31)及空格字符0010 0000(十六进制为20,十进制为32)(至于为什么要避开、又为什么只避开ASCII中0~32的不可显示字符和空格字符,后文有解释),国标码(又称为交换码)规定表示汉字的范围为(0010 0001,0010 0001) ~ (0111 1110,0111 1110),十六进制为(21,21) ~ (7E,7E),十进制为(33,33) ~ (126,126)(注意,GB类汉字编码为双字节编码)。

因此,必须将“区码”和“位码”分别加上32(十六进制为20H,后缀H表示十六进制),作为国标码。也就是说,国标码相当于将区位码向后偏移了32,以避免与ASCII字符中0~32的不可显示字符和空格字符相冲突。

2、注意,国标码中是分别将区位码中的“区”和“位”各自加上32(20H)的,因为GB2312是DBCS双字节字符集,国标码属于双字节码,“区”和“位”各作为一个单独的字节。

这样我们可以算出“万”字的国标码十进制为:(45+32,82+32) = (77,114),十六进制为:(4D,72H),二进制为:(0100 1101,0111 0010)。

三、内码(机内码)

1、不过国标码还不能直接在计算机上使用,因为这样还是会和早已通用的ASCII码冲突(导致乱码)。

比如,“万”字国标码中的高位字节77与ASCII的“M”冲突,低位字节114与ASCII的“r”冲突。因此,为避免与ASCII码冲突,规定国标码中的每个字节的最高位都从0换成1,即相当于每个字节都再加上128(十六进制为80,即80H;二进制为1000 0000),从而得到国标码的“机内码”表示,简称“内码”。

2、由于ASCII码只用了一个字节中的低7位,所以,这个首位(最高位)上的“1”就可以作为识别汉字编码的标志,计算机在处理到首位是“1”的编码时就把它理解为汉字,在处理到首位是“0”的编码时就把它理解为ASCII字符。

比如:

77 + 128 = 205(二进制为1100 1101,十六进制为CD)

114+ 128 = 242(二进制为1111 0010,十六进制为F2)

3、我们可以来检验一下。打开记事本输入“万”字,编码选择为ANSI(Windows记事本中的ANSI编码对于简体汉字而言就是GB类编码,详见后文解释),保存,如下图所示。

然后用二进制编辑器(比如UltraEdit)打开刚才保存的文件,切换到十六进制模式,会看到:CD F2,这就是“万”字的内码,如下图所示。

 4、总结一下:

从区位码(国家标准定义) —> 区码和位码分别+32(即+20H)得到国标码 —> 再分+128(即+80H)得到机内码(与ACSII码不再冲突)。

因此,区位码的区和位分别+160(即+A0H,32+128=160)可直接得到内码。用十六进制表示就是:

区位码(区码,位码) + (20H,20H) + (80H,80H) =区位码(区码,位码) + (A0H,A0H) = 内码(高字节,低字节)。

四、为什么要加上20H和80H?

1、区位码、国标码、内码的转换非常简单,但是令人迷惑的是为什么要这么转换?

首先,需要注意到一点,GB2312虽说是对中文编码,但是里面也有对26个英文字母和一些特殊符号的编码,按理说这些和ASCII重合的字符(33~127)应该无需再重新编码,直接沿用ASCII中的不就行了?

2、原来,当时在制定GB2312时,决定对ASCII中的可打印字符,也就是英文字母、数字和符号部分(33~126,127为不可打印的DEL)重新编入GB2312中,以两个字节表示,称之为全角字符(全角字符在屏幕上的显示宽度为ASCII字符的两倍,后来也因此而将对应的ASCII字符称之为半角字符)。

而对于ASCII中前32个不可显示也不可打印的控制字符(ASCII码为0~31),以及第33个可显示但不可打印的空格字符(ASCII码为32)等共33个不可打印字符的编码则直接沿用,不再重新编码。

3、因为要保留这33个不可打印字符,就不能直接采用区位码作为计算机直接处理的机内码,需要将区位码向后偏移32以避开冲突(为什么是偏移32,而不是偏移33?因为区位码中的区码和位码都是从1开始计数的,不像ASCII码是从0开始计数的)。

十进制数字32的十六进制表示就是20(为区别于十进制,记作20H),这也就是区位码要加上20H(区码和位码各自加上20H)才能得到国标码的原因。

4、很显然,如果直接采用国标码作为计算机直接处理的机内码的话,还将会产生另一个弊端,即用ASCII码编码的英文字符在GB2312编码环境中无法打开,一打开就会乱码。

因为国标码虽然相较于区位码避开了ASCII码中0~32的前33个不可打印字符,但并没有避开ASCII码中的英文字母、数字和符号(33~126,共94个字符,127为不可打印的DEL)等可打印字符。也就是说,国标码并不是完全兼容ASCII码的。

5、为了解决这个弊端,考虑到ASCII码只使用了一个字节中的低7位,最高位(即首位)为0,于是决定将国标码每个字节的最高位设为1(国标码的两个字节中的最高位都恒为0,即国标码中的每个字节实际上也只用了一个字节中的低7位),这就是GB2312的机内码(即内码),简称GB2312码。

这样一来就彻底区分开了ASCII码和GB2312码。这也是为什么国标码还要加上(80H,80H)才能得到机内码的原因。

6、看到这里,有人或许又要问了:如果仅仅是为了避免与ASCII编码相冲突,为什么最初不直接将区位码的区码和位码的最高位从0改为1(相当于各自直接加上128),这样不就无需经过国标码多此一举的中间转换了吗?而且还无需后移32,也就不用浪费这部分编码空间。

对此本人也很困惑,在网上搜了很久也没找到答案,因此具体原因不得而知。或许是一开始考虑不周?或许为了未来扩展所需而预留一部分空间?又或许是有其他不得已的原因?有知道的朋友还望能指点迷津。

五、外码(输入码、输入法编码)

1、外码也叫输入码、输入法编码,是用来将汉字输入到计算机中的一组键盘符号,是作为汉字输入用的编码。

英文字母只有26个,可以把所有的字符都放到键盘上,而使用这种办法把所有的汉字都放到键盘上,是不可能的。所以汉字系统需要有自己的输入码体系,使汉字与键盘能建立对应关系。

2、目前常用的外码分为以下几类:

1)数字编码,比如区位码;

2)拼音编码,比如全拼、双拼、自然码等;

3)字形编码,比如五笔、表形码、郑码等。

六、字形码(字型码、字模码、输出码)

1、字形码,又称为字型码、字模码、输出码,属于点阵代码的一种。

为了将汉字在显示器或打印机上输出,把汉字按图形符号设计成点阵图,就得到了相应的点阵代码(字形码)。

也就是用0、1表示汉字的字形,将汉字放入n行*n列的正方形(点阵)内,该正方形共有n^2个小方格,每个小方格用一位二进制表示,凡是笔划经过的方格值为1,未经过的值为0。

2、显示一个汉字一般采用16×16点阵或24×24点阵或48×48点阵。已知汉字点阵的大小,可以计算出存储一个汉字所需占用的字节空间。

比如,用16×16点阵表示一个汉字,就是将每个汉字用16行,每行16个点表示,一个点需要1位二进制代码,16个点需用16位二进制代码(即2个字节),共16行,所以需要16行×2字节/行=32字节,即16×16点阵表示一个汉字,字形码需用32字节。

因此,字节数=点阵行数×(点阵列数/8)。

3、显然,字形码所表示的字符,相对于抽象字符表ACR里的“抽象”字符,可称之为“具体”字符,因为具有了“具体”的外形。

4、为了将汉字的字形显示输出或打印输出,汉字信息处理系统还需要配有汉字字形库,也称字模库,简称字库,它集中了汉字的字形信息。

字库按输出方式可分为显示字库和打印字库。用于显示输出的字库叫显示字库,工作时需调入内存。用于打印输出的字库叫打印字库,工作时无需调入内存。

字库按存储方式也可分为软字库和硬字库。软字库以文件的形式存放在硬盘上,现多用这种方式。硬字库则将字库固化在一个单独的存储芯片中,再和其它必要的器件组成接口卡,插接在计算机上,通常称为汉卡。这种方式现已淘汰。

七、小结

可以这样理解,为在计算机内表示汉字而采取统一的编码方式所形成的汉字编码叫内码。为方便汉字输入而形成的汉字编码为外码,也叫输入码。为显示输出和打印输出汉字而形成的汉字编码为字形码,也称为字模码、输出码。

计算机通过键盘输入的外码(重码时还需附加选择编号)对应于汉字内码,将汉字外码转换(即映射)为汉字内码,以实现输入汉字的目的;通过汉字内码在字模库(即字库)中找出汉字的字形码,将汉字内码转换(即映射)为汉字字形码,以实现显示输出和打印输出汉字的目的。

事实上,英文字符的输入、处理和显示过程大致上也差不多,只不过英文字符不需要输入码(即外码),直接在键盘上输入对应的英文字母即可。