Maison  >  Article  >  Java  >  Comment créer un SaaS multi-tenant Springboot

Comment créer un SaaS multi-tenant Springboot

WBOY
WBOYavant
2023-05-12 16:49:061468parcourir

Cadre technique

La version Springboot est 2.3.4.RELEASE

La couche de persistance utilise JPA

Conception du modèle de locataire

Parce que tous les locataires de l'application saas utilisent le même service et la même base de données, afin d'isoler les données des locataires, une BaseSaasEntity est créé ici

Il n'y a qu'un seul champ tenantId dans
public abstract class BaseSaasEntity {
    @JsonIgnore
    @Column(nullable = false, updatable = false)
    protected Long tenantId;
    }

, qui correspond à l'ID du locataire. Toutes les entités commerciales locataires héritent de cette classe parent. Enfin, le tenantId est utilisé pour distinguer à quel locataire appartiennent les données.

Filtrage des données des locataires SQL

Comme d'habitude, une fois la table créée, le CURD du module correspondant doit être suivi. Mais l'exigence la plus fondamentale pour les applications Saas est l'isolation des données des locataires, c'est-à-dire que les personnes de l'entreprise B ne peuvent pas voir les données de l'entreprise A. Comment les filtrer ? La BaseSaasEntity que nous avons établie ci-dessus fonctionnera ici en distinguant de quelle entreprise provient la demande actuelle. à partir de, ajoutez Where tenant=? à tous les SQL d'entreprise locataire pour implémenter le filtrage des données du locataire.

Filtre Hibernate

Si nous ajoutons du code de filtrage SQL locataire à notre entreprise, la charge de travail sera non seulement énorme, mais la probabilité d'erreurs sera également élevée. L’idéal est de traiter ensemble l’épissage SQL filtré et d’activer le filtrage SQL sur l’interface métier du locataire. Parce que JPA est implémenté par hibernate, nous pouvons ici profiter de certaines fonctions d'hibernate

@MappedSuperclass
@Data
@FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantId", type = "long")})
@Filter(condition = "tenant_id=:tenantId", name = "tenantFilter")
public abstract class BaseSaasEntity {
    @JsonIgnore
    @Column(nullable = false, updatable = false)
    protected Long tenantId;


    @PrePersist
    public void onPrePersist() {
        if (getTenantId() != null) {
            return;
        }
        Long tenantId = TenantContext.getTenantId();
        Check.notNull(tenantId, "租户不存在");
        setTenantId(tenantId);
    }
}

Hibernate3 fournit une manière innovante de traiter les données avec des règles de "visibilité", c'est-à-dire en utilisant des filtres Hibernate. Les filtres Hibernate sont des filtres nommés globalement valides qui peuvent prendre des paramètres. Vous pouvez choisir d'activer (ou de désactiver) un filtre pour une session Hibernate spécifique.

Ici, nous prédéfinissons une condition de filtre SQL via @FilterDef et @Filter. Utilisez ensuite une annotation @TenantFilter pour identifier que l'interface nécessite un filtrage des données

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional
public @interface TenantFilter {

    boolean readOnly() default true;

}

On voit que cette interface est placée sur la méthode, qui correspond à la couche Contrôleur. L'importance de l'ajout de l'annotation de transaction @Transactional est qu'une transaction doit être activée pour activer le filtre de mise en veille prolongée. La valeur par défaut ici est une transaction en lecture seule. Enfin, définissez un aspect pour activer le filtre

@Aspect
@Slf4j
@RequiredArgsConstructor
public class TenantSQLAspect {
    private static final String FILTER_NAME = "tenantFilter";
    private final EntityManager entityManager;
    @SneakyThrows
    @Around("@annotation(com.lvjusoft.njcommon.annotation.TenantFilter)")
    public Object aspect(ProceedingJoinPoint joinPoint) {
        Session session = entityManager.unwrap(Session.class);
        try {
            Long tenantId = TenantContext.getTenantId();
            Check.notNull(tenantId, "租户不存在");
            session.enableFilter(FILTER_NAME).setParameter("tenantId", tenantId);
            return joinPoint.proceed();
        } finally {
            session.disableFilter(FILTER_NAME);
        }
    }
}

L'objet de l'aspect ici est l'annotation @TenantFilter qui vient d'être personnalisée. Obtenez l'identifiant du locataire actuel avant l'exécution de la méthode et activez le filtre de cette façon. est terminé. Il vous suffit d'ajouter l'annotation @TenantFilter à l'interface métier du locataire. Ajoutez simplement l'annotation @TenantFilter et le développement ne doit se soucier que du code métier. Le TenantContext dans l'image ci-dessus est le contexte actuel du locataire du thread. En accord avec le front-end, l'identifiant du locataire est ajouté à l'en-tête de la demande d'interface. Le serveur utilise l'intercepteur pour mettre en cache l'identifiant du locataire obtenu dans ThreadLocal

public class IdentityInterceptor extends HandlerInterceptorAdapter {
    public IdentityInterceptor() {
        log.info("IdentityInterceptor init");
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader(AuthConstant.USER_TOKEN_HEADER_NAME);
        UserContext.setToken(token);
        String tenantId = request.getHeader(AuthConstant.TENANT_TOKEN_HEADER_NAME);
        TenantContext.setTenantUUId(tenantId);
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        UserContext.clear();
        TenantContext.clear();
    }
}

Sous-bibliothèque.

Avec le locataire À mesure que le nombre augmente, les données d'une seule base de données MySQL et d'une seule table atteindront certainement un goulot d'étranglement. Ici, seule la méthode de sous-base de données est utilisée. Utilisez plusieurs sources de données pour effectuer un mappage plusieurs-à-un des locataires et des sources de données.

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    private Map<Object, Object> targetDataSources;
    public DynamicRoutingDataSource() {
        targetDataSources =new HashMap<>();
        DruidDataSource druidDataSource1 = new DruidDataSource();
        druidDataSource1.setUsername("username");
        druidDataSource1.setPassword("password");
        druidDataSource1.setUrl("jdbc:mysql://localhost:3306/db?useSSL=false&useUnicode=true&characterEncoding=utf-8");
        targetDataSources.put("db1",druidDataSource1);
        
        DruidDataSource druidDataSource2 = new DruidDataSource();
        druidDataSource2.setUsername("username");
        druidDataSource2.setPassword("password");
        druidDataSource2.setUrl("jdbc:mysql://localhost:3306/db?useSSL=false&useUnicode=true&characterEncoding=utf-8");
        targetDataSources.put("db2",druidDataSource1);
        
        this.targetDataSources = targetDataSources;
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    public void addDataSource(String key, DataSource dataSource) {
        if (targetDataSources.containsKey(key)) {
            throw new IllegalArgumentException("dataSource key exist");
        }
        targetDataSources.put(key, dataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContext.getSource();
    }
}

Déclarez une source de données de routage dynamique en implémentant AbstractRoutingDataSource Avant que le framework n'utilise datesource, spring appellera la méthode détermineCurrentLookupKey() pour déterminer quelle source de données utiliser. Le DataSourceContext ici est similaire au TenantContext ci-dessus. Après avoir obtenu le tenantInfo dans l'intercepteur, recherchez la clé de source de données correspondant au locataire actuel et définissez-la dans ThreadLocal.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer