分类
Spring Data Spring Persistence

spring data jpa优雅的实现软删除

软删除,即通过在标识数据表中的某个字段来达到看似删除的目的,由于软删除对数据库的侵入比较小,所以在删除时可以完美的规避数据约束的问题。

我曾在4年前写过一篇spring boot实现软删除,在该文章中给出了实现软删除的一种方式。本文将使用一种更优雅的方式来实现它。

项目源码

本文项目源码https://github.com/codedemo-club/spring-data-jpa-soft-delete

接口

首先我们需要建立一个接口,来标识哪些实体在查询时是需要软删除的:

public interface SoftDelete {
  Boolean getDeleted();
}

然后实体实现这一接口:

@Entity
public class Foo implements SoftDelete {
  private Boolean deleted = false;

  @Override
  public Boolean getDeleted() {
    return deleted;
  }

  // 设置为私有
  private void setDeleted(Boolean deleted) {
    this.deleted = deleted;
  }

注意:在此省略了其它的属性,其它的属性请参考源代码

@Entity
public class Bar implements SoftDelete {
  private Boolean deleted = false;

  @Override
  public Boolean getDeleted() {
    return deleted;
  }

  // 设置为私有
  private void setDeleted(Boolean deleted) {
    this.deleted = deleted;
  }

设置标识

接下来,使用@SQLDelete注解来设置删除语句,虽然此操作我们也可以通过相应的方法统一设置,但使用@SQLDelete会使软删除更加的灵活,特别是当我们解决一些数据唯一性的时候。

@Entity
@SQLDelete(sql = "update `bar` set deleted = 1 where id = ?")
public class Bar implements SoftDelete {
@Entity
@SQLDelete(sql = "update `foo` set deleted = 1 where id = ?")
public class Foo implements SoftDelete {

自定义软删除实现类

通过自定义的软删除实现类来达到在查询时加入软删除的目的。

package club.codedemo.springdatajpasoftdelete;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.lang.Nullable;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 软删除实现类
 * https://www.codedemo.club/spring-data-jpa-soft-delete/
 * https://developer.aliyun.com/article/465404
 * https://stackoverflow.com/questions/36721601/spring-boot-how-to-declare-a-custom-repository-factory-bean
 */
@Transactional(
    readOnly = true
)
public class SoftDeleteRepositoryImpl<T, ID> extends SimpleJpaRepository<T, ID>  implements PagingAndSortingRepository<T, ID>,
    CrudRepository<T, ID>,
    JpaSpecificationExecutor<T> {
  private final Logger logger = LoggerFactory.getLogger(this.getClass());

  public SoftDeleteRepositoryImpl(Class<T> domainClass, EntityManager em) {
    this(JpaEntityInformationSupport.getEntityInformation(domainClass, em), em);
  }

  public SoftDeleteRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
    super(entityInformation, entityManager);
  }

  @Override
  public boolean existsById(ID id) {
    return this.findById(id).isPresent();
  }

  @Override
  public List<T> findAll() {
    return this.findAll(this.andDeleteFalseSpecification(null));
  }

  @Override
  public Page<T> findAll(Pageable pageable) {
    return this.findAll(this.andDeleteFalseSpecification(null), pageable);
  }

  @Override
  public List<T> findAll(@Nullable Specification<T> specification) {
    return super.findAll(this.andDeleteFalseSpecification(specification));
  }

  @Override
  public Page<T> findAll(@Nullable Specification<T> specification, Pageable pageable) {
    return super.findAll(this.andDeleteFalseSpecification(specification), pageable);
  }

  @Override
  public List<T> findAll(@Nullable Specification<T> specification, Sort sort) {
    return super.findAll(this.andDeleteFalseSpecification(specification), sort);
  }

  @Override
  public Optional<T> findById(ID id) {
    Optional<T> entityOptional = super.findById(id);
    if (entityOptional.isPresent()) {
      T entity = entityOptional.get();
      if (entity instanceof SoftDelete) {
        if (!((SoftDelete) entity).getDeleted())
          return entityOptional;
      } else {
        this.logger.warn("未实现SoftDeleteEntity的实体" + entity.getClass() + "使用了软删除功能。请检查JpaRepositoryFactoryBean配置");
      }
    }
    return Optional.empty();
  }

  @Override
  public List<T> findAllById(Iterable<ID> ids) {
    return super.findAllById(ids).stream().filter(entity -> {
      if (entity instanceof SoftDelete) {
        return !((SoftDelete) entity).getDeleted();
      } else {
        this.logger.warn("未实现SoftDeleteEntity的实体" + entity.getClass() + "使用了软删除功能。请检查JpaRepositoryFactoryBean配置");
      }
      return false;
    }).collect(Collectors.toList());
  }

  @Override
  public long count() {
    return this.count(this.andDeleteFalseSpecification(null));
  }

  @Override
  public long count(@Nullable Specification<T> specification) {
    return super.count(this.andDeleteFalseSpecification(specification));
  }

  /**
   * 添加软查询条件
   *
   * @param specification 综合查询条件
   */
  private Specification<T> andDeleteFalseSpecification(Specification<T> specification) {
    Specification<T> deleteIsFalse = (root, criteriaQuery, criteriaBuilder) -> criteriaBuilder.equal(root.get("deleted").as(Boolean.class), false);
    if (specification == null) {
      specification = deleteIsFalse;
    } else {
      specification = specification.and(deleteIsFalse);
    }
    return specification;
  }
}

自定义JpaRepositoryFactoryBean

通过自定义JpaRepositoryFactoryBean达到:实现了SoftDelete的实体在查询时使用SoftDeleteRepositoryImpl,而未实现SoftDelete的实体在查询时使用原SimpleJpaRepository


/**
 * 自定义软件删除工厂
 * @param <R> 仓库
 * @param <T> 实体
 */
public class SoftDeleteRepositoryFactoryBean <R extends JpaRepository<T, Serializable>, T> extends JpaRepositoryFactoryBean<R, T, Serializable> {
  public SoftDeleteRepositoryFactoryBean(Class<? extends R> repositoryInterface) {
    super(repositoryInterface);
  }

  @Override
  protected RepositoryFactorySupport createRepositoryFactory(final EntityManager entityManager) {
    return new JpaRepositoryFactory(entityManager) {
      protected SimpleJpaRepository<T, Serializable> getTargetRepository(
          RepositoryInformation information, EntityManager entityManager) {
        Class<T> domainClass = (Class<T>) information.getDomainType();
        if(SoftDelete.class.isAssignableFrom(domainClass)) {
          return new SoftDeleteRepositoryImpl(domainClass, entityManager);
        } else {
          return new SimpleJpaRepository(domainClass, entityManager);
        }
      }

      @Override
      protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
        return metadata.getDomainType().isAssignableFrom(SoftDelete.class) ? SoftDeleteRepositoryImpl.class : SimpleJpaRepository.class;
      }
    };
  }
}

注册Bean

最后我们注册刚刚创建的工厂Bean,以使其生效:

@EnableJpaRepositories(value = "club.codedemo.springdatajpasoftdelete",
        repositoryFactoryBeanClass = SoftDeleteRepositoryFactoryBean.class)
public class SpringDataJpaSoftDeleteApplication {

此时,将我们在进行查询操作时,如果实体实现了SoftDelete,则会使用我们自定义的SoftDeleteRepositoryImpl,而如果没有实现SoftDelete,则仍然使用原查询。

SoftDeleteRepositoryImpl方法中,我们重写了所有的查询方法,并在查询中加入了deletedfasle的查询条件。

测试

此时,重新启动系统,点击删除操作后。数据仍然存在于数据中,而且关联查询也不会发生任何错误。如果我们希望在@OneToMany注解中也使用软删除生效(即不返回已删除的关联实体),则仅仅需要在相应的字段加入@Where(clause = "deleted = false")注解即可。

分类
Spring Data Spring Persistence

Spring Data JPA设置字段默认值的两种方法

1. 简介

本文将介绍如何在JPA中定义数据列的默认值。
通常有两种方式来实现默认值的定义:在实体类中设置默认值以及使用JPA注解直接操作数据表。

2. 实体类

第一种方法是直接在实体中定义:

@Entity
public class User {
    @Id
    private Long id;
    private String firstName = "Code demo";
    private Integer age = 25;
    private Boolean locked = false;
}

此时,当我们使用new关键字来实例化实体时,实体的各个字段将以默认值填充:

@Test
void saveUser_shouldSaveWithDefaultFieldValues() {
    User user = new User();
    user = userRepository.save(user);

    assertEquals(user.getName(), "Code demo");
    assertEquals(user.getAge(), 25);
    assertFalse(user.getLocked());
}

使用方法设置的默认值并未在数据表中定义中生效,查看相应的SQL语句如下:

create table user
(
    id     bigint not null constraint user_pkey primary key,
    name   varchar(255),
    age    integer,
    locked boolean
);

由以上SQL可知,该方法实际上并没有为数据表中的字段设置默认值,所以如果我们在代码中手动的将某个字段设置为null,并不会引发任何异常:

@Test
void saveUser_shouldSaveWithNullName() {
    User user = new User();
    user.setName(null);
    user.setAge(null);
    user.setLocked(null);
    user = userRepository.save(user);

    assertNull(user.getName());
    assertNull(user.getAge());
    assertNull(user.getLocked());
}

3. 定义数据表

在JPA中,我们可以使用@Column注解的columnDefinition参数来定义数据表字段的默认值:

@Entity
public class User {
    @Id
    Long id;

    @Column(columnDefinition = "varchar(255) default 'Code demo'")
    private String name;

    @Column(columnDefinition = "integer default 25")
    private Integer age;

    @Column(columnDefinition = "boolean default false")
    private Boolean locked;
}

使用上述方法定义后,JPA将对应生成以下SQL语句:

create table user
(
    id     bigint not null constraint user_pkey primary key,
    name   varchar(255) default 'Code demo',
    age    integer      default 35,
    locked boolean      default false
);

此时将未设置某个字段的值时,该字段将使用默认值填充:

@Test
void saveUser_shouldSaveWithDefaultSqlValues() {
    User user = new User();
    user = userRepository.save(user);

    assertEquals(user.getName(), "John Snow");
    assertEquals(user.getAge(), 25);
    assertFalse(user.getLocked());
}

值得注意的是:使用该方案在新建数据时,我们无法将某个字段的值设置null。因为如果我们将某个字段的值设置为null,则在进行数据保存操作时,将会以默认值来覆盖null值。

4. 总结

本文着重介绍了两个设置数据字段默认值的方法,在实际的应用中,还需要结合实际的情景来选择具体合适哪种方案。

https://www.baeldung.com/jpa-default-column-values

分类
tools

如何在ESXi7上安装macOS 11 Big Sur

软件需求:

  1. macOS 11 Big Sur ISO
  2. VMware ESXi7 Update 2A
  3. macOS Unlocker

macOS 11 Big Sur ISO

首先我们需要一个用于安装macOS系统的iso文件。如果你直接从相关网站上下载了相关文件,在下载后则需要确认该文件的sha1和md5值,以免文件在下载过程损坏影响安装。

下载 macOS 11 Big Sur ISO

如果你使用的是macOS操作系统,则可以打开Big Sur官方发布站点

进行界面后点击获取按钮。点击继续后便开始进行下载。至本文发布时,系统的版本为11.4 ,大小为12.4G。

下载完成后,如果系统自动打开了安装程序,则切可点下一步,需要按Command + Q 退出。

此时便表明已成功的下载了安装文件。

将安装文件转换为ISO文件

接下来,我们使用的一系列操作均需要root权限。我们打开shell,并输入sudo -i来切换至root用户。

sudo -i

输入密码后,便切换到了root权限。

接下来我们运行如下命令:

hdiutil create -o /tmp/bigsur -size 12900.1m -volname bigsur -layout SPUD -fs HFS+J

上述命令的作用是在/tmp文件夹下创建一个dmg格式的镜像文件,该文件的大小为12.9G。

接下来,我们使用命令来挂载这个dmg镜像文件:

hdiutil attach /tmp/bigsur.dmg -noverify -mountpoint /Volumes/bigsur

上述命令将刚刚创建的镜像文件挂载为/Volumes/bigsur

接着使用如下命令将此镜像设置为可启动macos的启动介质。

sudo /Applications/Install\ macOS\ Big\ Sur.app/Contents/Resources/createinstallmedia --volume /Volumes/bigsur --nointeraction

这需要一定的时间,在过程中会显示进度信息,请耐心等待。完成后将在/Volumes中生成Install macOS Big Sur。接着弹出它:

hdiutil eject -force /volumes/Install\ macOS\ Big\ Sur

此时dmg文件写入完成。下一步将其转换为cdr文件。cdr文件即是macOS系统上的iso文件。

hdiutil convert /tmp/bigsur.dmg -format UDTO -o /Users/yourname/Desktop/bigsur.cdr

注意:不要照抄上面的命令,你需要将上述命令的yourname替换为你自己的用户名。

执行该命令同样需要一定的时候,这取决于我们的硬盘速度,整个过程会有进度提示,请耐心等待。

上述命令完成后,我们将在桌面得到一个名为bigsur.cdr的文件,接着将其重命名为iso文件。

mv /Users/yourname/Desktop/bigsur.cdr /Users/yourname/Desktop/bigsur.iso

同样还是注意把yourname换成你的用户名,或者不使用命令也是可以的:

  1. 选择桌面上的bigsur.cdr文件
  2. 重命名为bigsur.iso文件

清除

得到iso文件后,历史在/tmp文件夹的dmg文件就已经完成历史使命了。

rm -rf /tmp/bigsur*

将ISO文件上传到数据存储

有了ISO文件后,我们接下来将其上传到VMWare ESXi的数据存储上。

  1. 登录ESXi 主机
  2. 选择左侧的存储后,选择一个预上传文件的存储。
  3. 点击右侧的Datastroe brower(数据浏览)
  4. 选择一个预上传的文件夹
  5. 点击上传

上传过程需要一定的时候,可以在下侧的最近任务状态中查看上传结果。

安装macOS Unlocker

经过上面的操作后,生成的用于安装macos的iso文件就已经被上传至vmware的数据存储中了。接下来我们需要为vmware安装一个unlocker。它的作用是防止在vmware上安装macos时出现的死循环(听说如果我们在是mac主机上安装的esxi,然后再在此exsi上安装macos的话,就不需要将unlocker).

下载Unlocker

点击下载链接将得到一个压缩包,解压被进入文件夹后执行./esxi-build.py命令:

./esxi-build.py

注意:执行过程需要调用系统中的tar,如果未找到则会终止执行。解决方法是安装gnu-tar。使用brew的话,执行:brew install gnu-tar

执行成功后,将得到一个名为esxi-unlocker-xxx.tgz的文件,该文件即我们需要的unlokcer文件。

上传至数据存储

与上传iso文件的方法相同,将esxi-unlocker-xxx.tgz上传至vmware的存储上。

安装Unlocker

vmware本质也是一个liunx操作系统,进行vmware并在服务上开启ssh登录后,使用ssh登录vmware。

存储位于root文件夹下vmfs/volumes文件夹下,进行上传的存储文件夹并执行:

tar xzvf esxi-unlocker-xxx.tgz

完成压缩包的解压。

安装

解压后进入触压文件夹,首先执行:

./esxi-smctest.sh

确认安装状态为fasle,即未安装。

接着执行:

./esxi-install.sh

完成安装后,将得到重新启动服务的提示。

reboot

注意:如果在安装过程中提示Permissin denied权限错误,则在执行上述命令前可以将整个文件的权限设置为775,比如:

chmod 775 -R esxi-unlocker-xxx/

验证

机器重新启动后,继续使用ssh登录vmware,进行相应的文件夹,重新执行以下命令查看安装结果:

./esxi-smctest.sh

此时将得到一个返回值为true的结果,说明安装成功。

安装macOS 11 Big Sur虚拟机

登录vmware后,点击管理虚拟机 -> 创建注册虚拟机 -> 创建一个全新的虚拟机, 然后点击next。

填写虚拟机的名称,兼容性选择最新,OS选择macos,版本选择11.4。接着点击下一步
然后选择存储的位置,下一步。
选择CPU核心数、内存、硬盘大小等,最后在CD/DVD中选择使用iso文件,并选择我们前面上传的bigsur.iso。点击下一步、完成。

再然后,开机,选择磁盘工具、下一步、格式化磁盘,格式选择APFS --GUID Partition Map,最后点击擦除,完成后点击完成,最后点击左上方的Disk Utility菜单,退出。

再根据情况选择第二项(一般用户)或是第一项(有时间机想直接恢复的)-> 继续 -> 同意 -> 选择磁盘 -> 下一步

再然后就是等待安装成功,选择语言。

然后,就没有然后了。iCloud、iMessage等一切正常!

参考原文:https://vmscrub.com/installing-macos-11-big-sur-on-esxi-7-update-1/#

分类
unit test

Angular单元测试

分类
Angular

一起学Angular

angular入门

angular进阶

分类
spring

Spring @Autowired 注解

Autowired并不是一个英文词汇,而是auto + wired两个单词的组合。auto在英文中的原意为汽车,其本意是自动的意思,由于汽车可以不用人推、不用马拉自己跑,所以给它起了个auto的名字。auto源于automatic,即为自动的意思。比如汽车的自动变速器简称为AT,这其中的A则是指automatic。wired是wire的被动式,wire本意为电线。比如我们常说的无线wireless ,本意则是没有线的电线。wire加d,可理解为:串在一起的、绑在一起的、连在一起的。

所以Autowired的本意应该是:(将物体)自动连在一起。在计算机术语中,我们称其为:自动装配。

1. 概述

Spring 自2.5版本引用了注解驱动的依赖注入功能。该功能中最核心的注解即是@Autowired@Autowired 可以轻松的解决对象与对象间的依赖关系,将指定的bean自动的装配到其它需要它的bean中。

本文中,我们首先给出3种使用@Autowired 的方法,然后对使用@Qualifier 解决bean冲突做个简单的介绍。

2. 启用@Autowired 注解

Spring框架默认启用了依赖注入功能,所以在Spring中我们不需要做任何的配置即启用了@Autowired 注解的自动装配功能。

Spring就像一个大管家,管着很多个bean,哪里声明需要什么bean,它就负责提供什么bean。Spring Boot提供的@SpringBootApplication注解的作用之一便是:告知Spring大管家,应该到哪去获取到它管理的bean。

@SpringBootApplication
public class SpringAutowireApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringAutowireApplication.class, args);
    }

}

上述代码使得Spring在启动时扫描@SpringBootApplication 所在包以及其子包中的所有bean,并将其纳入Spring大管家的管理范围。在术语中,我们又称这个大管为:Spring Application Context(Spring应用上下文)。

3. 使用@Autowired

我们可以在类的属性、setter方法以及构造函数上使用@Autowired 注解。

3.1 在属性上使用@Autowired

@Autowired可以直接应用到类的属性上,即使该属性的类型不是public、同时也没有为其定义setter方法,也不会影响自动装配的功能。

为了演示自动装配功能,我们首先定义一个FooFormatter bean:

@Component
public class FooFormatter {
    public String format() {
        return "foo";
    }
}

接着便可以在属性上应用@Autowired来完成自动装配:

@Component
public class FooService {  
    @Autowired
    private FooFormatter fooFormatter;
}

此时,Spring将自动为FooService中的FooFormatter属性提供fooFormatter bean。我们也可以认为是自动(auto)将FooFormatterFooFormatter连接(wire)了起来,所以此项功能叫做Autowired。

3.2 在setter方法上使用@Autowired

还可以在setter方法上使用@Autowired,比如3.1中的代码还可改写为:

@Component
public class FooService {
    private FooFormatter fooFormatter;
    @Autowired
    public void setFooFormatter(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}

与在属性上使用@Autowired来完成自动装配相比较,在setter上使用@Autowired没有什么明显的不 。当我们在装配某个属性的同时还希望执行某些逻辑操作时,往往会这么做。

3.3 在构造函数上使用@Autowired

最后,还可以在构造函数上使用@Autowired,这也是官方推荐的一种使用方式,比如3.2的代码还可以改写为:

@Component
public class FooService {
    private FooFormatter fooFormatter;
    @Autowired
    public FooService(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}

值得注意的是,在较新的Spring版本中完全可以省略在构造函数上声明的@Autowired注解。也就是说以下代码同样可以完成自动装配功能:

@Component
public class FooService {
    private FooFormatter fooFormatter;

    public FooService(FooFormatter fooFormatter) {
        this.fooFormatter = fooFormatter;
    }
}

4. NoSuchBeanDefinitionException

当声明自动装配的bean不存在时:

public interface BarRepository★ {
}

@Service
public class BarService {
    @Autowired
    BarRepository barRepository;
}
  • ★ 只声明了接口,并对接口进行实现,所以并不存在类型为BarRepository的bean。

在进行单元测试时,Spring将抛出NoSuchBeanDefinitionException:

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'barService': Unsatisfied dependency expressed through field 'barRepository'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'club.codedemo.springautowire.section4.BarRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

NoSuchBeanDefinitionException直译为:没有找到相关的bean。根据实际情况不同,解决这个异常的方法分为两种:

第一种是前期我们容易犯的错误,即我们的确需要这样的一个bean,而且该bean也是由我们提供的,但忘记使用@Component 相关注解声明了。解决的方法当然是声明该类型的bean即可。

第二种是该bean可以有,也可以没有。那么此时则可以将@Autowiredrequired 属性设置为false :

@Service
public class BarService {
    @Autowired(required = false)
    BarRepository barRepository;
}

值得注意的是Spring Boot很友好的处理了NoSuchBeanDefinitionException异常,当发生NoSuchBeanDefinitionException异常时,Spring Boot给我们的提示信息如下:

***************************
APPLICATION FAILED TO START
***************************

Description:

Field barRepository in club.codedemo.springautowire.section4.BarService required a bean of type 'club.codedemo.springautowire.section4.BarRepository' that could not be found.

The injection point has the following annotations:
    - @org.springframework.beans.factory.annotation.Autowired(required=true)

大体是说:没有找到类型为BarRepository的bean。

5. NoUniqueBeanDefinitionException

默认情况下@Autowired注解是根据类型来完成自动装配的,在装配的过程中如果同一类型的bean存在多个,则会发生NoUniqueBeanDefinitionException异常。

此时便需要告知Spring大管家如何解决此类问题。

5.1 @Qualifier

qualifier译为限定符、限定语。

@Qualifier注解能够非常好的解决此类问题,比如我们当前有两个实现了Message 接口的bean:

public interface Message {
    String hello();
}

@Component
public class FooMessage implements Message {

    @Override
    public String hello() {
        return "foo";
    }
}

@Component
public class BarMessage implements Message {

    @Override
    public String hello() {
        return "bar";
    }
}

此时若直接使用@Autowired 注解尝试注入Message:

@Service
public class MessageService {
    @Autowired
    Message message;
}

由于FooMessageBarMessage都实现了Message接口,所以Spring容器中现在有两个同样是实现了Message接口的bean,此时若使用@Autowired 尝试注入Message, Spring便不知道应该是注入FooMessage 还是BarMessage,所以发生NoUniqueBeanDefinitionException异常。

NoUniqueBeanDefinitionException的字面意思也很好的说明了这个问题:定义的bean不唯一。

当同一类型存在多个bean时,可以使用@Qualifier 来显示指定bean的名称,从而解决了Spring按类型注入时不知道应该注入哪个bean的尴尬情况。

@Service
public class MessageService {
    @Autowired
    @Qualifier("fooMessage")
    Message message;
}

默认情况下Spring自动为每个bean添加一个唯一的名称,该名称的命名原则为:将类的名称由大驼峰变为小驼峰。所以FooMessage 的bean名为:fooMessage;BarMessage 的bean名为: barMessage。除此以外,我们还可以自定义bean的名称,比如将BarMessage 的bean名称定义为customBarMessage:

@Component("customBarMessage")
public class BarMessage implements Message {

@Qualifier 将根据声明的fooFormatter来完成对Formatter的注入。此时FooService 中被注入的FormatterFooFormatter 而非BarFormatter

5.2 自定义装配限定器

Spring还允许我们基于Qualifer 自定义用于限定注入特定Bean的注解。

@Qualifier
@Target({
        ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER
})
@Retention(RetentionPolicy.RUNTIME)
public @interface MessageType {
}

此时我们将注解应用到FooMessage 上:

@Component
@MessageType
public class FooMessage implements Message {

则可以结合@Autowired 完成限定注入FooMessage :

@Service
public class MessageService {
    @Autowired
    @MessageType
    Message message1;
}

由于自定义的@MessageType 继承了@Qualifier ,所以其也拥有@Qualifier 的特性。当多个相同类型的Bean均使用@MessageType 声明时,也可以使用@MessageType(bean的名称) 来指定自动装配的bean。

5.3 依据属性名称自动装配

Spring在自动装配过程中,当同一类型存在多个bean时,还会继续按属性名称来进行查找,比如:

    @Autowired
    Message message;

Spring首先查找类型为Message的bean,最终查找到两个:fooMessage以及barMessage;接着将会在找到的两个bean中继续按属性名称message进行查找;最终发现fooMessage以及barMessage均不匹配message,这时候就视为查找失败,抛出异常。

根据上述规律,当某类型存在多个bean时,也可以使用指定属性名称与bean名相匹配的方法来完成限定注入:

@Service
public class MessageService {    
    @Autowired
    Message barMessage;
}

此时将成功注入一个BarMessage的实例。

总结

@Autowired 自动装配注解是Spring的核心注解之一,我们可以使用@Autowired 按接口类型轻松的完成bean的注入,而不必关心具体的实现。是对面向接口编程具体的阐释。在实际的使用过程中,往往会遇到多个bean实现同一接口而产生冲突的情况,此时则需要配合其它方法来告知Spring大管家冲突的解决方案。本文主要对@Autowried 注解的使用方法以及如何解决冲突进行阐述。

分类
spring

Spring @Primary 注解介绍

1. 概述

本文中我们将共同学习Spring自3.0版本引入的@Primary注解。

单词Primary意为首要的、主要的,其功能与名称相同:在依赖注入的过程中,当同一类型存在多个bean时,将首要(首先、优先)注入使用@Primary 注解的那个。

2. 适用场景

有些时候我们需要为同一类型注册多个不同的bean。

比如下述代码中我们为类型Employee(员工)提供了两个不同的bean:

@Configuration
public class Config {
 
    @Bean
    public Employee❶ JohnEmployee() {
        return new Employee("John")❷;
    }
 
    @Bean
    public Employee❶ TonyEmployee() {
        return new Employee("Tony")❷;
    }
}
  • ❶ 类型均为Employee
  • ❷ 返回两个不同的bean

然后使用@Autowired进行注入:

    @Autowired❶
    Employee❷ employee;
  • 此时当我们尝试 ❶注入 ❷Employee 时,则将发生NoUniqueBeanDefinitionException异常:
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springPrimaryApplication': Unsatisfied dependency expressed through field 'employee'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'club.codedemo.springprimary.Employee' available: expected single matching bean but found 2: JohnEmployee,TonyEmployee

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

我们通常使用@Qualifier(bean的名称) 来指定具体注入哪个bean以规避此类异常。关于@Quailfer的更多用法,请参考本文

本文我们将重点介绍采用@Primary注解来处理此类问题。

3. @Primary@Bean 结合使用

在注入过程中由于多个bean满足某一类型,同时这些bean之间又没有主次之分,所以Spring无法擅自做主为我们选择注入其中的某一个bean。

@Primary 则会告之Spring哪个bean的地位是主要的、首要的、要优先考虑的,此时当发生类型情况下,Spring在注入时则会优先使用这个bean。

    @Bean
    @Primary ★
    public Employee JohnEmployee() {
        return new Employee("John");
    }

值得注意的是:同类型被声明为Primary的bean最多只能有一个。如果我们在同一类型的Bean上声明多个Primary,同样会发生NoUniqueBeanDefinitionException异常,错误代码示例如下:

    @Bean
    @Primary ✘
    public Employee JohnEmployee() {
        return new Employee("John");
    }

    @Bean
    @Primary ✘
    public Employee TonyEmployee() {
        return new Employee("Tony");
    }
  • 同一Employee类型,在多个Bean中使用了 @Primary注解,将引发NoUniqueBeanDefinitionException异常。

此时Spring在注入时发现了多个被声明为的bean,两个bean的优先级相同。Spring同样没有办法自作主张的注入一个其中一个bean给我们,报错信息如下:

more than one 'primary' bean found among candidates: [JohnEmployee, TonyEmployee]
多于一个主bean被找到...

4. @Primary@Component 结合使用

@Primary还可以与@Component注解结合使用。

@Component
@Primary
public class SmsServiceAliImpl implements SmsService {
    @Override
    public void sendMessage(String phone, String message) {
    }
}

@Component
public class SmsServiceBaiduImpl implements SmsService {
    @Override
    public void sendMessage(String phone, String message) {
    }
}

由于@Service、@Controller等注解也属于@Component注解,所以@Primary同样可以与它们结合使用。

@Service
@Primary
public class FooServiceImpl implements FooService {
}

5. 总结

当同一类型存在多个bean时,使用@Primary注解可以轻松的确定一个bean出来,这为Spring在完成注入时提供了依据。既然被称为bean,则必须保证其唯一,所以相同类型被@Primary声明的Bean最多只能有一个。

分类
spring

Spring @Qualifier 注解

1. 概述

本文我们将共同学习@Qualifier注解的作用以及其使用方法。在此基础上,将与@Primary 注解做一个简单的对比。

2. 自动装配的唯一性要求

在Spring项目中 @Autowired 注解是完成依赖注入的方式之一。但有些时候,此注解会在应该注入谁的问题上犯嘀咕。

在默认情况下,Spring是通过类型来完成依赖注入的。

注入过程中如果当前容器中出现多个bean的类型相同,Spring框架则会抛出 NoUniqueBeanDefinitionException 异常,来告诉我们:由于要注入类型的Bean在当前容器中不唯一,所以Spring无法为我们做主此时应该注入哪个。

比如:

@Component("fooFormatter★")
public class FooFormatter implements Formatter ❶ {
 
    public String format() {
        return "foo";
    }
}
 
@Component("barFormatter★")
public class BarFormatter implements Formatter ❶{
 
    public String format() {
        return "bar";
    }
}
 
@Component
public class FooService {
     
    @Autowired ❷
    private Formatter formatter;
}
  • ★ 自定义bean的名字,将在后面用到它
  • ❶ 两个bean的类型均为Formatter
  • ❷ 自动注入时将发生NoUniqueBeanDefinitionException异常。

❷这是由于此时满足类型为FooService的Bean有两个(fooFormatter以及barFormatter),而Spring并不能确认我们的真实意图是注入哪一个。

解决上述问题有多种方案,使用@Qualifier注解的方式是解决方案之一。

3. @Qualifier 注解

 @Qualifier 注解将显式的告之Spring依赖注入时应该注入的具体bean。

比如:

public class FooService {
     
    @Autowired
    @Qualifier("fooFormatter❶")
    private Formatter formatter;
}
  • ❶ 该值为前面在定义Bean时@Component注解中的值

@Qualifier显式地指名此时注入的bean为fooFormatter,所以即使barFormatter的类型同样为Formatter,此时也不会面临Spring不知道应该注入谁的问题。

我们注意到❶中使用了名称"fooFormatter",这是由于我们在前面定义Bean时在@Component注解中使用了相同的名称"fooFormatter"。在实现自定义Bean的名称功能时,除了使用@Component注解以外,还可以使用@Qualifier注解,两者的作用相同。

@Component
@Qualifier("fooFormatter") ❶
public class FooFormatter implements Formatter {
    //...
}
 
@Component
@Qualifier("barFormatter") ❶
public class BarFormatter implements Formatter {
    //...
}
  • ❶ 在@Qualifier注解中自定义Bean的名称

4. @Qualifier 与 @Primary

除了@Qualifier以外,还有一个@Primary注解也拥有在依赖注入时消除歧义的功能。

Primary译为主要的、首要的,该注解的作用是:当某个声明注入的类型存在多个bean时,除非你显式地声明使用哪个bean,否则将使用那个以@Primary注解的首要Bean。

示例如下:

@Configuration
public class Config {
 
    @Bean
    public Employee johnEmployee() {
        return new Employee("John");
    }
 
    @Bean
    @Primary★
    public Employee❶ tonyEmployee() {
        return new Employee("Tony");
    }
}
  • ❶ 两个Bean的类型相同,均为Employee
  • ★ 此Bean被声明为:首要的、主要的。

此时,若未声明注入的Bean,则使用方法tonyEmployee()的返回值生成的Bean来完成注入:

public class FooService {
     
    @Autowired ★
    private Employee employee;
}
  • ★ 此时将注入以@Primary注解的方法tonyEmployee()的返回值生成的Bean。

注意:Bean是唯一的,多次注入Employee,仅仅会执行一次tonyEmployee()方法。

前面我们有介绍@Primary时,多次强调了:未显式声明时才生效。也就是说一旦在注入时,显式地声明了注入的Bean名称则@Primary将失效。也就是说,在优先级上做比较:@Qualifier("xx") 高于 @Primary

比如:

@Component("fooFormatter")
@Primary ❶
public class FooFormatter implements Formatter {
 
    public String format() {
        return "foo";
    }
}
 
@Component("barFormatter")
public class BarFormatter implements Formatter {
 
    public String format() {
        return "bar";
    }
}

@Component
public class FooService {
     
    @Autowired
    @Qualifier("barFormatter") ★
    private Formatter formatter1;
}
  • ❶ 被声明为Primary
  • ★ 此时注入的为barFormatter

5.  @Qualifier与按名称自动装配

还有一种解决冲突的方法是在@Authwired注解的字段上,使用一个与Bean名称相同的字段名,这也是在未显式声明依赖bean的情况下Spring使用的默认策略。

比如:

public class FooService {
     
    @Autowired
    private Formatter barFormatter★;
}
  • ★ 字段名barFormatter与Bean名相同,此时将忽略@Primary注解,而注入的为barFormatter

但当显式的声明bean时,Spring则会以以显示声明的为准:

    @Autowired
    @Qualifier("barFormatter") ★
    private Formatter fooFormatter❶;
  • ❶ 由于显式声明的存在,所以字段名匹配原则将失败
  • ★ 将按显式声明的bean名称进行自动装配

6. 总结

在依赖注入过程中Spring默认使用按类型进行匹配的装配原则,当存在多个bean均满足条件时,spring将优先查找是显式声明的bean,如果未显示声明bean则将按字段名称进行查找,如果字段名称未查找成功则将继续查找使用了@Primary注解的bean,以上方法均未查找成功,则将抛出NoUniqueBeanDefinitionException异常。在装配bean的优先级上:@Qualifier > 按字段名称匹配 > @Primary

分类
Java

Java中的不可变对象

1. 概述

本文我们将讨论什么是不可变对象,在JAVA中如何创建一个不可变的对象,以及为何要把某些对象设置为不可变的,这样做又有什么好处。

2. 什么是不可变对象

顾名思意,不可变对象就是说对象一旦通过实例化产生,其所有的属性将永远保持初始值,不会改变。

这也意味着:一旦不可变对象实例化创建完毕,在其被回收前,我们可以自由调用对象上任意暴露的方法,而对象将时刻保持实例化的初始状态。

字符串类型String是比较典型的不可变类,通过该类实例化的字符串为不可变对象。这同时意味着,字符串对象一旦创建,无论我们调用该对象是任意方法,该字符串均不会发生变化:

        String a = "codedemo.club";
        String b = a.replace("codedemo", "yunzhi");➊

        System.out.println(a);
        System.out.println(b);

        Assertions.assertEquals("codedemo.club", a);➋
        Assertions.assertEquals("yunzhi.club", b);
  • ➊ 调用a对象的replace方法来替换a字符串中的特定内容。
  • ➋ 替换方法执行后,a的值并未发生变化,我们说a为不可变对象。
codedemo.club
yunzhi.club

字符串String类型上的replace方法虽然提供了字符串替换的功能,但由于String被设计为不可变类,所以replace被调用后虽然返回了替换后的字符串,但原字符串a并不发生变化。我们把字符串a称为不可变对象,把字符串a属的String类称为不可变类

3. Java中的 final 关键字

在尝试自定义不可变类之前,我们先简单了解一下Java中的final关键字。

在Java中,变量默认是可以改变的(变量:可以改变的量),这意味着变量定义完成后,我们可以随时改变变量的值。

但如果我们使用final关键字来声明变量,Java编译器则不允许我们在后面改变该变量的值。比如以下代码将在编译时发生编译错误:

        final String a = "codedemo.club";

        // 以下代码将发生编译错误
        a = "yunzhi.club";

编译错误如下:

java: cannot assign a value to final variable a

值得注意的final关键字的作用仅是:禁止改变变量的引用值。但并不能禁止我们通过调用对象上的方法来改变其内部属性值(状态值),比如:

        final List<String> strings = new ArrayList<>(); 
        Assertions.assertEquals(0, strings.size());

        // 你不能改变strings的引用值,否则将发生编译错误
        // strings = new ArrayList<>();

        // 但可以调用ArrayList的add方法来改变strings的状态
        strings.add("codedemo.club"); 
        Assertions.assertEquals(1, strings.size());

上述代码成功的向被声明为final类型的strings列表中增加了子元素,也就是说strings虽然被声明了final,但其状态仍然是可改变不稳定的。这是由于ArrayList类型并不是一个不可变类型造成的。

4. Java中创建不可变对象

在了解了如何避免变量被改变的方法后,下面我们尝试创建不可变对象。

不可变对象的原则是:无论外部如何调用对象提供的公有方法,该对象的状态均不会发生改变。

比如我们可以直接将类中的属性全部声明为final:

public class Student {
    final private String name;
    final private int age;

上述代码中我们使用了final关键字来声明了Student类中的所有属性,而且这些属性的类型为主类型或不可变类型,这保证了Stduent类为不可变类,由该类实例化的对象为不可变对象。

但如果Student类型再增加一个Clazz班级属性,则欲保证Student为不可变类型,则需要保证Calzz类同样为不可变类。

public class Student {
    final private String name;
    final private int age;
    // 此时Student是否为可变类取决于Clazz是否为可变类
    final private Clazz clazz;

大多数时候,我们都需要在类中定义多个属性以存储对象的各个状态,对于声明为final类型的属性而言,只能在对象实例化时为其赋值:

public class Student {
    final private String name;
    
    // 声明属性时赋初值
    final private int age = 12;

    // 此时Student是否为可变类取决于Clazz是否为可变类
    final private Clazz clazz;

    /**
     * 或在构造函数中为其赋值
     */
    public Student(String name, Clazz clazz) {
        this.name = name;
        this.clazz = clazz;
    }
    
    // 此处省略了getter方法。所有属性均为final,所以该类无setter方法。

值得注意的是虽然Student在此为不可变类型,但Java提供的反射机制是可以忽视该不可变性,从而改变不可变对象的。在实际的使用中,我们往往不会(也不应该)这么做。

5. 不可变对象的优势

由于不可变对象状态的稳定性,所以在多线程情况下,我们可以放心地将不可变对象在不同的线程间传递、共享,而可以完全忽略各个线程是如何、何时利用该不可变对象的。

同时,我们也可以放心地将同一不可变对象分别嵌入到其它多个对象(可能是不同类型的)中,从而实现多对象共享某一相同对象的目标。重要的是,在此共享过程中,我们完全不必担心该共享对象可能会发生改变。

总之:不可变对象一旦实例化完成,我们便可以放心的使用它,而不必担心该对象可能会发生变化。

6. 总结

不可变对象一旦创建便会一直保持当前的状态,所以说它们是线程安全以及没有副作用的。正是由于这些特性,不可变对象往往被更多用于多线程环境中。

分类
Uncategorized

jackson

coming soon.