《SpringMVC系列》第六章:异常处理

划重点

  1. 异常处理自动配置类为ErrorMvcAutoConfiguration
  2. 异常信息保存在DefaultErrorAttributes
  3. 异常捕获在我们最熟悉的doDispatch()方法
  4. 异常处理首先会遍历处理异常解析器,判断是否自定义了异常处理,也就是@ExceptionHandlerResponseStatus等,这样我们不仅可以自定义异常处理逻辑,而且可以设定返回结果
  5. 当没有自定义异常处理类,则会默认请求/error,该请求会跳转到自动配置类里面注册的BasicErrorController,该控制类负责跳转到自定义的异常页面或者默认白页,这种我们是不可以自定义异常处理逻辑的
  6. 上面两者的区别,第一种是可以自定义异常处理逻辑,然后返回视图或者JSON,而第二种只能跳转到定义的异常页面,无法自定义处理逻辑
  7. 当存在自定义异常页面则正确跳转,否则就会跳转到默认的白页whitepage
  8. 我们之前经常看到的whitepage,都是在后台拼接成的网页内容,然后返回,处理类为StaticView

一、默认规则

1.官方解释

默认情况下,Spring Boot 提供了一个`/error`以合理方式处理所有错误的映射,并将其注册为 servlet 容器中的“全局”错误页面。对于机器客户端,它会生成一个 JSON 响应,其中包含错误、HTTP 状态和异常消息的详细信息。对于浏览器客户端,有一个“whitelabel”错误视图,它以 HTML 格式呈现相同的数据

server.error如果要自定义默认错误处理行为,可以设置许多属性。请参阅附录的“服务器属性”部分。

要完全替换默认行为,您可以实现ErrorController并注册该类型的 bean 定义或添加类型的 beanErrorAttributes以使用现有机制但替换内容。

2.自定义错误页面

如果想要根据错误状态码来自定义HTML错误页面,那么可以将文件添加到/error目录。错误页面可以是静态 HTML(即添加到任何静态资源目录下),也可以使用模板构建。文件名应该是准确的状态码或序列掩码。

即在静态目录templates下创建/error目录,错误页面存放在这里,文件名以状态码定义,例如:404.html500.html5xx.html

二、异常处理自动配置

ErrorMvcAutoConfiguration 自动配置类

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {}

DefaultErrorAttributes 错误属性

定义错误页面包含哪些数据

	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) // 存在用自定义的,没有则用默认的
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes();
	}

BasicErrorController 错误处理器

向容器中注册该Bean

	@Bean
	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) // 同样是存在则用自定义的,否则用默认的
	public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
			ObjectProvider<ErrorViewResolver> errorViewResolvers) {
		return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
				errorViewResolvers.orderedStream().collect(Collectors.toList()));
	}

注册的Bean是一个Controller,@RequestMapping里规定了映射路径,优先取server.error.path,没有则用 /error

处理默认的 /error 路径的请求

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {}

DefaultErrorViewResolverConfiguration 错误视图解析器

	@Configuration(proxyBeanMethods = false)
	@EnableConfigurationProperties({ WebProperties.class, WebMvcProperties.class })
	static class DefaultErrorViewResolverConfiguration {

		private final ApplicationContext applicationContext;

		private final Resources resources;

		DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, WebProperties webProperties) {
			this.applicationContext = applicationContext;
			this.resources = webProperties.getResources();
		}

		@Bean
		@ConditionalOnBean(DispatcherServlet.class)
		@ConditionalOnMissingBean(ErrorViewResolver.class)
		DefaultErrorViewResolver conventionErrorViewResolver() {
             // 默认的错误视图解析器
			return new DefaultErrorViewResolver(this.applicationContext, this.resources);
		}

	}

WhitelabelErrorViewConfiguration 白页错误视图

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
	@Conditional(ErrorTemplateMissingCondition.class)
	protected static class WhitelabelErrorViewConfiguration {

         // 这里创建的就是默认的白页视图
		private final StaticView defaultErrorView = new StaticView();

         // 向容器中注册一个id=error的视图
         // 出现异常时,首先会去找自定义的异常页面,不存在时则会返回一个error视图,及默认的视图
		@Bean(name = "error")
		@ConditionalOnMissingBean(name = "error")
		public View defaultErrorView() {
			return this.defaultErrorView;
		}

		// If the user adds @EnableWebMvc then the bean name view resolver from
		// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
		@Bean
		@ConditionalOnMissingBean
		public BeanNameViewResolver beanNameViewResolver() {
			BeanNameViewResolver resolver = new BeanNameViewResolver();
			resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
			return resolver;
		}
	}

StaticView 白页内容

静态视图,我们的错误视图是在这里拼接好的

	private static class StaticView implements View {

		private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);

		private static final Log logger = LogFactory.getLog(StaticView.class);

		@Override
		public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
				throws Exception {
			if (response.isCommitted()) {
				String message = getMessage(model);
				logger.error(message);
				return;
			}
			response.setContentType(TEXT_HTML_UTF8.toString());
			StringBuilder builder = new StringBuilder();
			Object timestamp = model.get("timestamp");
			Object message = model.get("message");
			Object trace = model.get("trace");
			if (response.getContentType() == null) {
				response.setContentType(getContentType());
			}
             // 拼接结果
			builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
					"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
					.append("<div id='created'>").append(timestamp).append("</div>")
					.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
					.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
			if (message != null) {
				builder.append("<div>").append(htmlEscape(message)).append("</div>");
			}
			if (trace != null) {
				builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
			}
			builder.append("</body></html>");
			response.getWriter().append(builder.toString());
		}

		private String htmlEscape(Object input) {
			return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
		}

		private String getMessage(Map<String, ?> model) {
			Object path = model.get("path");
			String message = "Cannot render error page for request [" + path + "]";
			if (model.get("message") != null) {
				message += " and exception [" + model.get("message") + "]";
			}
			message += " as the response has already been committed.";
			message += " As a result, the response may have the wrong status code.";
			return message;
		}

         // 获取内容类型
		@Override
		public String getContentType() {
			return "text/html";
		}
	}

三、异常处理流程

doDispatch()

在该方法中,主体都被try/catch包围

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
             // 定义1个异常信息
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = HttpMethod.GET.matches(method);
				if (isGet || HttpMethod.HEAD.matches(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
                 // 当执行目标方法出现异常,会立刻被catch
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
                  // 获取异常信息
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
             // 处理结果 这里会将异常信息dispatchException传入,根据此来判断是否存在异常
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

processDispatchResult()

根据异常信息判断是否存在出现了异常

  1. 存在则跳转到异常页面,返回一个ModelAndView,就和正常视图一样,不过从外面看就像异常处理,从源码看就是捕获到,返回到异常视图
  2. 不存在异常则顺利执行
	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {

		boolean errorView = false;

         // 判断是否存在异常信息
		if (exception != null) {
			if (exception instanceof ModelAndViewDefiningException) {
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			}
			else {
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                  // 处理异常
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			}
		}

		// Did the handler return a view to render?
		if (mv != null && !mv.wasCleared()) {
			render(mv, request, response);
			if (errorView) {
				WebUtils.clearErrorRequestAttributes(request);
			}
		}
		else {
			if (logger.isTraceEnabled()) {
				logger.trace("No view rendering, null ModelAndView returned.");
			}
		}

		if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
			// Concurrent handling started during a forward
			return;
		}

		if (mappedHandler != null) {
			// Exception (if any) is already handled..
			mappedHandler.triggerAfterCompletion(request, response, null);
		}
	}

processHandlerException()

通过处理异常解析器,遍历处理异常

	@Nullable
	protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
			@Nullable Object handler, Exception ex) throws Exception {

		// Success and error responses may use different content types
		request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

		// Check registered HandlerExceptionResolvers...
		ModelAndView exMv = null;
         // 处理器异常解析器
		if (this.handlerExceptionResolvers != null) {
             // 遍历所有的异常解析器
			for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
                  // 处理异常信息
				exMv = resolver.resolveException(request, response, handler, ex);
				if (exMv != null) {
					break;
				}
			}
		}
		if (exMv != null) {
			if (exMv.isEmpty()) {
				request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
				return null;
			}
			// We might still need view name translation for a plain error model...
			if (!exMv.hasView()) {
				String defaultViewName = getDefaultViewName(request);
				if (defaultViewName != null) {
					exMv.setViewName(defaultViewName);
				}
			}
			if (logger.isTraceEnabled()) {
				logger.trace("Using resolved error view: " + exMv, ex);
			}
			else if (logger.isDebugEnabled()) {
				logger.debug("Using resolved error view: " + exMv);
			}
			WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
			return exMv;
		}
         
         // 默认没有任何人可以处理异常 异常会被抛出
		throw ex;
	}

HandlerExceptionResolver

public interface HandlerExceptionResolver {

	@Nullable
	ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}

第一个异常解析器

  1. 将异常信息存储到request域中
  2. 该解析器永远返回一个null
  3. 因为异常处理是捕获到异常后,将请求转发到/error,但是异常信息需要携带过去,所以在这里只是设置了一个属性
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {

	private static final String ERROR_INTERNAL_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR";

	@Override
	public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
			Exception ex) {
		storeErrorAttributes(request, ex);
         // 直接返回空
		return null;
	}

	private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
         // 将异常信息存储到request域中
		request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);
	}
    
    // 省略部分代码...   
}

第二个异常解析器组合

  1. 遍历所有的处理异常解析器
  2. 具体内容放到下一章
public class HandlerExceptionResolverComposite implements HandlerExceptionResolver, Ordered {

	@Nullable
	private List<HandlerExceptionResolver> resolvers;

	@Override
	@Nullable
	public ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

		if (this.resolvers != null) {
             // 遍历所有的异常解析器
			for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
				ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
				if (mav != null) {
					return mav;
				}
			}
		}
		return null;
	}
}

四、自定义异常处理逻辑

捕获到异常,可以添加自定义代码逻辑,并且返回结果可以是ModelAndViewJSON

@ExceptionHandler

标注捕获的异常

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
	
    // 异常类型
	Class<? extends Throwable>[] value() default {};
}

@ControllerAdvice

标注异常处理类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
	
    // 异常类
	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<?>[] assignableTypes() default {};

	Class<? extends Annotation>[] annotations() default {};
}

ExceptionHandlerExceptionResolver

该类负责处理 @ExceptionHandler

@Override
@Nullable
// 返回结果也是一个ModelAndView
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
	
    // 判断是否需要解析异常
	if (shouldApplyTo(request, handler)) {
		prepareResponse(ex, response);
         // 解析异常
		ModelAndView result = doResolveException(request, response, handler, ex);
		if (result != null) {
			// Print debug message when warn logger is not enabled.
			if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
				logger.debug("Resolved [" + ex + "]" + (result.isEmpty() ? "" : " to " + result));
			}
			// Explicitly configured warn logger in logException method.
			logException(ex, request);
		}
		return result;
	}
	else {
		return null;
	}
}

@ResponseStatus

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseStatus {
	
    // 异常状态码
	@AliasFor("code")
	HttpStatus value() default HttpStatus.INTERNAL_SERVER_ERROR;

	@AliasFor("value")
	HttpStatus code() default HttpStatus.INTERNAL_SERVER_ERROR;

    // 异常原因
	String reason() default "";
}

ResponseStatusExceptionResolver

把responsestatus注解的信息底层调用,response.sendError(statusCode, resolvedReason);,tomcat发送的/error

DefaultHandlerExceptionResolver

Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常

response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());

自定义异常解析器

  1. 创建类实现HandlerExceptionResolver接口
  2. 可以通过@Order来修改优先级
  3. 通过response.sendError()来将请求转给Controller

五、异常请求 /error

当上面的异常解析器没有办法处理异常,那么这个异常不能没人处理吧,系统重定向到默认请求/error,该控制为BasicErrorController,在自动配置中注册组件

BasicErrorController

上面已经解释了,在异常处理自动配置类里面会注入该Bean,该Bean也是一个Controller,所以当之前的代码出现任何异常,都会跳转到/error请求,由该Controller负责处理

它相当于是最底层的一个异常处理,我们可以自定义异常处理,来处理,但是如果没有处理,则最终都会到这里

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") // 会调用配置的server.error.path,没有则使用默认的/error
public class BasicErrorController extends AbstractErrorController {}

浏览器请求

通过浏览器请求,当出现异常时,会跳转到异常页面,那么也就是errorHtml()

errorHtml()

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // 这里规定了响应的媒体类型,必须是text/html,也就是针对浏览器的响应
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    // 获取错误状态码
	HttpStatus status = getStatus(request);
    // 获取异常信息  包括:状态码、错误路径、时间戳等
	Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    // 设置响应状态码
	response.setStatus(status.value());
    // 处理错误视图
	ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    // 存在自定义异常页面直接跳转
    // 跳转到error,也就是自带的白页  error是在自动配置类里面设置的
	return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

resolveErrorView()

遍历视图解析器,默认只有1个视图解析器DefaultErrorViewResolver

	protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
			Map<String, Object> model) {
         // 遍历错误试图解析器 默认只有1个 也就是在ErrorMvcAutoConfiguration中注册的DefaultErrorViewResolver
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}

DefaultErrorViewResolver

  1. 尝试解析具体状态的异常页面,例如:404.html,500.html等,获取到直接返回
  2. 尝试从静态路径中获取异常页面,获取到直接返回
  3. 尝试匹配4xx.html,5xx.html等异常页面
	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
         // 解析 会匹配具体状态码的异常页面  或者  从静态路径中查找
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
             // 如果上面匹配到了直接返回,否则匹配类似于 4xx.html,5xx.html的页面
			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
		}
		return modelAndView;
	}

    // 解析试图
	private ModelAndView resolve(String viewName, Map<String, Object> model) {
         // 错误视图名为 error/ + 状态码,也就对应着我们的错误页面是以状态码命名
		String errorViewName = "error/" + viewName;
         // 通过模板引擎判断是否存在异常页面
         // 如果使用的400.html、500.html等直接返回
         // 如果使用的是4xx.html、5xxx.html等返回null
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {// 不为空 代表具体的异常页面存在 直接返回
			return new ModelAndView(errorViewName, model);
		}
         // 匹配不到具体的异常页面,则尝试从静态路径中查找
		return resolveResource(errorViewName, model);
	}

    // 解析来源
	private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
         // 遍历所有的静态路径
		for (String location : this.resources.getStaticLocations()) {
			try {
				Resource resource = this.applicationContext.getResource(location);
                  // 视图名拼接上.html  也就变成了最终的400.html
				resource = resource.createRelative(viewName + ".html");
                  // 判断异常页面是否存在
				if (resource.exists()) {
                      // 存在则创建试图返回
					return new ModelAndView(new HtmlResourceView(resource), model);
				}
			}
			catch (Exception ex) {
			}
		}
		return null;
	}

PostMan请求

当通过客户端请求,那么当出现异常的时候,返回的就是JSON串

error()

	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        // 状态码
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
         // 异常信息
		Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
         // 响应体
		return new ResponseEntity<>(body, status);
	}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为人师表好少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值