自定义注解及应用场景

自定义注解及应用场景

——深圳蜗牛学苑

课程目标

  1. 理解自定义注解概念
  2. 掌握自定义注解语法
  3. 自定义注解场景中的使用
  4. 熟练掌握自定义注解+AOP的使用方式
一、自定义注解

我们回顾一下方法的重写,会发现有@Override,我们把它称为重写的注解。那么注解到底什么呢?我们能不能自定义注解。

1、概念:

注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包都可以用注解来修饰。注解对于它所修饰的代码并没有直接的影响。

* 注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举类似。
* 注解用来修饰,类、方法、变量、参数、包。
* 注解不会对所修饰的代码产生直接的影响。

既然注解是一种数据类型,那我们自然可以去自定义的使用它

2、自定义注解的语法

​ 简单来说自定义注解分为三步:

  • 定义注解——相当于定义标记;

  • 配置注解——把标记打在需要用到的程序代码中;

  • 解析注解——在编译期或运行时检测到标记,并进行特殊操作。

    基本语法:

​ 注解在Java中,与类、接口、枚举类似,因此其声明语法基本一致,只是所使用的关键字有所不同@interface

在底层实现上,所有定义的注解都会自动继承java.lang.annotation.Annotation接口

public @interface TestAnnotation {
    
}

​ 根据我们在自定义类的经验,在类的实现部分无非就是书写构造、属性或方法。但是,在自定义注解中,其实现部分只能定义一个东西:注解类型元素(annotation type element)

/**
 * 自定义注解 @interface
 */
public @interface TestAnnotation {
    //定义注解类型元素
    public String name();
    int age() default 18;  //default代表带有默认值为18
    int[] array();
}

注解定义就只包括了上面的两部分内容:

  • 1、注解的名字
  • 2、注解包含的类型元素

定义注解类型元素时需要注意如下几点:

  • 访问修饰符必须为public,不写默认为public;
  • 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组;
  • 该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(后面使用会带来便利操作);
  • ()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法;
  • default代表默认值,值必须和第2点定义的类型一致;
  • 如果没有默认值,代表后续使用注解时必须给该类型元素赋值。

除了定义好注解,在哪用?怎么用?注意些什么?说这些之前,我们还需要补充一个概念:元注解

3、元注解

​ (1) @Target:是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的

public enum ElementType {
    /** 类,接口(包括注解类型)或枚举的声明 */
    TYPE,

    /** 属性的声明 */
    FIELD,

    /** 方法的声明 */
    METHOD,

    /** 方法形式参数声明 */
    PARAMETER,

    /** 构造方法的声明 */
    CONSTRUCTOR,

    /** 局部变量声明 */
    LOCAL_VARIABLE,

    /** 注解类型声明 */
    ANNOTATION_TYPE,

    /** 包的声明 */
    PACKAGE
}
//@CherryAnnotation被限定只能使用在类、接口或方法上面
@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface TestAnnotation {
    String name();
    int age() default 18;
    int[] array();
}

(2)@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。
注解的生命周期有三个阶段:1、Java源文件阶段;2、编译到class文件阶段->默认;3、运行期阶段。

​ 同样使用了RetentionPolicy枚举类型定义了三个阶段:

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     * (注解将被编译器忽略掉)
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     * (注解将被编译器记录在class文件中,但在运行时不会被虚拟机保留,这是一个默认的行为)
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     * (注解将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到)
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}
  • 如果一个注解被定义为RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读Java文件的人看到;
  • 如果一个注解被定义为RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器可以在编译时根据注解做一些处理动作,但是运行时JVM(Java虚拟机)会忽略它,我们在运行期也不能读取到;
  • 如果一个注解被定义为RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段,我们可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME;
  • 在默认的情况下,自定义注解是使用的RetentionPolicy.CLASS。

(3)@Documented,是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中。

(4)@Inherited注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解。@Inherited注解只对那些@Target被定义 为ElementType.TYPE的自定义注解起作用。

说完了元注解,我们可以看到元注解其实就是用来配置我们的自定义注解,让自定义注解更加精确的使用

8、自定义注解补充

@Retention(RetentionPolicy.RUNTIME) //规定运行时
@Target(value = {ElementType.METHOD}) //规定在方法上使用
@Documented//JavaDoc文档
public @interface CherryAnnotation {
    String name();
    int age() default 18;
    int[] score();
}

简单分析下:

  • CherryAnnotation的@Target定义为ElementType.METHOD,那么它书写的位置应该在方法定义的上方
  • 由于我们在CherryAnnotation中定义的有注解类型元素,而且有些元素是没有默认值的,这要求我们在使用的时候必须在标记名后面打上(),并且在()内以“元素名=元素值“的形式挨个填上所有没有默认值的注解类型元素(有默认值的也可以填上重新赋值),中间用“,”号分割;

9、使用注解

public class Student {
    @CherryAnnotation(name = "cherry-peng",age = 23,score = {99,66,77})
    public void study(int times){
        for(int i = 0; i < times; i++){
            System.out.println("Good Good Study, Day Day Up!");
        }
    }
}
4、自定义注解的value成员:

(1)如果注解本身没有注解类型元素,那么在使用注解的时候可以省略(),直接写为:@注解名,它和标准语法@注解名()等效!

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface FirstAnnotation {
}
//等效于@FirstAnnotation()
@FirstAnnotation
public class JavaBean{
	//省略实现部分
}

(2)如果注解本本身只有一个注解类型元素,而且命名为value,那么在使用注解的时候可以直接使用:@注解名(注解值),其等效于:@注解名(value = 注解值)

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface SecondAnnotation {
	String value();
}
//等效于@ SecondAnnotation(value = "this is second annotation")
@SecondAnnotation("this is annotation")
public class JavaBean{
	//省略实现部分
}

(3)如果注解中的某个注解类型元素是一个数组类型,在使用时又出现只需要填入一个值的情况,那么在使用注解时可以直接写为:@注解名(类型名 = 类型值),它和标准写法:@注解名(类型名 = {类型值})等效!

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface ThirdAnnotation {
	String[] name();
}
//等效于@ ThirdAnnotation(name = {"this is third annotation"})
@ThirdAnnotation(name = "this is third annotation")
public class JavaBean{
	//省略实现部分
}
二、自定义注解应用场景

1、自定义注解的使用场景很多,我们在造轮子写框架的过程经常会使用到。在我们平时的项目场景中,也可以随处可以看到自定义注解可以发挥作用的场景,例如:

​ 1)全局日志的打印

​ 2)用户权限的校验

​ 3)登录的校验

​ 4)数据脱敏

等等场景。

2、而在我们后面的微服务阶段,自定义注解也可以用来测试和熔断服务。

我们也在这里列举出几个业务场景,来模拟一下自定义注解在我们实际项目场景中的使用:

1、业务场景1:权限校验

自定义注解经常和springAOP一起配合使用,能让aop的增强更加轻松和优雅,消除冗余代码。例如日志,登录校验,权限校验等。

(1)首先我们定义了一个缓存类CacheManager来模拟保存两个用户zhangsan和lisi,并给予他们一定的角色

package com.woniu.test4;

import java.util.*;

public class CacheManager {
    //初始化一个map集合来存储用户名和该用户所拥有的的角色
    public static Map<String, List<String>> USER_ROLES = new HashMap<>();
    static {
        //初始化一个list集合来存储角色
        List<String> roles = new ArrayList<>();
        roles.add("admin");
        roles.add("user");

        List<String> roles2 = new ArrayList<>();
        roles2.add("user");

        USER_ROLES.put("zhangsan",roles);
        USER_ROLES.put("lisi",roles2);
    }
}

(2)controller 层中设计删除,新增,查询方法,我们将要实现使得这些方法拥有一些角色限定,如果没有该角色的用户,是无法执行该方法。

@RestController
public class UserController2 {
    @GetMapping("/deleteUser")
    public String deleteUser(){
        return "执行了删除用户的方法!";
    }

    @GetMapping("/addUser")
    public String addUser(){
        return "执行了新增用户的方法!";
    }

    @GetMapping("/list")
    public String list(){
        return "执行了查询用户的方法!";
    }
}

(3)首先定义一个 自定义注解HasRole,并且保存一个成员value来存储角色名称

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HasRole {
    //存储角色名称
    String value();
}

(3)在controller上面增加自定义注解,并且表名该方法所具有的权限名称

@RestController
public class UserController2 {
    @HasRole("admin")
    @GetMapping("/deleteUser")
    public String deleteUser(){
        return "执行了删除用户的方法!";
    }

    @HasRole("user")
    @GetMapping("/addUser")
    public String addUser(){
        return "执行了新增用户的方法!";
    }

    @GetMapping("/list")
    public String list(){
        return "执行了查询用户的方法!";
    }
}

(4)通过springAOP 来定义一个切面类,并且使用前置通知,获取用户名称从而来判定该用户是否具有响应的权限

@Aspect //定义切面类
@Component
public class AppConfig {
    //定义切点
    @Pointcut("@annotation(com.woniu.test4.HasRole)")
    public void pointcut(){}

    //前置通知
    @Before("pointcut()")
    public void before(JoinPoint joinPoint){
        //通过RequestContextHolder来获得ServletRequestAttributes
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //通过ServletRequestAttributes来获得请求对象request
        HttpServletRequest request = requestAttributes.getRequest();
        String username = request.getParameter("username");

        //获取当前角色的权限集合
        List<String> strings = CacheManager.USER_ROLES.get(username);
        //获取当前方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //反射获取当前方法对象
        Method method = signature.getMethod();
        //获取当前方法头顶的注解
        HasRole hasRole = method.getDeclaredAnnotation(HasRole.class);

        //判断是否为空
        if(hasRole!=null&&(strings==null || !strings.contains(hasRole.value()))){
            throw new RuntimeException("用户没有访问权限!");
        }

    }

}

(5)测试

当我们使用lisi用户时,李四用户只有user角色,所以是不能执行delete方法,报出异常

image-20230417173349841

image-20230417173432683

如果,我们传入的用户名为zhangsan,则具有admin权限,页面正常展示。

这样我们就用自定义注解来实现了角色权限的简单校验,当然,直接报出异常的方式不是很友好,我们只需要写出一个全局异常处理就能友好的解决了。

2、业务场景2:敏感数据的脱敏

在很多项目中,存储了大量的用户敏感信息,而这些信息如果直接显示在网页上,是具有很大的安全问题,所以我们需要在数据向网页展示时,先经过脱敏的处理。

所谓脱敏其实就是敏感数据的半隐藏,例如身份证,手机号,用户名等等。

image-20230417173918032

(1)首先我们准备好一个实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    // 用户手机号
    private String userPhone;
    // 用户身份证号
    private String id_card;
}

(2)在controller层初始化一个对象,并且将信息返回

@RestController
public class UserController {
    @GetMapping("/user2")
    public User getUserInfo(){
        return new User("13239687323","6229923199910010014");
    }
}

此时测试,数据是会完整的展现在页面上

image-20230417174435763

那么此处,我们同样可以使用springAOP+自定义注解来获取到属性中的数据,并且手动脱敏,这次我们可以尝试一下不一样的写法。

2.1 认识 @JsonSerialize注解的使用

在开发中,我们将对象序列化为JSON传输给前端,有时候我们的某个或者某些字段需要特殊处理。

比如我们有一个日期字段,我们希望当日期为NULL时给前端不返回NULL而返回为未完成等信息,或者我们有一些状态字段,我们需要给前端返回状态对应的信息,此时我们就需要自定义字段的序列化。这就是@JsonSerialize的用处

第一步,我们先写一个指定的处理类

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.util.Date;

//该类需要继承JsonSerializer
public class DateJsonSerialize extends JsonSerializer<Date> {
 		
    //重写serialize方法
    @Override
    public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        //自定义处理方式
        jsonGenerator.writeString("未填写");
    }
}

第二步,在此字段上加上@JsonSerialize注解,并且使用nullsUsing属性,即当此字段为NULL时就使用我们自定义的解析类序列化此字段(注意当此字段不为NULL时不执行此配置)

import java.util.Date;

public class Student {
    private String stuName;
    private Integer stuAge;
    //当该属性为null时,会触发json的序列化
    @JsonSerialize(nullsUsing = DateJsonSerialize.class)
    private Date birthday;
}

测试

image-20230417180206600

既然JsonSerialize可以帮助我们重新序列化属性数据,我们正好可以使用这个机制,配合自定义注解,来完成我们数据脱敏

2.2 重新封装一个SensitiveJsonSerializer类来处理user的数据脱敏
public class SensitiveJsonSerializer extends JsonSerializer<String>  {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        //手动脱敏
        String substring = value.substring(value.length() - 4);
        value = "***"+ substring;
        gen.writeString(value);
    }
2.3 封装出自定义注解
//自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside //这个注解用来标记Jackson复合注解,当你使用多个Jackson注解组合成一个自定义注解时会用到它
@JsonSerialize(using = SensitiveJsonSerializer.class) //指定使用自定义的序列化器
public @interface Sensitive {
}

@JsonSerialize来指明我们自己的序列化器

当我们自定义的注解要和Jackson注解组合时,需要标注@JacksonAnnotationsInside,来表明这是一个符合注解

测试

image-20230417182017563

2.4 优化代码

现在已经实现出了我们的效果,但是我们的代码仍可以做进一步的优化,因为现在数据脱敏的格式比较单一,而且如果当需要脱敏的规则改变或者需要脱敏的属性增加时,我们仍然需要回去修改源代码,不如我们用一个枚举enum来专门存储所有的脱敏规则

2.4.1 新建一个enum类SensitiveStrategy

在这个枚举中,我们需要定义出各种类型属性的脱敏规则

思考—enum中,成员的类型应该是什么?

既然每个成员除了脱敏规则不一样,脱敏的流程都是一样的,那我们就可以设计成jdk8中Function<T,R>作为成员类型,这样我们就可以使用Fuction中的apply方法来消除掉重复的调用脱敏方法的代码。

public enum SensitiveStrategy {
    //常量成员
    //正则表达式来存储脱敏规则,格式规定为:132***7323
    USERNAME(s -> s.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
    ID_CARD(s -> s.replaceAll("(\\d{3})\\d{13}(\\w{2})", "$1****$2")),
    PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
    ADDRESS(s -> s.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****"));
    //构造方法和get/set
    private Function<String, String> desensitizes;

    SensitiveStrategy(Function<String, String> desensitizes) {
        this.desensitizes = desensitizes;
    }

    public Function<String, String> getDesensitizes() {
        return desensitizes;
    }

    public void setDesensitizes(Function<String, String> desensitizes) {
        this.desensitizes = desensitizes;
    }
}

提示:正则表达式,大家只需要看懂即可,不需要记忆~

2.4.2 优化SensitiveJsonSerializer类

既然脱敏的代码封装到枚举中,那在SensitiveJsonSerializer类的serialize序列化方法中,只需要调用一下function的apply方法即可

public class SensitiveJsonSerializer extends JsonSerializer<String>  {
    //提供枚举成员属性
    private SensitiveStrategy strategy;
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        //调用enum中function的apply方法,即 脱敏方法,传入原数据
        gen.writeString(strategy.getDesensitizes().apply(value));
    }
}
2.4.3 优化自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside //这个注解用来标记Jackson复合注解,当你使用多个Jackson注解组合成一个自定义注解时会用到它
@JsonSerialize(using = SensitiveJsonSerializer.class) //指定使用自定义的序列化器
public @interface Sensitive {
    SensitiveStrategy value();   //该自定义注解需要的参数   strategy-参数名称    SensitiveStrategy-参数类型
}

在自定义注解中加入enum的成员属性

原因:之前我们是统一将数据脱敏程 ***XXX的格式,这样显然和真实场景不同。加入strategy成员,来标注出该属性需要走哪个类型的脱敏,从而让枚举自动识别出来并且进行脱敏

2.4.4 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    // 用户手机号
    @Sensitive(SensitiveStrategy.PHONE)
    private String userPhone;
    // 用户身份证号
    @Sensitive(SensitiveStrategy.ID_CARD)
    private String id_card;
}

测试,发现报出NullPointer异常,debug一下,不难发现如下问题:

image-20230418103716480

我们需要获取到属性头顶的注解对象,并且赋值到这个属性中,才可以正确执行

//ContextualSerializer是 Jackson 提供的另一个序列化相关的接口,它的作用是通过字段已知的上下文信息定制JsonSerializer,只需要实现createContextual方法即可
public class SensitiveJsonSerializer extends JsonSerializer<String> implements ContextualSerializer{
    private SensitiveStrategy strategy;
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        //调用fuction apply方法,传入数据
        gen.writeString(strategy.getDesensitizes().apply(value));
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        // 通过反射获取注解对象
        Sensitive annotation = property.getAnnotation(Sensitive.class);
        if (Objects.nonNull(annotation)) {
            this.strategy = annotation.value();
            return this;
        }
        //如果为null,则说明没有添加注解对象,则返回默认的序列化的对象即可
        return prov.findValueSerializer(property.getType(), property);
    }
}

测试

image-20230418120830440

3、业务场景3:全局日志
3.1 封装记录日志实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MaterialEamLog {
    private String params; //参数
    private String method; //方法
    private String result; //返回结果
    private String url; //URL地址
    private String ip; // ip地址
    private String time; //执行时间
}
3.2 定义serviceImpl层(模拟入库)
@Service
public class IMaterialEamLogService {
    public boolean insert(MaterialEamLog materialEamLog){
        System.out.println(materialEamLog);
        System.out.println("执行新增方法,将日志入库");
        return true;
    }
}
3.3 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RequiredLog {
}
3.4 定义AOP切面类
@Aspect
@Component
@Slf4j
public class SysLogAspect {
    @Autowired
    private IMaterialEamLogService materialEamLogService;

    /**
     * @Pointcut 注解用于描述或定义一个切入点
     *  切入点的定义需要遵循spring中指定的表达式规范
     */
    @Pointcut("@annotation(com.woniu.test6.RequiredLog)")
    public void logPointCut() {}

    /**
     * @Around 注解描述的方法为一个环绕通知方法,
     * 在此方法中可以添加扩展业务逻辑,可以调用下一个
    切面对象或目标方法
     * @param jp 连接点(此连接点只应用@Around描述的方法)
     * @return
     * @throws Throwable
     */
    @Around("logPointCut()")
    public Object aroundAdvice(ProceedingJoinPoint jp)
            throws Throwable{
        long start=System.currentTimeMillis();
        log.info("start:"+start);
        try {
            //调用下一个切面或目标方法
            result = jp.proceed();
        }finally {
            long end=System.currentTimeMillis();
            log.info("end:"+end);
            //记录日志(用户行为信息)
            saveLog(jp,result,String.valueOf(end-start));
        }
        return result;
    }


    //日志记录
    private void saveLog(ProceedingJoinPoint jp,Object result,String time) {
        //1.获取用户行为日志(ip,username,operation,method,params,time,createdTime)
        //获取类的字节码对象,通过字节码对象获取方法信息
        Class<?> targetCls=jp.getTarget().getClass();
        //获取方法签名(通过此签名获取目标方法信息)
        MethodSignature ms=(MethodSignature)jp.getSignature();
        //获取目标方法名(目标类型+方法名)
        String targetClsName=targetCls.getName();
        String targetObjectMethodName=targetClsName+"."+ms.getName();
        //获取请求参数
        String targetMethodParams= Arrays.toString(jp.getArgs());
        String url = "";
        String ipAddr = "";
        if(RequestContextHolder.getRequestAttributes() != null){
            //获取请求url
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            url = request.getRequestURL().toString();
            ipAddr = request.getRemoteHost();
        }
        //2.封装用户行为日志(SysLog)
        //插入日志--start
        MaterialEamLog log = new MaterialEamLog();
        log.setParams(targetMethodParams);
        log.setMethod(targetObjectMethodName);
        //获取注解中的参数
        log.setResult(String.valueOf(result));
        log.setUrl(url);
        log.setIp(ipAddr);
        log.setTime(time);
        //3.调用业务层对象方法(saveObject)将日志写入到数据库
        materialEamLogService.insert(log);
    }
3.5 测试

image-20230420152925336

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值